|
| 1 | +import compare from '@paulmillr/jsbt/bench-compare.js'; |
| 2 | + |
| 3 | +import * as js from '../../src/targets/js/index.ts'; |
| 4 | +import * as wasm from '../../src/targets/wasm/index.ts'; |
| 5 | +import * as wasm_threads from '../../src/targets/wasm_threads/index.ts'; |
| 6 | +import { WP } from '../../src/workers.ts'; |
| 7 | +import { NOBLE } from '../../test/noble-all.ts'; |
| 8 | +import { buf } from './utils-ciphers.ts'; |
| 9 | + |
| 10 | +// AES-KW/AES-KWP have atypical length rules (e.g. KW forbids 0-byte and 8-byte messages), |
| 11 | +// so we keep a local cross-validator instead of generalizing `utils-ciphers.ts`. |
| 12 | +const eql = (a: Uint8Array, b: Uint8Array, msg?: string) => { |
| 13 | + if (a.length !== b.length) throw new Error(`u8a.eql: length ${a.length}!==${b.length} (${msg})`); |
| 14 | + for (let i = 0; i < a.length; i++) { |
| 15 | + if (a[i] !== b[i]) throw new Error(`u8a.eql: a[${i}](${a[i]})!==b[${i}](${b[i]}), ${msg}`); |
| 16 | + } |
| 17 | +}; |
| 18 | +const crossValidateAesKw = async ( |
| 19 | + title: string, |
| 20 | + buffers: Record<string, Uint8Array>, |
| 21 | + ciphers: any, |
| 22 | + allowEnc: (len: number) => boolean |
| 23 | +) => { |
| 24 | + const bufs = Object.values(buffers).slice(); |
| 25 | + const bufMap = new Map(Object.entries(buffers).map(([k, v]) => [v, k])); |
| 26 | + for (let i = 0; i < 2048; i++) { |
| 27 | + if (!allowEnc(i)) continue; |
| 28 | + bufs.push(buf(i)); |
| 29 | + } |
| 30 | + |
| 31 | + const res: Record<string, Uint8Array> = {}; |
| 32 | + for (const msg of bufs) { |
| 33 | + const bname = bufMap.get(msg) || `${msg.length}`; |
| 34 | + const orig = msg.slice(); |
| 35 | + let encrypted: Uint8Array | undefined; |
| 36 | + const opts = ciphers.options; |
| 37 | + for (const [lib, fn] of Object.entries(ciphers)) { |
| 38 | + if (lib === 'options') continue; |
| 39 | + if (encrypted === undefined) encrypted = await fn.encrypt(msg, opts); |
| 40 | + else |
| 41 | + eql(encrypted, await fn.encrypt(msg, opts), `${title}/${bname}: encrypt verify (${lib})`); |
| 42 | + eql(msg, orig, `${title}/${bname}: encrypt mutates buffer (${lib})`); |
| 43 | + eql(await fn.decrypt(encrypted, opts), msg, `${title}/${bname}: decrypt verify (${lib})`); |
| 44 | + } |
| 45 | + const bufName = bufMap.get(msg); |
| 46 | + if (bufName) res[bufName] = encrypted!; |
| 47 | + } |
| 48 | + return res; |
| 49 | +}; |
| 50 | + |
| 51 | +const benchBuf = (n: number, seed = 0x9e3779b9) => { |
| 52 | + // Sequential buffers can bias AES table/cache access patterns. |
| 53 | + // Use deterministic pseudo-random data so noble/awasm are compared on less structured input. |
| 54 | + const out = new Uint8Array(n); |
| 55 | + let x = seed >>> 0; |
| 56 | + for (let i = 0; i < n; i++) { |
| 57 | + x ^= x << 13; |
| 58 | + x ^= x >>> 17; |
| 59 | + x ^= x << 5; |
| 60 | + out[i] = x & 0xff; |
| 61 | + } |
| 62 | + return out; |
| 63 | +}; |
| 64 | + |
| 65 | +const BUFFERS = { |
| 66 | + '64B': benchBuf(64), |
| 67 | + '1MB': benchBuf(1024 * 1024), |
| 68 | +}; |
| 69 | + |
| 70 | +const addAesKw = (algoName: 'aeskw' | 'aeskwp') => { |
| 71 | + const res: Record<string, { encrypt: (b: Uint8Array, o: any) => Uint8Array; decrypt: any }> = {}; |
| 72 | + // Keep noble first so diff baselines match "noble-ciphers" by default. |
| 73 | + for (const [name, lib] of Object.entries({ noble: NOBLE, wasm, wasm_threads, js })) { |
| 74 | + const fn = (lib as any)[algoName]; |
| 75 | + res[name] = { |
| 76 | + encrypt: (b, opts) => fn(opts.key).encrypt(b), |
| 77 | + decrypt: (b, opts) => fn(opts.key).decrypt(b), |
| 78 | + }; |
| 79 | + } |
| 80 | + return res; |
| 81 | +}; |
| 82 | + |
| 83 | +export const AESKW = { |
| 84 | + Wrap: { |
| 85 | + aeskw: { |
| 86 | + NoPadding: { |
| 87 | + 128: { |
| 88 | + options: { key: buf(16), blockSize: 8 }, |
| 89 | + ...addAesKw('aeskw'), |
| 90 | + }, |
| 91 | + 192: { |
| 92 | + options: { key: buf(24), blockSize: 8 }, |
| 93 | + ...addAesKw('aeskw'), |
| 94 | + }, |
| 95 | + 256: { |
| 96 | + options: { key: buf(32), blockSize: 8 }, |
| 97 | + ...addAesKw('aeskw'), |
| 98 | + }, |
| 99 | + }, |
| 100 | + }, |
| 101 | + aeskwp: { |
| 102 | + Padding: { |
| 103 | + 128: { |
| 104 | + options: { key: buf(16) }, |
| 105 | + ...addAesKw('aeskwp'), |
| 106 | + }, |
| 107 | + 192: { |
| 108 | + options: { key: buf(24) }, |
| 109 | + ...addAesKw('aeskwp'), |
| 110 | + }, |
| 111 | + 256: { |
| 112 | + options: { key: buf(32) }, |
| 113 | + ...addAesKw('aeskwp'), |
| 114 | + }, |
| 115 | + }, |
| 116 | + }, |
| 117 | + }, |
| 118 | +}; |
| 119 | + |
| 120 | +export async function main() { |
| 121 | + await WP.waitOnline(); |
| 122 | + const crossFilter = process.env.CROSS_FILTER; |
| 123 | + |
| 124 | + const encrypted: Record<string, Record<string, Uint8Array>> = {}; |
| 125 | + for (const [type, algos] of Object.entries(AESKW)) { |
| 126 | + for (const [algo, paddings] of Object.entries(algos as any)) { |
| 127 | + for (const [padding, keySizes] of Object.entries(paddings as any)) { |
| 128 | + for (const [keySize, libraries] of Object.entries(keySizes as any)) { |
| 129 | + const name = `${type}/${algo}/${padding}/${keySize}`; |
| 130 | + if (crossFilter && !name.includes(crossFilter)) continue; |
| 131 | + const allowEnc = |
| 132 | + algo === 'aeskw' |
| 133 | + ? (len: number) => !!len && len !== 8 && len % 8 === 0 |
| 134 | + : (len: number) => !!len; |
| 135 | + encrypted[name] = await crossValidateAesKw(name, BUFFERS, libraries as any, allowEnc); |
| 136 | + if (process.env.CROSS_WAIT) await WP.waitOnline(); |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + await WP.waitOnline(); |
| 142 | + console.log('Libraries cross-validated against each other correctly'); |
| 143 | + |
| 144 | + // Example: JSBT_BENCHMARK_FILTER=1MB JSBT_BENCHMARK_DIMENSIONS='buffer,type,algorithm,padding,key size,direction,library' node aeskw.ts |
| 145 | + await compare('AES-KW', { buffer: BUFFERS }, AESKW as any, { |
| 146 | + libraryDimensions: ['type', 'algorithm', 'padding', 'key size', 'library', 'direction'], |
| 147 | + defaults: { |
| 148 | + buffer: '64B', |
| 149 | + type: 'Wrap', |
| 150 | + library: 'noble', |
| 151 | + direction: 'encrypt', |
| 152 | + padding: 'Padding', |
| 153 | + algorithm: 'aeskwp', |
| 154 | + 'key size': '256', |
| 155 | + }, |
| 156 | + patchArgs: (args, obj) => { |
| 157 | + if (obj.direction === 'decrypt') { |
| 158 | + const buf = |
| 159 | + encrypted[`${obj.type}/${obj.algorithm}/${obj.padding}/${obj['key size']}`][obj.buffer]; |
| 160 | + return [buf, args[1]]; |
| 161 | + } |
| 162 | + return args; |
| 163 | + }, |
| 164 | + bytes: ({ args }) => args[0].length, |
| 165 | + }); |
| 166 | +} |
| 167 | + |
| 168 | +import url from 'node:url'; |
| 169 | +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { |
| 170 | + main(); |
| 171 | +} |
0 commit comments