Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
307 lines
11 KiB
JavaScript
Executable file
307 lines
11 KiB
JavaScript
Executable file
/* 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';
|
|
|
|
/* global
|
|
crypto,
|
|
KeyDerivation,
|
|
StringConversion
|
|
*/
|
|
|
|
/* exported
|
|
FxSyncWebCrypto
|
|
*/
|
|
|
|
// WebCrypto-based client for Firefox Sync.
|
|
|
|
const FxSyncWebCrypto = Object.freeze((() => {
|
|
|
|
const HKDF_INFO_STR = 'identity.mozilla.com/picl/v1/oldsync';
|
|
|
|
const importKeyBundle = (aesKeyAB, hmacKeyAB) => {
|
|
const pAes = crypto.subtle.importKey('raw', aesKeyAB,
|
|
{ name: 'AES-CBC', length: 256 },
|
|
true, [ 'encrypt', 'decrypt' ]);
|
|
const pHmac = crypto.subtle.importKey('raw', hmacKeyAB,
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
true, [ 'sign', 'verify' ]);
|
|
return Promise.all([pAes, pHmac]).then(results => {
|
|
return Object.freeze({
|
|
aes: results[0],
|
|
hmac: results[1]
|
|
});
|
|
}, () => {
|
|
// err might be a TypeError if aesKeyAB or hmacKeyAB was no ArrayBuffer.
|
|
throw new Error('Could not import key bundle.');
|
|
});
|
|
};
|
|
|
|
const importKb = function(kBByteArray) {
|
|
// The number 64 here comes from
|
|
// (256 bits for AES + 256 bits for HMAC) / (8 bits per byte).
|
|
return KeyDerivation.hkdf(kBByteArray,
|
|
StringConversion.stringToUtf8Uint8Array(
|
|
HKDF_INFO_STR),
|
|
new Uint8Array(64), 64).then(output => {
|
|
const aesKeyAB = output.slice(0, 32).buffer;
|
|
const hmacKeyAB = output.slice(32).buffer;
|
|
return Object.freeze(importKeyBundle(aesKeyAB, hmacKeyAB));
|
|
});
|
|
};
|
|
|
|
const verifySyncKeys = function(
|
|
signedTextByteArray,
|
|
cryptoKeysHmacByteArray,
|
|
hmacKey) {
|
|
return crypto.subtle.verify({ name: 'HMAC', hash: 'AES-256' },
|
|
hmacKey, cryptoKeysHmacByteArray, signedTextByteArray);
|
|
};
|
|
|
|
const importSyncKeys = function(
|
|
cryptoKeysIVByteArray,
|
|
cryptoKeysCiphertextByteArray,
|
|
aesKey) {
|
|
return crypto.subtle.decrypt({ name: 'AES-CBC', iv: cryptoKeysIVByteArray },
|
|
aesKey,
|
|
cryptoKeysCiphertextByteArray)
|
|
.then(keyBundleAB => {
|
|
const cryptoKeysJSON = String.fromCharCode.apply(
|
|
null,
|
|
new Uint8Array(keyBundleAB));
|
|
var bulkKeyBundle;
|
|
try {
|
|
bulkKeyBundle = JSON.parse(cryptoKeysJSON);
|
|
return importKeyBundle(
|
|
StringConversion.base64StringToUint8Array(
|
|
bulkKeyBundle.default[0]),
|
|
StringConversion.base64StringToUint8Array(
|
|
bulkKeyBundle.default[1])).then(keyBundle => {
|
|
bulkKeyBundle.defaultAsKeyBundle = keyBundle;
|
|
return Object.freeze(bulkKeyBundle);
|
|
});
|
|
} catch(e) {
|
|
throw new Error('Deciphered crypto keys, but not JSON');
|
|
}
|
|
}, () => {
|
|
throw new Error(`Could not decrypt crypto keys using AES part of stretche\
|
|
d kB key`);
|
|
});
|
|
};
|
|
|
|
const importFromStrings = (obj) => {
|
|
const ret = {};
|
|
try {
|
|
ret.ciphertext = StringConversion.
|
|
base64StringToUint8Array(obj.ciphertext);
|
|
} catch (e) {
|
|
throw new Error('Could not parse ciphertext as a base64 string');
|
|
}
|
|
|
|
// Intentionally using StringConversion.stringToUtf8Uint8Array
|
|
// instead of StringConversion.base64StringToUint8Array on the ciphertext
|
|
// here - see https://github.com/mozilla/firefox-ios/blob/ \
|
|
// 1cce59c8eac282e151568f1204ffbbcc27349eff/Sync/KeyBundle.swift#L178.
|
|
ret.hmacSignedText = StringConversion
|
|
.stringToUtf8Uint8Array(obj.ciphertext);
|
|
|
|
try {
|
|
ret.IV = StringConversion.base64StringToUint8Array(obj.IV);
|
|
} catch (e) {
|
|
throw new Error('Could not parse IV as a base64 string');
|
|
}
|
|
try {
|
|
ret.hmacSignature = StringConversion.hexStringToUint8Array(obj.hmac);
|
|
} catch (e) {
|
|
throw new Error('Could not parse hmac as a hex string');
|
|
}
|
|
return Object.freeze(ret);
|
|
};
|
|
|
|
const getBulkKeyBundle = (kBByteArray, cryptoKeys) => {
|
|
return importKb(kBByteArray).then(mainSyncKey => {
|
|
return verifySyncKeys(cryptoKeys.hmacSignedText,
|
|
cryptoKeys.hmacSignature,
|
|
mainSyncKey.hmac).then(verified => {
|
|
if (verified) {
|
|
return importSyncKeys(cryptoKeys.IV,
|
|
cryptoKeys.ciphertext,
|
|
mainSyncKey.aes);
|
|
}
|
|
throw new Error(new Error(`SyncKeys hmac could not be verified with cur\
|
|
rent main key`));
|
|
});
|
|
});
|
|
};
|
|
|
|
/*
|
|
* setKeys - import kB and crypto/keys
|
|
*
|
|
* @param {String} kB Hex string with kB from FxA onepw protocol
|
|
* @param {Object} cryptoKeysStrings Object with:
|
|
* - ciphertext {String} A Base64 String containing an AES-CBC ciphertext
|
|
* - IV {String} A Base64 String containing the AES-CBC Initialization
|
|
* Vector
|
|
* - hmac {String} A Hex String containing the HMAC-SHA256 signature
|
|
* @returns {Promise} A promise that will resolve after importing kB,
|
|
* decrypting cryptoKeys, and setting this.bulkKeyBundle.
|
|
*/
|
|
const setKeys = function(kB, cryptoKeysStrings) {
|
|
var kBByteArray, cryptoKeys;
|
|
|
|
// Input checking.
|
|
try {
|
|
kBByteArray = StringConversion.hexStringToUint8Array(kB);
|
|
} catch (e) {
|
|
return Promise.reject(new Error('Could not parse kB as a hex string'));
|
|
}
|
|
|
|
try {
|
|
cryptoKeys = importFromStrings(cryptoKeysStrings);
|
|
} catch(err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
return getBulkKeyBundle(kBByteArray, cryptoKeys).then(bulkKeyBundle => {
|
|
this.bulkKeyBundle = bulkKeyBundle;
|
|
});
|
|
};
|
|
|
|
/*
|
|
* decrypt - verify and decrypt a Weave Basic Object
|
|
*
|
|
* @param {Object} payloadStrings Object with:
|
|
* - ciphertext {String} A Base64 String containing an AES-CBC ciphertext
|
|
* - IV {String} A Base64 String containing the AES-CBC Initialization
|
|
* Vector
|
|
* - hmac {String} A Hex String containing the HMAC-SHA256 signature
|
|
* @param {String} collectionName String The name of the Sync collection
|
|
* (currently ignored, see
|
|
* https://github.com/michielbdejong/fxsync-webcrypto/issues/19)
|
|
* @returns {Promise} A promise for the decrypted Weave Basic Object.
|
|
*/
|
|
const decrypt = function(payloadStrings, collectionName) {
|
|
if (typeof payloadStrings !== 'object') {
|
|
return Promise.reject(new Error('PayloadStrings is not an object'));
|
|
}
|
|
if (typeof collectionName !== 'string') {
|
|
return Promise.reject(new Error('collectionName is not a string'));
|
|
}
|
|
|
|
var keyBundle;
|
|
|
|
try {
|
|
keyBundle = this.bulkKeyBundle.defaultAsKeyBundle;
|
|
} catch(e) {
|
|
return Promise.reject(new Error(`No key bundle found for \
|
|
${collectionName} - did you call setKeys?`));
|
|
}
|
|
|
|
const payload = importFromStrings(payloadStrings);
|
|
return crypto.subtle.verify({ name: 'HMAC', hash: 'SHA-256' },
|
|
keyBundle.hmac, payload.hmacSignature,
|
|
payload.hmacSignedText).then(result => {
|
|
if (!result) {
|
|
throw new Error(`Record verification failed with current hmac key for \
|
|
${collectionName}`);
|
|
}
|
|
}).then(() => {
|
|
return crypto.subtle.decrypt({ name: 'AES-CBC', iv: payload.IV },
|
|
keyBundle.aes,
|
|
payload.ciphertext)
|
|
.then(recordArrayBuffer => {
|
|
var recordObj;
|
|
// recordArrayBuffer which is the cleartext payload from FxSync contains
|
|
// UTF-8 JSON.
|
|
const recordJSON = StringConversion.utf8Uint8ArrayToString(
|
|
new Uint8Array(recordArrayBuffer));
|
|
try {
|
|
recordObj = JSON.parse(recordJSON);
|
|
} catch(e) {
|
|
throw new Error('Deciphered record, but not JSON');
|
|
}
|
|
return recordObj;
|
|
}, () => {
|
|
throw new Error(`Could not decrypt record using AES part of key bundle \
|
|
for collection ${collectionName}`);
|
|
});
|
|
});
|
|
};
|
|
|
|
const encryptAndSign = (keyBundle, cleartext) => {
|
|
// Generate a random IV using the PRNG of the device.
|
|
var IV = new Uint8Array(16);
|
|
crypto.getRandomValues(IV);
|
|
return crypto.subtle.encrypt({ name: 'AES-CBC', iv: IV }, keyBundle.aes,
|
|
cleartext).then(ciphertext => {
|
|
const ciphertextB64 = StringConversion.arrayBufferToBase64String(
|
|
ciphertext);
|
|
return crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' },
|
|
keyBundle.hmac,
|
|
StringConversion.stringToUtf8Uint8Array(
|
|
ciphertextB64)).
|
|
then(hmac => {
|
|
return {
|
|
hmac: StringConversion.arrayBufferToHexString(hmac),
|
|
ciphertext: ciphertextB64,
|
|
IV: StringConversion.uint8ArrayToBase64String(IV)
|
|
};
|
|
});
|
|
});
|
|
};
|
|
|
|
/*
|
|
* encrypt - encrypt and sign a record
|
|
*
|
|
* @param {Object} record Object The data to be JSON-stringified and stored
|
|
* @param {String} collectionName String The name of the Sync collection
|
|
* (currently ignored, see
|
|
* https://github.com/michielbdejong/fxsync-webcrypto/issues/19)
|
|
* @returns {Promise} A promise for an object with ciphertext, IV, and hmac.
|
|
*/
|
|
const encrypt = function(record, collectionName) {
|
|
if (typeof record !== 'object') {
|
|
return Promise.reject(new Error('Record should be an object'));
|
|
}
|
|
if (typeof collectionName !== 'string') {
|
|
return Promise.reject(new Error('collectionName is not a string'));
|
|
}
|
|
|
|
var cleartextStr, keyBundle;
|
|
|
|
try {
|
|
cleartextStr = JSON.stringify(record);
|
|
} catch(e) {
|
|
return Promise.reject(new Error('Record cannot be JSON-stringified'));
|
|
}
|
|
const cleartext = StringConversion.stringToUtf8Uint8Array(cleartextStr);
|
|
try {
|
|
keyBundle = this.bulkKeyBundle.defaultAsKeyBundle;
|
|
} catch(e) {
|
|
return Promise.reject(new Error(`No key bundle found for \
|
|
${collectionName} - did you call setKeys?`));
|
|
}
|
|
return encryptAndSign(keyBundle, cleartext);
|
|
};
|
|
|
|
const FxSyncWebCrypto = function() {
|
|
// Basic check for presence of WebCrypto.
|
|
if (!crypto || !crypto.subtle) {
|
|
throw new Error('This environment does not support WebCrypto');
|
|
}
|
|
|
|
this.bulkKeyBundle = null;
|
|
};
|
|
|
|
FxSyncWebCrypto.prototype = Object.freeze({
|
|
setKeys: setKeys,
|
|
decrypt: decrypt,
|
|
encrypt: encrypt
|
|
});
|
|
|
|
// Expose this for unit test:
|
|
FxSyncWebCrypto.importKeyBundle = Object.freeze(importKeyBundle);
|
|
|
|
return FxSyncWebCrypto;
|
|
})());
|