tor-browser

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

ece.js (4909B)


      1 async function HKDF({ salt, ikm, info, length }) {
      2  return await crypto.subtle.deriveBits(
      3    { name: "HKDF", hash: "SHA-256", salt, info },
      4    await crypto.subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
      5      "deriveBits",
      6    ]),
      7    length * 8
      8  );
      9 }
     10 
     11 // https://datatracker.ietf.org/doc/html/rfc8188#section-2.2
     12 // https://datatracker.ietf.org/doc/html/rfc8188#section-2.3
     13 async function deriveKeyAndNonce(header) {
     14  const { salt } = header;
     15  const ikm = await getInputKeyingMaterial(header);
     16 
     17  // cek_info = "Content-Encoding: aes128gcm" || 0x00
     18  const cekInfo = new TextEncoder().encode("Content-Encoding: aes128gcm\0");
     19  // nonce_info = "Content-Encoding: nonce" || 0x00
     20  const nonceInfo = new TextEncoder().encode("Content-Encoding: nonce\0");
     21 
     22  // (The XOR SEQ is skipped as we only create single record here, thus becoming noop)
     23  return {
     24    // the length (L) parameter to HKDF is 16
     25    key: await HKDF({ salt, ikm, info: cekInfo, length: 16 }),
     26    // The length (L) parameter is 12 octets
     27    nonce: await HKDF({ salt, ikm, info: nonceInfo, length: 12 }),
     28  };
     29 }
     30 
     31 // https://datatracker.ietf.org/doc/html/rfc8291#section-3.3
     32 // https://datatracker.ietf.org/doc/html/rfc8291#section-3.4
     33 async function getInputKeyingMaterial(header) {
     34  // IKM:  the shared secret derived using ECDH
     35  // ecdh_secret = ECDH(as_private, ua_public)
     36  const ikm = await crypto.subtle.deriveBits(
     37    {
     38      name: "ECDH",
     39      public: await crypto.subtle.importKey(
     40        "raw",
     41        header.userAgentPublicKey,
     42        { name: "ECDH", namedCurve: "P-256" },
     43        true,
     44        []
     45      ),
     46    },
     47    header.appServer.privateKey,
     48    256
     49  );
     50  // key_info = "WebPush: info" || 0x00 || ua_public || as_public
     51  const keyInfo = new Uint8Array([
     52    ...new TextEncoder().encode("WebPush: info\0"),
     53    ...header.userAgentPublicKey,
     54    ...header.appServer.publicKey,
     55  ])
     56  return await HKDF({ salt: header.authSecret, ikm, info: keyInfo, length: 32 });
     57 }
     58 
     59 // https://datatracker.ietf.org/doc/html/rfc8188#section-2
     60 async function encryptRecord(key, nonce, data) {
     61  // add a delimiter octet (0x01 or 0x02)
     62  // The last record uses a padding delimiter octet set to the value 2
     63  //
     64  // (This implementation only creates a single record, thus always 2,
     65  // per https://datatracker.ietf.org/doc/html/rfc8291/#section-4:
     66  // An application server MUST encrypt a push message with a single
     67  // record.)
     68  const padded = new Uint8Array([...data, 2]);
     69 
     70  // encrypt with AEAD_AES_128_GCM
     71  return await crypto.subtle.encrypt(
     72    { name: "AES-GCM", iv: nonce, tagLength: 128 },
     73    await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
     74      "encrypt",
     75    ]),
     76    padded
     77  );
     78 }
     79 
     80 // https://datatracker.ietf.org/doc/html/rfc8188#section-2.1
     81 function writeHeader(header) {
     82  var dataView = new DataView(new ArrayBuffer(5));
     83  dataView.setUint32(0, header.recordSize);
     84  dataView.setUint8(4, header.keyid.length);
     85  return new Uint8Array([
     86    ...header.salt,
     87    ...new Uint8Array(dataView.buffer),
     88    ...header.keyid,
     89  ]);
     90 }
     91 
     92 function validateParams(params) {
     93  const header = { ...params };
     94  if (!header.salt) {
     95    throw new Error("Must include a salt parameter");
     96  }
     97  if (header.salt.length !== 16) {
     98    // https://datatracker.ietf.org/doc/html/rfc8188#section-2.1
     99    // The "salt" parameter comprises the first 16 octets of the
    100    // "aes128gcm" content-coding header.
    101    throw new Error("The salt parameter must be 16 bytes");
    102  }
    103  if (header.appServer.publicKey.byteLength !== 65) {
    104    // https://datatracker.ietf.org/doc/html/rfc8291#section-4
    105    // A push message MUST include the application server ECDH public key in
    106    // the "keyid" parameter of the encrypted content coding header.  The
    107    // uncompressed point form defined in [X9.62] (that is, a 65-octet
    108    // sequence that starts with a 0x04 octet) forms the entirety of the
    109    // "keyid".
    110    throw new Error("The appServer.publicKey parameter must be 65 bytes");
    111  }
    112  if (!header.authSecret) {
    113    throw new Error("No authentication secret for webpush");
    114  }
    115  return header;
    116 }
    117 
    118 export async function encrypt(data, params) {
    119  const header = validateParams(params);
    120 
    121  // https://datatracker.ietf.org/doc/html/rfc8291#section-2
    122  // The ECDH public key is encoded into the "keyid" parameter of the encrypted content coding header
    123  header.keyid = header.appServer.publicKey;
    124  header.recordSize = data.byteLength + 18 + 1;
    125 
    126  // https://datatracker.ietf.org/doc/html/rfc8188#section-2
    127  // The final encoding consists of a header (see Section 2.1) and zero or more
    128  // fixed-size encrypted records; the final record can be smaller than the record size.
    129  const saltedHeader = writeHeader(header);
    130  const { key, nonce } = await deriveKeyAndNonce(header);
    131  const encrypt = await encryptRecord(key, nonce, data);
    132  return new Uint8Array([...saltedHeader, ...new Uint8Array(encrypt)]);
    133 }