123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- /* 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/. */
- this.EXPORTED_SYMBOLS = ["WeaveCrypto"];
- var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
- Cu.import("resource://gre/modules/XPCOMUtils.jsm");
- Cu.import("resource://gre/modules/Services.jsm");
- Cu.import("resource://services-common/async.js");
- Cu.importGlobalProperties(['crypto']);
- const CRYPT_ALGO = "AES-CBC";
- const CRYPT_ALGO_LENGTH = 256;
- const AES_CBC_IV_SIZE = 16;
- const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
- const UTF_LABEL = "utf-8";
- const KEY_DERIVATION_ALGO = "PBKDF2";
- const KEY_DERIVATION_HASHING_ALGO = "SHA-1";
- const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000.
- const DERIVED_KEY_ALGO = CRYPT_ALGO;
- this.WeaveCrypto = function WeaveCrypto() {
- this.init();
- };
- WeaveCrypto.prototype = {
- prefBranch : null,
- debug : true, // services.sync.log.cryptoDebug
- observer : {
- _self : null,
- QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
- Ci.nsISupportsWeakReference]),
- observe(subject, topic, data) {
- 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, false);
- this.observer._self = this;
- this.debug = this.prefBranch.getBoolPref("cryptoDebug", false);
- XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL));
- XPCOMUtils.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;
- },
- encrypt(clearTextUCS2, symmetricKey, iv) {
- this.log("encrypt() called");
- let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
- let encrypted = this._commonCrypt(clearTextBuffer, symmetricKey, iv, OPERATIONS.ENCRYPT);
- return this.encodeBase64(encrypted);
- },
- decrypt(cipherText, symmetricKey, iv) {
- this.log("decrypt() called");
- if (cipherText.length) {
- cipherText = atob(cipherText);
- }
- let cipherTextBuffer = this.byteCompressInts(cipherText);
- let decrypted = 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)
- */
- _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 "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes.";
- }
- let iv = this.byteCompressInts(ivStr);
- let symKey = this.importSymKey(symKeyStr, operation);
- let cryptMethod = (operation === OPERATIONS.ENCRYPT
- ? crypto.subtle.encrypt
- : crypto.subtle.decrypt)
- .bind(crypto.subtle);
- let algo = { name: CRYPT_ALGO, iv: iv };
- return Async.promiseSpinningly(
- cryptMethod(algo, symKey, data)
- .then(keyBytes => new Uint8Array(keyBytes))
- );
- },
- generateRandomKey() {
- this.log("generateRandomKey() called");
- let algo = {
- name: CRYPT_ALGO,
- length: CRYPT_ALGO_LENGTH
- };
- return Async.promiseSpinningly(
- crypto.subtle.generateKey(algo, true, [])
- .then(key => crypto.subtle.exportKey("raw", key))
- .then(keyBytes => {
- keyBytes = new Uint8Array(keyBytes);
- return this.encodeBase64(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: {},
- 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 "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"];
- return Async.promiseSpinningly(
- crypto.subtle.importKey("raw", symmetricKeyBuffer, algo, false, usages)
- .then(symKey => {
- 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);
- },
- /**
- * Returns the expanded data string for the derived key.
- */
- deriveKeyFromPassphrase(passphrase, saltStr, keyLength = 32) {
- this.log("deriveKeyFromPassphrase() called.");
- let keyData = this.makeUint8Array(passphrase, false);
- let salt = this.makeUint8Array(saltStr, true);
- let importAlgo = { name: KEY_DERIVATION_ALGO };
- let deriveAlgo = {
- name: KEY_DERIVATION_ALGO,
- salt: salt,
- iterations: KEY_DERIVATION_ITERATIONS,
- hash: { name: KEY_DERIVATION_HASHING_ALGO },
- };
- let derivedKeyType = {
- name: DERIVED_KEY_ALGO,
- length: keyLength * 8,
- };
- return Async.promiseSpinningly(
- crypto.subtle.importKey("raw", keyData, importAlgo, false, ["deriveKey"])
- .then(key => crypto.subtle.deriveKey(deriveAlgo, key, derivedKeyType, true, []))
- .then(derivedKey => crypto.subtle.exportKey("raw", derivedKey))
- .then(keyBytes => {
- keyBytes = new Uint8Array(keyBytes);
- return this.expandData(keyBytes);
- })
- );
- },
- };
|