WeaveCrypto.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. this.EXPORTED_SYMBOLS = ["WeaveCrypto"];
  5. var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
  6. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  7. Cu.import("resource://gre/modules/Services.jsm");
  8. Cu.import("resource://services-common/async.js");
  9. Cu.importGlobalProperties(['crypto']);
  10. const CRYPT_ALGO = "AES-CBC";
  11. const CRYPT_ALGO_LENGTH = 256;
  12. const AES_CBC_IV_SIZE = 16;
  13. const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
  14. const UTF_LABEL = "utf-8";
  15. const KEY_DERIVATION_ALGO = "PBKDF2";
  16. const KEY_DERIVATION_HASHING_ALGO = "SHA-1";
  17. const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000.
  18. const DERIVED_KEY_ALGO = CRYPT_ALGO;
  19. this.WeaveCrypto = function WeaveCrypto() {
  20. this.init();
  21. };
  22. WeaveCrypto.prototype = {
  23. prefBranch : null,
  24. debug : true, // services.sync.log.cryptoDebug
  25. observer : {
  26. _self : null,
  27. QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
  28. Ci.nsISupportsWeakReference]),
  29. observe(subject, topic, data) {
  30. let self = this._self;
  31. self.log("Observed " + topic + " topic.");
  32. if (topic == "nsPref:changed") {
  33. self.debug = self.prefBranch.getBoolPref("cryptoDebug");
  34. }
  35. }
  36. },
  37. init() {
  38. // Preferences. Add observer so we get notified of changes.
  39. this.prefBranch = Services.prefs.getBranch("services.sync.log.");
  40. this.prefBranch.addObserver("cryptoDebug", this.observer, false);
  41. this.observer._self = this;
  42. this.debug = this.prefBranch.getBoolPref("cryptoDebug", false);
  43. XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL));
  44. XPCOMUtils.defineLazyGetter(this, 'decoder', () => new TextDecoder(UTF_LABEL, { fatal: true }));
  45. },
  46. log(message) {
  47. if (!this.debug) {
  48. return;
  49. }
  50. dump("WeaveCrypto: " + message + "\n");
  51. Services.console.logStringMessage("WeaveCrypto: " + message);
  52. },
  53. // /!\ Only use this for tests! /!\
  54. _getCrypto() {
  55. return crypto;
  56. },
  57. encrypt(clearTextUCS2, symmetricKey, iv) {
  58. this.log("encrypt() called");
  59. let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
  60. let encrypted = this._commonCrypt(clearTextBuffer, symmetricKey, iv, OPERATIONS.ENCRYPT);
  61. return this.encodeBase64(encrypted);
  62. },
  63. decrypt(cipherText, symmetricKey, iv) {
  64. this.log("decrypt() called");
  65. if (cipherText.length) {
  66. cipherText = atob(cipherText);
  67. }
  68. let cipherTextBuffer = this.byteCompressInts(cipherText);
  69. let decrypted = this._commonCrypt(cipherTextBuffer, symmetricKey, iv, OPERATIONS.DECRYPT);
  70. return this.decoder.decode(decrypted);
  71. },
  72. /**
  73. * _commonCrypt
  74. *
  75. * @args
  76. * data: data to encrypt/decrypt (ArrayBuffer)
  77. * symKeyStr: symmetric key (Base64 String)
  78. * ivStr: initialization vector (Base64 String)
  79. * operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT)
  80. * @returns
  81. * the encrypted/decrypted data (ArrayBuffer)
  82. */
  83. _commonCrypt(data, symKeyStr, ivStr, operation) {
  84. this.log("_commonCrypt() called");
  85. ivStr = atob(ivStr);
  86. if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) {
  87. throw new Error("Unsupported operation in _commonCrypt.");
  88. }
  89. // We never want an IV longer than the block size, which is 16 bytes
  90. // for AES, neither do we want one smaller; throw in both cases.
  91. if (ivStr.length !== AES_CBC_IV_SIZE) {
  92. throw "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes.";
  93. }
  94. let iv = this.byteCompressInts(ivStr);
  95. let symKey = this.importSymKey(symKeyStr, operation);
  96. let cryptMethod = (operation === OPERATIONS.ENCRYPT
  97. ? crypto.subtle.encrypt
  98. : crypto.subtle.decrypt)
  99. .bind(crypto.subtle);
  100. let algo = { name: CRYPT_ALGO, iv: iv };
  101. return Async.promiseSpinningly(
  102. cryptMethod(algo, symKey, data)
  103. .then(keyBytes => new Uint8Array(keyBytes))
  104. );
  105. },
  106. generateRandomKey() {
  107. this.log("generateRandomKey() called");
  108. let algo = {
  109. name: CRYPT_ALGO,
  110. length: CRYPT_ALGO_LENGTH
  111. };
  112. return Async.promiseSpinningly(
  113. crypto.subtle.generateKey(algo, true, [])
  114. .then(key => crypto.subtle.exportKey("raw", key))
  115. .then(keyBytes => {
  116. keyBytes = new Uint8Array(keyBytes);
  117. return this.encodeBase64(keyBytes);
  118. })
  119. );
  120. },
  121. generateRandomIV() {
  122. return this.generateRandomBytes(AES_CBC_IV_SIZE);
  123. },
  124. generateRandomBytes(byteCount) {
  125. this.log("generateRandomBytes() called");
  126. let randBytes = new Uint8Array(byteCount);
  127. crypto.getRandomValues(randBytes);
  128. return this.encodeBase64(randBytes);
  129. },
  130. //
  131. // SymKey CryptoKey memoization.
  132. //
  133. // Memoize the import of symmetric keys. We do this by using the base64
  134. // string itself as a key.
  135. _encryptionSymKeyMemo: {},
  136. _decryptionSymKeyMemo: {},
  137. importSymKey(encodedKeyString, operation) {
  138. let memo;
  139. // We use two separate memos for thoroughness: operation is an input to
  140. // key import.
  141. switch (operation) {
  142. case OPERATIONS.ENCRYPT:
  143. memo = this._encryptionSymKeyMemo;
  144. break;
  145. case OPERATIONS.DECRYPT:
  146. memo = this._decryptionSymKeyMemo;
  147. break;
  148. default:
  149. throw "Unsupported operation in importSymKey.";
  150. }
  151. if (encodedKeyString in memo)
  152. return memo[encodedKeyString];
  153. let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true);
  154. let algo = { name: CRYPT_ALGO };
  155. let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"];
  156. return Async.promiseSpinningly(
  157. crypto.subtle.importKey("raw", symmetricKeyBuffer, algo, false, usages)
  158. .then(symKey => {
  159. memo[encodedKeyString] = symKey;
  160. return symKey;
  161. })
  162. );
  163. },
  164. //
  165. // Utility functions
  166. //
  167. /**
  168. * Returns an Uint8Array filled with a JS string,
  169. * which means we only keep utf-16 characters from 0x00 to 0xFF.
  170. */
  171. byteCompressInts(str) {
  172. let arrayBuffer = new Uint8Array(str.length);
  173. for (let i = 0; i < str.length; i++) {
  174. arrayBuffer[i] = str.charCodeAt(i) & 0xFF;
  175. }
  176. return arrayBuffer;
  177. },
  178. expandData(data) {
  179. let expanded = "";
  180. for (let i = 0; i < data.length; i++) {
  181. expanded += String.fromCharCode(data[i]);
  182. }
  183. return expanded;
  184. },
  185. encodeBase64(data) {
  186. return btoa(this.expandData(data));
  187. },
  188. makeUint8Array(input, isEncoded) {
  189. if (isEncoded) {
  190. input = atob(input);
  191. }
  192. return this.byteCompressInts(input);
  193. },
  194. /**
  195. * Returns the expanded data string for the derived key.
  196. */
  197. deriveKeyFromPassphrase(passphrase, saltStr, keyLength = 32) {
  198. this.log("deriveKeyFromPassphrase() called.");
  199. let keyData = this.makeUint8Array(passphrase, false);
  200. let salt = this.makeUint8Array(saltStr, true);
  201. let importAlgo = { name: KEY_DERIVATION_ALGO };
  202. let deriveAlgo = {
  203. name: KEY_DERIVATION_ALGO,
  204. salt: salt,
  205. iterations: KEY_DERIVATION_ITERATIONS,
  206. hash: { name: KEY_DERIVATION_HASHING_ALGO },
  207. };
  208. let derivedKeyType = {
  209. name: DERIVED_KEY_ALGO,
  210. length: keyLength * 8,
  211. };
  212. return Async.promiseSpinningly(
  213. crypto.subtle.importKey("raw", keyData, importAlgo, false, ["deriveKey"])
  214. .then(key => crypto.subtle.deriveKey(deriveAlgo, key, derivedKeyType, true, []))
  215. .then(derivedKey => crypto.subtle.exportKey("raw", derivedKey))
  216. .then(keyBytes => {
  217. keyBytes = new Uint8Array(keyBytes);
  218. return this.expandData(keyBytes);
  219. })
  220. );
  221. },
  222. };