Bug 1442128 - support aes128gcm encryption in PushCrypto.jsm. r=kitcambridge,mt

MozReview-Commit-ID: HqLxm7wuZuv

--HG--
extra : rebase_source : 10512ec769ab43674e829363af06805e1b4844d6
This commit is contained in:
Mark Hammond 2018-02-20 12:01:08 +11:00
Родитель 3fd394bab6
Коммит 4cae8128c3
4 изменённых файлов: 288 добавлений и 2 удалений

Просмотреть файл

@ -448,6 +448,7 @@ class OldSchemeDecoder extends Decoder {
var AES128GCM_ENCODING = 'aes128gcm';
var AES128GCM_KEY_INFO = UTF8.encode('Content-Encoding: aes128gcm\0');
var AES128GCM_AUTH_INFO = UTF8.encode('WebPush: info\0');
var AES128GCM_NONCE_INFO = UTF8.encode('Content-Encoding: nonce\0');
class aes128gcmDecoder extends Decoder {
/**
@ -466,7 +467,7 @@ class aes128gcmDecoder extends Decoder {
let prkKdf = new hkdf(this.salt, prk);
return Promise.all([
prkKdf.extract(AES128GCM_KEY_INFO, 16),
prkKdf.extract(concatArray([NONCE_INFO, new Uint8Array([0])]), 12)
prkKdf.extract(AES128GCM_NONCE_INFO, 12)
]);
}
@ -635,4 +636,137 @@ var PushCrypto = {
return decoder.decode();
},
/**
* Encrypts a payload suitable for using in a push message. The encryption
* is always done with a record size of 4096 and no padding.
*
* @throws {CryptoError} if encryption fails.
* @param {plaintext} Uint8Array The plaintext to encrypt.
* @param {receiverPublicKey} Uint8Array The public key of the recipient
* of the message as a buffer.
* @param {receiverAuthSecret} Uint8Array The auth secret of the of the
* message recipient as a buffer.
* @param {options} Object Encryption options, used for tests.
* @returns {ciphertext, encoding} The encrypted payload and encoding.
*/
async encrypt(plaintext, receiverPublicKey, receiverAuthSecret, options={}) {
const encoding = options.encoding || AES128GCM_ENCODING;
// We only support one encoding type.
if (encoding != AES128GCM_ENCODING) {
throw new CryptoError(`Only ${AES128GCM_ENCODING} is supported`,
BAD_ENCODING_HEADER);
}
// We typically use an ephemeral key for this message, but for testing
// purposes we allow it to be specified.
const senderKeyPair = options.senderKeyPair ||
await crypto.subtle.generateKey(ECDH_KEY, true, ["deriveBits"]);
// allowing a salt to be specified is useful for tests.
const salt = options.salt || crypto.getRandomValues(new Uint8Array(16));
const rs = options.rs === undefined ? 4096 : options.rs;
const encoder = new aes128gcmEncoder(plaintext, receiverPublicKey,
receiverAuthSecret, senderKeyPair,
salt, rs);
return encoder.encode();
},
};
// A class for aes128gcm encryption - the only kind we support.
class aes128gcmEncoder {
constructor(plaintext ,receiverPublicKey, receiverAuthSecret, senderKeyPair, salt, rs) {
this.receiverPublicKey = receiverPublicKey;
this.receiverAuthSecret = receiverAuthSecret;
this.senderKeyPair = senderKeyPair;
this.salt = salt;
this.rs = rs;
this.plaintext = plaintext;
}
async encode() {
const sharedSecret = await this.computeSharedSecret(this.receiverPublicKey,
this.senderKeyPair.privateKey);
const rawSenderPublicKey = await crypto.subtle.exportKey("raw", this.senderKeyPair.publicKey);
const [gcmBits, nonce] = await this.deriveKeyAndNonce(sharedSecret,
rawSenderPublicKey)
const contentEncryptionKey = await crypto.subtle.importKey("raw", gcmBits,
"AES-GCM", false,
["encrypt"]);
const payloadHeader = this.createHeader(rawSenderPublicKey);
const ciphertextChunks = await this.encrypt(contentEncryptionKey, nonce);
return {ciphertext: concatArray([payloadHeader, ...ciphertextChunks]),
encoding: "aes128gcm"};
}
// Perform the actual encryption of the payload.
async encrypt(key, nonce) {
if (this.rs < 18) {
throw new CryptoError("recordsize is too small", BAD_RS_PARAM);
}
let chunks;
if (this.plaintext.byteLength === 0) {
// Send an authentication tag for empty messages.
chunks = [await crypto.subtle.encrypt({
name: "AES-GCM",
iv: generateNonce(nonce, 0)
}, key, new Uint8Array([2]))];
} else {
// Use specified recordsize, though we burn 1 for padding and 16 byte
// overhead.
let inChunks = chunkArray(this.plaintext, this.rs - 1 - 16);
chunks = await Promise.all(inChunks.map(async function (slice, index) {
let isLast = index == inChunks.length - 1;
let padding = new Uint8Array([isLast ? 2 : 1]);
let input = concatArray([slice, padding]);
return await crypto.subtle.encrypt({
name: "AES-GCM",
iv: generateNonce(nonce, index),
}, key, input);
}));
}
return chunks;
}
// Note: this is a dupe of aes128gcmDecoder.deriveKeyAndNonce, but tricky
// to rationalize without a larger refactor.
async deriveKeyAndNonce(sharedSecret, senderPublicKey) {
const authKdf = new hkdf(this.receiverAuthSecret, sharedSecret);
const authInfo = concatArray([AES128GCM_AUTH_INFO,
this.receiverPublicKey,
senderPublicKey]);
const prk = await authKdf.extract(authInfo, 32);
const prkKdf = new hkdf(this.salt, prk);
return Promise.all([
prkKdf.extract(AES128GCM_KEY_INFO, 16),
prkKdf.extract(AES128GCM_NONCE_INFO, 12),
]);
}
// Note: this duplicates some of Decoder.computeSharedSecret, but the key
// management is slightly different.
async computeSharedSecret(receiverPublicKey, senderPrivateKey) {
const receiverPublicCryptoKey = await crypto.subtle.importKey("raw", receiverPublicKey,
ECDH_KEY, false, ["deriveBits"]);
return crypto.subtle.deriveBits({name: "ECDH", public: receiverPublicCryptoKey},
senderPrivateKey, 256);
}
// create aes128gcm's header.
createHeader(key) {
// layout is "salt|32-bit-int|8-bit-int|key"
if (key.byteLength != 65) {
throw new CryptoError("Invalid key length for header", BAD_DH_PARAM);
}
// the 2 ints
let ints = new Uint8Array(5);
let intsv = new DataView(ints.buffer);
intsv.setUint32(0, this.rs); // bigendian
intsv.setUint8(4, key.byteLength);
return concatArray([this.salt, ints, key]);
}
}

Просмотреть файл

@ -5,7 +5,11 @@
* http://creativecommons.org/licenses/publicdomain/
*
* Uses the WebCrypto API.
* Uses the fetch API. Polyfill: https://github.com/github/fetch
*
* Note that this test file uses the old, deprecated aesgcm128 encryption
* scheme. PushCrypto.encrypt() exists and uses the later aes128gcm, but
* there's no good reason to upgrade this at this time (and having mochitests
* use PushCrypto directly is easier said than done.)
*/
(function (g) {

Просмотреть файл

@ -0,0 +1,147 @@
// Test PushCrypto.encrypt()
"use strict";
Cu.importGlobalProperties(["crypto"]);
const {PushCrypto} = ChromeUtils.import("resource://gre/modules/PushCrypto.jsm");
let from64 = v => {
// allow whitespace in the strings.
let stripped = v.replace(/ |\t|\r|\n/g, '');
return new Uint8Array(ChromeUtils.base64URLDecode(stripped, {padding: "reject"}));
}
let to64 = v => ChromeUtils.base64URLEncode(v, {pad: false});
// A helper function to take a public key (as a buffer containing a 65-byte
// buffer of uncompressed EC points) and a private key (32byte buffer) and
// return 2 crypto keys.
async function importKeyPair(publicKeyBuffer, privateKeyBuffer) {
let jwk = {
kty: "EC",
crv: "P-256",
x: to64(publicKeyBuffer.slice(1, 33)),
y: to64(publicKeyBuffer.slice(33, 65)),
ext: true,
};
let publicKey = await crypto.subtle.importKey('jwk', jwk,
{ name: 'ECDH', namedCurve: 'P-256' },
true, []);
jwk.d = to64(privateKeyBuffer);
let privateKey = await crypto.subtle.importKey('jwk', jwk,
{ name: 'ECDH', namedCurve: 'P-256' },
true, ['deriveBits']);
return {publicKey, privateKey};
}
// The example from draft-ietf-webpush-encryption-09.
add_task(async function static_aes128gcm() {
let fixture = {
ciphertext: from64(`DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml
mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT
pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN`),
plaintext: new TextEncoder("utf-8").encode("When I grow up, I want to be a watermelon"),
authSecret: from64("BTBZMqHH6r4Tts7J_aSIgg"),
receiver: {
private: from64("q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94"),
public: from64(`BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcx
aOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4`),
},
sender: {
private: from64("yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw"),
public: from64(`BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIg
Dll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8`),
},
salt: from64("DGv6ra1nlYgDCS1FRnbzlw"),
};
let publicKeyBuffer = from64(`BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZ
IIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8`);
let privateKeyBuffer = from64("yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw");
let options = {
senderKeyPair: await importKeyPair(fixture.sender.public, fixture.sender.private),
salt: fixture.salt,
}
let {ciphertext, encoding} = await PushCrypto.encrypt(fixture.plaintext,
fixture.receiver.public,
fixture.authSecret,
options);
Assert.deepEqual(ciphertext, fixture.ciphertext);
Assert.equal(encoding, "aes128gcm");
// and for fun, decrypt it and check the plaintext.
let recvKeyPair = await importKeyPair(fixture.receiver.public, fixture.receiver.private);
let jwk = await crypto.subtle.exportKey("jwk", recvKeyPair.privateKey);
let plaintext = await PushCrypto.decrypt(jwk, fixture.receiver.public,
fixture.authSecret,
{encoding: "aes128gcm"},
ciphertext);
Assert.deepEqual(plaintext, fixture.plaintext);
});
// This is how we expect real code to interact with .encrypt.
add_task(async function aes128gcm_simple() {
let [recvPublicKey, recvPrivateKey] = await PushCrypto.generateKeys();
let message = new TextEncoder("utf-8").encode("Fast for good.");
let authSecret = crypto.getRandomValues(new Uint8Array(16));
let {ciphertext, encoding} = await PushCrypto.encrypt(message, recvPublicKey, authSecret);
Assert.equal(encoding, "aes128gcm");
// and decrypt it.
let plaintext = await PushCrypto.decrypt(recvPrivateKey, recvPublicKey,
authSecret,
{encoding},
ciphertext);
deepEqual(message, plaintext);
});
// Variable record size tests
add_task(async function aes128gcm_rs() {
let [recvPublicKey, recvPrivateKey] = await PushCrypto.generateKeys();
let payload = "x".repeat(1024 * 10);
for (let rs of [-1, 0, 1, 17]) {
info(`testing expected failure with rs=${rs}`);
let message = new TextEncoder("utf-8").encode(payload);
let authSecret = crypto.getRandomValues(new Uint8Array(16));
await Assert.rejects(PushCrypto.encrypt(message, recvPublicKey, authSecret, {rs}),
/recordsize is too small/);
}
for (let rs of [18, 50, 1024, 4096, 16384]) {
info(`testing expected success with rs=${rs}`);
let message = new TextEncoder("utf-8").encode(payload);
let authSecret = crypto.getRandomValues(new Uint8Array(16));
let {ciphertext, encoding} = await PushCrypto.encrypt(message, recvPublicKey, authSecret, {rs});
Assert.equal(encoding, "aes128gcm");
// and decrypt it.
let plaintext = await PushCrypto.decrypt(recvPrivateKey, recvPublicKey,
authSecret,
{encoding},
ciphertext);
deepEqual(message, plaintext);
}
});
// And try and hit some edge-cases.
add_task(async function aes128gcm_edgecases() {
let [recvPublicKey, recvPrivateKey] = await PushCrypto.generateKeys();
for (let size of [0, 4096-16, 4096-16-1, 4096-16+1,
4095, 4096, 4097,
1024*100]) {
info(`testing encryption of ${size} byte payload`);
let message = new TextEncoder("utf-8").encode("x".repeat(size));
let authSecret = crypto.getRandomValues(new Uint8Array(16));
let {ciphertext, encoding} = await PushCrypto.encrypt(message, recvPublicKey, authSecret);
Assert.equal(encoding, "aes128gcm");
// and decrypt it.
let plaintext = await PushCrypto.decrypt(recvPrivateKey, recvPublicKey,
authSecret,
{encoding},
ciphertext);
deepEqual(message, plaintext);
}
});

Просмотреть файл

@ -6,6 +6,7 @@ skip-if = toolkit == 'android'
[test_clear_forgetAboutSite.js]
[test_clear_origin_data.js]
[test_crypto.js]
[test_crypto_encrypt.js]
[test_drop_expired.js]
[test_handler_service.js]
support-files = PushServiceHandler.js PushServiceHandler.manifest