leviathan-crypto 2.1.0 → 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 (296) hide show
  1. package/CLAUDE.md +86 -443
  2. package/README.md +198 -65
  3. package/dist/aes/aes-cbc.d.ts +40 -0
  4. package/dist/aes/aes-cbc.js +158 -0
  5. package/dist/aes/aes-ctr.d.ts +50 -0
  6. package/dist/aes/aes-ctr.js +141 -0
  7. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  8. package/dist/aes/aes-gcm-siv.js +217 -0
  9. package/dist/aes/aes-gcm.d.ts +61 -0
  10. package/dist/aes/aes-gcm.js +226 -0
  11. package/dist/aes/cipher-suite.d.ts +21 -0
  12. package/dist/aes/cipher-suite.js +179 -0
  13. package/dist/aes/embedded.d.ts +1 -0
  14. package/dist/aes/embedded.js +26 -0
  15. package/dist/aes/generator.d.ts +14 -0
  16. package/dist/aes/generator.js +103 -0
  17. package/dist/aes/index.d.ts +58 -0
  18. package/dist/aes/index.js +125 -0
  19. package/dist/aes/ops.d.ts +60 -0
  20. package/dist/aes/ops.js +164 -0
  21. package/dist/aes/pool-worker.d.ts +1 -0
  22. package/dist/aes/pool-worker.js +92 -0
  23. package/dist/aes/types.d.ts +1 -0
  24. package/dist/aes/types.js +23 -0
  25. package/dist/aes.wasm +0 -0
  26. package/dist/blake3/embedded.d.ts +1 -0
  27. package/dist/blake3/embedded.js +26 -0
  28. package/dist/blake3/index.d.ts +143 -0
  29. package/dist/blake3/index.js +620 -0
  30. package/dist/blake3/types.d.ts +102 -0
  31. package/dist/blake3/types.js +31 -0
  32. package/dist/blake3/validate.d.ts +29 -0
  33. package/dist/blake3/validate.js +80 -0
  34. package/dist/blake3.wasm +0 -0
  35. package/dist/chacha20/cipher-suite.js +47 -25
  36. package/dist/chacha20/generator.d.ts +2 -2
  37. package/dist/chacha20/generator.js +4 -4
  38. package/dist/chacha20/index.d.ts +16 -15
  39. package/dist/chacha20/index.js +52 -46
  40. package/dist/chacha20/ops.d.ts +7 -7
  41. package/dist/chacha20/ops.js +34 -34
  42. package/dist/chacha20/pool-worker.js +5 -3
  43. package/dist/cte-wasm.d.ts +1 -0
  44. package/dist/cte-wasm.js +3 -0
  45. package/dist/curve25519.wasm +0 -0
  46. package/dist/ecdsa/der.d.ts +23 -0
  47. package/dist/ecdsa/der.js +192 -0
  48. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  49. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  50. package/dist/ecdsa/embedded.d.ts +1 -0
  51. package/dist/ecdsa/embedded.js +25 -0
  52. package/dist/ecdsa/index.d.ts +124 -0
  53. package/dist/ecdsa/index.js +366 -0
  54. package/dist/ecdsa/types.d.ts +31 -0
  55. package/dist/ecdsa/types.js +28 -0
  56. package/dist/ecdsa/validate.d.ts +18 -0
  57. package/dist/ecdsa/validate.js +92 -0
  58. package/dist/ed25519/embedded.d.ts +1 -0
  59. package/dist/ed25519/embedded.js +31 -0
  60. package/dist/ed25519/index.d.ts +70 -0
  61. package/dist/ed25519/index.js +308 -0
  62. package/dist/ed25519/types.d.ts +27 -0
  63. package/dist/ed25519/types.js +27 -0
  64. package/dist/ed25519/validate.d.ts +7 -0
  65. package/dist/ed25519/validate.js +77 -0
  66. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  67. package/dist/embedded/aes-pool-worker.js +5 -0
  68. package/dist/embedded/aes.d.ts +1 -0
  69. package/dist/embedded/aes.js +3 -0
  70. package/dist/embedded/blake3.d.ts +1 -0
  71. package/dist/embedded/blake3.js +3 -0
  72. package/dist/embedded/chacha20-pool-worker.d.ts +1 -1
  73. package/dist/embedded/chacha20-pool-worker.js +2 -2
  74. package/dist/embedded/chacha20.d.ts +1 -1
  75. package/dist/embedded/chacha20.js +2 -2
  76. package/dist/embedded/curve25519.d.ts +1 -0
  77. package/dist/embedded/curve25519.js +3 -0
  78. package/dist/embedded/mldsa.d.ts +1 -0
  79. package/dist/embedded/mldsa.js +3 -0
  80. package/dist/embedded/mlkem.d.ts +1 -0
  81. package/dist/embedded/mlkem.js +3 -0
  82. package/dist/embedded/p256.d.ts +1 -0
  83. package/dist/embedded/p256.js +3 -0
  84. package/dist/embedded/serpent-pool-worker.d.ts +1 -1
  85. package/dist/embedded/serpent-pool-worker.js +2 -2
  86. package/dist/embedded/serpent.d.ts +1 -1
  87. package/dist/embedded/serpent.js +2 -2
  88. package/dist/embedded/sha2.d.ts +1 -1
  89. package/dist/embedded/sha2.js +2 -2
  90. package/dist/embedded/sha3.d.ts +1 -1
  91. package/dist/embedded/sha3.js +2 -2
  92. package/dist/embedded/slhdsa.d.ts +1 -0
  93. package/dist/embedded/slhdsa.js +3 -0
  94. package/dist/errors.d.ts +92 -1
  95. package/dist/errors.js +111 -1
  96. package/dist/fortuna.d.ts +5 -5
  97. package/dist/fortuna.js +37 -64
  98. package/dist/index.d.ts +38 -9
  99. package/dist/index.js +63 -19
  100. package/dist/init.d.ts +1 -1
  101. package/dist/init.js +11 -25
  102. package/dist/keccak/embedded.js +1 -1
  103. package/dist/keccak/index.d.ts +2 -0
  104. package/dist/keccak/index.js +4 -2
  105. package/dist/loader.d.ts +1 -24
  106. package/dist/loader.js +13 -16
  107. package/dist/merkle/blake3-tree.d.ts +35 -0
  108. package/dist/merkle/blake3-tree.js +187 -0
  109. package/dist/merkle/checkpoint.d.ts +58 -0
  110. package/dist/merkle/checkpoint.js +217 -0
  111. package/dist/merkle/index.d.ts +19 -0
  112. package/dist/merkle/index.js +37 -0
  113. package/dist/merkle/merkle-log.d.ts +130 -0
  114. package/dist/merkle/merkle-log.js +207 -0
  115. package/dist/merkle/merkle-verifier.d.ts +126 -0
  116. package/dist/merkle/merkle-verifier.js +296 -0
  117. package/dist/merkle/proof.d.ts +70 -0
  118. package/dist/merkle/proof.js +300 -0
  119. package/dist/merkle/sha256-tree.d.ts +33 -0
  120. package/dist/merkle/sha256-tree.js +145 -0
  121. package/dist/merkle/signed-log.d.ts +156 -0
  122. package/dist/merkle/signed-log.js +356 -0
  123. package/dist/merkle/signed-note.d.ts +309 -0
  124. package/dist/merkle/signed-note.js +648 -0
  125. package/dist/merkle/sth.d.ts +31 -0
  126. package/dist/merkle/sth.js +31 -0
  127. package/dist/merkle/storage.d.ts +40 -0
  128. package/dist/merkle/storage.js +71 -0
  129. package/dist/merkle/tree.d.ts +68 -0
  130. package/dist/merkle/tree.js +94 -0
  131. package/dist/mldsa/embedded.d.ts +1 -0
  132. package/dist/{kyber → mldsa}/embedded.js +5 -5
  133. package/dist/mldsa/expand.d.ts +53 -0
  134. package/dist/mldsa/expand.js +188 -0
  135. package/dist/mldsa/format.d.ts +16 -0
  136. package/dist/mldsa/format.js +68 -0
  137. package/dist/mldsa/hashvariant.d.ts +32 -0
  138. package/dist/mldsa/hashvariant.js +248 -0
  139. package/dist/mldsa/index.d.ts +142 -0
  140. package/dist/mldsa/index.js +463 -0
  141. package/dist/mldsa/keygen.d.ts +16 -0
  142. package/dist/mldsa/keygen.js +232 -0
  143. package/dist/mldsa/params.d.ts +21 -0
  144. package/dist/mldsa/params.js +55 -0
  145. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  146. package/dist/mldsa/sha3-helpers.js +124 -0
  147. package/dist/mldsa/sign.d.ts +36 -0
  148. package/dist/mldsa/sign.js +380 -0
  149. package/dist/mldsa/types.d.ts +91 -0
  150. package/dist/mldsa/types.js +25 -0
  151. package/dist/mldsa/validate.d.ts +55 -0
  152. package/dist/mldsa/validate.js +125 -0
  153. package/dist/mldsa/verify.d.ts +29 -0
  154. package/dist/mldsa/verify.js +269 -0
  155. package/dist/mldsa.wasm +0 -0
  156. package/dist/mlkem/embedded.d.ts +1 -0
  157. package/dist/mlkem/embedded.js +27 -0
  158. package/dist/mlkem/indcpa.d.ts +49 -0
  159. package/dist/{kyber → mlkem}/indcpa.js +44 -44
  160. package/dist/mlkem/index.d.ts +37 -0
  161. package/dist/{kyber → mlkem}/index.js +24 -34
  162. package/dist/mlkem/kem.d.ts +21 -0
  163. package/dist/{kyber → mlkem}/kem.js +44 -64
  164. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  165. package/dist/{kyber → mlkem}/params.js +2 -2
  166. package/dist/mlkem/suite.d.ts +12 -0
  167. package/dist/{kyber → mlkem}/suite.js +17 -12
  168. package/dist/{kyber → mlkem}/types.d.ts +3 -3
  169. package/dist/{kyber → mlkem}/types.js +1 -1
  170. package/dist/{kyber → mlkem}/validate.d.ts +7 -7
  171. package/dist/{kyber → mlkem}/validate.js +7 -7
  172. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  173. package/dist/p256.wasm +0 -0
  174. package/dist/ratchet/index.d.ts +2 -0
  175. package/dist/ratchet/index.js +1 -0
  176. package/dist/ratchet/kdf-chain.js +3 -3
  177. package/dist/ratchet/ratchet-keypair.js +2 -2
  178. package/dist/ratchet/root-kdf.js +7 -7
  179. package/dist/ratchet/skipped-key-store.js +4 -4
  180. package/dist/ratchet/types.d.ts +1 -1
  181. package/dist/serpent/cipher-suite.js +20 -17
  182. package/dist/serpent/generator.d.ts +1 -1
  183. package/dist/serpent/generator.js +2 -2
  184. package/dist/serpent/index.d.ts +8 -7
  185. package/dist/serpent/index.js +18 -27
  186. package/dist/serpent/pool-worker.js +7 -5
  187. package/dist/serpent/serpent-cbc.d.ts +4 -4
  188. package/dist/serpent/serpent-cbc.js +11 -8
  189. package/dist/serpent/shared-ops.d.ts +3 -23
  190. package/dist/serpent/shared-ops.js +50 -85
  191. package/dist/serpent.wasm +0 -0
  192. package/dist/sha2/hkdf.js +5 -5
  193. package/dist/sha2/index.d.ts +21 -1
  194. package/dist/sha2/index.js +65 -10
  195. package/dist/sha2/types.d.ts +41 -2
  196. package/dist/sha2.wasm +0 -0
  197. package/dist/sha3/index.d.ts +72 -3
  198. package/dist/sha3/index.js +240 -14
  199. package/dist/sha3/kmac.d.ts +121 -0
  200. package/dist/sha3/kmac.js +800 -0
  201. package/dist/sha3.wasm +0 -0
  202. package/dist/shared/pkcs7.d.ts +22 -0
  203. package/dist/shared/pkcs7.js +84 -0
  204. package/dist/sign/ctx.d.ts +41 -0
  205. package/dist/sign/ctx.js +102 -0
  206. package/dist/sign/envelope.d.ts +45 -0
  207. package/dist/sign/envelope.js +152 -0
  208. package/dist/sign/hasher.d.ts +9 -0
  209. package/dist/sign/hasher.js +132 -0
  210. package/dist/sign/index.d.ts +11 -0
  211. package/dist/sign/index.js +34 -0
  212. package/dist/sign/sign-stream.d.ts +25 -0
  213. package/dist/sign/sign-stream.js +112 -0
  214. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  215. package/dist/sign/suites/ecdsa-p256.js +120 -0
  216. package/dist/sign/suites/ed25519.d.ts +3 -0
  217. package/dist/sign/suites/ed25519.js +165 -0
  218. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  219. package/dist/sign/suites/hybrid-classical.js +526 -0
  220. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  221. package/dist/sign/suites/hybrid-pq.js +234 -0
  222. package/dist/sign/suites/mldsa.d.ts +7 -0
  223. package/dist/sign/suites/mldsa.js +161 -0
  224. package/dist/sign/suites/slhdsa.d.ts +7 -0
  225. package/dist/sign/suites/slhdsa.js +176 -0
  226. package/dist/sign/types.d.ts +106 -0
  227. package/dist/sign/types.js +28 -0
  228. package/dist/sign/verify-stream.d.ts +30 -0
  229. package/dist/sign/verify-stream.js +227 -0
  230. package/dist/slhdsa/embedded.d.ts +1 -0
  231. package/dist/slhdsa/embedded.js +26 -0
  232. package/dist/slhdsa/index.d.ts +149 -0
  233. package/dist/slhdsa/index.js +493 -0
  234. package/dist/slhdsa/params.d.ts +26 -0
  235. package/dist/slhdsa/params.js +70 -0
  236. package/dist/slhdsa/prehash.d.ts +68 -0
  237. package/dist/slhdsa/prehash.js +307 -0
  238. package/dist/slhdsa/sign.d.ts +39 -0
  239. package/dist/slhdsa/sign.js +116 -0
  240. package/dist/slhdsa/types.d.ts +129 -0
  241. package/dist/slhdsa/types.js +27 -0
  242. package/dist/slhdsa/validate.d.ts +60 -0
  243. package/dist/slhdsa/validate.js +127 -0
  244. package/dist/slhdsa/verify.d.ts +32 -0
  245. package/dist/slhdsa/verify.js +107 -0
  246. package/dist/slhdsa.wasm +0 -0
  247. package/dist/stream/header.js +3 -3
  248. package/dist/stream/index.d.ts +1 -0
  249. package/dist/stream/index.js +1 -0
  250. package/dist/stream/open-stream.js +31 -10
  251. package/dist/stream/seal-stream-pool.d.ts +1 -0
  252. package/dist/stream/seal-stream-pool.js +63 -26
  253. package/dist/stream/seal-stream.d.ts +1 -1
  254. package/dist/stream/seal-stream.js +20 -9
  255. package/dist/stream/seal.js +6 -6
  256. package/dist/stream/types.d.ts +3 -1
  257. package/dist/stream/types.js +1 -1
  258. package/dist/types.d.ts +1 -1
  259. package/dist/types.js +1 -1
  260. package/dist/utils.d.ts +3 -3
  261. package/dist/utils.js +46 -54
  262. package/dist/wasm-source.d.ts +7 -7
  263. package/dist/wasm-source.js +1 -1
  264. package/dist/x25519/embedded.d.ts +1 -0
  265. package/dist/x25519/embedded.js +31 -0
  266. package/dist/x25519/index.d.ts +43 -0
  267. package/dist/x25519/index.js +159 -0
  268. package/dist/x25519/types.d.ts +25 -0
  269. package/dist/x25519/types.js +27 -0
  270. package/dist/x25519/validate.d.ts +2 -0
  271. package/dist/x25519/validate.js +39 -0
  272. package/package.json +70 -26
  273. package/SECURITY.md +0 -163
  274. package/dist/ct-wasm.d.ts +0 -1
  275. package/dist/ct-wasm.js +0 -3
  276. package/dist/docs/aead.md +0 -363
  277. package/dist/docs/architecture.md +0 -1011
  278. package/dist/docs/argon2id.md +0 -305
  279. package/dist/docs/chacha20.md +0 -781
  280. package/dist/docs/exports.md +0 -277
  281. package/dist/docs/fortuna.md +0 -530
  282. package/dist/docs/init.md +0 -301
  283. package/dist/docs/loader.md +0 -256
  284. package/dist/docs/serpent.md +0 -617
  285. package/dist/docs/sha2.md +0 -671
  286. package/dist/docs/sha3.md +0 -612
  287. package/dist/docs/types.md +0 -416
  288. package/dist/docs/utils.md +0 -457
  289. package/dist/embedded/kyber.d.ts +0 -1
  290. package/dist/embedded/kyber.js +0 -3
  291. package/dist/kyber/embedded.d.ts +0 -1
  292. package/dist/kyber/indcpa.d.ts +0 -49
  293. package/dist/kyber/index.d.ts +0 -38
  294. package/dist/kyber/kem.d.ts +0 -21
  295. package/dist/kyber/suite.d.ts +0 -12
  296. /package/dist/{ct.wasm → cte.wasm} +0 -0
@@ -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) {
@@ -383,7 +420,7 @@ export class SealStreamPool {
383
420
  this._workers = [];
384
421
  this._idle.length = 0;
385
422
  // Fire-and-forget: wipe each worker's key material, then terminate.
386
- // On timeout, terminate anyway the main-thread key handles are
423
+ // On timeout, terminate anyway, the main-thread key handles are
387
424
  // wiped below so the owning surface no longer has access.
388
425
  for (const w of workers)
389
426
  this._wipeThenTerminate(w);
@@ -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) {
@@ -121,7 +132,7 @@ export class SealStream {
121
132
  this.cipher.wipeKeys(this.keys);
122
133
  this.state = 'finalized';
123
134
  }
124
- // 'failed' already wiped keys; 'finalized' already wiped keys no-op.
135
+ // 'failed' already wiped keys; 'finalized' already wiped keys, no-op.
125
136
  }
126
137
  toTransformStream() {
127
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,7 @@ 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
  }
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
@@ -8,13 +8,13 @@ export declare const utf8ToBytes: (str: string) => Uint8Array;
8
8
  export declare const bytesToUtf8: (bytes: Uint8Array) => string;
9
9
  /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Throws RangeError on invalid input. */
10
10
  export declare const base64ToBytes: (b64: string) => Uint8Array;
11
- /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 no padding characters). */
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
16
  * Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
17
- * branch-free reduction). Throws on runtimes without SIMD support
17
+ * branch-free reduction). Throws on runtimes without SIMD support,
18
18
  * no JS fallback. Length check is not constant-time (length is
19
19
  * non-secret in all protocols). Max input size: 32768 bytes per side.
20
20
  */
package/dist/utils.js CHANGED
@@ -21,17 +21,16 @@
21
21
  //
22
22
  // src/ts/utils.ts
23
23
  //
24
- // Pure TypeScript utilities no init() dependency.
25
- // Ported from leviathan/src/base.ts (Convert namespace, Util namespace, constantTimeEqual).
24
+ // Pure TypeScript utilities, no init() dependency.
26
25
  // ── Encoding ────────────────────────────────────────────────────────────────
27
26
  /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length or non-hex input. */
28
27
  export const hexToBytes = (hex) => {
29
28
  if (hex.startsWith('0x') || hex.startsWith('0X'))
30
29
  hex = hex.slice(2);
31
30
  if (hex.length % 2)
32
- throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars) input must be an even-length hex string`);
31
+ throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars), input must be an even-length hex string`);
33
32
  // parseInt('0g', 16) returns 0 (not NaN) because it stops at the first
34
- // invalid char silent wrong-answer. Reject non-hex chars up front.
33
+ // invalid char, silent wrong-answer. Reject non-hex chars up front.
35
34
  if (hex.length > 0 && !/^[0-9a-fA-F]*$/.test(hex))
36
35
  throw new RangeError('hexToBytes: input contains non-hex characters');
37
36
  const bin = new Uint8Array(hex.length >>> 1);
@@ -114,7 +113,7 @@ export const base64ToBytes = (b64) => {
114
113
  }
115
114
  return bin;
116
115
  };
117
- /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 no padding characters). */
116
+ /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5, no padding characters). */
118
117
  export const bytesToBase64 = (bytes, url = false) => {
119
118
  if (typeof btoa !== 'undefined') {
120
119
  const raw = btoa(String.fromCharCode.apply(null, Array.from(bytes)));
@@ -141,88 +140,81 @@ export const bytesToBase64 = (bytes, url = false) => {
141
140
  return base64;
142
141
  };
143
142
  // ── Constant-time comparison ────────────────────────────────────────────────
144
- import { CT_WASM } from './ct-wasm.js';
145
- let _ctCompare = null;
146
- let _ctMem = null;
147
- let _ctInit = false;
148
- let _ctInitError = null;
149
- // CT WASM module uses 1 page (64KB) of linear memory with both buffers
150
- // laid out side-by-side: a at offset 0, b at offset a.length.
151
- // Max per-side = _ctMem.buffer.byteLength >>> 1 = 32768 bytes.
152
- // In practice the largest comparison is a 32-byte HMAC-SHA-256 tag.
153
- export const CT_MAX_BYTES = 32768;
154
- /**
155
- * Compile and instantiate the SIMD WASM ct module. On failure, caches the
156
- * branded error and re-throws on every subsequent call; no retries, no
157
- * fallback. Throws on runtimes without WebAssembly SIMD and on any
158
- * instantiation error.
159
- */
160
- function _initCt() {
161
- if (_ctInit) {
162
- if (_ctInitError)
163
- throw _ctInitError;
143
+ import { CTE_WASM } from './cte-wasm.js';
144
+ let _cteCompare = null;
145
+ let _cteMem = null;
146
+ let _cteMemView = null;
147
+ let _cteInit = false;
148
+ let _cteInitError = null;
149
+ export const CTE_MAX_BYTES = 32768;
150
+ function _initCte() {
151
+ if (_cteInit) {
152
+ if (_cteInitError)
153
+ throw _cteInitError;
164
154
  return;
165
155
  }
166
- _ctInit = true;
156
+ _cteInit = true;
167
157
  if (!hasSIMD()) {
168
- _ctInitError = new Error('leviathan-crypto: constantTimeEqual requires WebAssembly SIMD '
158
+ _cteInitError = new Error('leviathan-crypto: constantTimeEqual requires WebAssembly SIMD, '
169
159
  + 'this runtime does not support it');
170
- throw _ctInitError;
160
+ throw _cteInitError;
171
161
  }
172
162
  try {
173
- const buf = CT_WASM.buffer.slice(CT_WASM.byteOffset, CT_WASM.byteOffset + CT_WASM.byteLength);
163
+ const buf = CTE_WASM.buffer.slice(CTE_WASM.byteOffset, CTE_WASM.byteOffset + CTE_WASM.byteLength);
174
164
  const mod = new WebAssembly.Module(buf);
175
165
  const inst = new WebAssembly.Instance(mod);
176
166
  const exports = inst.exports;
177
- _ctMem = exports.memory;
178
- _ctCompare = exports.compare;
167
+ _cteMem = exports.memory;
168
+ _cteMemView = new Uint8Array(_cteMem.buffer);
169
+ _cteCompare = exports.compare;
179
170
  }
180
171
  catch (cause) {
181
- _ctInitError = new Error(`leviathan-crypto: ct WASM module failed to instantiate: ${cause.message}`);
182
- throw _ctInitError;
172
+ _cteInitError = new Error(`leviathan-crypto: cte WASM module failed to instantiate: ${cause.message}`);
173
+ throw _cteInitError;
183
174
  }
184
175
  }
185
176
  /**
186
177
  * Constant-time byte-array equality.
187
178
  * Runs entirely inside a WASM SIMD module (v128 XOR accumulate with
188
- * branch-free reduction). Throws on runtimes without SIMD support
179
+ * branch-free reduction). Throws on runtimes without SIMD support,
189
180
  * no JS fallback. Length check is not constant-time (length is
190
181
  * non-secret in all protocols). Max input size: 32768 bytes per side.
191
182
  */
192
183
  export const constantTimeEqual = (a, b) => {
193
184
  if (a.length !== b.length)
194
185
  return false;
195
- if (a.length > CT_MAX_BYTES)
196
- throw new RangeError(`constantTimeEqual: max ${CT_MAX_BYTES} bytes (got ${a.length})`);
197
- _initCt();
198
- // Copy module-level refs to locals. _initCt() either populates both
199
- // _ctMem and _ctCompare or throws; the null check below is a defensive
200
- // invariant guard that is unreachable on a correctly-initialized module.
201
- const memObj = _ctMem;
202
- const compare = _ctCompare;
203
- if (!memObj || !compare)
204
- throw new Error('leviathan-crypto: ct init invariant violated');
205
- const mem = new Uint8Array(memObj.buffer);
186
+ if (a.length > CTE_MAX_BYTES)
187
+ throw new RangeError(`constantTimeEqual: max ${CTE_MAX_BYTES} bytes (got ${a.length})`);
188
+ _initCte();
189
+ const mem = _cteMemView;
190
+ const compare = _cteCompare;
191
+ if (!mem || !compare)
192
+ throw new Error('leviathan-crypto: cte init invariant violated');
206
193
  mem.set(a, 0);
207
194
  mem.set(b, a.length);
208
195
  try {
209
196
  return compare(0, a.length, a.length) === 1;
210
197
  }
211
198
  finally {
212
- mem.fill(0, 0, a.length * 2);
199
+ // Wipe the full cte memory region, not just the bytes we wrote.
200
+ // Defense in depth against stale residue from longer prior calls.
201
+ // The module-private WASM memory is never read from outside this
202
+ // function, but we keep the surface clean regardless.
203
+ mem.fill(0, 0, CTE_MAX_BYTES * 2);
213
204
  }
214
205
  };
215
206
  /**
216
- * Reset the internal CT WASM cache, including any cached initialization
207
+ * Reset the internal CTE WASM cache, including any cached initialization
217
208
  * error. Exists so the test suite can force re-instantiation across
218
209
  * describe blocks.
219
210
  * @internal
220
211
  */
221
- export function _ctResetForTesting() {
222
- _ctInit = false;
223
- _ctCompare = null;
224
- _ctMem = null;
225
- _ctInitError = null;
212
+ export function _cteResetForTesting() {
213
+ _cteInit = false;
214
+ _cteCompare = null;
215
+ _cteMem = null;
216
+ _cteMemView = null;
217
+ _cteInitError = null;
226
218
  }
227
219
  /** Zero a typed array in place. */
228
220
  export const wipe = (data) => {
@@ -249,7 +241,7 @@ export const concat = (...arrays) => {
249
241
  export const randomBytes = (n) => {
250
242
  if (typeof globalThis.crypto === 'undefined'
251
243
  || typeof globalThis.crypto.getRandomValues !== 'function')
252
- throw new Error('leviathan-crypto: crypto.getRandomValues is required '
244
+ throw new Error('leviathan-crypto: crypto.getRandomValues is required, '
253
245
  + 'this runtime does not expose the Web Crypto API');
254
246
  const buf = new Uint8Array(n);
255
247
  globalThis.crypto.getRandomValues(buf);
@@ -269,7 +261,7 @@ export function hasSIMD() {
269
261
  _simd = false;
270
262
  return _simd;
271
263
  }
272
- // Minimal WASM module using v128 validates iff SIMD is supported
264
+ // Minimal WASM module using v128, validates iff SIMD is supported
273
265
  try {
274
266
  _simd = WebAssembly.validate(new Uint8Array([
275
267
  0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * All accepted forms of WASM input for init functions.
3
3
  *
4
- * - `string` gzip+base64 embedded blob (from `/embedded` subpath)
5
- * - `URL` streaming-compiled from `fetch(url)`
6
- * - `ArrayBuffer` raw WASM bytes, compiled inline
7
- * - `Uint8Array` raw WASM bytes, compiled inline
8
- * - `WebAssembly.Module` pre-compiled module (Cloudflare Workers, edge runtimes)
9
- * - `Response` streaming-compiled from an in-flight fetch
10
- * - `PromiseLike<WasmSource>` any thenable resolving to another `WasmSource`; nesting
4
+ * - `string` , gzip+base64 embedded blob (from `/embedded` subpath)
5
+ * - `URL` , streaming-compiled from `fetch(url)`
6
+ * - `ArrayBuffer` , raw WASM bytes, compiled inline
7
+ * - `Uint8Array` , raw WASM bytes, compiled inline
8
+ * - `WebAssembly.Module` , pre-compiled module (Cloudflare Workers, edge runtimes)
9
+ * - `Response` , streaming-compiled from an in-flight fetch
10
+ * - `PromiseLike<WasmSource>` , any thenable resolving to another `WasmSource`; nesting
11
11
  * is resolved recursively (max depth 3).
12
12
  */
13
13
  export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | PromiseLike<WasmSource>;
@@ -22,5 +22,5 @@
22
22
  // src/ts/wasm-source.ts
23
23
  //
24
24
  // Union type for all accepted WASM loading strategies.
25
- // The argument type determines the loading path no mode string required.
25
+ // The argument type determines the loading path, no mode string required.
26
26
  export {};
@@ -0,0 +1 @@
1
+ export { WASM_GZ_BASE64 as curve25519Wasm, WASM_GZ_BASE64 as x25519Wasm, } from '../embedded/curve25519.js';