tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

jwcrypto.sys.mjs (7414B)


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