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
@@ -0,0 +1,60 @@
1
+ import type { SlhDsaParams } from './params.js';
2
+ import { type PreHashAlgorithm } from './prehash.js';
3
+ /**
4
+ * FIPS 205 §10.2.1 Algorithm 22 line 1 / §10.2.2 Algorithm 23 line 1,
5
+ * ctx must be ≤ 255 bytes (the byte that follows the domain separator in
6
+ * M' encodes |ctx|, so longer values cannot be represented). Throws
7
+ * `SigningError('sig-ctx-too-long')` per the SigningError discriminator contract.
8
+ */
9
+ export declare function validateContext(ctx: Uint8Array): void;
10
+ /**
11
+ * FIPS 205 §3.6.2, public key must be exactly pkBytes long for its
12
+ * parameter set. Throws here; the public verify*() surfaces catch the
13
+ * throw and return false so a wrong-length pk reads as "not a valid
14
+ * signature" rather than a caller error.
15
+ */
16
+ export declare function validatePublicKey(pk: Uint8Array, params: SlhDsaParams): void;
17
+ /**
18
+ * Signing key must be exactly skBytes long for its parameter set. Wrong
19
+ * length is a caller error (the caller produced this sk via keygen* or
20
+ * loaded it from storage they own); throw RangeError unconditionally.
21
+ */
22
+ export declare function validateSigningKey(sk: Uint8Array, params: SlhDsaParams): void;
23
+ /**
24
+ * FIPS 205 §3.6.2, signature must be exactly sigBytes long for its
25
+ * parameter set. Throws here; the public verify*() surfaces catch and
26
+ * return false (same protocol shape as wrong-length pk).
27
+ */
28
+ export declare function validateSignature(sig: Uint8Array, params: SlhDsaParams): void;
29
+ /**
30
+ * FIPS 205 §3.4 / §9.2 Algorithm 19 line 4, opt_rand (addrnd) must be
31
+ * exactly n bytes for the parameter set. Used by signDerand (the
32
+ * testing / CAVP API). Hedged sign supplies opt_rand internally;
33
+ * deterministic sign substitutes PK.seed; only signDerand exposes
34
+ * opt_rand to the caller.
35
+ */
36
+ export declare function validateRnd(rnd: Uint8Array, params: SlhDsaParams): void;
37
+ /**
38
+ * Confirms M is a Uint8Array. FIPS 205 places no length restriction on
39
+ * the message; M is absorbed into Hmsg via SHAKE256 / SHA-2 depending on
40
+ * the chosen instantiation (§11.2 SHAKE family in the shipped scope), so
41
+ * any byte length is admissible.
42
+ */
43
+ export declare function validateMessage(M: Uint8Array): void;
44
+ /**
45
+ * FIPS 205 §10.2.2 Algorithm 23 lines 9-20, the prehash PH_M passed to
46
+ * HashSLH-DSA's Sign_internal / Verify_internal must be exactly the
47
+ * digest size of `algo`. Used by the `*Prehashed` family where the
48
+ * caller computes PH externally; the non-prehashed family produces PH
49
+ * internally so this check is implicit.
50
+ *
51
+ * Throws `SigningError('sig-malformed-input')` on mismatch. The verify
52
+ * surface intercepts this to return false (a wrong-size digest is a
53
+ * structural mismatch, indistinguishable from a wrong signature), while
54
+ * the sign surface lets the throw propagate (the caller supplied bad
55
+ * input; that is a contract violation).
56
+ *
57
+ * Duplicated from `src/ts/mldsa/validate.ts:validateDigest`; extraction is
58
+ * deferred until a third consumer materialises.
59
+ */
60
+ export declare function validateDigest(digest: Uint8Array, algo: PreHashAlgorithm): void;
@@ -0,0 +1,127 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/slhdsa/validate.ts
23
+ //
24
+ // SLH-DSA caller-side input validation. Pure length / type checks. Mirrors
25
+ // `src/ts/mldsa/validate.ts`. Length checks throw RangeError; the public
26
+ // verify() / verifyHash() / verifyHashPrehashed() surfaces intercept pk /
27
+ // sig / digest length mismatches and return false instead of propagating the
28
+ // throw (FIPS 205 §3.6.2 / §10 structural-mismatch posture). ctx, sk, and
29
+ // rnd / opt_rand are caller contract violations and let the throw propagate.
30
+ import { SigningError } from '../errors.js';
31
+ import { digestSize } from './prehash.js';
32
+ /**
33
+ * FIPS 205 §10.2.1 Algorithm 22 line 1 / §10.2.2 Algorithm 23 line 1,
34
+ * ctx must be ≤ 255 bytes (the byte that follows the domain separator in
35
+ * M' encodes |ctx|, so longer values cannot be represented). Throws
36
+ * `SigningError('sig-ctx-too-long')` per the SigningError discriminator contract.
37
+ */
38
+ export function validateContext(ctx) {
39
+ if (!(ctx instanceof Uint8Array))
40
+ throw new TypeError('leviathan-crypto: ctx must be a Uint8Array');
41
+ if (ctx.length > 255)
42
+ throw new SigningError('sig-ctx-too-long', `leviathan-crypto: ctx must be ≤ 255 bytes (got ${ctx.length})`);
43
+ }
44
+ /**
45
+ * FIPS 205 §3.6.2, public key must be exactly pkBytes long for its
46
+ * parameter set. Throws here; the public verify*() surfaces catch the
47
+ * throw and return false so a wrong-length pk reads as "not a valid
48
+ * signature" rather than a caller error.
49
+ */
50
+ export function validatePublicKey(pk, params) {
51
+ if (!(pk instanceof Uint8Array))
52
+ throw new TypeError('leviathan-crypto: public key must be a Uint8Array');
53
+ if (pk.length !== params.pkBytes)
54
+ throw new RangeError(`leviathan-crypto: public key must be ${params.pkBytes} bytes for ${params.paramSet} `
55
+ + `(got ${pk.length})`);
56
+ }
57
+ /**
58
+ * Signing key must be exactly skBytes long for its parameter set. Wrong
59
+ * length is a caller error (the caller produced this sk via keygen* or
60
+ * loaded it from storage they own); throw RangeError unconditionally.
61
+ */
62
+ export function validateSigningKey(sk, params) {
63
+ if (!(sk instanceof Uint8Array))
64
+ throw new TypeError('leviathan-crypto: signing key must be a Uint8Array');
65
+ if (sk.length !== params.skBytes)
66
+ throw new RangeError(`leviathan-crypto: signing key must be ${params.skBytes} bytes for ${params.paramSet} `
67
+ + `(got ${sk.length})`);
68
+ }
69
+ /**
70
+ * FIPS 205 §3.6.2, signature must be exactly sigBytes long for its
71
+ * parameter set. Throws here; the public verify*() surfaces catch and
72
+ * return false (same protocol shape as wrong-length pk).
73
+ */
74
+ export function validateSignature(sig, params) {
75
+ if (!(sig instanceof Uint8Array))
76
+ throw new TypeError('leviathan-crypto: signature must be a Uint8Array');
77
+ if (sig.length !== params.sigBytes)
78
+ throw new RangeError(`leviathan-crypto: signature must be ${params.sigBytes} bytes for ${params.paramSet} `
79
+ + `(got ${sig.length})`);
80
+ }
81
+ /**
82
+ * FIPS 205 §3.4 / §9.2 Algorithm 19 line 4, opt_rand (addrnd) must be
83
+ * exactly n bytes for the parameter set. Used by signDerand (the
84
+ * testing / CAVP API). Hedged sign supplies opt_rand internally;
85
+ * deterministic sign substitutes PK.seed; only signDerand exposes
86
+ * opt_rand to the caller.
87
+ */
88
+ export function validateRnd(rnd, params) {
89
+ if (!(rnd instanceof Uint8Array))
90
+ throw new TypeError('leviathan-crypto: opt_rand must be a Uint8Array');
91
+ if (rnd.length !== params.n)
92
+ throw new RangeError(`leviathan-crypto: opt_rand must be ${params.n} bytes for ${params.paramSet} `
93
+ + `(got ${rnd.length})`);
94
+ }
95
+ /**
96
+ * Confirms M is a Uint8Array. FIPS 205 places no length restriction on
97
+ * the message; M is absorbed into Hmsg via SHAKE256 / SHA-2 depending on
98
+ * the chosen instantiation (§11.2 SHAKE family in the shipped scope), so
99
+ * any byte length is admissible.
100
+ */
101
+ export function validateMessage(M) {
102
+ if (!(M instanceof Uint8Array))
103
+ throw new TypeError('leviathan-crypto: message must be a Uint8Array');
104
+ }
105
+ /**
106
+ * FIPS 205 §10.2.2 Algorithm 23 lines 9-20, the prehash PH_M passed to
107
+ * HashSLH-DSA's Sign_internal / Verify_internal must be exactly the
108
+ * digest size of `algo`. Used by the `*Prehashed` family where the
109
+ * caller computes PH externally; the non-prehashed family produces PH
110
+ * internally so this check is implicit.
111
+ *
112
+ * Throws `SigningError('sig-malformed-input')` on mismatch. The verify
113
+ * surface intercepts this to return false (a wrong-size digest is a
114
+ * structural mismatch, indistinguishable from a wrong signature), while
115
+ * the sign surface lets the throw propagate (the caller supplied bad
116
+ * input; that is a contract violation).
117
+ *
118
+ * Duplicated from `src/ts/mldsa/validate.ts:validateDigest`; extraction is
119
+ * deferred until a third consumer materialises.
120
+ */
121
+ export function validateDigest(digest, algo) {
122
+ if (!(digest instanceof Uint8Array))
123
+ throw new SigningError('sig-malformed-input', 'digest must be a Uint8Array');
124
+ const expected = digestSize(algo);
125
+ if (digest.length !== expected)
126
+ throw new SigningError('sig-malformed-input', `digest length ${digest.length} != ${expected} for ${algo}`);
127
+ }
@@ -0,0 +1,32 @@
1
+ import type { SlhDsaExports } from './types.js';
2
+ import type { SlhDsaParams } from './params.js';
3
+ import { type PreHashAlgorithm } from './prehash.js';
4
+ /**
5
+ * Drive slhVerifyInternal with caller-built M' bytes.
6
+ *
7
+ * Layout written into INPUT (per src/asm/slhdsa/slh.ts §slhVerifyInternal):
8
+ * INPUT = pk (2n) ‖ M' (msgLen) ‖ sig (sigBytes)
9
+ *
10
+ * The caller has already filtered wrong-length pk and sig (the public
11
+ * verify*() surface returns false for those before calling this driver).
12
+ * Inside this function, length-mismatch only manifests if pk / sig were
13
+ * truncated post-validation; we defensively re-check to keep the WASM
14
+ * call within its declared INPUT bounds.
15
+ *
16
+ * Returns true iff slhVerifyInternal returns 1 (the §9.3 constant-time
17
+ * PK.root comparison succeeded plus all FORS / hypertree path checks).
18
+ */
19
+ export declare function slhVerifyInternalTs(x: SlhDsaExports, params: SlhDsaParams, pk: Uint8Array, MPrime: Uint8Array, sig: Uint8Array): boolean;
20
+ /**
21
+ * HashSLH-DSA verify, post-prehash. FIPS 205 §10.3 Algorithm 25 lines
22
+ * 16-19. Builds M' = 0x01 ‖ |ctx| ‖ ctx ‖ OID(algo) ‖ prehash and drives
23
+ * Verify_internal.
24
+ *
25
+ * Same return / throw posture as `slhVerifyInternalTs`: returns a pure
26
+ * boolean for every signature outcome. Caller (in index.ts) is expected
27
+ * to have already filtered wrong-length vk / sig / digest with the
28
+ * appropriate verdict (false) before calling this helper.
29
+ *
30
+ * The caller owns `prehash`; this helper never wipes it.
31
+ */
32
+ export declare function verifyWithPrehash(x: SlhDsaExports, params: SlhDsaParams, pk: Uint8Array, prehash: Uint8Array, sig: Uint8Array, algo: PreHashAlgorithm, ctx: Uint8Array): boolean;
@@ -0,0 +1,107 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/slhdsa/verify.ts
23
+ //
24
+ // SLH-DSA verify drivers, FIPS 205 §10.3 wrappers around the WASM
25
+ // slhVerifyInternal entry point (FIPS 205 §9.3 Algorithm 20).
26
+ //
27
+ // Two drivers:
28
+ // • slhVerifyInternalTs, writes (pk ‖ M' ‖ sig) into INPUT, runs
29
+ // slhVerifyInternal, returns a boolean. M' is the already-domain-
30
+ // separated bytes built by the caller.
31
+ // • verifyWithPrehash, builds the HashSLH-DSA M' from
32
+ // (digest, oid(ph), ctx) and forwards. Mirrors
33
+ // src/ts/mldsa/verify.ts:verifyWithPrehash.
34
+ //
35
+ // Posture: returns boolean. Any WASM exception during the inner call is
36
+ // swallowed and converted to false; FIPS 205 §10.3 / §3.6.2 model verify
37
+ // as a pure predicate. Caller-side contract violations (ctx > 255 bytes,
38
+ // unsupported ph) are surfaced at the public-method layer, not here.
39
+ //
40
+ // Buffer hygiene: INPUT held pk + M' + sig (all public inputs). The WASM
41
+ // wipe is still applied for consistency with the sign path and to clear
42
+ // any sk residue that might have lingered from a previous sign call.
43
+ import { wipe } from '../utils.js';
44
+ import { constructMPrimeHash } from './prehash.js';
45
+ /**
46
+ * Drive slhVerifyInternal with caller-built M' bytes.
47
+ *
48
+ * Layout written into INPUT (per src/asm/slhdsa/slh.ts §slhVerifyInternal):
49
+ * INPUT = pk (2n) ‖ M' (msgLen) ‖ sig (sigBytes)
50
+ *
51
+ * The caller has already filtered wrong-length pk and sig (the public
52
+ * verify*() surface returns false for those before calling this driver).
53
+ * Inside this function, length-mismatch only manifests if pk / sig were
54
+ * truncated post-validation; we defensively re-check to keep the WASM
55
+ * call within its declared INPUT bounds.
56
+ *
57
+ * Returns true iff slhVerifyInternal returns 1 (the §9.3 constant-time
58
+ * PK.root comparison succeeded plus all FORS / hypertree path checks).
59
+ */
60
+ export function slhVerifyInternalTs(x, params, pk, MPrime, sig) {
61
+ const inOff = x.getInputOffset();
62
+ const pkLen = params.pkBytes;
63
+ const msgLen = MPrime.length;
64
+ const sigLen = sig.length;
65
+ const inputTotal = pkLen + msgLen + sigLen;
66
+ const mem = new Uint8Array(x.memory.buffer);
67
+ try {
68
+ params.wasmSelector();
69
+ mem.set(pk, inOff);
70
+ mem.set(MPrime, inOff + pkLen);
71
+ mem.set(sig, inOff + pkLen + msgLen);
72
+ return x.slhVerifyInternal(msgLen) === 1;
73
+ }
74
+ catch {
75
+ // FIPS 205 §10.3 verify is a pure predicate. Any unexpected
76
+ // exception (out-of-memory, kernel argument error) is treated
77
+ // as "did not authenticate". The public surface already filters
78
+ // caller contract violations (ctx > 255) before reaching this
79
+ // driver, so swallowing here cannot mask a contract bug.
80
+ return false;
81
+ }
82
+ finally {
83
+ mem.fill(0, inOff, inOff + inputTotal);
84
+ x.wipeBuffers();
85
+ }
86
+ }
87
+ /**
88
+ * HashSLH-DSA verify, post-prehash. FIPS 205 §10.3 Algorithm 25 lines
89
+ * 16-19. Builds M' = 0x01 ‖ |ctx| ‖ ctx ‖ OID(algo) ‖ prehash and drives
90
+ * Verify_internal.
91
+ *
92
+ * Same return / throw posture as `slhVerifyInternalTs`: returns a pure
93
+ * boolean for every signature outcome. Caller (in index.ts) is expected
94
+ * to have already filtered wrong-length vk / sig / digest with the
95
+ * appropriate verdict (false) before calling this helper.
96
+ *
97
+ * The caller owns `prehash`; this helper never wipes it.
98
+ */
99
+ export function verifyWithPrehash(x, params, pk, prehash, sig, algo, ctx) {
100
+ const MPrime = constructMPrimeHash(prehash, algo, ctx);
101
+ try {
102
+ return slhVerifyInternalTs(x, params, pk, MPrime, sig);
103
+ }
104
+ finally {
105
+ wipe(MPrime);
106
+ }
107
+ }
Binary file
@@ -22,8 +22,8 @@
22
22
  // src/ts/stream/header.ts
23
23
  //
24
24
  // Wire format header encoding/decoding and counter nonce construction.
25
- import { FLAG_FRAMED, HEADER_SIZE, TAG_DATA, TAG_FINAL } from './constants.js';
26
- // The 16-byte nonce is a HKDF salt not a direct cipher nonce.
25
+ import { CHUNK_MAX, CHUNK_MIN, FLAG_FRAMED, HEADER_SIZE, TAG_DATA, TAG_FINAL } from './constants.js';
26
+ // The 16-byte nonce is a HKDF salt, not a direct cipher nonce.
27
27
  // Both XChaCha20Cipher and SerpentCipher derive their actual key material
28
28
  // and nonces from this value via HKDF-SHA-256. The 16-byte size is chosen
29
29
  // to satisfy HChaCha20's 16-byte input requirement while also serving as a
@@ -33,8 +33,8 @@ export function writeHeader(formatEnum, framed, nonce, chunkSize) {
33
33
  throw new RangeError(`formatEnum must be an integer in [0, 0x3f] (got ${formatEnum})`);
34
34
  if (nonce.length !== 16)
35
35
  throw new RangeError(`nonce must be 16 bytes (got ${nonce.length})`);
36
- if (!Number.isInteger(chunkSize) || chunkSize < 0 || chunkSize > 0xffffff)
37
- throw new RangeError(`chunkSize must be an integer in [0, 0xFFFFFF] (got ${chunkSize})`);
36
+ if (!Number.isInteger(chunkSize) || chunkSize < CHUNK_MIN || chunkSize > CHUNK_MAX)
37
+ throw new RangeError(`chunkSize must be an integer in [${CHUNK_MIN}, ${CHUNK_MAX}] (got ${chunkSize})`);
38
38
  const h = new Uint8Array(HEADER_SIZE);
39
39
  h[0] = (framed ? FLAG_FRAMED : 0) | formatEnum;
40
40
  h.set(nonce, 1);
@@ -49,7 +49,7 @@ export function readHeader(header) {
49
49
  throw new RangeError(`header must be exactly ${HEADER_SIZE} bytes (got ${header.length})`);
50
50
  const byte0 = header[0];
51
51
  if (byte0 & 0x40)
52
- throw new RangeError(`header has reserved bit 6 set (byte0=0x${byte0.toString(16).padStart(2, '0')}) unknown or malformed wire format`);
52
+ throw new RangeError(`header has reserved bit 6 set (byte0=0x${byte0.toString(16).padStart(2, '0')}), unknown or malformed wire format`);
53
53
  return {
54
54
  formatEnum: byte0 & 0x3f,
55
55
  framed: !!(byte0 & FLAG_FRAMED),
@@ -59,13 +59,13 @@ export function readHeader(header) {
59
59
  }
60
60
  /** 12-byte counter nonce: 11-byte BE counter + 1-byte final flag. */
61
61
  export function makeCounterNonce(counter, finalFlag) {
62
- if (!Number.isInteger(counter) || counter < 0 || counter > Number.MAX_SAFE_INTEGER)
63
- throw new RangeError(`counter must be an integer in [0, ${Number.MAX_SAFE_INTEGER}]`);
62
+ if (!Number.isSafeInteger(counter) || counter < 0)
63
+ throw new RangeError(`counter must be a safe integer in [0, ${Number.MAX_SAFE_INTEGER}]`);
64
64
  if (finalFlag !== TAG_DATA && finalFlag !== TAG_FINAL)
65
65
  throw new RangeError(`finalFlag must be TAG_DATA (0x00) or TAG_FINAL (0x01) (got 0x${finalFlag.toString(16).padStart(2, '0')})`);
66
66
  const n = new Uint8Array(12);
67
67
  // Write counter as 11-byte big-endian.
68
- // JS safe integers fit in 53 bits we only need the lower 53 bits.
68
+ // JS safe integers fit in 53 bits, we only need the lower 53 bits.
69
69
  // Pack from the right (byte 10 down to byte 0).
70
70
  let c = counter;
71
71
  for (let i = 10; i >= 0; i--) {
@@ -1,3 +1,4 @@
1
+ export { isInitialized } from '../init.js';
1
2
  export { SealStream } from './seal-stream.js';
2
3
  export { OpenStream } from './open-stream.js';
3
4
  export { Seal } from './seal.js';
@@ -20,6 +20,7 @@
20
20
  // ▀█████▀▀
21
21
  //
22
22
  // src/ts/stream/index.ts
23
+ export { isInitialized } from '../init.js';
23
24
  export { SealStream } from './seal-stream.js';
24
25
  export { OpenStream } from './open-stream.js';
25
26
  export { Seal } from './seal.js';
@@ -21,9 +21,11 @@
21
21
  //
22
22
  // src/ts/stream/open-stream.ts
23
23
  //
24
- // OpenStream cipher-agnostic streaming decryption using the STREAM
24
+ // OpenStream, cipher-agnostic streaming decryption using the STREAM
25
25
  // construction (Hoang/Reyhanitabar/Rogaway/Vizár, CRYPTO 2015).
26
26
  import { isInitialized } from '../init.js';
27
+ import { constantTimeEqual } from '../utils.js';
28
+ import { AuthenticationError } from '../errors.js';
27
29
  import { TAG_DATA, TAG_FINAL, CHUNK_MIN, CHUNK_MAX, HEADER_SIZE } from './constants.js';
28
30
  import { readHeader, makeCounterNonce } from './header.js';
29
31
  export class OpenStream {
@@ -37,15 +39,16 @@ export class OpenStream {
37
39
  constructor(cipher, key, preamble) {
38
40
  this.cipher = cipher;
39
41
  if (!isInitialized('sha2'))
40
- throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation '
42
+ throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation, '
41
43
  + 'call init({ sha2: ... }) before creating an OpenStream');
42
44
  const decKeySize = cipher.decKeySize ?? cipher.keySize;
43
45
  if (key.length !== decKeySize)
44
46
  throw new RangeError(`key must be ${decKeySize} bytes (got ${key.length})`);
45
- const expectedPreambleLen = HEADER_SIZE + cipher.kemCtSize;
47
+ const expectedPreambleLen = HEADER_SIZE + cipher.kemCtSize + cipher.commitmentSize;
46
48
  if (preamble.length !== expectedPreambleLen)
47
49
  throw new RangeError(`preamble must be exactly ${expectedPreambleLen} bytes (got ${preamble.length})`);
48
- const h = readHeader(preamble.subarray(0, HEADER_SIZE));
50
+ const headerBytes = preamble.subarray(0, HEADER_SIZE);
51
+ const h = readHeader(headerBytes);
49
52
  if (h.formatEnum !== cipher.formatEnum)
50
53
  throw new Error(`expected format 0x${cipher.formatEnum.toString(16).padStart(2, '0')} (${cipher.formatName}), `
51
54
  + `got 0x${h.formatEnum.toString(16).padStart(2, '0')}`);
@@ -54,7 +57,25 @@ export class OpenStream {
54
57
  const kemCt = cipher.kemCtSize > 0
55
58
  ? preamble.subarray(HEADER_SIZE, HEADER_SIZE + cipher.kemCtSize)
56
59
  : undefined;
57
- this.keys = cipher.deriveKeys(key, h.nonce, kemCt);
60
+ const commitmentOffset = HEADER_SIZE + cipher.kemCtSize;
61
+ const recvCommitment = cipher.commitmentSize > 0
62
+ ? preamble.subarray(commitmentOffset, commitmentOffset + cipher.commitmentSize)
63
+ : undefined;
64
+ this.keys = cipher.deriveKeys(key, h.nonce, kemCt, headerBytes);
65
+ // Verify commitment before any chunk is processed. Wrong key fails
66
+ // fast with AuthenticationError, before Poly1305 is consulted.
67
+ if (cipher.commitmentSize > 0) {
68
+ const derivedCommitment = this.keys.commitment;
69
+ if (!derivedCommitment || derivedCommitment.length !== cipher.commitmentSize) {
70
+ cipher.wipeKeys(this.keys);
71
+ throw new Error(`leviathan-crypto: ${cipher.formatName}.deriveKeys returned `
72
+ + `${derivedCommitment?.length ?? 'no'} commitment bytes, expected ${cipher.commitmentSize}`);
73
+ }
74
+ if (!recvCommitment || !constantTimeEqual(derivedCommitment, recvCommitment)) {
75
+ cipher.wipeKeys(this.keys);
76
+ throw new AuthenticationError(`commitment-${cipher.formatName}`);
77
+ }
78
+ }
58
79
  this.chunkSize = h.chunkSize;
59
80
  this.framed = h.framed;
60
81
  // Max ciphertext chunk: padded plaintext + tag
@@ -65,51 +86,73 @@ export class OpenStream {
65
86
  }
66
87
  pull(chunk, opts) {
67
88
  if (this.state !== 'ready')
68
- throw new Error('OpenStream: cannot pull after finalize');
89
+ throw new Error(`OpenStream: cannot pull in state '${this.state}'`);
90
+ // Argument and wire-format validation runs before the crypto-failure
91
+ // try/catch so a malformed chunk throws without wiping keys or
92
+ // transitioning to 'failed'. The caller can retry with a corrected
93
+ // chunk. Symmetric with SealStream.push.
69
94
  const data = this.framed ? this._stripFrame(chunk) : chunk;
70
95
  if (data.length < this.cipher.tagSize)
71
96
  throw new RangeError(`chunk too short to contain ${this.cipher.tagSize}-byte tag (got ${data.length} bytes)`);
72
97
  if (data.length > this.maxWireChunk)
73
98
  throw new RangeError(`chunk exceeds max wire size (${data.length} > ${this.maxWireChunk})`);
74
- const nonce = makeCounterNonce(this.counter, TAG_DATA);
75
- const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
76
- this.counter++;
77
- return plaintext;
99
+ try {
100
+ const nonce = makeCounterNonce(this.counter, TAG_DATA);
101
+ const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
102
+ this.counter++;
103
+ return plaintext;
104
+ }
105
+ catch (err) {
106
+ this.cipher.wipeKeys(this.keys);
107
+ this.state = 'failed';
108
+ throw err;
109
+ }
78
110
  }
79
111
  finalize(chunk, opts) {
80
112
  if (this.state !== 'ready')
81
- throw new Error('OpenStream: already finalized');
113
+ throw new Error(`OpenStream: cannot finalize in state '${this.state}'`);
82
114
  const data = this.framed ? this._stripFrame(chunk) : chunk;
83
115
  if (data.length < this.cipher.tagSize)
84
116
  throw new RangeError(`chunk too short to contain ${this.cipher.tagSize}-byte tag (got ${data.length} bytes)`);
85
117
  if (data.length > this.maxWireChunk)
86
118
  throw new RangeError(`chunk exceeds max wire size (${data.length} > ${this.maxWireChunk})`);
87
- const nonce = makeCounterNonce(this.counter, TAG_FINAL);
88
- const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
89
- this.cipher.wipeKeys(this.keys);
90
- this.state = 'finalized';
91
- return plaintext;
119
+ try {
120
+ const nonce = makeCounterNonce(this.counter, TAG_FINAL);
121
+ const plaintext = this.cipher.openChunk(this.keys, nonce, data, opts?.aad);
122
+ this.cipher.wipeKeys(this.keys);
123
+ this.state = 'finalized';
124
+ return plaintext;
125
+ }
126
+ catch (err) {
127
+ this.cipher.wipeKeys(this.keys);
128
+ this.state = 'failed';
129
+ throw err;
130
+ }
92
131
  }
93
132
  dispose() {
94
133
  if (this.state === 'ready') {
95
134
  this.cipher.wipeKeys(this.keys);
96
135
  this.state = 'finalized';
97
136
  }
137
+ // 'failed' already wiped keys; 'finalized' already wiped keys, no-op.
98
138
  }
99
139
  seek(index) {
100
140
  if (this.state !== 'ready')
101
- throw new Error('OpenStream: cannot seek after finalize');
102
- if (!Number.isInteger(index) || index < 0)
103
- throw new RangeError(`seek index must be a non-negative integer (got ${index})`);
141
+ throw new Error(`OpenStream: cannot seek in state '${this.state}'`);
142
+ if (!Number.isSafeInteger(index) || index < 0)
143
+ throw new RangeError(`seek index must be a non-negative safe integer ≤ Number.MAX_SAFE_INTEGER (got ${index})`);
144
+ if (index < this.counter)
145
+ throw new RangeError(`OpenStream: seek is forward-only, current counter ${this.counter}, requested ${index}. `
146
+ + 'Backward seeks would permit plaintext replay; construct a new OpenStream to restart.');
104
147
  this.counter = index;
105
148
  }
106
149
  _stripFrame(chunk) {
107
150
  if (chunk.length < 4)
108
- throw new RangeError(`framed chunk too short need at least 4 bytes (got ${chunk.length})`);
151
+ throw new RangeError(`framed chunk too short, need at least 4 bytes (got ${chunk.length})`);
109
152
  const dv = new DataView(chunk.buffer, chunk.byteOffset);
110
153
  const payloadLen = dv.getUint32(0, false);
111
154
  if (payloadLen !== chunk.length - 4)
112
- throw new RangeError(`framed chunk length mismatch prefix says ${payloadLen}, actual payload is ${chunk.length - 4}`);
155
+ throw new RangeError(`framed chunk length mismatch, prefix says ${payloadLen}, actual payload is ${chunk.length - 4}`);
113
156
  return chunk.subarray(4);
114
157
  }
115
158
  toTransformStream() {
@@ -133,7 +176,7 @@ export class OpenStream {
133
176
  controller.enqueue(this.finalize(buffered));
134
177
  }
135
178
  else {
136
- this.dispose(); // no chunks piped wipe keys, emit nothing
179
+ this.dispose(); // no chunks piped, wipe keys, emit nothing
137
180
  }
138
181
  }
139
182
  catch (err) {
@@ -13,6 +13,7 @@ export declare class SealStreamPool {
13
13
  private readonly _framed;
14
14
  private readonly _timeout;
15
15
  private readonly _header;
16
+ private readonly _commitment;
16
17
  private _workers;
17
18
  private _idle;
18
19
  private _queue;
@@ -35,4 +36,5 @@ export declare class SealStreamPool {
35
36
  private _onMessage;
36
37
  private _onError;
37
38
  private _killAll;
39
+ private _wipeThenTerminate;
38
40
  }