--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -242,18 +242,60 @@ ManifestInvalidType=Expected the %1$S’s %2$S member to be a %3$S.
ManifestInvalidCSSColor=%1$S: %2$S is not a valid CSS color.
PatternAttributeCompileFailure=Unable to check <input pattern='%S'> because the pattern is not a valid regexp: %S
# LOCALIZATION NOTE: Do not translate "postMessage" or DOMWindow. %S values are origins, like https://domain.com:port
TargetPrincipalDoesNotMatch=Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘%S’) does not match the recipient window’s origin (‘%S’).
# LOCALIZATION NOTE: Do not translate 'YouTube'. %S values are origins, like https://domain.com:port
RewriteYouTubeEmbed=Rewriting old-style YouTube Flash embed (%S) to iframe embed (%S). Please update page to use iframe instead of embed/object, if possible.
# LOCALIZATION NOTE: Do not translate 'YouTube'. %S values are origins, like https://domain.com:port
RewriteYouTubeEmbedPathParams=Rewriting old-style YouTube Flash embed (%S) to iframe embed (%S). Params were unsupported by iframe embeds and converted. Please update page to use iframe instead of embed/object, if possible.
-# LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is the ServiceWorker scope URL. %2$S is an error string.
-PushMessageDecryptionFailure=The ServiceWorker for scope ‘%1$S’ encountered an error decrypting a push message: ‘%2$S’. For help with encryption, please see https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API#Encryption
+# LOCALIZATION NOTE: This error is reported when the "Encryption" header for an
+# incoming push message is missing or invalid. Do not translate "ServiceWorker",
+# "Encryption", and "salt". %1$S is the ServiceWorker scope URL.
+PushMessageBadEncryptionHeader=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘Encryption’ header must include a unique ‘salt‘ parameter for each message.
+# LOCALIZATION NOTE: This error is reported when the "Crypto-Key" header for an
+# incoming push message is missing or invalid. Do not translate "ServiceWorker",
+# "Crypto-Key", and "dh". %1$S is the ServiceWorker scope URL.
+PushMessageBadCryptoKeyHeader=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘Crypto-Key‘ header must include a ‘dh‘ parameter containing the app server’s public key.
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt because the deprecated
+# "Encryption-Key" header for an incoming push message is missing or invalid.
+# Do not translate "ServiceWorker", "Encryption-Key", "dh", "Crypto-Key", and
+# "Content-Encoding: aesgcm". %1$S is the ServiceWorker scope URL.
+PushMessageBadEncryptionKeyHeader=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘Encryption-Key’ header must include a ‘dh‘ parameter. This header is deprecated and will soon be removed. Please use ‘Crypto-Key‘ with ‘Content-Encoding: aesgcm‘ instead.
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt
+# because the "Content-Encoding" header is missing or contains an
+# unsupported encoding. Do not translate "ServiceWorker", "Content-Encoding",
+# "aesgcm", and "aesgcm128". %1$S is the ServiceWorker scope URL.
+PushMessageBadEncodingHeader=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘Content-Encoding‘ header must be ‘aesgcm‘. ‘aesgcm128‘ is allowed, but deprecated and will soon be removed.
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt
+# because the "dh" parameter is not valid base64url. Do not translate
+# "ServiceWorker", "dh", "Crypto-Key", and "base64url". %1$S is the
+# ServiceWorker scope URL.
+PushMessageBadSenderKey=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘dh‘ parameter in the ‘Crypto-Key‘ header must be the app server’s Diffie-Hellman public key, base64url-encoded (RFC 7515, Appendix C) and in “uncompressed” or “raw” form (65 bytes before encoding).
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt
+# because the "salt" parameter is not valid base64url. Do not translate
+# "ServiceWorker", "salt", "Encryption", and "base64url". %1$S is the
+# ServiceWorker scope URL.
+PushMessageBadSalt=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘salt‘ parameter in the ‘Encryption‘ header must be base64url-encoded (RFC 7515, Appendix C), and be at least 16 bytes before encoding.
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt
+# because the "rs" parameter is not a number, or is less than the pad size.
+# Do not translate "ServiceWorker", "rs", or "Encryption". %1$S is the
+# ServiceWorker scope URL. %2$S is the minimum value (1 for aesgcm128, 2 for
+# aesgcm).
+PushMessageBadRecordSize=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. The ‘rs‘ parameter of the ‘Encryption‘ header must be between %2$S and 2^36-31, or omitted entirely.
+# LOCALIZATION NOTE: This error is reported when a push message fails to decrypt
+# because an encrypted record is shorter than the pad size, the pad is larger
+# than the record, or any of the padding bytes are non-zero. Do not translate
+# "ServiceWorker". %1$S is the ServiceWorker scope URL. %2$S is the pad size
+# (1 for aesgcm128, 2 for aesgcm).
+PushMessageBadPaddingError=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. Each record in the encrypted message must be padded with a %2$S byte big-endian unsigned integer, followed by that number of zero-valued bytes.
+# LOCALIZATION NOTE: This error is reported when push message decryption fails
+# and no specific error info is available. Do not translate "ServiceWorker".
+# %1$S is the ServiceWorker scope URL.
+PushMessageBadCryptoError=The ServiceWorker for scope ‘%1$S’ failed to decrypt a push message. For help with encryption, please see https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API#Encryption
# LOCALIZATION NOTE: %1$S is the type of a DOM event. 'passive' is a literal parameter from the DOM spec.
PreventDefaultFromPassiveListenerWarning=Ignoring ‘preventDefault()’ call on event of type ‘%1$S’ from a listener registered as ‘passive’.
FileLastModifiedDateWarning=File.lastModifiedDate is deprecated. Use File.lastModified instead.
ChromeScriptedDOMParserWithoutPrincipal=Creating DOMParser without a principal is deprecated.
IIRFilterChannelCountChangeWarning=IIRFilterNode channel count changes may produce audio glitches.
BiquadFilterChannelCountChangeWarning=BiquadFilterNode channel count changes may produce audio glitches.
# LOCALIZATION NOTE: %1$S is the unanimatable paced property.
UnanimatablePacedProperty=Paced property ‘%1$S’ is not an animatable property.
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -2,20 +2,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const Cu = Components.utils;
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyGetter(this, 'gDOMBundle', () =>
+ Services.strings.createBundle('chrome://global/locale/dom/dom.properties'));
+
Cu.importGlobalProperties(['crypto']);
-this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
- 'getCryptoParams'];
+this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray'];
var UTF8 = new TextEncoder('utf-8');
// Legacy encryption scheme (draft-thomson-http-encryption-02).
var AESGCM128_ENCODING = 'aesgcm128';
var AESGCM128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
// New encryption scheme (draft-ietf-httpbis-encryption-encoding-01).
@@ -25,16 +30,69 @@ var AESGCM_ENCRYPT_INFO = UTF8.encode('C
var NONCE_INFO = UTF8.encode('Content-Encoding: nonce');
var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus
var P256DH_INFO = UTF8.encode('P-256\0');
var ECDH_KEY = { name: 'ECDH', namedCurve: 'P-256' };
var ECDSA_KEY = { name: 'ECDSA', namedCurve: 'P-256' };
// A default keyid with a name that won't conflict with a real keyid.
var DEFAULT_KEYID = '';
+/** Localized error property names. */
+
+// `Encryption` header missing or malformed.
+const BAD_ENCRYPTION_HEADER = 'PushMessageBadEncryptionHeader';
+// `Crypto-Key` or legacy `Encryption-Key` header missing.
+const BAD_CRYPTO_KEY_HEADER = 'PushMessageBadCryptoKeyHeader';
+const BAD_ENCRYPTION_KEY_HEADER = 'PushMessageBadEncryptionKeyHeader';
+// `Content-Encoding` header missing or contains unsupported encoding.
+const BAD_ENCODING_HEADER = 'PushMessageBadEncodingHeader';
+// `dh` parameter of `Crypto-Key` header missing or not base64url-encoded.
+const BAD_DH_PARAM = 'PushMessageBadSenderKey';
+// `salt` parameter of `Encryption` header missing or not base64url-encoded.
+const BAD_SALT_PARAM = 'PushMessageBadSalt';
+// `rs` parameter of `Encryption` header not a number or less than pad size.
+const BAD_RS_PARAM = 'PushMessageBadRecordSize';
+// Invalid or insufficient padding for encrypted chunk.
+const BAD_PADDING = 'PushMessageBadPaddingError';
+// Generic crypto error.
+const BAD_CRYPTO = 'PushMessageBadCryptoError';
+
+class CryptoError extends Error {
+ /**
+ * Creates an error object indicating an incoming push message could not be
+ * decrypted.
+ *
+ * @param {String} message A human-readable error message. This is only for
+ * internal module logging, and doesn't need to be localized.
+ * @param {String} property The localized property name from `dom.properties`.
+ * @param {String...} params Substitutions to insert into the localized
+ * string.
+ */
+ constructor(message, property, ...params) {
+ super(message);
+ this.isCryptoError = true;
+ this.property = property;
+ this.params = params;
+ }
+
+ /**
+ * Formats a localized string for reporting decryption errors to the Web
+ * Console.
+ *
+ * @param {String} scope The scope of the service worker receiving the
+ * message, prepended to any other substitutions in the string.
+ * @returns {String} The localized string.
+ */
+ format(scope) {
+ let params = [scope, ...this.params].map(String);
+ return gDOMBundle.formatStringFromName(this.property, params,
+ params.length);
+ }
+}
+
function getEncryptionKeyParams(encryptKeyField) {
if (!encryptKeyField) {
return null;
}
var params = encryptKeyField.split(',');
return params.reduce((m, p) => {
var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
if (pmap.keyid && pmap.dh) {
@@ -43,60 +101,94 @@ function getEncryptionKeyParams(encryptK
if (!m[DEFAULT_KEYID] && pmap.dh) {
m[DEFAULT_KEYID] = pmap.dh;
}
return m;
}, {});
}
function getEncryptionParams(encryptField) {
+ if (!encryptField) {
+ throw new CryptoError('Missing encryption header',
+ BAD_ENCRYPTION_HEADER);
+ }
var p = encryptField.split(',', 1)[0];
if (!p) {
- return null;
+ throw new CryptoError('Encryption header missing params',
+ BAD_ENCRYPTION_HEADER);
}
return p.split(';').reduce(parseHeaderFieldParams, {});
}
-this.getCryptoParams = function(headers) {
+function getCryptoParams(headers) {
if (!headers) {
return null;
}
var keymap;
var padSize;
+ if (!headers.encoding) {
+ throw new CryptoError('Missing Content-Encoding header',
+ BAD_ENCODING_HEADER);
+ }
if (headers.encoding == AESGCM_ENCODING) {
// aesgcm uses the Crypto-Key header, 2 bytes for the pad length, and an
// authentication secret.
// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-01
keymap = getEncryptionKeyParams(headers.crypto_key);
+ if (!keymap) {
+ throw new CryptoError('Missing Crypto-Key header',
+ BAD_CRYPTO_KEY_HEADER);
+ }
padSize = 2;
} else if (headers.encoding == AESGCM128_ENCODING) {
// aesgcm128 uses Encryption-Key, 1 byte for the pad length, and no secret.
// https://tools.ietf.org/html/draft-thomson-http-encryption-02
keymap = getEncryptionKeyParams(headers.encryption_key);
+ if (!keymap) {
+ throw new CryptoError('Missing Encryption-Key header',
+ BAD_ENCRYPTION_KEY_HEADER);
+ }
padSize = 1;
- }
- if (!keymap) {
- return null;
+ } else {
+ throw new CryptoError('Unsupported Content-Encoding: ' + headers.encoding,
+ BAD_ENCODING_HEADER);
}
var enc = getEncryptionParams(headers.encryption);
- if (!enc) {
- return null;
+ var dh = keymap[enc.keyid || DEFAULT_KEYID];
+ if (!dh) {
+ throw new CryptoError('Missing dh parameter', BAD_DH_PARAM);
}
- var dh = keymap[enc.keyid || DEFAULT_KEYID];
var salt = enc.salt;
- var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
-
- if (!dh || !salt || isNaN(rs) || (rs <= padSize)) {
- return null;
+ if (!salt) {
+ throw new CryptoError('Missing salt parameter', BAD_SALT_PARAM);
+ }
+ var rs = enc.rs ? parseInt(enc.rs, 10) : 4096;
+ if (isNaN(rs)) {
+ throw new CryptoError('rs parameter must be a number', BAD_RS_PARAM);
+ }
+ if (rs <= padSize) {
+ throw new CryptoError('rs parameter must be at least ' + padSize,
+ BAD_RS_PARAM, padSize);
}
return {dh, salt, rs, padSize};
}
+// Decodes an unpadded, base64url-encoded string.
+function base64URLDecode(string) {
+ try {
+ return ChromeUtils.base64URLDecode(string, {
+ // draft-ietf-httpbis-encryption-encoding-01 prohibits padding.
+ padding: 'reject',
+ });
+ } catch (ex) {}
+ return null;
+}
+
var parseHeaderFieldParams = (m, v) => {
var i = v.indexOf('=');
if (i >= 0) {
// A quoted string with internal quotes is invalid for all the possible
// values of this header field.
m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
.replace(/^"(.*)"$/, '$1');
}
@@ -145,26 +237,26 @@ function hkdf(salt, ikm) {
}
hkdf.prototype.extract = function(info, len) {
var input = concatArray([info, new Uint8Array([1])]);
return this.prkhPromise
.then(prkh => prkh.hash(input))
.then(h => {
if (h.byteLength < len) {
- throw new Error('Length is too long');
+ throw new CryptoError('HKDF length is too long', BAD_CRYPTO);
}
return h.slice(0, len);
});
};
/* generate a 96-bit nonce for use in GCM, 48-bits of which are populated */
function generateNonce(base, index) {
if (index >= Math.pow(2, 48)) {
- throw new Error('Error generating nonce - index is too large.');
+ throw new CryptoError('Nonce index is too large', BAD_CRYPTO);
}
var nonce = base.slice(0, 12);
nonce = new Uint8Array(nonce);
for (var i = 0; i < 6; ++i) {
nonce[nonce.byteLength - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
}
return nonce;
}
@@ -185,48 +277,89 @@ this.PushCrypto = {
return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
.then(cryptoKey =>
Promise.all([
crypto.subtle.exportKey('raw', cryptoKey.publicKey),
crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
]));
},
- decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
- aAuthenticationSecret, aPadSize) {
+ /**
+ * Decrypts a push message.
+ *
+ * @param {JsonWebKey} privateKey The ECDH private key of the subscription
+ * receiving the message, in JWK form.
+ * @param {BufferSource} publicKey The ECDH public key of the subscription
+ * receiving the message, in raw form.
+ * @param {BufferSource} authenticationSecret The 16-byte shared
+ * authentication secret of the subscription receiving the message.
+ * @param {Object} headers The encryption headers passed to `getCryptoParams`.
+ * @param {BufferSource} ciphertext The encrypted message data.
+ * @returns {Promise} Resolves with a `Uint8Array` containing the decrypted
+ * message data. Rejects with a `CryptoError` if decryption fails.
+ */
+ decrypt(privateKey, publicKey, authenticationSecret, headers, ciphertext) {
+ return Promise.resolve().then(_ => {
+ let cryptoParams = getCryptoParams(headers);
+ if (!cryptoParams) {
+ return null;
+ }
+ return this._decodeMsg(ciphertext, privateKey, publicKey,
+ cryptoParams.dh, cryptoParams.salt,
+ cryptoParams.rs, authenticationSecret,
+ cryptoParams.padSize);
+ }).catch(error => {
+ if (error.isCryptoError) {
+ throw error;
+ }
+ // Web Crypto returns an unhelpful "operation failed for an
+ // operation-specific reason" error if decryption fails. We don't have
+ // context about what went wrong, so we throw a generic error instead.
+ throw new CryptoError('Bad encryption', BAD_CRYPTO);
+ });
+ },
+
+ _decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
+ aAuthenticationSecret, aPadSize) {
if (aData.byteLength === 0) {
// Zero length messages will be passed as null.
- return Promise.resolve(null);
+ return null;
}
// The last chunk of data must be less than aRs, if it is not return an
// error.
if (aData.byteLength % (aRs + 16) === 0) {
- return Promise.reject(new Error('Data truncated'));
+ throw new CryptoError('Encrypted data truncated', BAD_CRYPTO);
}
- let senderKey = ChromeUtils.base64URLDecode(aSenderPublicKey, {
- // draft-ietf-httpbis-encryption-encoding-01 prohibits padding.
- padding: "reject",
- });
+ let senderKey = base64URLDecode(aSenderPublicKey);
+ if (!senderKey) {
+ throw new CryptoError('dh parameter is not base64url-encoded',
+ BAD_DH_PARAM);
+ }
+
+ let salt = base64URLDecode(aSalt);
+ if (!salt) {
+ throw new CryptoError('salt parameter is not base64url-encoded',
+ BAD_SALT_PARAM);
+ }
return Promise.all([
crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
false, ['deriveBits']),
crypto.subtle.importKey('jwk', aPrivateKey, ECDH_KEY,
false, ['deriveBits'])
])
.then(([appServerKey, subscriptionPrivateKey]) =>
crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey },
subscriptionPrivateKey, 256))
.then(ikm => this._deriveKeyAndNonce(aPadSize,
new Uint8Array(ikm),
- ChromeUtils.base64URLDecode(aSalt,
- { padding: "reject" }),
+ salt,
aPublicKey,
senderKey,
aAuthenticationSecret))
.then(r =>
// AEAD_AES_128_GCM expands ciphertext to be 16 octets longer.
Promise.all(chunkArray(aData, aRs + 16).map((slice, index) =>
this._decodeChunk(aPadSize, slice, index, r[1], r[0]))))
.then(r => concatArray(r));
@@ -293,29 +426,30 @@ this.PushCrypto = {
* @param {Number} padSize The size of the padding length prepended to each
* chunk. For aesgcm, the padding length is expressed as a 16-bit unsigned
* big endian integer. For aesgcm128, the padding is an 8-bit integer.
* @param {Uint8Array} decoded The decrypted, padded chunk.
* @returns {Uint8Array} The chunk with padding removed.
*/
_unpadChunk(padSize, decoded) {
if (padSize < 1 || padSize > 2) {
- throw new Error('Unsupported pad size');
+ throw new CryptoError('Unsupported pad size', BAD_CRYPTO);
}
if (decoded.length < padSize) {
- throw new Error('Decoded array is too short!');
+ throw new CryptoError('Decoded array is too short!', BAD_PADDING,
+ padSize);
}
var pad = decoded[0];
if (padSize == 2) {
pad = (pad << 8) | decoded[1];
}
if (pad > decoded.length) {
- throw new Error ('Padding is wrong!');
+ throw new CryptoError('Padding is wrong!', BAD_PADDING, padSize);
}
// All padded bytes must be zero except the first one.
for (var i = padSize; i <= pad; i++) {
if (decoded[i] !== 0) {
- throw new Error('Padding is wrong!');
+ throw new CryptoError('Padding is wrong!', BAD_PADDING, padSize);
}
}
return decoded.slice(pad + padSize);
},
};
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -14,16 +14,17 @@ Cu.import("resource://gre/modules/AppCon
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {
PushCrypto,
getCryptoParams,
+ CryptoError,
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
const CONNECTION_PROTOCOLS = (function() {
if ('android' != AppConstants.MOZ_WIDGET_TOOLKIT) {
const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
const {PushServiceHttp2} = Cu.import("resource://gre/modules/PushServiceHttp2.jsm");
return [PushServiceWebSocket, PushServiceHttp2];
@@ -32,19 +33,16 @@ const CONNECTION_PROTOCOLS = (function()
return [PushServiceAndroidGCM];
}
})();
XPCOMUtils.defineLazyServiceGetter(this, "gPushNotifier",
"@mozilla.org/push/Notifier;1",
"nsIPushNotifier");
-XPCOMUtils.defineLazyGetter(this, "gDOMBundle", () =>
- Services.strings.createBundle("chrome://global/locale/dom/dom.properties"));
-
this.EXPORTED_SYMBOLS = ["PushService"];
XPCOMUtils.defineLazyGetter(this, "console", () => {
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
return new ConsoleAPI({
maxLogLevelPref: "dom.push.loglevel",
prefix: "PushService",
});
@@ -866,59 +864,34 @@ this.PushService = {
}
gPushNotifier.notifySubscriptionModified(record.scope,
record.principal);
return record;
});
},
/**
- * Decrypts a message. Will resolve with null if cryptoParams is falsy.
- *
- * @param {PushRecord} record The receiving registration.
- * @param {Object} headers The encryption headers.
- * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
-
- * @returns {Promise} Resolves with the decrypted message.
- */
- _decryptMessage(record, headers, data) {
- return Promise.resolve().then(_ => {
- let cryptoParams = getCryptoParams(headers);
- if (!cryptoParams) {
- return null;
- }
- return PushCrypto.decodeMsg(
- data,
- record.p256dhPrivateKey,
- record.p256dhPublicKey,
- cryptoParams.dh,
- cryptoParams.salt,
- cryptoParams.rs,
- record.authenticationSecret,
- cryptoParams.padSize
- );
- });
- },
-
- /**
* Decrypts an incoming message and notifies the associated service worker.
*
* @param {PushRecord} record The receiving registration.
* @param {String} messageID The message ID.
* @param {Object} headers The encryption headers.
* @param {ArrayBuffer|Uint8Array} data The encrypted message data.
* @returns {Promise} Resolves with an ack status code.
*/
_decryptAndNotifyApp(record, messageID, headers, data) {
- return this._decryptMessage(record, headers, data)
+ return PushCrypto.decrypt(record.p256dhPrivateKey, record.p256dhPublicKey,
+ record.authenticationSecret, headers, data)
.then(
message => this._notifyApp(record, messageID, message),
error => {
- let message = gDOMBundle.formatStringFromName(
- "PushMessageDecryptionFailure", [record.scope, String(error)], 2);
+ console.warn("decryptAndNotifyApp: Error decrypting message",
+ record.scope, messageID, error);
+
+ let message = error.format(record.scope);
gPushNotifier.notifyError(record.scope, record.principal, message,
Ci.nsIScriptError.errorFlag);
return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
});
},
_updateQuota: function(keyID) {
console.debug("updateQuota()");
--- a/dom/push/test/xpcshell/test_crypto.js
+++ b/dom/push/test/xpcshell/test_crypto.js
@@ -5,19 +5,18 @@ const {
PushCrypto,
} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
function run_test() {
run_next_test();
}
add_task(function* test_crypto_getCryptoParams() {
- let testData = [
// These headers should parse correctly.
- {
+ let shouldParse = [{
desc: 'aesgcm with multiple keys',
headers: {
encoding: 'aesgcm',
crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI',
encryption: 'keyid=p256dh;salt=upk1yFkp1xI',
},
params: {
dh: 'Iy1Je2Kv11A',
@@ -72,63 +71,59 @@ add_task(function* test_crypto_getCrypto
encryption: 'keyid=v2; salt=khtpyXhpDKM',
},
params: {
dh: 'VA6wmY1IpiE',
salt: 'khtpyXhpDKM',
rs: 4096,
padSize: 1,
}
- },
+ }];
+ for (let test of shouldParse) {
+ let params = getCryptoParams(test.headers);
+ deepEqual(params, test.params, test.desc);
+ }
// These headers should be rejected.
- {
+ let shouldThrow = [{
desc: 'aesgcm128 with Crypto-Key',
headers: {
encoding: 'aesgcm128',
crypto_key: 'keyid=v2; dh=VA6wmY1IpiE',
encryption: 'keyid=v2; salt=F0Im7RtGgNY',
},
- params: null,
- },
- {
+ }, {
desc: 'Invalid encoding',
headers: {
encoding: 'nonexistent',
},
- params: null,
}, {
desc: 'Invalid record size',
headers: {
encoding: 'aesgcm',
crypto_key: 'dh=pbmv1QkcEDY',
encryption: 'dh=Esao8aTBfIk;rs=bad',
},
- params: null,
}, {
desc: 'Insufficiently large record size',
headers: {
encoding: 'aesgcm',
crypto_key: 'dh=fK0EXaw5IU8',
encryption: 'salt=orbLLmlbJfM;rs=1',
},
- params: null,
}, {
desc: 'aesgcm with Encryption-Key',
headers: {
encoding: 'aesgcm',
encryption_key: 'dh=FplK5KkvUF0',
encryption: 'salt=p6YHhFF3BQY',
},
- params: null,
}];
-
- for (let test of testData) {
- let params = getCryptoParams(test.headers);
- deepEqual(params, test.params, test.desc);
+ for (let test of shouldThrow) {
+ throws(() => getCryptoParams(test.headers), test.desc);
}
});
add_task(function* test_crypto_decodeMsg() {
let privateKey = {
crv: 'P-256',
d: '4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg',
ext: true,
@@ -140,106 +135,115 @@ add_task(function* test_crypto_decodeMsg
let publicKey = ChromeUtils.base64URLDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs', {
padding: "reject",
});
let expectedSuccesses = [{
desc: 'padSize = 2, rs = 24, pad = 0',
result: 'Some message',
data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
- senderPublicKey: 'BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
- salt: 'zCU18Rw3A5aB_Xi-vfixmA',
- rs: 24,
authSecret: 'aTDc6JebzR6eScy2oLo4RQ',
- padSize: 2,
+ headers: {
+ crypto_key: 'dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
+ encryption: 'salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24',
+ encoding: 'aesgcm',
+ },
}, {
desc: 'padSize = 2, rs = 8, pad = 16',
result: 'Yet another message',
data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg',
- senderPublicKey: 'BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
- salt: 'ZFhzj0S-n29g9P2p4-I7tA',
- rs: 8,
authSecret: '6plwZnSpVUbF7APDXus3UQ',
- padSize: 2,
+ headers: {
+ crypto_key: 'dh=BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
+ encryption: 'salt=ZFhzj0S-n29g9P2p4-I7tA; rs=8',
+ encoding: 'aesgcm',
+ },
}, {
desc: 'padSize = 1, rs = 4096, pad = 2',
result: 'aesgcm128 encrypted message',
data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE',
- senderPublicKey: 'BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
- salt: 'btxxUtclbmgcc30b9rT3Bg',
- rs: 4096,
- padSize: 1,
+ headers: {
+ encryption_key: 'dh=BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
+ encryption: 'salt=btxxUtclbmgcc30b9rT3Bg; rs=4096',
+ encoding: 'aesgcm128',
+ },
}, {
desc: 'padSize = 2, rs = 3, pad = 0',
result: 'Small record size',
data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM',
- senderPublicKey: 'BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
- salt: '5LIDBXbvkBvvb7ZdD-T4PQ',
- rs: 3,
authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ',
- padSize: 2,
+ headers: {
+ crypto_key: 'dh=BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
+ encryption: 'salt=5LIDBXbvkBvvb7ZdD-T4PQ; rs=3',
+ encoding: 'aesgcm',
+ },
}];
for (let test of expectedSuccesses) {
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
padding: "reject",
}) : null;
let data = ChromeUtils.base64URLDecode(test.data, {
padding: "reject",
});
- let result = yield PushCrypto.decodeMsg(data,
- privateKey, publicKey,
- test.senderPublicKey, test.salt,
- test.rs, authSecret, test.padSize);
+ let result = yield PushCrypto.decrypt(privateKey, publicKey, authSecret,
+ test.headers, data);
let decoder = new TextDecoder('utf-8');
equal(decoder.decode(new Uint8Array(result)), test.result, test.desc);
}
let expectedFailures = [{
desc: 'padSize = 1, rs = 4096, auth secret, pad = 8',
data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r',
- senderPublicKey: 'BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
- salt: 'aGBpoKklLtrLcAUCcCr7JQ',
- rs: 4096,
+ senderPublicKey: '',
authSecret: 'Sxb6u0gJIhGEogyLawjmCw',
- padSize: 1,
+ headers: {
+ crypto_key: 'dh=BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
+ encryption: 'salt=aGBpoKklLtrLcAUCcCr7JQ',
+ encoding: 'aesgcm128',
+ },
}, {
desc: 'Missing padding',
data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg',
- senderPublicKey: 'BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
- salt: 'Czx2i18rar8XWOXAVDnUuw',
- rs: 4096,
- padSize: 1,
+ headers: {
+ crypto_key: 'dh=BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
+ encryption: 'salt=Czx2i18rar8XWOXAVDnUuw',
+ encoding: 'aesgcm128',
+ },
}, {
desc: 'padSize > rs',
data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu',
- senderPublicKey: 'BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
- salt: 'NQVTKhB0rpL7ZzKkotTGlA',
- rs: 1,
- authSecret: '6plwZnSpVUbF7APDXus3UQ',
- padSize: 2,
+ headers: {
+ crypto_key: 'dh=BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
+ encryption: 'salt=NQVTKhB0rpL7ZzKkotTGlA; rs=1',
+ encoding: 'aesgcm',
+ },
}, {
desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret',
data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8',
- senderPublicKey: 'BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
- salt: 'c6JQl9eJ0VvwrUVCQDxY7Q',
- rs: 4096,
authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
- padSize: 2,
+ headers: {
+ crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+ encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q',
+ encoding: 'aesgcm',
+ },
}, {
desc: 'Truncated input',
data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
- rs: 25,
+ headers: {
+ crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+ encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q; rs=25',
+ encoding: 'aesgcm',
+ },
}];
for (let test of expectedFailures) {
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
padding: "reject",
}) : null;
let data = ChromeUtils.base64URLDecode(test.data, {
padding: "reject",
});
yield rejects(
- PushCrypto.decodeMsg(data, privateKey, publicKey,
- test.senderPublicKey, test.salt, test.rs,
- authSecret, test.padSize),
+ PushCrypto.decrypt(privateKey, publicKey, authSecret,
+ test.headers, data),
test.desc
);
}
});
--- a/mobile/android/components/FxAccountsPush.js
+++ b/mobile/android/components/FxAccountsPush.js
@@ -96,74 +96,64 @@ FxAccountsPush.prototype = {
}).catch(err => {
Log.e("Error during unsubscribe", err);
});
},
_decodePushMessage(data) {
Log.i("FxAccountsPush _decodePushMessage");
data = JSON.parse(data);
- let { message, cryptoParams } = this._messageAndCryptoParams(data);
+ let { headers, message } = this._messageAndHeaders(data);
return new Promise((resolve, reject) => {
PushService.getSubscription(FXA_PUSH_SCOPE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (!subscription) {
return reject(new Error("No subscription found"));
}
return resolve(subscription);
});
}).then(subscription => {
- if (!cryptoParams) {
- return new Uint8Array();
- }
- return PushCrypto.decodeMsg(
- message,
- subscription.p256dhPrivateKey,
- new Uint8Array(subscription.getKey("p256dh")),
- cryptoParams.dh,
- cryptoParams.salt,
- cryptoParams.rs,
- new Uint8Array(subscription.getKey("auth")),
- cryptoParams.padSize
- );
+ return PushCrypto.decrypt(subscription.p256dhPrivateKey,
+ new Uint8Array(subscription.getKey("p256dh")),
+ new Uint8Array(subscription.getKey("auth")),
+ headers, message);
})
- .then(decryptedMessage => {
- decryptedMessage = _decoder.decode(decryptedMessage);
+ .then(plaintext => {
+ let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
Messaging.sendRequestForResult({
type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
message: decryptedMessage
});
})
.catch(err => {
Log.d("Error while decoding incoming message : " + err);
});
},
// Copied from PushServiceAndroidGCM
- _messageAndCryptoParams(data) {
+ _messageAndHeaders(data) {
// Default is no data (and no encryption).
let message = null;
- let cryptoParams = null;
+ let headers = null;
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
let headers = {
encryption_key: data.enckey,
crypto_key: data.cryptokey,
encryption: data.enc,
encoding: data.con,
};
- cryptoParams = getCryptoParams(headers);
// Ciphertext is (urlsafe) Base 64 encoded.
message = ChromeUtils.base64URLDecode(data.message, {
// The Push server may append padding.
padding: "ignore",
});
}
- return { message, cryptoParams };
+ return { headers, message };
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
};
function urlsafeBase64Encode(key) {