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,158 @@
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/aes/aes-cbc.ts
23
+ //
24
+ // AESCbc, AES-128/192/256 CBC + PKCS7, stateful TS wrapper.
25
+ // SP 800-38A §6.2 (mode), RFC 5652 §6.3 (PKCS7 padding).
26
+ // Mirrors `src/ts/serpent/serpent-cbc.ts` exactly in shape; only the
27
+ // underlying WASM module and the PKCS7 import path differ.
28
+ import { getInstance, _acquireModule, _releaseModule } from '../init.js';
29
+ import { pkcs7Pad, pkcs7Strip, PKCS7_INVALID } from '../shared/pkcs7.js';
30
+ /** Returns the raw AES WASM export object. @internal */
31
+ function getExports() {
32
+ return getInstance('aes').exports;
33
+ }
34
+ // ── AESCbc ──────────────────────────────────────────────────────────────────
35
+ /**
36
+ * AES-128/192/256 in CBC mode with PKCS7 padding.
37
+ *
38
+ * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
39
+ * with HMAC-SHA256 (Encrypt-then-MAC) or use an authenticated cipher
40
+ * (`XChaCha20Poly1305`, `Seal` with `SerpentCipher`) instead.
41
+ *
42
+ * Holds exclusive access to the `aes` WASM module from construction until
43
+ * `dispose()`. Constructing a second AES-using class while this instance
44
+ * is live throws. Call `dispose()` when done.
45
+ */
46
+ export class AESCbc {
47
+ x;
48
+ _tok;
49
+ constructor(opts) {
50
+ if (!opts?.dangerUnauthenticated) {
51
+ throw new Error('leviathan-crypto: AESCbc is unauthenticated, use Seal with SerpentCipher or XChaCha20Cipher instead. ' +
52
+ 'To use AESCbc directly, pass { dangerUnauthenticated: true }.');
53
+ }
54
+ this.x = getExports();
55
+ this._tok = _acquireModule('aes');
56
+ }
57
+ /** View over WASM linear memory. Rebind on every access, memory can be detached after grow. @internal */
58
+ get mem() {
59
+ return new Uint8Array(this.x.memory.buffer);
60
+ }
61
+ /**
62
+ * Encrypt plaintext with AES CBC + PKCS7 padding.
63
+ *
64
+ * @param key 16, 24, or 32 bytes (AES-128 / 192 / 256)
65
+ * @param iv 16 bytes, must be random and unique per (key, message)
66
+ * @param plaintext any length, PKCS7 padding applied automatically
67
+ * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
68
+ */
69
+ encrypt(key, iv, plaintext) {
70
+ if (this._tok === undefined)
71
+ throw new Error('AESCbc: instance has been disposed');
72
+ this._loadKey(key);
73
+ this._setIv(iv);
74
+ const padded = pkcs7Pad(plaintext);
75
+ const output = new Uint8Array(padded.length);
76
+ const ptOff = this.x.getChunkPtOffset();
77
+ const ctOff = this.x.getChunkCtOffset();
78
+ const maxChunk = this.x.getChunkSize();
79
+ for (let off = 0; off < padded.length; off += maxChunk) {
80
+ const chunk = padded.subarray(off, Math.min(off + maxChunk, padded.length));
81
+ this.mem.set(chunk, ptOff);
82
+ const ret = this.x.cbcEncryptChunk(chunk.length);
83
+ if (ret < 0)
84
+ throw new RangeError(`cbcEncryptChunk rejected len=${chunk.length}` +
85
+ ` (WASM CHUNK_SIZE=${this.x.getChunkSize()})`);
86
+ output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
87
+ }
88
+ return output;
89
+ }
90
+ /**
91
+ * Decrypt AES CBC + PKCS7.
92
+ *
93
+ * All failure modes, empty input, non-multiple-of-16 length, and any
94
+ * PKCS7 validation failure, throw the same generic `RangeError` with
95
+ * message `'invalid ciphertext'`. Padding validation runs branch-free
96
+ * over the last 16 bytes regardless of where the mismatch is, closing
97
+ * the Vaudenay 2002 padding-oracle surface for callers using
98
+ * `{ dangerUnauthenticated: true }` without an outer HMAC.
99
+ */
100
+ decrypt(key, iv, ciphertext) {
101
+ if (this._tok === undefined)
102
+ throw new Error('AESCbc: instance has been disposed');
103
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
104
+ throw new RangeError(PKCS7_INVALID);
105
+ this._loadKey(key);
106
+ this._setIv(iv);
107
+ const output = new Uint8Array(ciphertext.length);
108
+ const ctOff = this.x.getChunkCtOffset();
109
+ const ptOff = this.x.getChunkPtOffset();
110
+ const maxChunk = this.x.getChunkSize();
111
+ for (let off = 0; off < ciphertext.length; off += maxChunk) {
112
+ const chunk = ciphertext.subarray(off, Math.min(off + maxChunk, ciphertext.length));
113
+ this.mem.set(chunk, ctOff);
114
+ const ret = this.x.cbcDecryptChunk_simd(chunk.length);
115
+ if (ret < 0)
116
+ throw new RangeError(`cbcDecryptChunk_simd rejected len=${chunk.length}` +
117
+ ` (WASM CHUNK_SIZE=${this.x.getChunkSize()})`);
118
+ output.set(new Uint8Array(this.x.memory.buffer).subarray(ptOff, ptOff + chunk.length), off);
119
+ }
120
+ return pkcs7Strip(output);
121
+ }
122
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
123
+ dispose() {
124
+ if (this._tok === undefined)
125
+ return;
126
+ try {
127
+ this.x.wipeBuffers();
128
+ }
129
+ finally {
130
+ _releaseModule('aes', this._tok);
131
+ this._tok = undefined;
132
+ }
133
+ }
134
+ /**
135
+ * Validate and load `key` into the WASM key schedule.
136
+ * @param key 16, 24, or 32 bytes
137
+ * @internal
138
+ */
139
+ _loadKey(key) {
140
+ if (key.length !== 16 && key.length !== 24 && key.length !== 32)
141
+ throw new RangeError(`AES key must be 16, 24, or 32 bytes (got ${key.length})`);
142
+ this.mem.set(key, this.x.getKeyOffset());
143
+ if (this.x.loadKey(key.length) !== 0) {
144
+ this.x.wipeBuffers();
145
+ throw new Error('AESCbc: loadKey failed');
146
+ }
147
+ }
148
+ /**
149
+ * Write `iv` into the WASM CBC IV buffer.
150
+ * @param iv 16 bytes
151
+ * @internal
152
+ */
153
+ _setIv(iv) {
154
+ if (iv.length !== 16)
155
+ throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
156
+ this.mem.set(iv, this.x.getCbcIvOffset());
157
+ }
158
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AES-128/192/256 in CTR mode.
3
+ *
4
+ * **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
5
+ * bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
6
+ * or use an authenticated cipher (`AESGCM`, `AESGCMSIV`, or `Seal` with
7
+ * `AESGCMSIVCipher` / `SerpentCipher` / `XChaCha20Cipher`) instead.
8
+ *
9
+ * The constructor requires `{ dangerUnauthenticated: true }` so callers
10
+ * cannot reach the unauthenticated path by accident, same gate as
11
+ * `AESCbc` and `SerpentCtr`.
12
+ *
13
+ * The counter is 128-bit big-endian (SP 800-38A Appendix B.1 / §F.5).
14
+ *
15
+ * Stateful, the counter advances across `encrypt`/`decrypt` calls. Reset
16
+ * with `setNonce()` before each new message. Holds exclusive access to the
17
+ * `aes` WASM module from construction until `dispose()`.
18
+ */
19
+ export declare class AESCtr {
20
+ private readonly x;
21
+ private _tok;
22
+ constructor(opts?: {
23
+ dangerUnauthenticated: true;
24
+ });
25
+ /**
26
+ * Expand `key` into the WASM key schedule. Must be called before
27
+ * `setNonce` / `encrypt` / `decrypt`.
28
+ * @param key 16, 24, or 32 bytes (AES-128 / 192 / 256)
29
+ */
30
+ loadKey(key: Uint8Array): void;
31
+ /**
32
+ * Set the 128-bit initial counter block (the full IC, not a separate
33
+ * nonce/counter split). Resets the working counter so subsequent
34
+ * encrypt/decrypt calls start at this value.
35
+ * @param nonce 16 bytes, must be unique per (key, message)
36
+ */
37
+ setNonce(nonce: Uint8Array): void;
38
+ /**
39
+ * XOR `plaintext` with AES CTR keystream. The counter advances by
40
+ * ceil(plaintext.length / 16) blocks; counter state persists across
41
+ * calls until `setNonce()` resets it.
42
+ * @param plaintext any length; internally chunked to WASM CHUNK_SIZE
43
+ * @returns ciphertext of the same length
44
+ */
45
+ encrypt(plaintext: Uint8Array): Uint8Array;
46
+ /** Alias for `encrypt`, CTR mode is symmetric. */
47
+ decrypt(ciphertext: Uint8Array): Uint8Array;
48
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
49
+ dispose(): void;
50
+ }
@@ -0,0 +1,141 @@
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/aes/aes-ctr.ts
23
+ //
24
+ // AESCtr, AES-128/192/256 in CTR mode, stateful TS wrapper.
25
+ // SP 800-38A §6.5 (mode), Appendix B.1 (counter increment).
26
+ // Counter direction: 128-bit big-endian, matching the SP 800-38A §F.5
27
+ // worked examples and the canonical AES CTR convention. Configured in
28
+ // the underlying WASM (`src/asm/aes/ctr.ts`).
29
+ import { getInstance, _acquireModule, _releaseModule } from '../init.js';
30
+ /** Returns the raw AES WASM export object. @internal */
31
+ function getExports() {
32
+ return getInstance('aes').exports;
33
+ }
34
+ // ── AESCtr ──────────────────────────────────────────────────────────────────
35
+ /**
36
+ * AES-128/192/256 in CTR mode.
37
+ *
38
+ * **WARNING: CTR mode is unauthenticated.** An attacker can flip ciphertext
39
+ * bits without detection. Always pair with HMAC-SHA256 (Encrypt-then-MAC)
40
+ * or use an authenticated cipher (`AESGCM`, `AESGCMSIV`, or `Seal` with
41
+ * `AESGCMSIVCipher` / `SerpentCipher` / `XChaCha20Cipher`) instead.
42
+ *
43
+ * The constructor requires `{ dangerUnauthenticated: true }` so callers
44
+ * cannot reach the unauthenticated path by accident, same gate as
45
+ * `AESCbc` and `SerpentCtr`.
46
+ *
47
+ * The counter is 128-bit big-endian (SP 800-38A Appendix B.1 / §F.5).
48
+ *
49
+ * Stateful, the counter advances across `encrypt`/`decrypt` calls. Reset
50
+ * with `setNonce()` before each new message. Holds exclusive access to the
51
+ * `aes` WASM module from construction until `dispose()`.
52
+ */
53
+ export class AESCtr {
54
+ x;
55
+ _tok;
56
+ constructor(opts) {
57
+ if (!opts?.dangerUnauthenticated) {
58
+ throw new Error('leviathan-crypto: AESCtr is unauthenticated, use Seal with AESGCMSIVCipher, SerpentCipher, or XChaCha20Cipher instead. ' +
59
+ 'To use AESCtr directly, pass { dangerUnauthenticated: true }.');
60
+ }
61
+ this.x = getExports();
62
+ this._tok = _acquireModule('aes');
63
+ }
64
+ /** View over WASM linear memory. Rebind on every access, memory can be detached after grow. @internal */
65
+ get mem() {
66
+ return new Uint8Array(this.x.memory.buffer);
67
+ }
68
+ /**
69
+ * Expand `key` into the WASM key schedule. Must be called before
70
+ * `setNonce` / `encrypt` / `decrypt`.
71
+ * @param key 16, 24, or 32 bytes (AES-128 / 192 / 256)
72
+ */
73
+ loadKey(key) {
74
+ if (this._tok === undefined)
75
+ throw new Error('AESCtr: instance has been disposed');
76
+ if (key.length !== 16 && key.length !== 24 && key.length !== 32)
77
+ throw new RangeError(`AES key must be 16, 24, or 32 bytes (got ${key.length})`);
78
+ this.mem.set(key, this.x.getKeyOffset());
79
+ if (this.x.loadKey(key.length) !== 0) {
80
+ this.x.wipeBuffers();
81
+ throw new Error('loadKey failed');
82
+ }
83
+ }
84
+ /**
85
+ * Set the 128-bit initial counter block (the full IC, not a separate
86
+ * nonce/counter split). Resets the working counter so subsequent
87
+ * encrypt/decrypt calls start at this value.
88
+ * @param nonce 16 bytes, must be unique per (key, message)
89
+ */
90
+ setNonce(nonce) {
91
+ if (this._tok === undefined)
92
+ throw new Error('AESCtr: instance has been disposed');
93
+ if (nonce.length !== 16)
94
+ throw new RangeError(`AES CTR nonce must be 16 bytes (got ${nonce.length})`);
95
+ this.mem.set(nonce, this.x.getNonceOffset());
96
+ this.x.resetCounter();
97
+ }
98
+ /**
99
+ * XOR `plaintext` with AES CTR keystream. The counter advances by
100
+ * ceil(plaintext.length / 16) blocks; counter state persists across
101
+ * calls until `setNonce()` resets it.
102
+ * @param plaintext any length; internally chunked to WASM CHUNK_SIZE
103
+ * @returns ciphertext of the same length
104
+ */
105
+ encrypt(plaintext) {
106
+ if (this._tok === undefined)
107
+ throw new Error('AESCtr: instance has been disposed');
108
+ const output = new Uint8Array(plaintext.length);
109
+ if (plaintext.length === 0)
110
+ return output;
111
+ const ptOff = this.x.getChunkPtOffset();
112
+ const ctOff = this.x.getChunkCtOffset();
113
+ const maxChunk = this.x.getChunkSize();
114
+ for (let off = 0; off < plaintext.length; off += maxChunk) {
115
+ const chunk = plaintext.subarray(off, Math.min(off + maxChunk, plaintext.length));
116
+ this.mem.set(chunk, ptOff);
117
+ const ret = this.x.encryptChunk_simd(chunk.length);
118
+ if (ret < 0)
119
+ throw new RangeError(`encryptChunk_simd rejected len=${chunk.length}` +
120
+ ` (WASM CHUNK_SIZE=${this.x.getChunkSize()})`);
121
+ output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
122
+ }
123
+ return output;
124
+ }
125
+ /** Alias for `encrypt`, CTR mode is symmetric. */
126
+ decrypt(ciphertext) {
127
+ return this.encrypt(ciphertext);
128
+ }
129
+ /** Wipe WASM state and release exclusive module access. Idempotent. */
130
+ dispose() {
131
+ if (this._tok === undefined)
132
+ return;
133
+ try {
134
+ this.x.wipeBuffers();
135
+ }
136
+ finally {
137
+ _releaseModule('aes', this._tok);
138
+ this._tok = undefined;
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * AES-128-GCM-SIV / AES-256-GCM-SIV (RFC 8452). Nonce-misuse-resistant
3
+ * authenticated AEAD with a 128-bit tag. AES-192 keys are rejected
4
+ * (RFC 8452 §6, no AES-192-GCM-SIV variant exists).
5
+ *
6
+ * Single-shot only: each `seal` / `open` call processes one complete
7
+ * message bounded by 64 KiB of plaintext. Larger messages are out of
8
+ * scope for this primitive; a future streaming variant will lift the
9
+ * cap via the seal/sealstream layer.
10
+ *
11
+ * `seal(nonce, plaintext, aad?)` returns `ciphertext || tag` (length
12
+ * pt.length + 16). `open(nonce, sealed, aad?)` verifies the tag and
13
+ * returns the plaintext; throws `AuthenticationError('siv')` on any
14
+ * verification failure.
15
+ *
16
+ * Atomic, does not hold exclusive access between calls. `dispose()`
17
+ * wipes the stored key from the JS-side cache.
18
+ */
19
+ export declare class AESGCMSIV {
20
+ private readonly _key;
21
+ private _disposed;
22
+ /**
23
+ * @param key 16 bytes (AES-128-GCM-SIV) or 32 bytes (AES-256-GCM-SIV).
24
+ * 24-byte keys are rejected, RFC 8452 §6 does not define
25
+ * an AES-192-GCM-SIV variant.
26
+ */
27
+ constructor(key: Uint8Array);
28
+ /**
29
+ * Authenticated encryption.
30
+ *
31
+ * @param nonce exactly 12 bytes (RFC 8452 §6 fixes nonce length)
32
+ * @param plaintext any length up to 64 KiB; may be empty
33
+ * @param aad any length up to 64 KiB; may be empty
34
+ * @returns ciphertext concatenated with the 128-bit tag
35
+ * (length = plaintext.length + 16)
36
+ *
37
+ * @throws RangeError if any input length violates the spec or the
38
+ * buffer-bounded API.
39
+ */
40
+ seal(nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
41
+ /**
42
+ * Authenticated decryption. `sealed` is the output of a matching
43
+ * `seal(nonce, plaintext, aad)` call.
44
+ *
45
+ * Verification routes through `constantTimeEqual` from
46
+ * `../utils.js` (the dedicated `ct` WASM module). On mismatch the
47
+ * WASM `sivWipeOnFail` helper zeroes the decrypted-but-
48
+ * unauthenticated plaintext at CHUNK_PT_OFFSET before this method
49
+ * throws, the bytes never become reachable from JS.
50
+ *
51
+ * @throws AuthenticationError('siv') if the tag fails to verify, or
52
+ * if `sealed` is too short, or any input length violates the
53
+ * spec.
54
+ */
55
+ open(nonce: Uint8Array, sealed: Uint8Array, aad?: Uint8Array): Uint8Array;
56
+ /**
57
+ * Wipe the in-memory copy of the key. Idempotent. Subsequent calls
58
+ * to `seal` / `open` throw. WASM-side state is wiped at the end of
59
+ * every successful operation regardless of `dispose()`.
60
+ */
61
+ dispose(): void;
62
+ private _mem;
63
+ private _assertAlive;
64
+ private _validate;
65
+ /** Push KGK + nonce + AAD into WASM memory. Common to seal/open. */
66
+ private _stage;
67
+ }
@@ -0,0 +1,217 @@
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/aes/aes-gcm-siv.ts
23
+ import { getInstance, _acquireModule, _releaseModule } from '../init.js';
24
+ import { constantTimeEqual, wipe } from '../utils.js';
25
+ import { AuthenticationError } from '../errors.js';
26
+ // RFC 8452 §6: K_LEN ∈ {16, 32}, AES-128-GCM-SIV or AES-256-GCM-SIV.
27
+ const KEY_LEN_128 = 16;
28
+ const KEY_LEN_256 = 32;
29
+ // RFC 8452 §6: nonce length is fixed at 96 bits (12 bytes).
30
+ const NONCE_LEN = 12;
31
+ // Single-shot bound: plaintext fits in CHUNK_PT (64 KiB). Larger inputs
32
+ // would need a streaming SIV API; not currently in scope.
33
+ const MAX_PT_BYTES = 65536;
34
+ // AAD is bounded by the dedicated AAD_BUFFER (64 KiB).
35
+ const MAX_AAD_BYTES = 65536;
36
+ // 16-byte authentication tag.
37
+ const TAG_LEN = 16;
38
+ /** Returns the raw AES WASM export object. @internal */
39
+ function getExports() {
40
+ return getInstance('aes').exports;
41
+ }
42
+ // ── AESGCMSIV ───────────────────────────────────────────────────────────────
43
+ /**
44
+ * AES-128-GCM-SIV / AES-256-GCM-SIV (RFC 8452). Nonce-misuse-resistant
45
+ * authenticated AEAD with a 128-bit tag. AES-192 keys are rejected
46
+ * (RFC 8452 §6, no AES-192-GCM-SIV variant exists).
47
+ *
48
+ * Single-shot only: each `seal` / `open` call processes one complete
49
+ * message bounded by 64 KiB of plaintext. Larger messages are out of
50
+ * scope for this primitive; a future streaming variant will lift the
51
+ * cap via the seal/sealstream layer.
52
+ *
53
+ * `seal(nonce, plaintext, aad?)` returns `ciphertext || tag` (length
54
+ * pt.length + 16). `open(nonce, sealed, aad?)` verifies the tag and
55
+ * returns the plaintext; throws `AuthenticationError('siv')` on any
56
+ * verification failure.
57
+ *
58
+ * Atomic, does not hold exclusive access between calls. `dispose()`
59
+ * wipes the stored key from the JS-side cache.
60
+ */
61
+ export class AESGCMSIV {
62
+ _key;
63
+ _disposed = false;
64
+ /**
65
+ * @param key 16 bytes (AES-128-GCM-SIV) or 32 bytes (AES-256-GCM-SIV).
66
+ * 24-byte keys are rejected, RFC 8452 §6 does not define
67
+ * an AES-192-GCM-SIV variant.
68
+ */
69
+ constructor(key) {
70
+ if (key.length !== KEY_LEN_128 && key.length !== KEY_LEN_256) {
71
+ throw new RangeError(`AESGCMSIV key must be 16 or 32 bytes (got ${key.length}); `
72
+ + 'AES-192-GCM-SIV is not defined by RFC 8452');
73
+ }
74
+ // Defensive copy so external mutation cannot change the live key.
75
+ this._key = new Uint8Array(key);
76
+ }
77
+ /**
78
+ * Authenticated encryption.
79
+ *
80
+ * @param nonce exactly 12 bytes (RFC 8452 §6 fixes nonce length)
81
+ * @param plaintext any length up to 64 KiB; may be empty
82
+ * @param aad any length up to 64 KiB; may be empty
83
+ * @returns ciphertext concatenated with the 128-bit tag
84
+ * (length = plaintext.length + 16)
85
+ *
86
+ * @throws RangeError if any input length violates the spec or the
87
+ * buffer-bounded API.
88
+ */
89
+ seal(nonce, plaintext, aad = new Uint8Array(0)) {
90
+ this._assertAlive();
91
+ this._validate(nonce, plaintext.length, aad);
92
+ const x = getExports();
93
+ const tok = _acquireModule('aes');
94
+ try {
95
+ this._stage(x, nonce, aad);
96
+ this._mem(x).set(plaintext, x.getChunkPtOffset());
97
+ x.sivDeriveKeys(x.getNonceOffset());
98
+ x.sivSeal(aad.length, plaintext.length);
99
+ const ctOff = x.getChunkPtOffset();
100
+ const tagOff = x.getTagOffset();
101
+ const out = new Uint8Array(plaintext.length + TAG_LEN);
102
+ out.set(this._mem(x).subarray(ctOff, ctOff + plaintext.length), 0);
103
+ out.set(this._mem(x).subarray(tagOff, tagOff + TAG_LEN), plaintext.length);
104
+ return out;
105
+ }
106
+ finally {
107
+ x.wipeBuffers();
108
+ _releaseModule('aes', tok);
109
+ }
110
+ }
111
+ /**
112
+ * Authenticated decryption. `sealed` is the output of a matching
113
+ * `seal(nonce, plaintext, aad)` call.
114
+ *
115
+ * Verification routes through `constantTimeEqual` from
116
+ * `../utils.js` (the dedicated `ct` WASM module). On mismatch the
117
+ * WASM `sivWipeOnFail` helper zeroes the decrypted-but-
118
+ * unauthenticated plaintext at CHUNK_PT_OFFSET before this method
119
+ * throws, the bytes never become reachable from JS.
120
+ *
121
+ * @throws AuthenticationError('siv') if the tag fails to verify, or
122
+ * if `sealed` is too short, or any input length violates the
123
+ * spec.
124
+ */
125
+ open(nonce, sealed, aad = new Uint8Array(0)) {
126
+ this._assertAlive();
127
+ if (sealed.length < TAG_LEN) {
128
+ throw new AuthenticationError('siv');
129
+ }
130
+ const ctLen = sealed.length - TAG_LEN;
131
+ try {
132
+ this._validate(nonce, ctLen, aad);
133
+ }
134
+ catch {
135
+ // Same generic error so failure modes are indistinguishable.
136
+ throw new AuthenticationError('siv');
137
+ }
138
+ const ct = sealed.subarray(0, ctLen);
139
+ const providedTag = sealed.subarray(ctLen, sealed.length);
140
+ const x = getExports();
141
+ const tok = _acquireModule('aes');
142
+ try {
143
+ this._stage(x, nonce, aad);
144
+ this._mem(x).set(ct, x.getChunkCtOffset());
145
+ // Stage the provided tag at SIV_IC_OFFSET, sivOpen will
146
+ // read it from there as the input to the CTR initial counter.
147
+ this._mem(x).set(providedTag, x.getSivIcOffset());
148
+ x.sivDeriveKeys(x.getNonceOffset());
149
+ x.sivOpen(aad.length, ctLen);
150
+ // Read the EXPECTED tag computed by sivOpen from TAG_OFFSET.
151
+ // Use slice() rather than subarray() so the buffer survives
152
+ // any subsequent WASM memory growth or wipe.
153
+ const expectedTag = this._mem(x).slice(x.getTagOffset(), x.getTagOffset() + TAG_LEN);
154
+ // Defensive copy of providedTag for the constant-time compare,
155
+ // the input may be a view over a caller-controlled buffer.
156
+ const providedTagCopy = new Uint8Array(providedTag);
157
+ const ok = constantTimeEqual(expectedTag, providedTagCopy);
158
+ if (!ok) {
159
+ // Wipe before finally; covers JS-heap tag copies.
160
+ x.sivWipeOnFail();
161
+ wipe(expectedTag);
162
+ wipe(providedTagCopy);
163
+ throw new AuthenticationError('siv');
164
+ }
165
+ // Match, read PT before wiping. pt is a JS-heap slice copy.
166
+ const ptOff = x.getChunkPtOffset();
167
+ const pt = this._mem(x).slice(ptOff, ptOff + ctLen);
168
+ wipe(expectedTag);
169
+ wipe(providedTagCopy);
170
+ return pt;
171
+ }
172
+ finally {
173
+ x.wipeBuffers();
174
+ _releaseModule('aes', tok);
175
+ }
176
+ }
177
+ /**
178
+ * Wipe the in-memory copy of the key. Idempotent. Subsequent calls
179
+ * to `seal` / `open` throw. WASM-side state is wiped at the end of
180
+ * every successful operation regardless of `dispose()`.
181
+ */
182
+ dispose() {
183
+ if (this._disposed)
184
+ return;
185
+ this._key.fill(0);
186
+ this._disposed = true;
187
+ }
188
+ // ── Internal helpers ───────────────────────────────────────────────────
189
+ _mem(x) {
190
+ return new Uint8Array(x.memory.buffer);
191
+ }
192
+ _assertAlive() {
193
+ if (this._disposed)
194
+ throw new Error('AESGCMSIV: instance has been disposed');
195
+ }
196
+ _validate(nonce, dataLen, aad) {
197
+ if (nonce.length !== NONCE_LEN)
198
+ throw new RangeError(`AESGCMSIV nonce must be ${NONCE_LEN} bytes (got ${nonce.length})`);
199
+ if (dataLen > MAX_PT_BYTES)
200
+ throw new RangeError(`AESGCMSIV plaintext must be ≤ ${MAX_PT_BYTES} bytes (got ${dataLen})`);
201
+ if (aad.length > MAX_AAD_BYTES)
202
+ throw new RangeError(`AESGCMSIV AAD must be ≤ ${MAX_AAD_BYTES} bytes (got ${aad.length})`);
203
+ }
204
+ /** Push KGK + nonce + AAD into WASM memory. Common to seal/open. */
205
+ _stage(x, nonce, aad) {
206
+ const mem = this._mem(x);
207
+ mem.set(this._key, x.getKeyOffset());
208
+ if (x.loadKey(this._key.length) !== 0) {
209
+ x.wipeBuffers();
210
+ throw new Error('AESGCMSIV: loadKey failed');
211
+ }
212
+ mem.set(nonce, x.getNonceOffset());
213
+ if (aad.length > 0) {
214
+ mem.set(aad, x.getAadOffset());
215
+ }
216
+ }
217
+ }