From 3aaa21bbc0989dab2233c1b687ddbf15e655e123 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sun, 3 May 2026 21:06:59 -0700 Subject: [PATCH] fix(channel-decrypt): pure-JS SHA-256/HMAC fallback for HTTP context (P0 follow-up to #1021) (#1027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## P0: PSK channel decryption silently failed on HTTP origins User reported PSK key `372a9c93260507adcbf36a84bec0f33d` "still doesn't work" after PRs #1021 (AES-ECB pure-JS) and #1024 (PSK UX) merged. Reproduced end-to-end and found the actual remaining bug. ### Root cause PR #1021 fixed the AES-ECB path by vendoring a pure-JS core, but **SHA-256 and HMAC-SHA256 in `public/channel-decrypt.js` are still pinned to `crypto.subtle`**. `SubtleCrypto` is exposed **only in secure contexts** (HTTPS / localhost); when CoreScope is served over plain HTTP — common for self-hosted instances — `crypto.subtle` is `undefined`, and: - `computeChannelHash(key)` → `Cannot read properties of undefined (reading 'digest')` - `verifyMAC(...)` → `Cannot read properties of undefined (reading 'importKey')` Both throws are swallowed by `addUserChannel`'s `try/catch`, so the only user-visible signal is the toast `"Failed to decrypt"` with no console-friendly explanation. Verdict: PR #1021 only fixed half of the crypto-in-insecure-context problem. ### Reproduction (no browser required) `test-channel-decrypt-insecure-context.js` loads the production `public/channel-decrypt.js` in a `vm` sandbox where `crypto.subtle` is undefined (mirrors HTTP browser). Pre-fix it failed 8/8 with the exact error above; post-fix it passes 8/8. ### Fix - New `public/vendor/sha256-hmac.js`: minimal pure-JS SHA-256 + HMAC-SHA256 (FIPS-180-4 + RFC 2104, ~120 LOC, MIT). Verified against Node `crypto` for SHA-256 (empty / "abc" / 1000 bytes) and RFC 4231 HMAC-SHA256 TC1. - `public/channel-decrypt.js`: `hasSubtle()` guard. `deriveKey`, `computeChannelHash`, and `verifyMAC` use `crypto.subtle` when available and fall back to `window.PureCrypto` otherwise. Same API, same return types, same async signatures. - `public/index.html`: load `vendor/sha256-hmac.js` immediately before `channel-decrypt.js` (mirrors the `vendor/aes-ecb.js` wiring from #1021). ### TDD - **Red** (`8075b55`): `test-channel-decrypt-insecure-context.js` — runs the **unmodified** prod module in a no-`subtle` sandbox, asserts on the known PSK key (hash byte `0xb7`) and synthetic encrypted packet round-trip. Compiles, runs, **fails 8/8 on assertions** (not on import errors). - **Green** (`232add6`): vendor + delegate. Test passes 8/8. - Wired into `test-all.sh` and `.github/workflows/deploy.yml` so CI gates the regression. ### Validation (all green post-fix) | Test | Result | |---|---| | `test-channel-decrypt-insecure-context.js` | 8/8 | | `test-channel-decrypt-ecb.js` (#1021 KAT) | 7/7 | | `test-channel-decrypt-m345.js` (existing) | 24/24 | | `test-channel-psk-ux.js` (#1024) | 19/19 | | `test-packet-filter.js` | 69/69 | ### Files changed - `public/vendor/sha256-hmac.js` — **new** (~150 LOC, MIT, decrypt-side only) - `public/channel-decrypt.js` — `hasSubtle()` guard + fallback in `deriveKey`/`computeChannelHash`/`verifyMAC` - `public/index.html` — script tag for `vendor/sha256-hmac.js` - `test-channel-decrypt-insecure-context.js` — **new** (8 assertions, pure Node, no browser) - `test-all.sh` + `.github/workflows/deploy.yml` — wire the test ### Risk / scope - Frontend-only, decrypt-side only. No server, schema, or config changes (Config Documentation Rule N/A). - Secure-context behaviour unchanged (still uses Web Crypto when present). - HMAC `secret` building, MAC truncation (2 bytes), and AES-ECB delegation untouched. - Hash vector for the user's PSK key matches: `SHA-256(372a9c93260507adcbf36a84bec0f33d) = b7ce04…`, channel hash byte `0xb7` (183) — confirmed against Node `crypto` and against the new pure-JS path. ### Note on the FIPS test data in the new test The PSK `372a9c93260507adcbf36a84bec0f33d` is shared test data from the bug report, not a real channel secret. --------- Co-authored-by: openclaw-bot --- .github/workflows/deploy.yml | 1 + public/channel-decrypt.js | 50 +++++-- public/index.html | 1 + public/vendor/sha256-hmac.js | 152 +++++++++++++++++++ test-all.sh | 1 + test-channel-decrypt-insecure-context.js | 181 +++++++++++++++++++++++ 6 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 public/vendor/sha256-hmac.js create mode 100644 test-channel-decrypt-insecure-context.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ef708ddf..cb085dee 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -83,6 +83,7 @@ jobs: run: | set -e node test-packet-filter.js + node test-channel-decrypt-insecure-context.js - name: Verify proto syntax run: | diff --git a/public/channel-decrypt.js b/public/channel-decrypt.js index e0395c0f..d73af665 100644 --- a/public/channel-decrypt.js +++ b/public/channel-decrypt.js @@ -38,6 +38,25 @@ window.ChannelDecrypt = (function () { // ---- Key derivation ---- + // Detect whether SubtleCrypto is available. SubtleCrypto is only exposed + // in **secure contexts** (HTTPS or localhost) — when CoreScope is served + // over plain HTTP, `crypto.subtle` is undefined and any digest/HMAC call + // throws. We fall back to the vendored pure-JS implementation in + // public/vendor/sha256-hmac.js. PR #1021 did the same for AES-ECB. + function hasSubtle() { + return typeof crypto !== 'undefined' && crypto && crypto.subtle && typeof crypto.subtle.digest === 'function'; + } + + function pureCryptoOrThrow() { + var host = (typeof window !== 'undefined') ? window + : (typeof self !== 'undefined') ? self : null; + if (!host || !host.PureCrypto || !host.PureCrypto.sha256 || !host.PureCrypto.hmacSha256) { + throw new Error('PureCrypto vendor module not loaded (public/vendor/sha256-hmac.js). ' + + 'crypto.subtle is unavailable (HTTP context) and no fallback present.'); + } + return host.PureCrypto; + } + /** * Derive AES-128 key from channel name: SHA-256("#channelname")[:16]. * @param {string} channelName - e.g. "#LongFast" @@ -45,8 +64,12 @@ window.ChannelDecrypt = (function () { */ async function deriveKey(channelName) { var enc = new TextEncoder(); - var hash = await crypto.subtle.digest('SHA-256', enc.encode(channelName)); - return new Uint8Array(hash).slice(0, 16); + var data = enc.encode(channelName); + if (hasSubtle()) { + var hash = await crypto.subtle.digest('SHA-256', data); + return new Uint8Array(hash).slice(0, 16); + } + return pureCryptoOrThrow().sha256(data).slice(0, 16); } /** @@ -55,8 +78,11 @@ window.ChannelDecrypt = (function () { * @returns {Promise} single byte (0-255) */ async function computeChannelHash(key) { - var hash = await crypto.subtle.digest('SHA-256', key); - return new Uint8Array(hash)[0]; + if (hasSubtle()) { + var hash = await crypto.subtle.digest('SHA-256', key); + return new Uint8Array(hash)[0]; + } + return pureCryptoOrThrow().sha256(key)[0]; } // ---- AES-128-ECB via vendored pure-JS implementation ---- @@ -104,13 +130,17 @@ window.ChannelDecrypt = (function () { secret.set(key, 0); // remaining 16 bytes are already 0 - var cryptoKey = await crypto.subtle.importKey( - 'raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] - ); - var sig = await crypto.subtle.sign('HMAC', cryptoKey, ciphertext); - var sigBytes = new Uint8Array(sig); - var macBytes = hexToBytes(macHex); + var sigBytes; + if (hasSubtle() && typeof crypto.subtle.importKey === 'function' && typeof crypto.subtle.sign === 'function') { + var cryptoKey = await crypto.subtle.importKey( + 'raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ); + var sig = await crypto.subtle.sign('HMAC', cryptoKey, ciphertext); + sigBytes = new Uint8Array(sig); + } else { + sigBytes = pureCryptoOrThrow().hmacSha256(secret, ciphertext); + } return sigBytes[0] === macBytes[0] && sigBytes[1] === macBytes[1]; } diff --git a/public/index.html b/public/index.html index ec6ec5e0..2fbb512e 100644 --- a/public/index.html +++ b/public/index.html @@ -98,6 +98,7 @@ + diff --git a/public/vendor/sha256-hmac.js b/public/vendor/sha256-hmac.js new file mode 100644 index 00000000..b8d74601 --- /dev/null +++ b/public/vendor/sha256-hmac.js @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: MIT + * + * Minimal pure-JS SHA-256 + HMAC-SHA256. + * + * Why: Web Crypto's SubtleCrypto (`window.crypto.subtle`) is only exposed + * in **secure contexts** (HTTPS or localhost). When CoreScope is served + * over plain HTTP — common for self-hosted instances and LAN-side + * deployments — `crypto.subtle` is undefined and any + * `crypto.subtle.digest(...)` / `crypto.subtle.importKey(...)` call + * throws `Cannot read properties of undefined`. PR #1021 fixed the + * AES-ECB path for the same reason; this module does the same for the + * SHA-256 / HMAC paths used by `computeChannelHash` and `verifyMAC`. + * + * Implementation: textbook FIPS-180-4 SHA-256 + RFC 2104 HMAC. Operates + * on Uint8Array inputs; returns Uint8Array outputs. ~120 LOC, no deps. + * + * API: + * window.PureCrypto.sha256(bytes: Uint8Array) -> Uint8Array(32) + * window.PureCrypto.hmacSha256(key: Uint8Array, msg: Uint8Array) -> Uint8Array(32) + */ +/* eslint-disable no-var */ +(function (root) { + 'use strict'; + + // SHA-256 round constants (FIPS-180-4 §4.2.2). + var K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]); + + function ror(x, n) { return (x >>> n) | (x << (32 - n)); } + + // Process a single 64-byte block, mutating `H` (8 × uint32 state). + function processBlock(H, M) { + var W = new Uint32Array(64); + for (var i = 0; i < 16; i++) { + W[i] = (M[i * 4] << 24) | (M[i * 4 + 1] << 16) | (M[i * 4 + 2] << 8) | M[i * 4 + 3]; + } + for (var t = 16; t < 64; t++) { + var s0 = ror(W[t - 15], 7) ^ ror(W[t - 15], 18) ^ (W[t - 15] >>> 3); + var s1 = ror(W[t - 2], 17) ^ ror(W[t - 2], 19) ^ (W[t - 2] >>> 10); + W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0; + } + + var a = H[0], b = H[1], c = H[2], d = H[3]; + var e = H[4], f = H[5], g = H[6], h = H[7]; + + for (var j = 0; j < 64; j++) { + var S1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25); + var ch = (e & f) ^ ((~e) & g); + var temp1 = (h + S1 + ch + K[j] + W[j]) >>> 0; + var S0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22); + var maj = (a & b) ^ (a & c) ^ (b & c); + var temp2 = (S0 + maj) >>> 0; + + h = g; g = f; f = e; + e = (d + temp1) >>> 0; + d = c; c = b; b = a; + a = (temp1 + temp2) >>> 0; + } + + H[0] = (H[0] + a) >>> 0; + H[1] = (H[1] + b) >>> 0; + H[2] = (H[2] + c) >>> 0; + H[3] = (H[3] + d) >>> 0; + H[4] = (H[4] + e) >>> 0; + H[5] = (H[5] + f) >>> 0; + H[6] = (H[6] + g) >>> 0; + H[7] = (H[7] + h) >>> 0; + } + + function sha256(bytes) { + if (!(bytes instanceof Uint8Array)) { + throw new Error('sha256: input must be a Uint8Array'); + } + var bitLen = bytes.length * 8; + // Padding: 0x80 then zeros until length ≡ 56 (mod 64), then 8-byte big-endian bit-length. + var padLen = ((bytes.length + 9 + 63) & ~63) - bytes.length; + var padded = new Uint8Array(bytes.length + padLen); + padded.set(bytes, 0); + padded[bytes.length] = 0x80; + // 64-bit big-endian bit length. JS bitwise ops are 32-bit, so split. + var hi = Math.floor(bitLen / 0x100000000); + var lo = bitLen >>> 0; + var off = padded.length - 8; + padded[off] = (hi >>> 24) & 0xff; + padded[off + 1] = (hi >>> 16) & 0xff; + padded[off + 2] = (hi >>> 8) & 0xff; + padded[off + 3] = hi & 0xff; + padded[off + 4] = (lo >>> 24) & 0xff; + padded[off + 5] = (lo >>> 16) & 0xff; + padded[off + 6] = (lo >>> 8) & 0xff; + padded[off + 7] = lo & 0xff; + + var H = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ]); + + for (var i = 0; i < padded.length; i += 64) { + processBlock(H, padded.subarray(i, i + 64)); + } + + var out = new Uint8Array(32); + for (var k = 0; k < 8; k++) { + out[k * 4] = (H[k] >>> 24) & 0xff; + out[k * 4 + 1] = (H[k] >>> 16) & 0xff; + out[k * 4 + 2] = (H[k] >>> 8) & 0xff; + out[k * 4 + 3] = H[k] & 0xff; + } + return out; + } + + // RFC 2104 HMAC. + function hmacSha256(key, msg) { + if (!(key instanceof Uint8Array) || !(msg instanceof Uint8Array)) { + throw new Error('hmacSha256: key and msg must be Uint8Array'); + } + var blockSize = 64; + var k = key; + if (k.length > blockSize) k = sha256(k); + if (k.length < blockSize) { + var padded = new Uint8Array(blockSize); + padded.set(k, 0); + k = padded; + } + var oKeyPad = new Uint8Array(blockSize); + var iKeyPad = new Uint8Array(blockSize); + for (var i = 0; i < blockSize; i++) { + oKeyPad[i] = k[i] ^ 0x5c; + iKeyPad[i] = k[i] ^ 0x36; + } + var inner = new Uint8Array(blockSize + msg.length); + inner.set(iKeyPad, 0); + inner.set(msg, blockSize); + var innerHash = sha256(inner); + var outer = new Uint8Array(blockSize + innerHash.length); + outer.set(oKeyPad, 0); + outer.set(innerHash, blockSize); + return sha256(outer); + } + + root.PureCrypto = { sha256: sha256, hmacSha256: hmacSha256 }; +})(typeof window !== 'undefined' ? window + : typeof self !== 'undefined' ? self + : this); diff --git a/test-all.sh b/test-all.sh index f47a754d..8b8a6bb7 100755 --- a/test-all.sh +++ b/test-all.sh @@ -14,6 +14,7 @@ node test-aging.js node test-frontend-helpers.js node test-perf-go-runtime.js node test-channel-psk-ux.js +node test-channel-decrypt-insecure-context.js echo "" echo "═══════════════════════════════════════" diff --git a/test-channel-decrypt-insecure-context.js b/test-channel-decrypt-insecure-context.js new file mode 100644 index 00000000..7f69179a --- /dev/null +++ b/test-channel-decrypt-insecure-context.js @@ -0,0 +1,181 @@ +/** + * Tests that channel decryption works in an "insecure context" — i.e. when + * `window.crypto.subtle` is undefined. + * + * Why: when CoreScope is served over plain HTTP (or accessed via a non-https + * origin like `http://:8080`), browsers refuse to expose + * `crypto.subtle` (it requires a secure context). The original + * `channel-decrypt.js` used `crypto.subtle.digest('SHA-256', …)` for + * `computeChannelHash` and `crypto.subtle.importKey(…)` + + * `crypto.subtle.sign('HMAC', …)` for `verifyMAC`. PR #1021 fixed only the + * AES-ECB path with a pure-JS vendor module, but left SHA-256 and HMAC paths + * pinned to `crypto.subtle`. Result on HTTP origins: + * + * addUserChannel("372a9c93260507adcbf36a84bec0f33d") + * -> computeChannelHash(key) throws "Cannot read properties of undefined + * (reading 'digest')" + * -> caught silently by addUserChannel's try/catch + * -> user sees "Failed to decrypt" + * + * This test sandboxes channel-decrypt.js with `crypto.subtle === undefined` + * and asserts both `computeChannelHash` and `verifyMAC` still work, using + * a pure-JS SHA-256 / HMAC-SHA256 fallback. + * + * Reference vectors: + * key bytes = 0x37,0x2a,0x9c,0x93,0x26,0x05,0x07,0xad,0xcb,0xf3,0x6a,0x84,0xbe,0xc0,0xf3,0x3d + * SHA256(key) = b7ce04f7d9019788b69e709ffb796a36d00225818b444ad4f8979bc1d1445f47 + * -> first byte (channel hash) = 0xb7 = 183 + * + * HMAC-SHA256 KAT (RFC 4231 Test Case 1): + * key = 0x0b * 20 + * data = "Hi There" + * mac = b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 + */ +'use strict'; + +const vm = require('vm'); +const fs = require('fs'); +const path = require('path'); + +let passed = 0; +let failed = 0; + +function assert(cond, msg) { + if (cond) { passed++; console.log(' ✓ ' + msg); } + else { failed++; console.error(' ✗ ' + msg); } +} + +function loadChannelDecryptInsecureContext() { + const storage = {}; + const localStorage = { + getItem: (k) => storage[k] !== undefined ? storage[k] : null, + setItem: (k, v) => { storage[k] = String(v); }, + removeItem: (k) => { delete storage[k]; }, + }; + // CRITICAL: crypto present, but no .subtle. Mirrors browser HTTP context. + const insecureCrypto = {}; + const sandbox = { + window: {}, crypto: insecureCrypto, TextEncoder, TextDecoder, Uint8Array, + localStorage, console, Date, JSON, parseInt, Math, String, Number, + Object, Array, RegExp, Error, Promise, setTimeout, + }; + sandbox.window = sandbox; sandbox.self = sandbox; + vm.createContext(sandbox); + + // Vendored AES (must load before channel-decrypt.js — same as index.html). + const vendorAesPath = path.join(__dirname, 'public/vendor/aes-ecb.js'); + if (fs.existsSync(vendorAesPath)) { + vm.runInContext(fs.readFileSync(vendorAesPath, 'utf8'), sandbox); + } + // Optional vendored SHA-256 / HMAC (the fix). Load if present so the test + // works whether the fix vendors it as a separate file OR inlines it into + // channel-decrypt.js. + const vendorShaPath = path.join(__dirname, 'public/vendor/sha256-hmac.js'); + if (fs.existsSync(vendorShaPath)) { + vm.runInContext(fs.readFileSync(vendorShaPath, 'utf8'), sandbox); + } + + vm.runInContext( + fs.readFileSync(path.join(__dirname, 'public/channel-decrypt.js'), 'utf8'), + sandbox + ); + return sandbox.window.ChannelDecrypt; +} + +async function runTests() { + console.log('\n=== channel-decrypt.js works without crypto.subtle (HTTP-context) ==='); + const CD = loadChannelDecryptInsecureContext(); + + // 1) computeChannelHash() — pure SHA-256 of 16-byte key, take byte 0. + const KEY_HEX = '372a9c93260507adcbf36a84bec0f33d'; + const keyBytes = CD.hexToBytes(KEY_HEX); + + let hashByte, threwHash = null; + try { + hashByte = await CD.computeChannelHash(keyBytes); + } catch (e) { + threwHash = e; + } + assert(threwHash === null, + 'computeChannelHash does not throw without crypto.subtle (got: ' + + (threwHash && threwHash.message) + ')'); + assert(hashByte === 0xb7, + 'computeChannelHash returns 0xb7 for known PSK key (got: ' + hashByte + ')'); + + // 2) verifyMAC() — RFC 4231 HMAC-SHA256 Test Case 1. + // We feed a hand-built scenario: + // verifyMAC's HMAC key is `aesKey ++ 16 zero bytes` (32 bytes). + // To exercise RFC 4231 TC1 we set aesKey = 16 * 0x0b and pad another 4 + // bytes of 0x0b in the second half (since verifyMAC zero-fills bytes + // 16..31, we instead use the channel-decrypt API directly here only to + // prove HMAC-SHA256 is computed correctly with the standard secret). + // + // We construct the secret manually and call verifyMAC on a synthetic + // ciphertext whose HMAC-SHA256 first 2 bytes we precompute with Node's + // crypto module (independent oracle). + const nodeCrypto = require('crypto'); + const aesKey = new Uint8Array(16); for (let i = 0; i < 16; i++) aesKey[i] = 0xab; + const ct = new Uint8Array(16); for (let i = 0; i < 16; i++) ct[i] = i; + const secret = Buffer.alloc(32); Buffer.from(aesKey).copy(secret, 0); + const fullMac = nodeCrypto.createHmac('sha256', secret).update(Buffer.from(ct)).digest(); + const expectedMacHex = fullMac.slice(0, 2).toString('hex'); + + let macOk, threwMac = null; + try { + macOk = await CD.verifyMAC(aesKey, ct, expectedMacHex); + } catch (e) { + threwMac = e; + } + assert(threwMac === null, + 'verifyMAC does not throw without crypto.subtle (got: ' + + (threwMac && threwMac.message) + ')'); + assert(macOk === true, + 'verifyMAC returns true for valid 2-byte MAC (got: ' + macOk + ')'); + + // 3) verifyMAC must still REJECT a wrong MAC. + let macBad, threwMacBad = null; + try { + macBad = await CD.verifyMAC(aesKey, ct, '0000'); + } catch (e) { + threwMacBad = e; + } + assert(threwMacBad === null, + 'verifyMAC does not throw on wrong MAC (got: ' + (threwMacBad && threwMacBad.message) + ')'); + assert(macBad === false, + 'verifyMAC returns false for wrong 2-byte MAC (got: ' + macBad + ')'); + + // 4) End-to-end: decrypt() must work with subtle absent — exercises + // SHA-256 (key derivation already done) + HMAC + AES-ECB together. + // Build a synthetic encrypted packet from a known plaintext. + const aesKey2 = nodeCrypto.randomBytes(16); + const plaintext = Buffer.alloc(16); + // timestamp(4 LE) + flags(1) + "alice: hi\0" then padded + plaintext.writeUInt32LE(0x12345678, 0); + plaintext[4] = 0x00; + Buffer.from('alice: hi\0', 'utf8').copy(plaintext, 5); + + const cipher = nodeCrypto.createCipheriv('aes-128-ecb', aesKey2, null); + cipher.setAutoPadding(false); + const ct2 = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const secret2 = Buffer.alloc(32); aesKey2.copy(secret2, 0); + const macHex2 = nodeCrypto.createHmac('sha256', secret2).update(ct2).digest().slice(0, 2).toString('hex'); + + let decResult = null, threwDec = null; + try { + decResult = await CD.decrypt(new Uint8Array(aesKey2), macHex2, ct2.toString('hex')); + } catch (e) { + threwDec = e; + } + assert(threwDec === null, + 'decrypt() does not throw without crypto.subtle (got: ' + + (threwDec && threwDec.message) + ')'); + assert(decResult && decResult.sender === 'alice' && decResult.message === 'hi', + 'decrypt() recovers sender + message in HTTP context (got: ' + + JSON.stringify(decResult) + ')'); + + console.log('\n=== Results ==='); + console.log('Passed: ' + passed + ', Failed: ' + failed); + process.exit(failed > 0 ? 1 : 0); +} + +runTests().catch(e => { console.error(e); process.exit(1); });