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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7const { XPCOMUtils } = ChromeUtils.import( 8 "resource://gre/modules/XPCOMUtils.jsm" 9); 10 11XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]); 12 13const EXPORTED_SYMBOLS = ["jwcrypto"]; 14 15const ECDH_PARAMS = { 16 name: "ECDH", 17 namedCurve: "P-256", 18}; 19const AES_PARAMS = { 20 name: "AES-GCM", 21 length: 256, 22}; 23const AES_TAG_LEN = 128; 24const AES_GCM_IV_SIZE = 12; 25const UTF8_ENCODER = new TextEncoder(); 26const UTF8_DECODER = new TextDecoder(); 27 28class JWCrypto { 29 /** 30 * Encrypts the given data into a JWE using AES-256-GCM content encryption. 31 * 32 * This function implements a very small subset of the JWE encryption standard 33 * from https://tools.ietf.org/html/rfc7516. The only supported content encryption 34 * algorithm is enc="A256GCM" [1] and the only supported key encryption algorithm 35 * is alg="ECDH-ES" [2]. 36 * 37 * @param {Object} key Peer Public JWK. 38 * @param {ArrayBuffer} data 39 * 40 * [1] https://tools.ietf.org/html/rfc7518#section-5.3 41 * [2] https://tools.ietf.org/html/rfc7518#section-4.6 42 * 43 * @returns {Promise<String>} 44 */ 45 async generateJWE(key, data) { 46 // Generate an ephemeral key to use just for this encryption. 47 // The public component gets embedded in the JWE header. 48 const epk = await crypto.subtle.generateKey(ECDH_PARAMS, true, [ 49 "deriveKey", 50 ]); 51 const ownPublicJWK = await crypto.subtle.exportKey("jwk", epk.publicKey); 52 // Remove properties added by our WebCrypto implementation but that aren't typically 53 // used with JWE in the wild. This saves space in the resulting JWE, and makes it easier 54 // to re-import the resulting JWK. 55 delete ownPublicJWK.key_ops; 56 delete ownPublicJWK.ext; 57 let header = { alg: "ECDH-ES", enc: "A256GCM", epk: ownPublicJWK }; 58 // Import the peer's public key. 59 const peerPublicKey = await crypto.subtle.importKey( 60 "jwk", 61 key, 62 ECDH_PARAMS, 63 false, 64 ["deriveKey"] 65 ); 66 if (key.hasOwnProperty("kid")) { 67 header.kid = key.kid; 68 } 69 // Do ECDH agreement to get the content encryption key. 70 const contentKey = await deriveECDHSharedAESKey( 71 epk.privateKey, 72 peerPublicKey, 73 ["encrypt"] 74 ); 75 // Encrypt with AES-GCM using the generated key. 76 // Note that the IV is generated randomly, which *in general* is not safe to do with AES-GCM because 77 // it's too short to guarantee uniqueness. But we know that the AES-GCM key itself is unique and will 78 // only be used for this single encryption, making a random IV safe to use for this particular use-case. 79 let iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_SIZE)); 80 // Yes, additionalData is the byte representation of the base64 representation of the stringified header. 81 const additionalData = UTF8_ENCODER.encode( 82 ChromeUtils.base64URLEncode(UTF8_ENCODER.encode(JSON.stringify(header)), { 83 pad: false, 84 }) 85 ); 86 const encrypted = await crypto.subtle.encrypt( 87 { 88 name: "AES-GCM", 89 iv, 90 additionalData, 91 tagLength: AES_TAG_LEN, 92 }, 93 contentKey, 94 data 95 ); 96 // JWE needs the authentication tag as a separate string. 97 const tagIdx = encrypted.byteLength - ((AES_TAG_LEN + 7) >> 3); 98 let ciphertext = encrypted.slice(0, tagIdx); 99 let tag = encrypted.slice(tagIdx); 100 // JWE serialization in compact format. 101 header = UTF8_ENCODER.encode(JSON.stringify(header)); 102 header = ChromeUtils.base64URLEncode(header, { pad: false }); 103 tag = ChromeUtils.base64URLEncode(tag, { pad: false }); 104 ciphertext = ChromeUtils.base64URLEncode(ciphertext, { pad: false }); 105 iv = ChromeUtils.base64URLEncode(iv, { pad: false }); 106 return `${header}..${iv}.${ciphertext}.${tag}`; // No CEK 107 } 108 109 /** 110 * Decrypts the given JWE using AES-256-GCM content encryption into a byte array. 111 * This function does the opposite of `JWCrypto.generateJWE`. 112 * The only supported content encryption algorithm is enc="A256GCM" [1] 113 * and the only supported key encryption algorithm is alg="ECDH-ES" [2]. 114 * 115 * @param {"ECDH-ES"} algorithm 116 * @param {CryptoKey} key Local private key 117 * 118 * [1] https://tools.ietf.org/html/rfc7518#section-5.3 119 * [2] https://tools.ietf.org/html/rfc7518#section-4.6 120 * 121 * @returns {Promise<Uint8Array>} 122 */ 123 async decryptJWE(jwe, key) { 124 let [header, cek, iv, ciphertext, authTag] = jwe.split("."); 125 const additionalData = UTF8_ENCODER.encode(header); 126 header = JSON.parse( 127 UTF8_DECODER.decode( 128 ChromeUtils.base64URLDecode(header, { padding: "reject" }) 129 ) 130 ); 131 if ( 132 cek.length > 0 || 133 header.enc !== "A256GCM" || 134 header.alg !== "ECDH-ES" 135 ) { 136 throw new Error("Unknown algorithm."); 137 } 138 if ("apu" in header || "apv" in header) { 139 throw new Error("apu and apv header values are not supported."); 140 } 141 const peerPublicKey = await crypto.subtle.importKey( 142 "jwk", 143 header.epk, 144 ECDH_PARAMS, 145 false, 146 ["deriveKey"] 147 ); 148 // Do ECDH agreement to get the content encryption key. 149 const contentKey = await deriveECDHSharedAESKey(key, peerPublicKey, [ 150 "decrypt", 151 ]); 152 iv = ChromeUtils.base64URLDecode(iv, { padding: "reject" }); 153 ciphertext = new Uint8Array( 154 ChromeUtils.base64URLDecode(ciphertext, { padding: "reject" }) 155 ); 156 authTag = new Uint8Array( 157 ChromeUtils.base64URLDecode(authTag, { padding: "reject" }) 158 ); 159 const bundle = new Uint8Array([...ciphertext, ...authTag]); 160 161 const decrypted = await crypto.subtle.decrypt( 162 { 163 name: "AES-GCM", 164 iv, 165 tagLength: AES_TAG_LEN, 166 additionalData, 167 }, 168 contentKey, 169 bundle 170 ); 171 return new Uint8Array(decrypted); 172 } 173} 174 175/** 176 * Do an ECDH agreement between a public and private key, 177 * returning the derived encryption key as specced by 178 * JWA RFC. 179 * The raw ECDH secret is derived into a key using 180 * Concat KDF, as defined in Section 5.8.1 of [NIST.800-56A]. 181 * @param {CryptoKey} privateKey 182 * @param {CryptoKey} publicKey 183 * @param {String[]} keyUsages See `SubtleCrypto.deriveKey` 5th paramater documentation. 184 * @returns {Promise<CryptoKey>} 185 */ 186async function deriveECDHSharedAESKey(privateKey, publicKey, keyUsages) { 187 const params = { ...ECDH_PARAMS, ...{ public: publicKey } }; 188 const sharedKey = await crypto.subtle.deriveKey( 189 params, 190 privateKey, 191 AES_PARAMS, 192 true, 193 keyUsages 194 ); 195 // This is the NIST Concat KDF specialized to a specific set of parameters, 196 // which basically turn it into a single application of SHA256. 197 // The details are from the JWA RFC. 198 let sharedKeyBytes = await crypto.subtle.exportKey("raw", sharedKey); 199 sharedKeyBytes = new Uint8Array(sharedKeyBytes); 200 const info = [ 201 "\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier 202 "\x00\x00\x00\x00", // empty PartyUInfo 203 "\x00\x00\x00\x00", // empty PartyVInfo 204 "\x00\x00\x01\x00", // keylen == 256 205 ].join(""); 206 const pkcs = `\x00\x00\x00\x01${String.fromCharCode.apply( 207 null, 208 sharedKeyBytes 209 )}${info}`; 210 const pkcsBuf = Uint8Array.from( 211 Array.prototype.map.call(pkcs, c => c.charCodeAt(0)) 212 ); 213 const derivedKeyBytes = await crypto.subtle.digest( 214 { 215 name: "SHA-256", 216 }, 217 pkcsBuf 218 ); 219 return crypto.subtle.importKey( 220 "raw", 221 derivedKeyBytes, 222 AES_PARAMS, 223 false, 224 keyUsages 225 ); 226} 227 228const jwcrypto = new JWCrypto(); 229