зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
3fd394bab6
Коммит
4cae8128c3
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче