WeaveCrypto.sys.mjs (6357B)
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 5 const CRYPT_ALGO = "AES-CBC"; 6 const CRYPT_ALGO_LENGTH = 256; 7 const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"]; 8 const AES_CBC_IV_SIZE = 16; 9 const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 }; 10 const UTF_LABEL = "utf-8"; 11 12 export function WeaveCrypto() { 13 this.init(); 14 } 15 16 WeaveCrypto.prototype = { 17 prefBranch: null, 18 debug: true, // services.sync.log.cryptoDebug 19 20 observer: { 21 _self: null, 22 23 QueryInterface: ChromeUtils.generateQI([ 24 "nsIObserver", 25 "nsISupportsWeakReference", 26 ]), 27 28 observe(subject, topic) { 29 let self = this._self; 30 self.log("Observed " + topic + " topic."); 31 if (topic == "nsPref:changed") { 32 self.debug = self.prefBranch.getBoolPref("cryptoDebug"); 33 } 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); 41 this.observer._self = this; 42 this.debug = this.prefBranch.getBoolPref("cryptoDebug", false); 43 ChromeUtils.defineLazyGetter( 44 this, 45 "encoder", 46 () => new TextEncoder(UTF_LABEL) 47 ); 48 ChromeUtils.defineLazyGetter( 49 this, 50 "decoder", 51 () => new TextDecoder(UTF_LABEL, { fatal: true }) 52 ); 53 }, 54 55 log(message) { 56 if (!this.debug) { 57 return; 58 } 59 dump("WeaveCrypto: " + message + "\n"); 60 Services.console.logStringMessage("WeaveCrypto: " + message); 61 }, 62 63 // /!\ Only use this for tests! /!\ 64 _getCrypto() { 65 return crypto; 66 }, 67 68 async encrypt(clearTextUCS2, symmetricKey, iv) { 69 this.log("encrypt() called"); 70 let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer; 71 let encrypted = await this._commonCrypt( 72 clearTextBuffer, 73 symmetricKey, 74 iv, 75 OPERATIONS.ENCRYPT 76 ); 77 return this.encodeBase64(encrypted); 78 }, 79 80 async decrypt(cipherText, symmetricKey, iv) { 81 this.log("decrypt() called"); 82 if (cipherText.length) { 83 cipherText = atob(cipherText); 84 } 85 let cipherTextBuffer = this.byteCompressInts(cipherText); 86 let decrypted = await this._commonCrypt( 87 cipherTextBuffer, 88 symmetricKey, 89 iv, 90 OPERATIONS.DECRYPT 91 ); 92 return this.decoder.decode(decrypted); 93 }, 94 95 /** 96 * _commonCrypt 97 * 98 * @param {ArrayBuffer} data - data to encrypt/decrypt. 99 * @param {string} symKeyStr - symmetric key (Base64 String). 100 * @param {string} ivStr - initialization vector (Base64 String). 101 * @param {number} operation - operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT) 102 * @returns {ArrayBuffer} 103 * The encrypted/decrypted data. 104 */ 105 async _commonCrypt(data, symKeyStr, ivStr, operation) { 106 this.log("_commonCrypt() called"); 107 ivStr = atob(ivStr); 108 109 if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) { 110 throw new Error("Unsupported operation in _commonCrypt."); 111 } 112 // We never want an IV longer than the block size, which is 16 bytes 113 // for AES, neither do we want one smaller; throw in both cases. 114 if (ivStr.length !== AES_CBC_IV_SIZE) { 115 throw new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`); 116 } 117 118 let iv = this.byteCompressInts(ivStr); 119 let symKey = await this.importSymKey(symKeyStr, operation); 120 let cryptMethod = ( 121 operation === OPERATIONS.ENCRYPT 122 ? crypto.subtle.encrypt 123 : crypto.subtle.decrypt 124 ).bind(crypto.subtle); 125 let algo = { name: CRYPT_ALGO, iv }; 126 127 let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data); 128 return new Uint8Array(keyBytes); 129 }, 130 131 async generateRandomKey() { 132 this.log("generateRandomKey() called"); 133 let algo = { 134 name: CRYPT_ALGO, 135 length: CRYPT_ALGO_LENGTH, 136 }; 137 let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES); 138 let keyBytes = await crypto.subtle.exportKey("raw", key); 139 return this.encodeBase64(new Uint8Array(keyBytes)); 140 }, 141 142 generateRandomIV() { 143 return this.generateRandomBytes(AES_CBC_IV_SIZE); 144 }, 145 146 generateRandomBytes(byteCount) { 147 this.log("generateRandomBytes() called"); 148 149 let randBytes = new Uint8Array(byteCount); 150 crypto.getRandomValues(randBytes); 151 152 return this.encodeBase64(randBytes); 153 }, 154 155 // 156 // SymKey CryptoKey memoization. 157 // 158 159 // Memoize the import of symmetric keys. We do this by using the base64 160 // string itself as a key. 161 _encryptionSymKeyMemo: {}, 162 _decryptionSymKeyMemo: {}, 163 async importSymKey(encodedKeyString, operation) { 164 let memo; 165 166 // We use two separate memos for thoroughness: operation is an input to 167 // key import. 168 switch (operation) { 169 case OPERATIONS.ENCRYPT: 170 memo = this._encryptionSymKeyMemo; 171 break; 172 case OPERATIONS.DECRYPT: 173 memo = this._decryptionSymKeyMemo; 174 break; 175 default: 176 throw new Error("Unsupported operation in importSymKey."); 177 } 178 179 if (encodedKeyString in memo) { 180 return memo[encodedKeyString]; 181 } 182 183 let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true); 184 let algo = { name: CRYPT_ALGO }; 185 let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"]; 186 let symKey = await crypto.subtle.importKey( 187 "raw", 188 symmetricKeyBuffer, 189 algo, 190 false, 191 usages 192 ); 193 memo[encodedKeyString] = symKey; 194 return symKey; 195 }, 196 197 // 198 // Utility functions 199 // 200 201 /** 202 * Returns an Uint8Array filled with a JS string, 203 * which means we only keep utf-16 characters from 0x00 to 0xFF. 204 */ 205 byteCompressInts(str) { 206 let arrayBuffer = new Uint8Array(str.length); 207 for (let i = 0; i < str.length; i++) { 208 arrayBuffer[i] = str.charCodeAt(i) & 0xff; 209 } 210 return arrayBuffer; 211 }, 212 213 expandData(data) { 214 let expanded = ""; 215 for (let i = 0; i < data.length; i++) { 216 expanded += String.fromCharCode(data[i]); 217 } 218 return expanded; 219 }, 220 221 encodeBase64(data) { 222 return btoa(this.expandData(data)); 223 }, 224 225 makeUint8Array(input, isEncoded) { 226 if (isEncoded) { 227 input = atob(input); 228 } 229 return this.byteCompressInts(input); 230 }, 231 };