Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
const CRYPT_ALGO = "AES-CBC";
const CRYPT_ALGO_LENGTH = 256;
const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"];
const AES_CBC_IV_SIZE = 16;
const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
const UTF_LABEL = "utf-8";
export function WeaveCrypto() {
this.init();
}
WeaveCrypto.prototype = {
prefBranch: null,
debug: true, // services.sync.log.cryptoDebug
observer: {
_self: null,
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
observe(subject, topic) {
let self = this._self;
self.log("Observed " + topic + " topic.");
if (topic == "nsPref:changed") {
self.debug = self.prefBranch.getBoolPref("cryptoDebug");
}
},
},
init() {
// Preferences. Add observer so we get notified of changes.
this.prefBranch = Services.prefs.getBranch("services.sync.log.");
this.prefBranch.addObserver("cryptoDebug", this.observer);
this.observer._self = this;
this.debug = this.prefBranch.getBoolPref("cryptoDebug", false);
ChromeUtils.defineLazyGetter(
this,
"encoder",
() => new TextEncoder(UTF_LABEL)
);
ChromeUtils.defineLazyGetter(
this,
"decoder",
() => new TextDecoder(UTF_LABEL, { fatal: true })
);
},
log(message) {
if (!this.debug) {
return;
}
dump("WeaveCrypto: " + message + "\n");
Services.console.logStringMessage("WeaveCrypto: " + message);
},
// /!\ Only use this for tests! /!\
_getCrypto() {
return crypto;
},
async encrypt(clearTextUCS2, symmetricKey, iv) {
this.log("encrypt() called");
let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
let encrypted = await this._commonCrypt(
clearTextBuffer,
symmetricKey,
iv,
OPERATIONS.ENCRYPT
);
return this.encodeBase64(encrypted);
},
async decrypt(cipherText, symmetricKey, iv) {
this.log("decrypt() called");
if (cipherText.length) {
cipherText = atob(cipherText);
}
let cipherTextBuffer = this.byteCompressInts(cipherText);
let decrypted = await this._commonCrypt(
cipherTextBuffer,
symmetricKey,
iv,
OPERATIONS.DECRYPT
);
return this.decoder.decode(decrypted);
},
/**
* _commonCrypt
*
* @args
* data: data to encrypt/decrypt (ArrayBuffer)
* symKeyStr: symmetric key (Base64 String)
* ivStr: initialization vector (Base64 String)
* operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT)
* @returns
* the encrypted/decrypted data (ArrayBuffer)
*/
async _commonCrypt(data, symKeyStr, ivStr, operation) {
this.log("_commonCrypt() called");
ivStr = atob(ivStr);
if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) {
throw new Error("Unsupported operation in _commonCrypt.");
}
// We never want an IV longer than the block size, which is 16 bytes
// for AES, neither do we want one smaller; throw in both cases.
if (ivStr.length !== AES_CBC_IV_SIZE) {
throw new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`);
}
let iv = this.byteCompressInts(ivStr);
let symKey = await this.importSymKey(symKeyStr, operation);
let cryptMethod = (
operation === OPERATIONS.ENCRYPT
? crypto.subtle.encrypt
: crypto.subtle.decrypt
).bind(crypto.subtle);
let algo = { name: CRYPT_ALGO, iv };
let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data);
return new Uint8Array(keyBytes);
},
async generateRandomKey() {
this.log("generateRandomKey() called");
let algo = {
name: CRYPT_ALGO,
length: CRYPT_ALGO_LENGTH,
};
let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES);
let keyBytes = await crypto.subtle.exportKey("raw", key);
return this.encodeBase64(new Uint8Array(keyBytes));
},
generateRandomIV() {
return this.generateRandomBytes(AES_CBC_IV_SIZE);
},
generateRandomBytes(byteCount) {
this.log("generateRandomBytes() called");
let randBytes = new Uint8Array(byteCount);
crypto.getRandomValues(randBytes);
return this.encodeBase64(randBytes);
},
//
// SymKey CryptoKey memoization.
//
// Memoize the import of symmetric keys. We do this by using the base64
// string itself as a key.
_encryptionSymKeyMemo: {},
_decryptionSymKeyMemo: {},
async importSymKey(encodedKeyString, operation) {
let memo;
// We use two separate memos for thoroughness: operation is an input to
// key import.
switch (operation) {
case OPERATIONS.ENCRYPT:
memo = this._encryptionSymKeyMemo;
break;
case OPERATIONS.DECRYPT:
memo = this._decryptionSymKeyMemo;
break;
default:
throw new Error("Unsupported operation in importSymKey.");
}
if (encodedKeyString in memo) {
return memo[encodedKeyString];
}
let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true);
let algo = { name: CRYPT_ALGO };
let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"];
let symKey = await crypto.subtle.importKey(
"raw",
symmetricKeyBuffer,
algo,
false,
usages
);
memo[encodedKeyString] = symKey;
return symKey;
},
//
// Utility functions
//
/**
* Returns an Uint8Array filled with a JS string,
* which means we only keep utf-16 characters from 0x00 to 0xFF.
*/
byteCompressInts(str) {
let arrayBuffer = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arrayBuffer[i] = str.charCodeAt(i) & 0xff;
}
return arrayBuffer;
},
expandData(data) {
let expanded = "";
for (let i = 0; i < data.length; i++) {
expanded += String.fromCharCode(data[i]);
}
return expanded;
},
encodeBase64(data) {
return btoa(this.expandData(data));
},
makeUint8Array(input, isEncoded) {
if (isEncoded) {
input = atob(input);
}
return this.byteCompressInts(input);
},
};