leviathan-crypto 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/CLAUDE.md +88 -281
  2. package/LICENSE +4 -0
  3. package/README.md +275 -87
  4. package/dist/aes/aes-cbc.d.ts +40 -0
  5. package/dist/aes/aes-cbc.js +158 -0
  6. package/dist/aes/aes-ctr.d.ts +50 -0
  7. package/dist/aes/aes-ctr.js +141 -0
  8. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  9. package/dist/aes/aes-gcm-siv.js +217 -0
  10. package/dist/aes/aes-gcm.d.ts +61 -0
  11. package/dist/aes/aes-gcm.js +226 -0
  12. package/dist/aes/cipher-suite.d.ts +21 -0
  13. package/dist/aes/cipher-suite.js +179 -0
  14. package/dist/aes/embedded.d.ts +1 -0
  15. package/dist/aes/embedded.js +26 -0
  16. package/dist/aes/generator.d.ts +14 -0
  17. package/dist/aes/generator.js +103 -0
  18. package/dist/aes/index.d.ts +58 -0
  19. package/dist/aes/index.js +125 -0
  20. package/dist/aes/ops.d.ts +60 -0
  21. package/dist/aes/ops.js +164 -0
  22. package/dist/aes/pool-worker.d.ts +1 -0
  23. package/dist/aes/pool-worker.js +92 -0
  24. package/dist/aes/types.d.ts +1 -0
  25. package/dist/aes/types.js +23 -0
  26. package/dist/aes.wasm +0 -0
  27. package/dist/blake3/embedded.d.ts +1 -0
  28. package/dist/blake3/embedded.js +26 -0
  29. package/dist/blake3/index.d.ts +143 -0
  30. package/dist/blake3/index.js +620 -0
  31. package/dist/blake3/types.d.ts +102 -0
  32. package/dist/blake3/types.js +31 -0
  33. package/dist/blake3/validate.d.ts +29 -0
  34. package/dist/blake3/validate.js +80 -0
  35. package/dist/blake3.wasm +0 -0
  36. package/dist/chacha20/cipher-suite.d.ts +10 -0
  37. package/dist/chacha20/cipher-suite.js +98 -13
  38. package/dist/chacha20/generator.d.ts +12 -0
  39. package/dist/chacha20/generator.js +91 -0
  40. package/dist/chacha20/index.d.ts +100 -3
  41. package/dist/chacha20/index.js +169 -35
  42. package/dist/chacha20/ops.d.ts +57 -6
  43. package/dist/chacha20/ops.js +107 -27
  44. package/dist/chacha20/pool-worker.js +14 -0
  45. package/dist/chacha20/types.d.ts +1 -32
  46. package/dist/cte-wasm.d.ts +1 -0
  47. package/dist/cte-wasm.js +3 -0
  48. package/dist/cte.wasm +0 -0
  49. package/dist/curve25519.wasm +0 -0
  50. package/dist/ecdsa/der.d.ts +23 -0
  51. package/dist/ecdsa/der.js +192 -0
  52. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  53. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  54. package/dist/ecdsa/embedded.d.ts +1 -0
  55. package/dist/ecdsa/embedded.js +25 -0
  56. package/dist/ecdsa/index.d.ts +124 -0
  57. package/dist/ecdsa/index.js +366 -0
  58. package/dist/ecdsa/types.d.ts +31 -0
  59. package/dist/ecdsa/types.js +28 -0
  60. package/dist/ecdsa/validate.d.ts +18 -0
  61. package/dist/ecdsa/validate.js +92 -0
  62. package/dist/ed25519/embedded.d.ts +1 -0
  63. package/dist/ed25519/embedded.js +31 -0
  64. package/dist/ed25519/index.d.ts +70 -0
  65. package/dist/ed25519/index.js +308 -0
  66. package/dist/ed25519/types.d.ts +27 -0
  67. package/dist/ed25519/types.js +27 -0
  68. package/dist/ed25519/validate.d.ts +7 -0
  69. package/dist/ed25519/validate.js +77 -0
  70. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  71. package/dist/embedded/aes-pool-worker.js +5 -0
  72. package/dist/embedded/aes.d.ts +1 -0
  73. package/dist/embedded/aes.js +3 -0
  74. package/dist/embedded/blake3.d.ts +1 -0
  75. package/dist/embedded/blake3.js +3 -0
  76. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  77. package/dist/embedded/chacha20-pool-worker.js +5 -0
  78. package/dist/embedded/chacha20.d.ts +1 -1
  79. package/dist/embedded/chacha20.js +2 -2
  80. package/dist/embedded/curve25519.d.ts +1 -0
  81. package/dist/embedded/curve25519.js +3 -0
  82. package/dist/embedded/mldsa.d.ts +1 -0
  83. package/dist/embedded/mldsa.js +3 -0
  84. package/dist/embedded/mlkem.d.ts +1 -0
  85. package/dist/embedded/mlkem.js +3 -0
  86. package/dist/embedded/p256.d.ts +1 -0
  87. package/dist/embedded/p256.js +3 -0
  88. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  89. package/dist/embedded/serpent-pool-worker.js +5 -0
  90. package/dist/embedded/serpent.d.ts +1 -1
  91. package/dist/embedded/serpent.js +2 -2
  92. package/dist/embedded/sha2.d.ts +1 -1
  93. package/dist/embedded/sha2.js +2 -2
  94. package/dist/embedded/sha3.d.ts +1 -1
  95. package/dist/embedded/sha3.js +2 -2
  96. package/dist/embedded/slhdsa.d.ts +1 -0
  97. package/dist/embedded/slhdsa.js +3 -0
  98. package/dist/errors.d.ts +92 -1
  99. package/dist/errors.js +111 -1
  100. package/dist/fortuna.d.ts +18 -12
  101. package/dist/fortuna.js +166 -99
  102. package/dist/index.d.ts +42 -11
  103. package/dist/index.js +65 -20
  104. package/dist/init.d.ts +1 -3
  105. package/dist/init.js +73 -7
  106. package/dist/keccak/embedded.js +1 -1
  107. package/dist/keccak/index.d.ts +2 -0
  108. package/dist/keccak/index.js +4 -2
  109. package/dist/loader.d.ts +1 -19
  110. package/dist/loader.js +26 -32
  111. package/dist/merkle/blake3-tree.d.ts +35 -0
  112. package/dist/merkle/blake3-tree.js +187 -0
  113. package/dist/merkle/checkpoint.d.ts +58 -0
  114. package/dist/merkle/checkpoint.js +217 -0
  115. package/dist/merkle/index.d.ts +19 -0
  116. package/dist/merkle/index.js +37 -0
  117. package/dist/merkle/merkle-log.d.ts +130 -0
  118. package/dist/merkle/merkle-log.js +207 -0
  119. package/dist/merkle/merkle-verifier.d.ts +126 -0
  120. package/dist/merkle/merkle-verifier.js +296 -0
  121. package/dist/merkle/proof.d.ts +70 -0
  122. package/dist/merkle/proof.js +300 -0
  123. package/dist/merkle/sha256-tree.d.ts +33 -0
  124. package/dist/merkle/sha256-tree.js +145 -0
  125. package/dist/merkle/signed-log.d.ts +156 -0
  126. package/dist/merkle/signed-log.js +356 -0
  127. package/dist/merkle/signed-note.d.ts +309 -0
  128. package/dist/merkle/signed-note.js +648 -0
  129. package/dist/merkle/sth.d.ts +31 -0
  130. package/dist/merkle/sth.js +31 -0
  131. package/dist/merkle/storage.d.ts +40 -0
  132. package/dist/merkle/storage.js +71 -0
  133. package/dist/merkle/tree.d.ts +68 -0
  134. package/dist/merkle/tree.js +94 -0
  135. package/dist/mldsa/embedded.d.ts +1 -0
  136. package/dist/{kyber → mldsa}/embedded.js +5 -5
  137. package/dist/mldsa/expand.d.ts +53 -0
  138. package/dist/mldsa/expand.js +188 -0
  139. package/dist/mldsa/format.d.ts +16 -0
  140. package/dist/mldsa/format.js +68 -0
  141. package/dist/mldsa/hashvariant.d.ts +32 -0
  142. package/dist/mldsa/hashvariant.js +248 -0
  143. package/dist/mldsa/index.d.ts +142 -0
  144. package/dist/mldsa/index.js +463 -0
  145. package/dist/mldsa/keygen.d.ts +16 -0
  146. package/dist/mldsa/keygen.js +232 -0
  147. package/dist/mldsa/params.d.ts +21 -0
  148. package/dist/mldsa/params.js +55 -0
  149. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  150. package/dist/mldsa/sha3-helpers.js +124 -0
  151. package/dist/mldsa/sign.d.ts +36 -0
  152. package/dist/mldsa/sign.js +380 -0
  153. package/dist/mldsa/types.d.ts +91 -0
  154. package/dist/mldsa/types.js +25 -0
  155. package/dist/mldsa/validate.d.ts +55 -0
  156. package/dist/mldsa/validate.js +125 -0
  157. package/dist/mldsa/verify.d.ts +29 -0
  158. package/dist/mldsa/verify.js +269 -0
  159. package/dist/mldsa.wasm +0 -0
  160. package/dist/mlkem/embedded.d.ts +1 -0
  161. package/dist/mlkem/embedded.js +27 -0
  162. package/dist/mlkem/indcpa.d.ts +49 -0
  163. package/dist/{kyber → mlkem}/indcpa.js +48 -48
  164. package/dist/mlkem/index.d.ts +37 -0
  165. package/dist/{kyber → mlkem}/index.js +41 -31
  166. package/dist/mlkem/kem.d.ts +21 -0
  167. package/dist/{kyber → mlkem}/kem.js +48 -13
  168. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  169. package/dist/{kyber → mlkem}/params.js +2 -2
  170. package/dist/mlkem/suite.d.ts +12 -0
  171. package/dist/{kyber → mlkem}/suite.js +17 -12
  172. package/dist/{kyber → mlkem}/types.d.ts +4 -3
  173. package/dist/{kyber → mlkem}/types.js +1 -1
  174. package/dist/mlkem/validate.d.ts +23 -0
  175. package/dist/{kyber → mlkem}/validate.js +24 -20
  176. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  177. package/dist/p256.wasm +0 -0
  178. package/dist/ratchet/index.d.ts +8 -0
  179. package/dist/ratchet/index.js +38 -0
  180. package/dist/ratchet/kdf-chain.d.ts +13 -0
  181. package/dist/ratchet/kdf-chain.js +85 -0
  182. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  183. package/dist/ratchet/ratchet-keypair.js +61 -0
  184. package/dist/ratchet/root-kdf.d.ts +4 -0
  185. package/dist/ratchet/root-kdf.js +124 -0
  186. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  187. package/dist/ratchet/skipped-key-store.js +154 -0
  188. package/dist/ratchet/types.d.ts +36 -0
  189. package/dist/ratchet/types.js +26 -0
  190. package/dist/serpent/cipher-suite.d.ts +10 -0
  191. package/dist/serpent/cipher-suite.js +144 -56
  192. package/dist/serpent/generator.d.ts +12 -0
  193. package/dist/serpent/generator.js +97 -0
  194. package/dist/serpent/index.d.ts +62 -1
  195. package/dist/serpent/index.js +97 -21
  196. package/dist/serpent/pool-worker.js +28 -102
  197. package/dist/serpent/serpent-cbc.d.ts +16 -6
  198. package/dist/serpent/serpent-cbc.js +58 -37
  199. package/dist/serpent/shared-ops.d.ts +63 -0
  200. package/dist/serpent/shared-ops.js +178 -0
  201. package/dist/serpent/types.d.ts +1 -5
  202. package/dist/serpent.wasm +0 -0
  203. package/dist/sha2/hash.d.ts +2 -0
  204. package/dist/sha2/hash.js +53 -0
  205. package/dist/sha2/hkdf.js +5 -5
  206. package/dist/sha2/index.d.ts +22 -1
  207. package/dist/sha2/index.js +80 -11
  208. package/dist/sha2/types.d.ts +41 -2
  209. package/dist/sha2.wasm +0 -0
  210. package/dist/sha3/hash.d.ts +2 -0
  211. package/dist/sha3/hash.js +53 -0
  212. package/dist/sha3/index.d.ts +87 -3
  213. package/dist/sha3/index.js +317 -19
  214. package/dist/sha3/kmac.d.ts +121 -0
  215. package/dist/sha3/kmac.js +800 -0
  216. package/dist/sha3.wasm +0 -0
  217. package/dist/shared/pkcs7.d.ts +22 -0
  218. package/dist/shared/pkcs7.js +84 -0
  219. package/dist/sign/ctx.d.ts +41 -0
  220. package/dist/sign/ctx.js +102 -0
  221. package/dist/sign/envelope.d.ts +45 -0
  222. package/dist/sign/envelope.js +152 -0
  223. package/dist/sign/hasher.d.ts +9 -0
  224. package/dist/sign/hasher.js +132 -0
  225. package/dist/sign/index.d.ts +11 -0
  226. package/dist/sign/index.js +34 -0
  227. package/dist/sign/sign-stream.d.ts +25 -0
  228. package/dist/sign/sign-stream.js +112 -0
  229. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  230. package/dist/sign/suites/ecdsa-p256.js +120 -0
  231. package/dist/sign/suites/ed25519.d.ts +3 -0
  232. package/dist/sign/suites/ed25519.js +165 -0
  233. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  234. package/dist/sign/suites/hybrid-classical.js +526 -0
  235. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  236. package/dist/sign/suites/hybrid-pq.js +234 -0
  237. package/dist/sign/suites/mldsa.d.ts +7 -0
  238. package/dist/sign/suites/mldsa.js +161 -0
  239. package/dist/sign/suites/slhdsa.d.ts +7 -0
  240. package/dist/sign/suites/slhdsa.js +176 -0
  241. package/dist/sign/types.d.ts +106 -0
  242. package/dist/sign/types.js +28 -0
  243. package/dist/sign/verify-stream.d.ts +30 -0
  244. package/dist/sign/verify-stream.js +227 -0
  245. package/dist/slhdsa/embedded.d.ts +1 -0
  246. package/dist/slhdsa/embedded.js +26 -0
  247. package/dist/slhdsa/index.d.ts +149 -0
  248. package/dist/slhdsa/index.js +493 -0
  249. package/dist/slhdsa/params.d.ts +26 -0
  250. package/dist/slhdsa/params.js +70 -0
  251. package/dist/slhdsa/prehash.d.ts +68 -0
  252. package/dist/slhdsa/prehash.js +307 -0
  253. package/dist/slhdsa/sign.d.ts +39 -0
  254. package/dist/slhdsa/sign.js +116 -0
  255. package/dist/slhdsa/types.d.ts +129 -0
  256. package/dist/slhdsa/types.js +27 -0
  257. package/dist/slhdsa/validate.d.ts +60 -0
  258. package/dist/slhdsa/validate.js +127 -0
  259. package/dist/slhdsa/verify.d.ts +32 -0
  260. package/dist/slhdsa/verify.js +107 -0
  261. package/dist/slhdsa.wasm +0 -0
  262. package/dist/stream/header.js +8 -8
  263. package/dist/stream/index.d.ts +1 -0
  264. package/dist/stream/index.js +1 -0
  265. package/dist/stream/open-stream.js +65 -22
  266. package/dist/stream/seal-stream-pool.d.ts +2 -0
  267. package/dist/stream/seal-stream-pool.js +100 -33
  268. package/dist/stream/seal-stream.d.ts +1 -1
  269. package/dist/stream/seal-stream.js +48 -19
  270. package/dist/stream/seal.js +6 -6
  271. package/dist/stream/types.d.ts +3 -1
  272. package/dist/stream/types.js +1 -1
  273. package/dist/types.d.ts +22 -1
  274. package/dist/types.js +1 -1
  275. package/dist/utils.d.ts +9 -10
  276. package/dist/utils.js +84 -59
  277. package/dist/wasm-source.d.ts +9 -8
  278. package/dist/wasm-source.js +1 -1
  279. package/dist/x25519/embedded.d.ts +1 -0
  280. package/dist/x25519/embedded.js +31 -0
  281. package/dist/x25519/index.d.ts +43 -0
  282. package/dist/x25519/index.js +159 -0
  283. package/dist/x25519/types.d.ts +25 -0
  284. package/dist/x25519/types.js +27 -0
  285. package/dist/x25519/validate.d.ts +2 -0
  286. package/dist/x25519/validate.js +39 -0
  287. package/package.json +123 -64
  288. package/SECURITY.md +0 -276
  289. package/dist/ct-wasm.d.ts +0 -1
  290. package/dist/ct-wasm.js +0 -3
  291. package/dist/ct.wasm +0 -0
  292. package/dist/docs/aead.md +0 -323
  293. package/dist/docs/architecture.md +0 -932
  294. package/dist/docs/argon2id.md +0 -302
  295. package/dist/docs/chacha20.md +0 -674
  296. package/dist/docs/exports.md +0 -241
  297. package/dist/docs/fortuna.md +0 -313
  298. package/dist/docs/init.md +0 -302
  299. package/dist/docs/loader.md +0 -161
  300. package/dist/docs/serpent.md +0 -519
  301. package/dist/docs/sha2.md +0 -613
  302. package/dist/docs/sha3.md +0 -546
  303. package/dist/docs/types.md +0 -276
  304. package/dist/docs/utils.md +0 -367
  305. package/dist/embedded/kyber.d.ts +0 -1
  306. package/dist/embedded/kyber.js +0 -3
  307. package/dist/kyber/embedded.d.ts +0 -1
  308. package/dist/kyber/indcpa.d.ts +0 -49
  309. package/dist/kyber/index.d.ts +0 -38
  310. package/dist/kyber/kem.d.ts +0 -21
  311. package/dist/kyber/suite.d.ts +0 -13
  312. package/dist/kyber/validate.d.ts +0 -19
@@ -21,11 +21,11 @@
21
21
  //
22
22
  // src/ts/stream/seal-stream-pool.ts
23
23
  //
24
- // SealStreamPool parallel batch encryption/decryption using the STREAM
24
+ // SealStreamPool, parallel batch encryption/decryption using the STREAM
25
25
  // construction. Dispatches per-chunk seal/open jobs across Web Workers.
26
26
  // Any error is fatal: auth failure, crash, or timeout kills all workers,
27
27
  // wipes keys, and rejects all pending promises.
28
- import { randomBytes, wipe } from '../utils.js';
28
+ import { randomBytes, wipe, constantTimeEqual } from '../utils.js';
29
29
  import { isInitialized } from '../init.js';
30
30
  import { compileWasm } from '../loader.js';
31
31
  import { AuthenticationError } from '../errors.js';
@@ -44,6 +44,7 @@ export class SealStreamPool {
44
44
  _framed;
45
45
  _timeout;
46
46
  _header;
47
+ _commitment;
47
48
  _workers;
48
49
  _idle;
49
50
  _queue;
@@ -53,7 +54,7 @@ export class SealStreamPool {
53
54
  _sealed;
54
55
  _keys;
55
56
  _masterKey;
56
- constructor(cipher, workers, keys, masterKey, header, chunkSize, framed, timeout) {
57
+ constructor(cipher, workers, keys, masterKey, header, commitment, chunkSize, framed, timeout) {
57
58
  this._cipher = cipher;
58
59
  this._workers = workers;
59
60
  this._idle = [...workers];
@@ -65,6 +66,7 @@ export class SealStreamPool {
65
66
  this._keys = keys;
66
67
  this._masterKey = masterKey;
67
68
  this._header = header;
69
+ this._commitment = commitment;
68
70
  this._chunkSize = chunkSize;
69
71
  this._framed = framed;
70
72
  this._timeout = timeout;
@@ -75,10 +77,10 @@ export class SealStreamPool {
75
77
  }
76
78
  static async create(cipher, key, opts) {
77
79
  if (!isInitialized('sha2'))
78
- throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation '
80
+ throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation, '
79
81
  + 'call init({ sha2: ... }) before creating a SealStreamPool');
80
82
  if (cipher.kemCtSize > 0)
81
- throw new Error('leviathan-crypto: SealStreamPool does not support KEM-enabled cipher suites '
83
+ throw new Error('leviathan-crypto: SealStreamPool does not support KEM-enabled cipher suites, '
82
84
  + 'KEM encryption is asymmetric (seal uses encapsulation key, open requires decapsulation key) '
83
85
  + 'and cannot share a single key across both directions. '
84
86
  + 'Use SealStream / OpenStream directly for hybrid KEM encryption.');
@@ -101,7 +103,7 @@ export class SealStreamPool {
101
103
  }
102
104
  else {
103
105
  if (required.length > 1)
104
- throw new Error(`leviathan-crypto: cipher requires ${required.length} WASM modules (${required.join(', ')}) provide a Record`);
106
+ throw new Error(`leviathan-crypto: cipher requires ${required.length} WASM modules (${required.join(', ')}), provide a Record`);
105
107
  modules[required[0]] = await compileWasm(opts.wasm);
106
108
  }
107
109
  // For padded ciphers, validate that a full plaintext chunk fits in the WASM
@@ -115,10 +117,20 @@ export class SealStreamPool {
115
117
  }
116
118
  if (key.length !== cipher.keySize)
117
119
  throw new RangeError(`key must be ${cipher.keySize} bytes (got ${key.length})`);
118
- // Generate nonce and derive keys
120
+ // Generate nonce and build header before deriveKeys, XChaCha20 binds
121
+ // the header into HKDF info; SerpentCipher accepts and ignores it.
119
122
  const nonce = randomBytes(16);
120
- const keys = cipher.deriveKeys(key, nonce);
121
123
  const header = writeHeader(cipher.formatEnum, framed, nonce, chunkSize);
124
+ const keys = cipher.deriveKeys(key, nonce, undefined, header);
125
+ let commitment = null;
126
+ if (cipher.commitmentSize > 0) {
127
+ if (!keys.commitment || keys.commitment.length !== cipher.commitmentSize) {
128
+ cipher.wipeKeys(keys);
129
+ throw new Error(`leviathan-crypto: ${cipher.formatName}.deriveKeys returned `
130
+ + `${keys.commitment?.length ?? 'no'} commitment bytes, expected ${cipher.commitmentSize}`);
131
+ }
132
+ commitment = keys.commitment;
133
+ }
122
134
  // Spawn workers sequentially (compatible with @vitest/web-worker)
123
135
  const workers = [];
124
136
  for (let i = 0; i < n; i++) {
@@ -147,7 +159,7 @@ export class SealStreamPool {
147
159
  });
148
160
  workers.push(w);
149
161
  }
150
- return new SealStreamPool(cipher, workers, keys, key.slice(), header, chunkSize, framed, timeout);
162
+ return new SealStreamPool(cipher, workers, keys, key.slice(), header, commitment, chunkSize, framed, timeout);
151
163
  }
152
164
  get header() {
153
165
  return this._header;
@@ -176,12 +188,17 @@ export class SealStreamPool {
176
188
  }
177
189
  try {
178
190
  const results = await Promise.all(jobs);
179
- let totalLen = HEADER_SIZE;
191
+ const commitmentLen = this._cipher.commitmentSize;
192
+ let totalLen = HEADER_SIZE + commitmentLen;
180
193
  for (const r of results)
181
194
  totalLen += this._framed ? r.length + 4 : r.length;
182
195
  const ciphertext = new Uint8Array(totalLen);
183
196
  ciphertext.set(this._header, 0);
184
197
  let pos = HEADER_SIZE;
198
+ if (this._commitment) {
199
+ ciphertext.set(this._commitment, pos);
200
+ pos += commitmentLen;
201
+ }
185
202
  for (const r of results) {
186
203
  if (this._framed) {
187
204
  new DataView(ciphertext.buffer, pos).setUint32(0, r.length, false);
@@ -202,30 +219,52 @@ export class SealStreamPool {
202
219
  if (this._dead)
203
220
  throw new Error('leviathan-crypto: pool is dead');
204
221
  if (ciphertext.length < HEADER_SIZE)
205
- throw new RangeError(`leviathan-crypto: ciphertext too short need at least ${HEADER_SIZE} bytes for header`);
222
+ throw new RangeError(`leviathan-crypto: ciphertext too short, need at least ${HEADER_SIZE} bytes for header`);
206
223
  // Validate header before splitting chunks
207
224
  const h = readHeader(ciphertext.subarray(0, HEADER_SIZE));
208
225
  if (h.formatEnum !== this._cipher.formatEnum)
209
226
  throw new Error(`leviathan-crypto: pool expected format 0x${this._cipher.formatEnum.toString(16).padStart(2, '0')}, `
210
227
  + `got 0x${h.formatEnum.toString(16).padStart(2, '0')}`);
211
228
  if (h.chunkSize !== this._chunkSize)
212
- throw new RangeError(`leviathan-crypto: pool chunkSize mismatch pool expects ${this._chunkSize}, `
229
+ throw new RangeError(`leviathan-crypto: pool chunkSize mismatch, pool expects ${this._chunkSize}, `
213
230
  + `header says ${h.chunkSize}`);
214
231
  if (h.framed !== this._framed)
215
- throw new Error(`leviathan-crypto: pool framing mismatch pool is ${this._framed ? 'framed' : 'unframed'}, `
232
+ throw new Error(`leviathan-crypto: pool framing mismatch, pool is ${this._framed ? 'framed' : 'unframed'}, `
216
233
  + `header says ${h.framed ? 'framed' : 'unframed'}`);
217
234
  // Re-derive keys from the nonce embedded in this ciphertext's header.
218
- // The pool's _keys are tied to its own seal nonce for arbitrary incoming
235
+ // The pool's _keys are tied to its own seal nonce, for arbitrary incoming
219
236
  // ciphertext the nonce may differ, so we derive fresh keys here.
220
237
  if (!this._masterKey)
221
238
  throw new Error('leviathan-crypto: pool master key has been wiped');
222
- const openKeys = this._cipher.deriveKeys(this._masterKey, h.nonce);
239
+ const headerBytes = ciphertext.subarray(0, HEADER_SIZE);
240
+ const commitmentLen = this._cipher.commitmentSize;
241
+ const minLen = HEADER_SIZE + commitmentLen;
242
+ if (ciphertext.length < minLen)
243
+ throw new RangeError(`leviathan-crypto: ciphertext too short, need at least ${minLen} bytes for header + commitment`);
244
+ const openKeys = this._cipher.deriveKeys(this._masterKey, h.nonce, undefined, headerBytes);
223
245
  let openKeysWiped = false;
246
+ // Verify commitment before any chunk dispatch. Wrong key fails fast
247
+ // with AuthenticationError, before Poly1305 is consulted.
248
+ if (commitmentLen > 0) {
249
+ const derivedCommitment = openKeys.commitment;
250
+ if (!derivedCommitment || derivedCommitment.length !== commitmentLen) {
251
+ this._cipher.wipeKeys(openKeys);
252
+ throw new Error(`leviathan-crypto: ${this._cipher.formatName}.deriveKeys returned `
253
+ + `${derivedCommitment?.length ?? 'no'} commitment bytes, expected ${commitmentLen}`);
254
+ }
255
+ const recvCommitment = ciphertext.subarray(HEADER_SIZE, HEADER_SIZE + commitmentLen);
256
+ if (!constantTimeEqual(derivedCommitment, recvCommitment)) {
257
+ this._cipher.wipeKeys(openKeys);
258
+ const err = new AuthenticationError(`commitment-${this._cipher.formatName}`);
259
+ this._killAll(err);
260
+ throw err;
261
+ }
262
+ }
224
263
  try {
225
- // Strip header before chunk splitting
226
- const body = ciphertext.subarray(HEADER_SIZE);
264
+ // Strip header + commitment before chunk splitting
265
+ const body = ciphertext.subarray(HEADER_SIZE + commitmentLen);
227
266
  if (body.length === 0)
228
- throw new RangeError('leviathan-crypto: empty ciphertext seal() always produces at least one chunk');
267
+ throw new RangeError('leviathan-crypto: empty ciphertext, seal() always produces at least one chunk');
229
268
  // Compute max wire chunk size for per-chunk validation
230
269
  const tagSize = this._cipher.tagSize;
231
270
  const paddedSize = this._cipher.padded
@@ -265,7 +304,7 @@ export class SealStreamPool {
265
304
  const jobs = [];
266
305
  for (let i = 0; i < chunks.length; i++) {
267
306
  if (chunks[i].length < tagSize)
268
- throw new RangeError(`leviathan-crypto: chunk ${i} too short need at least ${tagSize} bytes for tag `
307
+ throw new RangeError(`leviathan-crypto: chunk ${i} too short, need at least ${tagSize} bytes for tag `
269
308
  + `(got ${chunks[i].length})`);
270
309
  if (chunks[i].length > maxWireChunk)
271
310
  throw new RangeError(`leviathan-crypto: chunk ${i} exceeds max wire size `
@@ -277,10 +316,8 @@ export class SealStreamPool {
277
316
  derivedKeyBytes: openKeys.bytes.slice(),
278
317
  }));
279
318
  }
280
- // All per-job key copies made wipe the main-thread openKeys immediately
281
- // rather than waiting for Promise.all. earlyWiped tracks this so the
282
- // finally below only fires on pre-dispatch throws (empty body, frame errors,
283
- // chunk validation), not as a redundant second call on the normal path.
319
+ // Per-job copies made; wipe main-thread openKeys immediately. openKeysWiped
320
+ // guards the finally so it only fires on pre-dispatch throws.
284
321
  this._cipher.wipeKeys(openKeys);
285
322
  openKeysWiped = true;
286
323
  const results = await Promise.all(jobs);
@@ -329,7 +366,7 @@ export class SealStreamPool {
329
366
  _send(worker, job) {
330
367
  const transfer = [];
331
368
  // Only transfer data.buffer when the Uint8Array owns the buffer exclusively.
332
- // Subarrays from open() share the caller's ciphertext buffer transferring
369
+ // Subarrays from open() share the caller's ciphertext buffer, transferring
333
370
  // one would detach all sibling views dispatched as parallel jobs.
334
371
  if (job.data.buffer instanceof ArrayBuffer
335
372
  && job.data.byteOffset === 0
@@ -340,7 +377,7 @@ export class SealStreamPool {
340
377
  transfer.push(job.counterNonce.buffer);
341
378
  if (job.derivedKeyBytes?.buffer instanceof ArrayBuffer)
342
379
  transfer.push(job.derivedKeyBytes.buffer);
343
- // aad is intentionally not transferred caller may retain the reference
380
+ // aad is intentionally not transferred, caller may retain the reference
344
381
  worker.postMessage(job, { transfer });
345
382
  }
346
383
  _onMessage(worker, e) {
@@ -379,15 +416,14 @@ export class SealStreamPool {
379
416
  reject(error);
380
417
  this._pending.clear();
381
418
  this._queue.length = 0;
382
- for (const w of this._workers) {
383
- try {
384
- w.postMessage({ type: 'wipe' });
385
- }
386
- catch { /* worker may be terminated */ }
387
- w.terminate();
388
- }
389
- this._workers.length = 0;
419
+ const workers = this._workers;
420
+ this._workers = [];
390
421
  this._idle.length = 0;
422
+ // Fire-and-forget: wipe each worker's key material, then terminate.
423
+ // On timeout, terminate anyway, the main-thread key handles are
424
+ // wiped below so the owning surface no longer has access.
425
+ for (const w of workers)
426
+ this._wipeThenTerminate(w);
391
427
  if (this._keys) {
392
428
  wipe(this._keys.bytes);
393
429
  this._keys = null;
@@ -397,4 +433,35 @@ export class SealStreamPool {
397
433
  this._masterKey = null;
398
434
  }
399
435
  }
436
+ _wipeThenTerminate(w) {
437
+ const WIPE_ACK_TIMEOUT_MS = 100;
438
+ let done = false;
439
+ // prefer-const false positive: `finish` (defined below) closes over
440
+ // `t` and is invoked synchronously from the catch path before the
441
+ // `t = setTimeout(...)` assignment runs.
442
+ // eslint-disable-next-line prefer-const
443
+ let t;
444
+ const finish = () => {
445
+ if (done)
446
+ return;
447
+ done = true;
448
+ if (t !== undefined)
449
+ clearTimeout(t);
450
+ w.removeEventListener('message', onMsg);
451
+ w.terminate();
452
+ };
453
+ const onMsg = (e) => {
454
+ if (e.data && e.data.type === 'wiped')
455
+ finish();
456
+ };
457
+ w.addEventListener('message', onMsg);
458
+ try {
459
+ w.postMessage({ type: 'wipe' });
460
+ }
461
+ catch {
462
+ finish();
463
+ return;
464
+ }
465
+ t = setTimeout(finish, WIPE_ACK_TIMEOUT_MS);
466
+ }
400
467
  }
@@ -1,6 +1,6 @@
1
1
  import type { CipherSuite, SealStreamOpts } from './types.js';
2
2
  export declare class SealStream {
3
- /** Preamble sent before the first chunk: header [|| kemCiphertext]. */
3
+ /** Preamble sent before the first chunk: header [|| kemCiphertext] [|| commitment]. */
4
4
  readonly preamble: Uint8Array;
5
5
  private readonly cipher;
6
6
  private readonly keys;
@@ -21,7 +21,7 @@
21
21
  //
22
22
  // src/ts/stream/seal-stream.ts
23
23
  //
24
- // SealStream cipher-agnostic streaming encryption using the STREAM
24
+ // SealStream, cipher-agnostic streaming encryption using the STREAM
25
25
  // construction (Hoang/Reyhanitabar/Rogaway/Vizár, CRYPTO 2015).
26
26
  import { randomBytes, concat } from '../utils.js';
27
27
  import { isInitialized } from '../init.js';
@@ -32,11 +32,11 @@ function u32beFrame(n) {
32
32
  new DataView(b.buffer).setUint32(0, n, false);
33
33
  return b;
34
34
  }
35
- // Module-level nonce injection slot used only by _fromNonce for KAT tests.
35
+ // Module-level nonce injection slot, used only by _fromNonce for KAT tests.
36
36
  // Set immediately before constructing, cleared inside the constructor.
37
37
  let _injectNonce;
38
38
  export class SealStream {
39
- /** Preamble sent before the first chunk: header [|| kemCiphertext]. */
39
+ /** Preamble sent before the first chunk: header [|| kemCiphertext] [|| commitment]. */
40
40
  preamble;
41
41
  cipher;
42
42
  keys;
@@ -49,7 +49,7 @@ export class SealStream {
49
49
  this.chunkSize = opts?.chunkSize ?? 65536;
50
50
  this.framed = opts?.framed ?? false;
51
51
  if (!isInitialized('sha2'))
52
- throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation '
52
+ throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation, '
53
53
  + 'call init({ sha2: ... }) before creating a SealStream');
54
54
  if (key.length !== cipher.keySize)
55
55
  throw new RangeError(`key must be ${cipher.keySize} bytes (got ${key.length})`);
@@ -57,14 +57,25 @@ export class SealStream {
57
57
  throw new RangeError(`chunkSize must be in [${CHUNK_MIN}, ${CHUNK_MAX}] (got ${this.chunkSize})`);
58
58
  const nonce = _injectNonce ?? randomBytes(16);
59
59
  _injectNonce = undefined;
60
- this.keys = cipher.deriveKeys(key, nonce);
61
- const kemCt = this.keys.kemCiphertext;
60
+ // Header must be built before deriveKeys, XChaCha20 binds it into
61
+ // the HKDF info string. SerpentCipher accepts and ignores it.
62
62
  const header = writeHeader(cipher.formatEnum, this.framed, nonce, this.chunkSize);
63
- this.preamble = kemCt ? concat(header, kemCt) : header;
63
+ this.keys = cipher.deriveKeys(key, nonce, undefined, header);
64
+ const kemCt = this.keys.kemCiphertext;
65
+ const commitment = cipher.commitmentSize > 0 ? this.keys.commitment : undefined;
66
+ if (cipher.commitmentSize > 0 && (!commitment || commitment.length !== cipher.commitmentSize))
67
+ throw new Error(`leviathan-crypto: ${cipher.formatName}.deriveKeys returned `
68
+ + `${commitment?.length ?? 'no'} commitment bytes, expected ${cipher.commitmentSize}`);
69
+ const parts = [header];
70
+ if (kemCt)
71
+ parts.push(kemCt);
72
+ if (commitment)
73
+ parts.push(commitment);
74
+ this.preamble = parts.length === 1 ? header : concat(...parts);
64
75
  }
65
76
  /**
66
77
  * @internal
67
- * KAT-only factory injects a fixed nonce so seal output is deterministic.
78
+ * KAT-only factory, injects a fixed nonce so seal output is deterministic.
68
79
  * Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
69
80
  */
70
81
  static _fromNonce(cipher, key, opts, nonce) {
@@ -80,30 +91,48 @@ export class SealStream {
80
91
  }
81
92
  push(chunk, opts) {
82
93
  if (this.state !== 'ready')
83
- throw new Error('SealStream: cannot push after finalize');
94
+ throw new Error(`SealStream: cannot push in state '${this.state}'`);
95
+ // Argument validation runs before the crypto-failure try/catch so a
96
+ // too-big chunk throws without wiping keys or transitioning to 'failed'.
97
+ // The caller can retry with a correctly-sized chunk.
84
98
  if (chunk.length > this.chunkSize)
85
99
  throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
86
- const nonce = makeCounterNonce(this.counter, TAG_DATA);
87
- const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
88
- this.counter++;
89
- return this.framed ? concat(u32beFrame(result.length), result) : result;
100
+ try {
101
+ const nonce = makeCounterNonce(this.counter, TAG_DATA);
102
+ const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
103
+ this.counter++;
104
+ return this.framed ? concat(u32beFrame(result.length), result) : result;
105
+ }
106
+ catch (err) {
107
+ this.cipher.wipeKeys(this.keys);
108
+ this.state = 'failed';
109
+ throw err;
110
+ }
90
111
  }
91
112
  finalize(chunk, opts) {
92
113
  if (this.state !== 'ready')
93
- throw new Error('SealStream: already finalized');
114
+ throw new Error(`SealStream: cannot finalize in state '${this.state}'`);
94
115
  if (chunk.length > this.chunkSize)
95
116
  throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
96
- const nonce = makeCounterNonce(this.counter, TAG_FINAL);
97
- const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
98
- this.cipher.wipeKeys(this.keys);
99
- this.state = 'finalized';
100
- return this.framed ? concat(u32beFrame(result.length), result) : result;
117
+ try {
118
+ const nonce = makeCounterNonce(this.counter, TAG_FINAL);
119
+ const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
120
+ this.cipher.wipeKeys(this.keys);
121
+ this.state = 'finalized';
122
+ return this.framed ? concat(u32beFrame(result.length), result) : result;
123
+ }
124
+ catch (err) {
125
+ this.cipher.wipeKeys(this.keys);
126
+ this.state = 'failed';
127
+ throw err;
128
+ }
101
129
  }
102
130
  dispose() {
103
131
  if (this.state === 'ready') {
104
132
  this.cipher.wipeKeys(this.keys);
105
133
  this.state = 'finalized';
106
134
  }
135
+ // 'failed' already wiped keys; 'finalized' already wiped keys, no-op.
107
136
  }
108
137
  toTransformStream() {
109
138
  let headerSent = false;
@@ -21,7 +21,7 @@
21
21
  //
22
22
  // src/ts/stream/seal.ts
23
23
  //
24
- // Seal unified single-shot encrypt/decrypt using the STREAM construction.
24
+ // Seal, unified single-shot encrypt/decrypt using the STREAM construction.
25
25
  // Seal blobs are valid SealStream blobs with a single final chunk.
26
26
  // OpenStream can decrypt a Seal blob without modification.
27
27
  import { concat } from '../utils.js';
@@ -32,7 +32,7 @@ import { HEADER_SIZE, CHUNK_MAX, CHUNK_MIN } from './constants.js';
32
32
  export class Seal {
33
33
  static encrypt(suite, key, pt, opts) {
34
34
  if (pt.length > CHUNK_MAX)
35
- throw new RangeError(`Seal.encrypt: plaintext exceeds maximum (${CHUNK_MAX} bytes) use SealStream for large data`);
35
+ throw new RangeError(`Seal.encrypt: plaintext exceeds maximum (${CHUNK_MAX} bytes), use SealStream for large data`);
36
36
  const sealer = new SealStream(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) });
37
37
  try {
38
38
  const ct = sealer.finalize(pt, opts);
@@ -43,9 +43,9 @@ export class Seal {
43
43
  }
44
44
  }
45
45
  static decrypt(suite, key, blob, opts) {
46
- const preambleLen = HEADER_SIZE + suite.kemCtSize;
46
+ const preambleLen = HEADER_SIZE + suite.kemCtSize + suite.commitmentSize;
47
47
  if (blob.length < preambleLen)
48
- throw new RangeError(`Seal.decrypt: blob too short need at least ${preambleLen} bytes (got ${blob.length})`);
48
+ throw new RangeError(`Seal.decrypt: blob too short, need at least ${preambleLen} bytes (got ${blob.length})`);
49
49
  const preamble = blob.subarray(0, preambleLen);
50
50
  const opener = new OpenStream(suite, key, preamble);
51
51
  try {
@@ -57,12 +57,12 @@ export class Seal {
57
57
  }
58
58
  /**
59
59
  * @internal
60
- * KAT-only injects a fixed nonce so output is deterministic.
60
+ * KAT-only, injects a fixed nonce so output is deterministic.
61
61
  * Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
62
62
  */
63
63
  static _fromNonce(suite, key, pt, nonce, opts) {
64
64
  if (pt.length > CHUNK_MAX)
65
- throw new RangeError(`Seal._fromNonce: plaintext exceeds maximum (${CHUNK_MAX} bytes) use SealStream for large data`);
65
+ throw new RangeError(`Seal._fromNonce: plaintext exceeds maximum (${CHUNK_MAX} bytes), use SealStream for large data`);
66
66
  const sealer = SealStream._fromNonce(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) }, nonce);
67
67
  try {
68
68
  const ct = sealer.finalize(pt, opts);
@@ -1,6 +1,7 @@
1
1
  export interface DerivedKeys {
2
2
  readonly bytes: Uint8Array;
3
3
  readonly kemCiphertext?: Uint8Array;
4
+ readonly commitment?: Uint8Array;
4
5
  }
5
6
  export interface CipherSuite {
6
7
  readonly formatEnum: number;
@@ -9,10 +10,11 @@ export interface CipherSuite {
9
10
  readonly keySize: number;
10
11
  readonly decKeySize?: number;
11
12
  readonly kemCtSize: number;
13
+ readonly commitmentSize: number;
12
14
  readonly tagSize: number;
13
15
  readonly padded: boolean;
14
16
  readonly wasmChunkSize: number;
15
- deriveKeys(key: Uint8Array, nonce: Uint8Array, kemCt?: Uint8Array): DerivedKeys;
17
+ deriveKeys(key: Uint8Array, nonce: Uint8Array, kemCt?: Uint8Array, header?: Uint8Array): DerivedKeys;
16
18
  sealChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
17
19
  openChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
18
20
  wipeKeys(keys: DerivedKeys): void;
@@ -21,6 +21,6 @@
21
21
  //
22
22
  // src/ts/stream/types.ts
23
23
  //
24
- // CipherSuite interface cipher-specific logic injected into SealStream
24
+ // CipherSuite interface, cipher-specific logic injected into SealStream
25
25
  // and OpenStream. Implementations are plain objects (not classes).
26
26
  export {};
package/dist/types.d.ts CHANGED
@@ -18,7 +18,28 @@ export interface Streamcipher {
18
18
  }
19
19
  export interface AEAD {
20
20
  encrypt(msg: Uint8Array, aad?: Uint8Array): Uint8Array;
21
- /** Decrypt and authenticate. Throws `Error` on authentication failure never returns null. */
21
+ /** Decrypt and authenticate. Throws `Error` on authentication failure, never returns null. */
22
22
  decrypt(ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
23
23
  dispose(): void;
24
24
  }
25
+ /**
26
+ * Stateless cipher PRF for Fortuna's generator slot. Produces `n` bytes of
27
+ * keystream from `(key, counter)` without mutating either input. Implementations
28
+ * are plain const objects, not classes.
29
+ */
30
+ export interface Generator {
31
+ readonly keySize: number;
32
+ readonly blockSize: number;
33
+ readonly counterSize: number;
34
+ readonly wasmModules: readonly string[];
35
+ generate(key: Uint8Array, counter: Uint8Array, n: number): Uint8Array;
36
+ }
37
+ /**
38
+ * Stateless hash function for Fortuna's accumulator and reseed slots. Output
39
+ * size must match the generator's key size when paired in Fortuna.
40
+ */
41
+ export interface HashFn {
42
+ readonly outputSize: number;
43
+ readonly wasmModules: readonly string[];
44
+ digest(msg: Uint8Array): Uint8Array;
45
+ }
package/dist/types.js CHANGED
@@ -22,5 +22,5 @@
22
22
  // src/ts/types.ts
23
23
  //
24
24
  // Primitive interfaces for leviathan-crypto.
25
- // No init() dependency available at import time.
25
+ // No init() dependency, available at import time.
26
26
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length input. */
1
+ /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length or non-hex input. */
2
2
  export declare const hexToBytes: (hex: string) => Uint8Array;
3
3
  /** Uint8Array to lowercase hex string. */
4
4
  export declare const bytesToHex: (bytes: Uint8Array) => string;
@@ -6,18 +6,17 @@ export declare const bytesToHex: (bytes: Uint8Array) => string;
6
6
  export declare const utf8ToBytes: (str: string) => Uint8Array;
7
7
  /** Uint8Array to UTF-8 string. */
8
8
  export declare const bytesToUtf8: (bytes: Uint8Array) => string;
9
- /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Returns undefined on invalid input. */
10
- export declare const base64ToBytes: (b64: string) => Uint8Array | undefined;
11
- /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 no padding characters). */
9
+ /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Throws RangeError on invalid input. */
10
+ export declare const base64ToBytes: (b64: string) => Uint8Array;
11
+ /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5, no padding characters). */
12
12
  export declare const bytesToBase64: (bytes: Uint8Array, url?: boolean) => string;
13
- export declare const CT_MAX_BYTES = 32768;
13
+ export declare const CTE_MAX_BYTES = 32768;
14
14
  /**
15
15
  * Constant-time byte-array equality.
16
- * Uses WASM SIMD when available (no JIT short-circuiting, no speculative
17
- * optimization). Falls back to a JS XOR-accumulate loop on runtimes
18
- * without SIMD support.
19
- * Length check is not constant-time (length is non-secret in all protocols).
20
- * Max input size: 32768 bytes per side (enforced regardless of code path).
16
+ * Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
17
+ * branch-free reduction). Throws on runtimes without SIMD support,
18
+ * no JS fallback. Length check is not constant-time (length is
19
+ * non-secret in all protocols). Max input size: 32768 bytes per side.
21
20
  */
22
21
  export declare const constantTimeEqual: (a: Uint8Array, b: Uint8Array) => boolean;
23
22
  /** Zero a typed array in place. */