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,154 @@
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/ratchet/skipped-key-store.ts
23
+ //
24
+ // SkippedKeyStore, MKSKIPPED cache for a single KDFChain (DR spec §3.2/§3.5).
25
+ // Manages out-of-order and skipped message key storage with transactional
26
+ // resolve via ResolveHandle. Split budgets: maxCacheSize bounds memory;
27
+ // maxSkipPerResolve bounds per-message HKDF work (DoS mitigation).
28
+ import { wipe } from '../utils.js';
29
+ // Best-effort wipe if a handle is GC'd without settling. GC is non-deterministic
30
+ // so this is a safety net only; the contract remains that callers MUST settle.
31
+ const finalizer = new FinalizationRegistry(key => wipe(key));
32
+ export class SkippedKeyStore {
33
+ _store;
34
+ _maxCacheSize;
35
+ _maxSkipPerResolve;
36
+ constructor(opts) {
37
+ const cache = opts?.maxCacheSize ?? opts?.ceiling ?? 100;
38
+ const skip = opts?.maxSkipPerResolve ?? opts?.ceiling ?? 50;
39
+ if (!Number.isSafeInteger(cache) || cache < 1)
40
+ throw new RangeError('SkippedKeyStore: maxCacheSize must be a safe integer >= 1');
41
+ if (!Number.isSafeInteger(skip) || skip < 1)
42
+ throw new RangeError('SkippedKeyStore: maxSkipPerResolve must be a safe integer >= 1');
43
+ if (skip > cache)
44
+ throw new RangeError(`SkippedKeyStore: maxSkipPerResolve (${skip}) must not exceed maxCacheSize (${cache})`);
45
+ this._store = new Map();
46
+ this._maxCacheSize = cache;
47
+ this._maxSkipPerResolve = skip;
48
+ }
49
+ // O(1) eviction, Map iteration is insertion order, and keys are inserted
50
+ // in strictly increasing counter order, so the first key yielded IS the
51
+ // oldest (lowest counter).
52
+ _evictOldest() {
53
+ const oldest = this._store.keys().next().value;
54
+ if (oldest === undefined)
55
+ return;
56
+ const val = this._store.get(oldest);
57
+ if (val !== undefined)
58
+ wipe(val);
59
+ this._store.delete(oldest);
60
+ }
61
+ // Resolve a message key for the given counter. Returns a ResolveHandle the
62
+ // caller settles via commit() (success, key wiped) or rollback()
63
+ // (failure, key returned to the store so a later legitimate message at
64
+ // the same counter can still decrypt).
65
+ //
66
+ // Three paths based on counter vs chain.n:
67
+ // in-order (=== n+1): step chain, wrap final key
68
+ // skip-ahead (> n+1): step chain storing intermediates, wrap final
69
+ // past (<= n): look up in map and delete; throw if absent
70
+ resolve(chain, counter) {
71
+ if (!Number.isSafeInteger(counter) || counter < 1)
72
+ throw new RangeError(`SkippedKeyStore: invalid counter ${counter}`);
73
+ let key;
74
+ if (counter === chain.n + 1) {
75
+ key = chain.step();
76
+ }
77
+ else if (counter > chain.n + 1) {
78
+ const skipNeeded = counter - chain.n - 1;
79
+ if (skipNeeded > this._maxSkipPerResolve)
80
+ throw new RangeError(`SkippedKeyStore: counter ${counter} requires ${skipNeeded} skip derivations, `
81
+ + `exceeds maxSkipPerResolve=${this._maxSkipPerResolve}`);
82
+ while (chain.n < counter - 1) {
83
+ const k = chain.step();
84
+ if (this._store.size >= this._maxCacheSize)
85
+ this._evictOldest();
86
+ this._store.set(chain.n, k);
87
+ }
88
+ key = chain.step();
89
+ }
90
+ else {
91
+ const stored = this._store.get(counter);
92
+ if (stored === undefined)
93
+ throw new Error(`SkippedKeyStore: unrecoverable. key for counter ${counter} not found`);
94
+ this._store.delete(counter);
95
+ key = stored;
96
+ }
97
+ return this._makeHandle(key, counter);
98
+ }
99
+ _makeHandle(key, counter) {
100
+ let settled = false;
101
+ const handle = {
102
+ get key() {
103
+ if (settled)
104
+ throw new Error('SkippedKeyStore: handle already settled');
105
+ return key;
106
+ },
107
+ commit: () => {
108
+ if (settled)
109
+ throw new Error('SkippedKeyStore: handle already settled');
110
+ settled = true;
111
+ finalizer.unregister(handle);
112
+ wipe(key);
113
+ },
114
+ rollback: () => {
115
+ if (settled)
116
+ throw new Error('SkippedKeyStore: handle already settled');
117
+ settled = true;
118
+ finalizer.unregister(handle);
119
+ if (this._store.size >= this._maxCacheSize)
120
+ this._evictOldest();
121
+ this._store.set(counter, key);
122
+ },
123
+ };
124
+ finalizer.register(handle, key, handle);
125
+ return handle;
126
+ }
127
+ // Step chain from its current position up to and including pn, storing each
128
+ // key. Used at epoch transitions so late-arriving old-epoch messages can
129
+ // still be decrypted. No-op when pn <= chain.n. Enforces maxSkipPerResolve
130
+ // so a malicious header can't force unbounded HKDF work.
131
+ advanceToBoundary(chain, pn) {
132
+ if (!Number.isSafeInteger(pn) || pn < 0)
133
+ throw new RangeError(`SkippedKeyStore: invalid pn ${pn}`);
134
+ const skipNeeded = pn - chain.n;
135
+ if (skipNeeded > this._maxSkipPerResolve)
136
+ throw new RangeError(`SkippedKeyStore: pn=${pn} requires ${skipNeeded} skip derivations, `
137
+ + `exceeds maxSkipPerResolve=${this._maxSkipPerResolve}`);
138
+ while (chain.n < pn) {
139
+ const key = chain.step();
140
+ if (this._store.size >= this._maxCacheSize)
141
+ this._evictOldest();
142
+ this._store.set(chain.n, key);
143
+ }
144
+ }
145
+ get size() {
146
+ return this._store.size;
147
+ }
148
+ // Wipe all stored key buffers and clear the map. Idempotent.
149
+ wipeAll() {
150
+ for (const v of this._store.values())
151
+ wipe(v);
152
+ this._store.clear();
153
+ }
154
+ }
@@ -0,0 +1,36 @@
1
+ export type { MlKemLike } from '../mlkem/suite.js';
2
+ export interface RatchetInitResult {
3
+ readonly nextRootKey: Uint8Array;
4
+ readonly sendChainKey: Uint8Array;
5
+ readonly recvChainKey: Uint8Array;
6
+ }
7
+ export interface KemEncapResult {
8
+ readonly nextRootKey: Uint8Array;
9
+ readonly sendChainKey: Uint8Array;
10
+ readonly recvChainKey: Uint8Array;
11
+ readonly kemCt: Uint8Array;
12
+ }
13
+ export interface KemDecapResult {
14
+ readonly nextRootKey: Uint8Array;
15
+ readonly sendChainKey: Uint8Array;
16
+ readonly recvChainKey: Uint8Array;
17
+ }
18
+ export interface RatchetMessageHeader {
19
+ readonly epoch: number;
20
+ readonly counter: number;
21
+ readonly pn?: number;
22
+ readonly kemCt?: Uint8Array;
23
+ }
24
+ export interface ResolveHandle {
25
+ readonly key: Uint8Array;
26
+ commit(): void;
27
+ rollback(): void;
28
+ }
29
+ export interface SkippedKeyStoreOpts {
30
+ /** Max keys held in cache. Default 100. Must be >= maxSkipPerResolve. */
31
+ maxCacheSize?: number;
32
+ /** Max skip-ahead derivations per resolve() call. Default 50. */
33
+ maxSkipPerResolve?: number;
34
+ /** @deprecated use maxCacheSize + maxSkipPerResolve. If provided, sets both. */
35
+ ceiling?: number;
36
+ }
@@ -0,0 +1,26 @@
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/ratchet/types.ts
23
+ //
24
+ // Shared types for the ratchet KDF construction.
25
+ // Re-exports MlKemLike so consumers import from one place.
26
+ export {};
@@ -1,4 +1,14 @@
1
1
  import type { CipherSuite } from '../stream/types.js';
2
+ /**
3
+ * `CipherSuite` implementation for the stream construction using Serpent-256.
4
+ *
5
+ * Each chunk is encrypted with Serpent-CBC (PKCS7) and authenticated with
6
+ * HMAC-SHA-256. Keys are derived via 3-way HKDF-SHA-256 (enc / mac / iv keys).
7
+ * Verify-then-decrypt ordering prevents padding oracle attacks (Vaudenay 2002).
8
+ *
9
+ * Pass to `SealStream` / `OpenStream` / `SealStreamPool` instead of constructing
10
+ * this object directly. Use `SerpentCipher.keygen()` to generate a 32-byte key.
11
+ */
2
12
  export declare const SerpentCipher: CipherSuite & {
3
13
  keygen(): Uint8Array;
4
14
  };
@@ -21,102 +21,190 @@
21
21
  //
22
22
  // src/ts/serpent/cipher-suite.ts
23
23
  //
24
- // SerpentCipher CipherSuite implementation for the STREAM construction.
24
+ // SerpentCipher, CipherSuite implementation for the STREAM construction.
25
25
  // 3-key HKDF derivation, HMAC-derived CBC IV, Serpent-CBC + HMAC-SHA-256.
26
26
  // Verify-then-decrypt ordering prevents padding oracle attacks (Vaudenay 2002).
27
- import { SerpentCbc } from './serpent-cbc.js';
28
- import { HKDF_SHA256, HMAC_SHA256 } from '../sha2/index.js';
27
+ //
28
+ // Salamander immunity: HMAC-SHA-256 is key-committing under SHA-256 collision
29
+ // resistance, so Serpent suites need no separate commitment in the preamble
30
+ // (commitmentSize: 0). Two distinct master keys cannot produce a tag that
31
+ // validates under both for the same chunk.
32
+ import { HKDF_SHA256 } from '../sha2/index.js';
29
33
  import { constantTimeEqual, wipe, concat, randomBytes } from '../utils.js';
30
34
  import { AuthenticationError } from '../errors.js';
31
- import { getInstance } from '../init.js';
32
- const INFO = new TextEncoder().encode('serpent-sealstream-v2');
35
+ import { getInstance, _assertNotOwned } from '../init.js';
36
+ import { hmacSha256, cbcEncryptChunk, cbcDecryptChunk, } from './shared-ops.js';
37
+ import { WORKER_SOURCE } from '../embedded/serpent-pool-worker.js';
38
+ const INFO = new TextEncoder().encode('serpent-sealstream-v3');
39
+ /**
40
+ * `CipherSuite` implementation for the stream construction using Serpent-256.
41
+ *
42
+ * Each chunk is encrypted with Serpent-CBC (PKCS7) and authenticated with
43
+ * HMAC-SHA-256. Keys are derived via 3-way HKDF-SHA-256 (enc / mac / iv keys).
44
+ * Verify-then-decrypt ordering prevents padding oracle attacks (Vaudenay 2002).
45
+ *
46
+ * Pass to `SealStream` / `OpenStream` / `SealStreamPool` instead of constructing
47
+ * this object directly. Use `SerpentCipher.keygen()` to generate a 32-byte key.
48
+ */
33
49
  export const SerpentCipher = {
34
50
  formatEnum: 0x02,
35
51
  formatName: 'serpent',
36
- hkdfInfo: 'serpent-sealstream-v2',
52
+ hkdfInfo: 'serpent-sealstream-v3',
37
53
  keySize: 32,
38
54
  kemCtSize: 0,
55
+ commitmentSize: 0,
39
56
  tagSize: 32,
40
57
  padded: true,
41
58
  wasmChunkSize: 65552, // src/asm/serpent/buffers.ts CHUNK_SIZE (65536 + 16 PKCS7 max overhead)
42
59
  wasmModules: ['serpent', 'sha2'],
60
+ /** Generate a random 32-byte master key suitable for use with `SerpentCipher`. @returns 32 cryptographically random bytes */
43
61
  keygen() {
44
62
  return randomBytes(32);
45
63
  },
46
- deriveKeys(masterKey, nonce, _kemCt) {
64
+ /**
65
+ * Derive 96 bytes of keying material from `masterKey` and `nonce` via HKDF-SHA-256.
66
+ * Layout: bytes[0:32]=enc_key, bytes[32:64]=mac_key, bytes[64:96]=iv_key.
67
+ * @param masterKey 32-byte master key
68
+ * @param nonce Stream nonce (16 bytes minimum)
69
+ * @returns `DerivedKeys` holding the 96-byte material
70
+ */
71
+ deriveKeys(masterKey, nonce, _kemCt, _header) {
47
72
  const hkdf = new HKDF_SHA256();
48
- const derived = hkdf.derive(masterKey, nonce, INFO, 96);
49
- hkdf.dispose();
73
+ let derived;
74
+ try {
75
+ derived = hkdf.derive(masterKey, nonce, INFO, 96);
76
+ }
77
+ finally {
78
+ hkdf.dispose();
79
+ }
50
80
  // bytes[0:32]=enc_key, bytes[32:64]=mac_key, bytes[64:96]=iv_key
51
81
  return { bytes: derived };
52
82
  },
83
+ /**
84
+ * Encrypt and authenticate one stream chunk.
85
+ * IV is derived from `counterNonce` via HMAC-SHA-256 with the iv_key.
86
+ * Output: ciphertext (PKCS7-padded) || 32-byte HMAC tag.
87
+ * @param keys Derived keys from `deriveKeys`
88
+ * @param counterNonce Per-chunk nonce (unique per chunk in the stream)
89
+ * @param chunk Plaintext chunk
90
+ * @param aad Optional additional authenticated data
91
+ * @returns Authenticated ciphertext
92
+ */
53
93
  sealChunk(keys, counterNonce, chunk, aad) {
94
+ // shared-ops functions operate directly on the module exports without
95
+ // going through `_acquireModule`. Assert no stateful instance owns
96
+ // either module before touching WASM memory.
97
+ _assertNotOwned('serpent');
98
+ _assertNotOwned('sha2');
99
+ const sx = getInstance('sha2').exports;
100
+ const kx = getInstance('serpent').exports;
54
101
  const encKey = keys.bytes.subarray(0, 32);
55
102
  const macKey = keys.bytes.subarray(32, 64);
56
103
  const ivKey = keys.bytes.subarray(64, 96);
57
104
  const aadBytes = aad ?? new Uint8Array(0);
58
- const hmac = new HMAC_SHA256();
59
- // Derive IV from counter nonce
60
- const ivFull = hmac.hash(ivKey, counterNonce);
61
- const iv = ivFull.slice(0, 16);
62
- wipe(ivFull);
63
- // Encrypt: Serpent-CBC with PKCS7 padding
64
- const cbc = new SerpentCbc({ dangerUnauthenticated: true });
65
- const ct = cbc.encrypt(encKey, iv, chunk);
66
- cbc.dispose();
67
- // Compute HMAC tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
68
- const aadLenBuf = new Uint8Array(4);
69
- new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
70
- const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
71
- const tag = hmac.hash(macKey, tagInput);
72
- hmac.dispose();
73
- wipe(iv);
74
- wipe(tagInput);
75
- // Output: ct || tag (IV is NOT included)
76
- return concat(ct, tag);
105
+ let iv;
106
+ let tagInput;
107
+ try {
108
+ // Derive IV from counter nonce
109
+ const ivFull = hmacSha256(sx, ivKey, counterNonce);
110
+ iv = ivFull.slice(0, 16);
111
+ wipe(ivFull);
112
+ // Encrypt: Serpent-CBC with PKCS7 padding
113
+ const ct = cbcEncryptChunk(kx, encKey, iv, chunk);
114
+ // Compute HMAC tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
115
+ const aadLenBuf = new Uint8Array(4);
116
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
117
+ tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
118
+ const tag = hmacSha256(sx, macKey, tagInput);
119
+ // Output: ct || tag (IV is NOT included)
120
+ return concat(ct, tag);
121
+ }
122
+ finally {
123
+ if (iv)
124
+ wipe(iv);
125
+ if (tagInput)
126
+ wipe(tagInput);
127
+ // No hmac/cbc instance to dispose, shared-ops functions are instance-free.
128
+ }
77
129
  },
130
+ /**
131
+ * Verify and decrypt one stream chunk. HMAC is verified before decryption
132
+ * to prevent padding oracle attacks (Vaudenay 2002). Throws
133
+ * `AuthenticationError` on tag mismatch.
134
+ * @param keys Derived keys from `deriveKeys`
135
+ * @param counterNonce Per-chunk nonce, must match the value used by `sealChunk`
136
+ * @param chunk Ciphertext || 32-byte HMAC tag
137
+ * @param aad Optional additional authenticated data
138
+ * @returns Plaintext with PKCS7 padding removed
139
+ */
78
140
  openChunk(keys, counterNonce, chunk, aad) {
79
141
  if (chunk.length < 32)
80
142
  throw new RangeError(`chunk too short for 32-byte tag (got ${chunk.length})`);
143
+ _assertNotOwned('serpent');
144
+ _assertNotOwned('sha2');
145
+ const sx = getInstance('sha2').exports;
146
+ const kx = getInstance('serpent').exports;
81
147
  const encKey = keys.bytes.subarray(0, 32);
82
148
  const macKey = keys.bytes.subarray(32, 64);
83
149
  const ivKey = keys.bytes.subarray(64, 96);
84
150
  const aadBytes = aad ?? new Uint8Array(0);
85
151
  const ct = chunk.subarray(0, chunk.length - 32);
86
152
  const receivedTag = chunk.subarray(chunk.length - 32);
87
- const hmac = new HMAC_SHA256();
88
- // Derive IV from counter nonce
89
- const ivFull = hmac.hash(ivKey, counterNonce);
90
- const iv = ivFull.slice(0, 16);
91
- wipe(ivFull);
92
- // Compute expected tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
93
- const aadLenBuf = new Uint8Array(4);
94
- new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
95
- const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
96
- const expectedTag = hmac.hash(macKey, tagInput);
97
- hmac.dispose();
98
- // CRITICAL: Verify HMAC BEFORE decrypting.
99
- // Evaluating PKCS7 padding on unauthenticated data is a padding oracle (Vaudenay 2002).
100
- if (!constantTimeEqual(expectedTag, receivedTag)) {
101
- wipe(iv);
102
- wipe(tagInput);
103
- wipe(expectedTag);
104
- getInstance('serpent').exports.wipeBuffers();
105
- throw new AuthenticationError('serpent');
153
+ let iv;
154
+ let tagInput;
155
+ let expectedTag;
156
+ try {
157
+ // Derive IV from counter nonce
158
+ const ivFull = hmacSha256(sx, ivKey, counterNonce);
159
+ iv = ivFull.slice(0, 16);
160
+ wipe(ivFull);
161
+ // Compute expected tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
162
+ const aadLenBuf = new Uint8Array(4);
163
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
164
+ tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
165
+ expectedTag = hmacSha256(sx, macKey, tagInput);
166
+ // CRITICAL: Verify HMAC BEFORE decrypting.
167
+ // Evaluating PKCS7 padding on unauthenticated data is a padding oracle (Vaudenay 2002).
168
+ // Belt-and-suspenders: explicit wipes here cover the auth-fail path before
169
+ // throwing; the finally block below covers every other path.
170
+ if (!constantTimeEqual(expectedTag, receivedTag)) {
171
+ wipe(iv);
172
+ wipe(tagInput);
173
+ wipe(expectedTag);
174
+ getInstance('serpent').exports.wipeBuffers();
175
+ throw new AuthenticationError('serpent');
176
+ }
177
+ // ONLY decrypt after authentication succeeds
178
+ return cbcDecryptChunk(kx, encKey, iv, ct);
179
+ }
180
+ finally {
181
+ if (iv)
182
+ wipe(iv);
183
+ if (tagInput)
184
+ wipe(tagInput);
185
+ if (expectedTag)
186
+ wipe(expectedTag);
106
187
  }
107
- wipe(tagInput);
108
- wipe(expectedTag);
109
- // ONLY decrypt after authentication succeeds
110
- const cbc = new SerpentCbc({ dangerUnauthenticated: true });
111
- const plaintext = cbc.decrypt(encKey, iv, ct);
112
- cbc.dispose();
113
- wipe(iv);
114
- return plaintext;
115
188
  },
189
+ /**
190
+ * Zero all derived key material in `keys`. Called by the stream layer on
191
+ * teardown and after auth failure.
192
+ * @param keys Derived keys to wipe
193
+ */
116
194
  wipeKeys(keys) {
117
195
  wipe(keys.bytes);
118
196
  },
197
+ /**
198
+ * Spawn a Serpent pool worker from the embedded IIFE bundle.
199
+ * The worker holds its own serpent + sha2 WASM instances.
200
+ * @returns Newly constructed `Worker` instance
201
+ */
119
202
  createPoolWorker() {
120
- return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
203
+ // See docs/architecture.md#pool-worker-spawn-pattern.
204
+ const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });
205
+ const url = URL.createObjectURL(blob);
206
+ const w = new Worker(url);
207
+ setTimeout(() => URL.revokeObjectURL(url), 0);
208
+ return w;
121
209
  },
122
210
  };
@@ -0,0 +1,12 @@
1
+ import type { Generator } from '../types.js';
2
+ /**
3
+ * Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
4
+ *
5
+ * Each 16-byte counter value is encrypted as a plaintext block to produce
6
+ * one block of pseudorandom output. Practical Cryptography (Ferguson &
7
+ * Schneier, 2003) §9.4.
8
+ *
9
+ * Pass to `Fortuna.create({ generator: SerpentGenerator, ... })`, do not
10
+ * call `generate()` directly outside of Fortuna.
11
+ */
12
+ export declare const SerpentGenerator: Generator;
@@ -0,0 +1,97 @@
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/serpent/generator.ts
23
+ //
24
+ // Practical Cryptography (Ferguson & Schneier, 2003) §9.4, generator
25
+ // Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
26
+ import { _assertNotOwned, getInstance } from '../init.js';
27
+ import { wipe } from '../utils.js';
28
+ /**
29
+ * Serpent-256 ECB counter-mode PRF for Fortuna's generator slot.
30
+ *
31
+ * Each 16-byte counter value is encrypted as a plaintext block to produce
32
+ * one block of pseudorandom output. Practical Cryptography (Ferguson &
33
+ * Schneier, 2003) §9.4.
34
+ *
35
+ * Pass to `Fortuna.create({ generator: SerpentGenerator, ... })`, do not
36
+ * call `generate()` directly outside of Fortuna.
37
+ */
38
+ export const SerpentGenerator = {
39
+ keySize: 32,
40
+ blockSize: 16,
41
+ counterSize: 16,
42
+ wasmModules: ['serpent'],
43
+ /**
44
+ * Generate `n` pseudorandom bytes by encrypting successive 16-byte counter
45
+ * values in ECB mode. The counter is incremented as a 128-bit little-endian
46
+ * integer after each block.
47
+ * @param key 32-byte Serpent-256 key
48
+ * @param counter 16-byte initial counter value (little-endian)
49
+ * @param n Number of bytes to generate (0 ≤ n ≤ 2^30)
50
+ * @returns `n` pseudorandom bytes
51
+ */
52
+ generate(key, counter, n) {
53
+ _assertNotOwned('serpent');
54
+ if (key.length !== 32)
55
+ throw new RangeError(`SerpentGenerator: key must be 32 bytes (got ${key.length})`);
56
+ if (counter.length !== 16)
57
+ throw new RangeError(`SerpentGenerator: counter must be 16 bytes (got ${counter.length})`);
58
+ if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 30)
59
+ throw new RangeError(`SerpentGenerator: n must be a non-negative safe integer <= 2^30 (got ${n})`);
60
+ const x = getInstance('serpent').exports;
61
+ const mem = new Uint8Array(x.memory.buffer);
62
+ const c = counter.slice();
63
+ try {
64
+ mem.set(key, x.getKeyOffset());
65
+ if (x.loadKey(32) !== 0)
66
+ throw new Error('SerpentGenerator: loadKey failed');
67
+ const blocks = Math.ceil(n / 16);
68
+ const output = new Uint8Array(n);
69
+ const ptOff = x.getBlockPtOffset();
70
+ const ctOff = x.getBlockCtOffset();
71
+ for (let i = 0; i < blocks; i++) {
72
+ mem.set(c, ptOff);
73
+ x.encryptBlock();
74
+ // Last-block trim: copy only what the caller asked for. The
75
+ // unused tail stays in WASM memory (wiped in finally) instead
76
+ // of landing on the JS heap where callers could reach it via
77
+ // `result.buffer`. Mirrors ChaCha20Generator's exact-size output.
78
+ const offset = i * 16;
79
+ const writeLen = Math.min(16, n - offset);
80
+ output.set(mem.subarray(ctOff, ctOff + writeLen), offset);
81
+ // Increment c as a 16-byte little-endian integer
82
+ for (let j = 0; j < 16; j++) {
83
+ if (++c[j] !== 0)
84
+ break;
85
+ }
86
+ }
87
+ return output;
88
+ }
89
+ finally {
90
+ // Wipe WASM key/key-schedule/last-block scratch and the JS-heap
91
+ // counter copy so secret-derived state does not outlive this call
92
+ // in either the WASM linear memory or the JS heap.
93
+ x.wipeBuffers();
94
+ wipe(c);
95
+ }
96
+ },
97
+ };