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 }