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.
- package/CLAUDE.md +88 -281
- package/LICENSE +4 -0
- package/README.md +275 -87
- package/dist/aes/aes-cbc.d.ts +40 -0
- package/dist/aes/aes-cbc.js +158 -0
- package/dist/aes/aes-ctr.d.ts +50 -0
- package/dist/aes/aes-ctr.js +141 -0
- package/dist/aes/aes-gcm-siv.d.ts +67 -0
- package/dist/aes/aes-gcm-siv.js +217 -0
- package/dist/aes/aes-gcm.d.ts +61 -0
- package/dist/aes/aes-gcm.js +226 -0
- package/dist/aes/cipher-suite.d.ts +21 -0
- package/dist/aes/cipher-suite.js +179 -0
- package/dist/aes/embedded.d.ts +1 -0
- package/dist/aes/embedded.js +26 -0
- package/dist/aes/generator.d.ts +14 -0
- package/dist/aes/generator.js +103 -0
- package/dist/aes/index.d.ts +58 -0
- package/dist/aes/index.js +125 -0
- package/dist/aes/ops.d.ts +60 -0
- package/dist/aes/ops.js +164 -0
- package/dist/aes/pool-worker.d.ts +1 -0
- package/dist/aes/pool-worker.js +92 -0
- package/dist/aes/types.d.ts +1 -0
- package/dist/aes/types.js +23 -0
- package/dist/aes.wasm +0 -0
- package/dist/blake3/embedded.d.ts +1 -0
- package/dist/blake3/embedded.js +26 -0
- package/dist/blake3/index.d.ts +143 -0
- package/dist/blake3/index.js +620 -0
- package/dist/blake3/types.d.ts +102 -0
- package/dist/blake3/types.js +31 -0
- package/dist/blake3/validate.d.ts +29 -0
- package/dist/blake3/validate.js +80 -0
- package/dist/blake3.wasm +0 -0
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +98 -13
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +100 -3
- package/dist/chacha20/index.js +169 -35
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +107 -27
- package/dist/chacha20/pool-worker.js +14 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/cte-wasm.d.ts +1 -0
- package/dist/cte-wasm.js +3 -0
- package/dist/cte.wasm +0 -0
- package/dist/curve25519.wasm +0 -0
- package/dist/ecdsa/der.d.ts +23 -0
- package/dist/ecdsa/der.js +192 -0
- package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
- package/dist/ecdsa/ecprivatekey-der.js +230 -0
- package/dist/ecdsa/embedded.d.ts +1 -0
- package/dist/ecdsa/embedded.js +25 -0
- package/dist/ecdsa/index.d.ts +124 -0
- package/dist/ecdsa/index.js +366 -0
- package/dist/ecdsa/types.d.ts +31 -0
- package/dist/ecdsa/types.js +28 -0
- package/dist/ecdsa/validate.d.ts +18 -0
- package/dist/ecdsa/validate.js +92 -0
- package/dist/ed25519/embedded.d.ts +1 -0
- package/dist/ed25519/embedded.js +31 -0
- package/dist/ed25519/index.d.ts +70 -0
- package/dist/ed25519/index.js +308 -0
- package/dist/ed25519/types.d.ts +27 -0
- package/dist/ed25519/types.js +27 -0
- package/dist/ed25519/validate.d.ts +7 -0
- package/dist/ed25519/validate.js +77 -0
- package/dist/embedded/aes-pool-worker.d.ts +1 -0
- package/dist/embedded/aes-pool-worker.js +5 -0
- package/dist/embedded/aes.d.ts +1 -0
- package/dist/embedded/aes.js +3 -0
- package/dist/embedded/blake3.d.ts +1 -0
- package/dist/embedded/blake3.js +3 -0
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/chacha20.d.ts +1 -1
- package/dist/embedded/chacha20.js +2 -2
- package/dist/embedded/curve25519.d.ts +1 -0
- package/dist/embedded/curve25519.js +3 -0
- package/dist/embedded/mldsa.d.ts +1 -0
- package/dist/embedded/mldsa.js +3 -0
- package/dist/embedded/mlkem.d.ts +1 -0
- package/dist/embedded/mlkem.js +3 -0
- package/dist/embedded/p256.d.ts +1 -0
- package/dist/embedded/p256.js +3 -0
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +2 -2
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +2 -2
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +2 -2
- package/dist/embedded/slhdsa.d.ts +1 -0
- package/dist/embedded/slhdsa.js +3 -0
- package/dist/errors.d.ts +92 -1
- package/dist/errors.js +111 -1
- package/dist/fortuna.d.ts +18 -12
- package/dist/fortuna.js +166 -99
- package/dist/index.d.ts +42 -11
- package/dist/index.js +65 -20
- package/dist/init.d.ts +1 -3
- package/dist/init.js +73 -7
- package/dist/keccak/embedded.js +1 -1
- package/dist/keccak/index.d.ts +2 -0
- package/dist/keccak/index.js +4 -2
- package/dist/loader.d.ts +1 -19
- package/dist/loader.js +26 -32
- package/dist/merkle/blake3-tree.d.ts +35 -0
- package/dist/merkle/blake3-tree.js +187 -0
- package/dist/merkle/checkpoint.d.ts +58 -0
- package/dist/merkle/checkpoint.js +217 -0
- package/dist/merkle/index.d.ts +19 -0
- package/dist/merkle/index.js +37 -0
- package/dist/merkle/merkle-log.d.ts +130 -0
- package/dist/merkle/merkle-log.js +207 -0
- package/dist/merkle/merkle-verifier.d.ts +126 -0
- package/dist/merkle/merkle-verifier.js +296 -0
- package/dist/merkle/proof.d.ts +70 -0
- package/dist/merkle/proof.js +300 -0
- package/dist/merkle/sha256-tree.d.ts +33 -0
- package/dist/merkle/sha256-tree.js +145 -0
- package/dist/merkle/signed-log.d.ts +156 -0
- package/dist/merkle/signed-log.js +356 -0
- package/dist/merkle/signed-note.d.ts +309 -0
- package/dist/merkle/signed-note.js +648 -0
- package/dist/merkle/sth.d.ts +31 -0
- package/dist/merkle/sth.js +31 -0
- package/dist/merkle/storage.d.ts +40 -0
- package/dist/merkle/storage.js +71 -0
- package/dist/merkle/tree.d.ts +68 -0
- package/dist/merkle/tree.js +94 -0
- package/dist/mldsa/embedded.d.ts +1 -0
- package/dist/{kyber → mldsa}/embedded.js +5 -5
- package/dist/mldsa/expand.d.ts +53 -0
- package/dist/mldsa/expand.js +188 -0
- package/dist/mldsa/format.d.ts +16 -0
- package/dist/mldsa/format.js +68 -0
- package/dist/mldsa/hashvariant.d.ts +32 -0
- package/dist/mldsa/hashvariant.js +248 -0
- package/dist/mldsa/index.d.ts +142 -0
- package/dist/mldsa/index.js +463 -0
- package/dist/mldsa/keygen.d.ts +16 -0
- package/dist/mldsa/keygen.js +232 -0
- package/dist/mldsa/params.d.ts +21 -0
- package/dist/mldsa/params.js +55 -0
- package/dist/mldsa/sha3-helpers.d.ts +30 -0
- package/dist/mldsa/sha3-helpers.js +124 -0
- package/dist/mldsa/sign.d.ts +36 -0
- package/dist/mldsa/sign.js +380 -0
- package/dist/mldsa/types.d.ts +91 -0
- package/dist/mldsa/types.js +25 -0
- package/dist/mldsa/validate.d.ts +55 -0
- package/dist/mldsa/validate.js +125 -0
- package/dist/mldsa/verify.d.ts +29 -0
- package/dist/mldsa/verify.js +269 -0
- package/dist/mldsa.wasm +0 -0
- package/dist/mlkem/embedded.d.ts +1 -0
- package/dist/mlkem/embedded.js +27 -0
- package/dist/mlkem/indcpa.d.ts +49 -0
- package/dist/{kyber → mlkem}/indcpa.js +48 -48
- package/dist/mlkem/index.d.ts +37 -0
- package/dist/{kyber → mlkem}/index.js +41 -31
- package/dist/mlkem/kem.d.ts +21 -0
- package/dist/{kyber → mlkem}/kem.js +48 -13
- package/dist/{kyber → mlkem}/params.d.ts +4 -4
- package/dist/{kyber → mlkem}/params.js +2 -2
- package/dist/mlkem/suite.d.ts +12 -0
- package/dist/{kyber → mlkem}/suite.js +17 -12
- package/dist/{kyber → mlkem}/types.d.ts +4 -3
- package/dist/{kyber → mlkem}/types.js +1 -1
- package/dist/mlkem/validate.d.ts +23 -0
- package/dist/{kyber → mlkem}/validate.js +24 -20
- package/dist/{kyber.wasm → mlkem.wasm} +0 -0
- package/dist/p256.wasm +0 -0
- package/dist/ratchet/index.d.ts +8 -0
- package/dist/ratchet/index.js +38 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +144 -56
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +62 -1
- package/dist/serpent/index.js +97 -21
- package/dist/serpent/pool-worker.js +28 -102
- package/dist/serpent/serpent-cbc.d.ts +16 -6
- package/dist/serpent/serpent-cbc.js +58 -37
- package/dist/serpent/shared-ops.d.ts +63 -0
- package/dist/serpent/shared-ops.js +178 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/hkdf.js +5 -5
- package/dist/sha2/index.d.ts +22 -1
- package/dist/sha2/index.js +80 -11
- package/dist/sha2/types.d.ts +41 -2
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +87 -3
- package/dist/sha3/index.js +317 -19
- package/dist/sha3/kmac.d.ts +121 -0
- package/dist/sha3/kmac.js +800 -0
- package/dist/sha3.wasm +0 -0
- package/dist/shared/pkcs7.d.ts +22 -0
- package/dist/shared/pkcs7.js +84 -0
- package/dist/sign/ctx.d.ts +41 -0
- package/dist/sign/ctx.js +102 -0
- package/dist/sign/envelope.d.ts +45 -0
- package/dist/sign/envelope.js +152 -0
- package/dist/sign/hasher.d.ts +9 -0
- package/dist/sign/hasher.js +132 -0
- package/dist/sign/index.d.ts +11 -0
- package/dist/sign/index.js +34 -0
- package/dist/sign/sign-stream.d.ts +25 -0
- package/dist/sign/sign-stream.js +112 -0
- package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
- package/dist/sign/suites/ecdsa-p256.js +120 -0
- package/dist/sign/suites/ed25519.d.ts +3 -0
- package/dist/sign/suites/ed25519.js +165 -0
- package/dist/sign/suites/hybrid-classical.d.ts +23 -0
- package/dist/sign/suites/hybrid-classical.js +526 -0
- package/dist/sign/suites/hybrid-pq.d.ts +4 -0
- package/dist/sign/suites/hybrid-pq.js +234 -0
- package/dist/sign/suites/mldsa.d.ts +7 -0
- package/dist/sign/suites/mldsa.js +161 -0
- package/dist/sign/suites/slhdsa.d.ts +7 -0
- package/dist/sign/suites/slhdsa.js +176 -0
- package/dist/sign/types.d.ts +106 -0
- package/dist/sign/types.js +28 -0
- package/dist/sign/verify-stream.d.ts +30 -0
- package/dist/sign/verify-stream.js +227 -0
- package/dist/slhdsa/embedded.d.ts +1 -0
- package/dist/slhdsa/embedded.js +26 -0
- package/dist/slhdsa/index.d.ts +149 -0
- package/dist/slhdsa/index.js +493 -0
- package/dist/slhdsa/params.d.ts +26 -0
- package/dist/slhdsa/params.js +70 -0
- package/dist/slhdsa/prehash.d.ts +68 -0
- package/dist/slhdsa/prehash.js +307 -0
- package/dist/slhdsa/sign.d.ts +39 -0
- package/dist/slhdsa/sign.js +116 -0
- package/dist/slhdsa/types.d.ts +129 -0
- package/dist/slhdsa/types.js +27 -0
- package/dist/slhdsa/validate.d.ts +60 -0
- package/dist/slhdsa/validate.js +127 -0
- package/dist/slhdsa/verify.d.ts +32 -0
- package/dist/slhdsa/verify.js +107 -0
- package/dist/slhdsa.wasm +0 -0
- package/dist/stream/header.js +8 -8
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/open-stream.js +65 -22
- package/dist/stream/seal-stream-pool.d.ts +2 -0
- package/dist/stream/seal-stream-pool.js +100 -33
- package/dist/stream/seal-stream.d.ts +1 -1
- package/dist/stream/seal-stream.js +48 -19
- package/dist/stream/seal.js +6 -6
- package/dist/stream/types.d.ts +3 -1
- package/dist/stream/types.js +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +9 -10
- package/dist/utils.js +84 -59
- package/dist/wasm-source.d.ts +9 -8
- package/dist/wasm-source.js +1 -1
- package/dist/x25519/embedded.d.ts +1 -0
- package/dist/x25519/embedded.js +31 -0
- package/dist/x25519/index.d.ts +43 -0
- package/dist/x25519/index.js +159 -0
- package/dist/x25519/types.d.ts +25 -0
- package/dist/x25519/types.js +27 -0
- package/dist/x25519/validate.d.ts +2 -0
- package/dist/x25519/validate.js +39 -0
- package/package.json +123 -64
- package/SECURITY.md +0 -276
- package/dist/ct-wasm.d.ts +0 -1
- package/dist/ct-wasm.js +0 -3
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +0 -323
- package/dist/docs/architecture.md +0 -932
- package/dist/docs/argon2id.md +0 -302
- package/dist/docs/chacha20.md +0 -674
- package/dist/docs/exports.md +0 -241
- package/dist/docs/fortuna.md +0 -313
- package/dist/docs/init.md +0 -302
- package/dist/docs/loader.md +0 -161
- package/dist/docs/serpent.md +0 -519
- package/dist/docs/sha2.md +0 -613
- package/dist/docs/sha3.md +0 -546
- package/dist/docs/types.md +0 -276
- package/dist/docs/utils.md +0 -367
- package/dist/embedded/kyber.d.ts +0 -1
- package/dist/embedded/kyber.js +0 -3
- package/dist/kyber/embedded.d.ts +0 -1
- package/dist/kyber/indcpa.d.ts +0 -49
- package/dist/kyber/index.d.ts +0 -38
- package/dist/kyber/kem.d.ts +0 -21
- package/dist/kyber/suite.d.ts +0 -13
- package/dist/kyber/validate.d.ts +0 -19
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-128/192/256 in GCM mode (SP 800-38D §7). Authenticated AEAD with
|
|
3
|
+
* 128-bit tag. Tag length is fixed; shorter tags (32/64/96/104/112/120)
|
|
4
|
+
* are out of scope for this version.
|
|
5
|
+
*
|
|
6
|
+
* `seal(key, iv, aad, pt)` returns `ciphertext || tag` (length pt.length + 16).
|
|
7
|
+
* `open(key, iv, aad, sealed)` verifies the tag and returns the plaintext;
|
|
8
|
+
* throws `RangeError('authentication failed')` on any verification failure
|
|
9
|
+
* (the same generic error as a tag mismatch, no detail leak).
|
|
10
|
+
*
|
|
11
|
+
* Holds exclusive access to the `aes` WASM module from construction until
|
|
12
|
+
* `dispose()`. Constructing a second AES-using class while this instance is
|
|
13
|
+
* live throws. Always dispose when done so key material is wiped.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AESGCM {
|
|
16
|
+
private readonly x;
|
|
17
|
+
private _tok;
|
|
18
|
+
constructor();
|
|
19
|
+
/**
|
|
20
|
+
* Authenticated encryption.
|
|
21
|
+
*
|
|
22
|
+
* @param key 16, 24, or 32 bytes (AES-128 / 192 / 256)
|
|
23
|
+
* @param iv 1+ bytes; 12-byte (96-bit) IV is the recommended fast path
|
|
24
|
+
* @param aad any length up to 64 KiB; may be empty
|
|
25
|
+
* @param pt any length up to 2^36 - 32 bytes; may be empty
|
|
26
|
+
* @returns ciphertext concatenated with the 128-bit tag
|
|
27
|
+
* (length = pt.length + 16)
|
|
28
|
+
*
|
|
29
|
+
* @throws RangeError if key/iv/aad/pt lengths violate the spec or the
|
|
30
|
+
* buffer-bounded API.
|
|
31
|
+
*/
|
|
32
|
+
seal(key: Uint8Array, iv: Uint8Array, aad: Uint8Array, pt: Uint8Array): Uint8Array;
|
|
33
|
+
/**
|
|
34
|
+
* Authenticated decryption.
|
|
35
|
+
*
|
|
36
|
+
* Performs verify-before-decrypt (SP 800-38D §7.2 permits the tag check
|
|
37
|
+
* to precede plaintext computation): the entire ciphertext is absorbed
|
|
38
|
+
* into GHASH, the tag is computed and constant-time-compared with the
|
|
39
|
+
* received tag, and only then is the ciphertext decrypted to plaintext.
|
|
40
|
+
* This avoids leaking decrypted bytes to higher layers when the tag
|
|
41
|
+
* fails to verify.
|
|
42
|
+
*
|
|
43
|
+
* @param key same constraints as `seal`
|
|
44
|
+
* @param iv same iv used during the matching `seal` call
|
|
45
|
+
* @param aad same aad used during the matching `seal` call
|
|
46
|
+
* @param sealed output of a previous `seal` call (ciphertext || tag)
|
|
47
|
+
* @returns plaintext (length = sealed.length - 16)
|
|
48
|
+
*
|
|
49
|
+
* @throws RangeError('authentication failed') if the tag fails to
|
|
50
|
+
* verify, or if the sealed input is too short, or any input
|
|
51
|
+
* length violates the spec. The same generic error covers all
|
|
52
|
+
* failure modes, no detail is leaked about which check failed.
|
|
53
|
+
*/
|
|
54
|
+
open(key: Uint8Array, iv: Uint8Array, aad: Uint8Array, sealed: Uint8Array): Uint8Array;
|
|
55
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
56
|
+
dispose(): void;
|
|
57
|
+
private _validateInputs;
|
|
58
|
+
private _loadKey;
|
|
59
|
+
private _writeIv;
|
|
60
|
+
private _writeAad;
|
|
61
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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.ts
|
|
23
|
+
//
|
|
24
|
+
// AESGCM, AES-128/192/256 in GCM mode (NIST SP 800-38D §7), atomic
|
|
25
|
+
// authenticated AEAD. Tag length is fixed at 128 bits.
|
|
26
|
+
import { getInstance, _acquireModule, _releaseModule } from '../init.js';
|
|
27
|
+
import { constantTimeEqual } from '../utils.js';
|
|
28
|
+
// SP 800-38D §5.2.1.1: max plaintext = 2^39 - 256 bits = 2^36 - 32 bytes.
|
|
29
|
+
const MAX_PT_BYTES = 0x1000000000 - 32;
|
|
30
|
+
// AAD_BUFFER is 64 KiB.
|
|
31
|
+
const MAX_AAD_BYTES = 65536;
|
|
32
|
+
// IV reuses CHUNK_PT_BUFFER scratch, 64 KiB cap.
|
|
33
|
+
const MAX_IV_BYTES = 65536;
|
|
34
|
+
// Per-call chunk size; seal/open iterate across this for larger inputs.
|
|
35
|
+
const PT_CHUNK_LIMIT = 65536;
|
|
36
|
+
const AUTH_FAILED = 'authentication failed';
|
|
37
|
+
/** Returns the raw AES WASM export object. @internal */
|
|
38
|
+
function getExports() {
|
|
39
|
+
return getInstance('aes').exports;
|
|
40
|
+
}
|
|
41
|
+
// ── AESGCM ──────────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* AES-128/192/256 in GCM mode (SP 800-38D §7). Authenticated AEAD with
|
|
44
|
+
* 128-bit tag. Tag length is fixed; shorter tags (32/64/96/104/112/120)
|
|
45
|
+
* are out of scope for this version.
|
|
46
|
+
*
|
|
47
|
+
* `seal(key, iv, aad, pt)` returns `ciphertext || tag` (length pt.length + 16).
|
|
48
|
+
* `open(key, iv, aad, sealed)` verifies the tag and returns the plaintext;
|
|
49
|
+
* throws `RangeError('authentication failed')` on any verification failure
|
|
50
|
+
* (the same generic error as a tag mismatch, no detail leak).
|
|
51
|
+
*
|
|
52
|
+
* Holds exclusive access to the `aes` WASM module from construction until
|
|
53
|
+
* `dispose()`. Constructing a second AES-using class while this instance is
|
|
54
|
+
* live throws. Always dispose when done so key material is wiped.
|
|
55
|
+
*/
|
|
56
|
+
export class AESGCM {
|
|
57
|
+
x;
|
|
58
|
+
_tok;
|
|
59
|
+
constructor() {
|
|
60
|
+
this.x = getExports();
|
|
61
|
+
this._tok = _acquireModule('aes');
|
|
62
|
+
}
|
|
63
|
+
/** View over WASM linear memory. Rebind every access, memory can be detached. @internal */
|
|
64
|
+
get mem() {
|
|
65
|
+
return new Uint8Array(this.x.memory.buffer);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Authenticated encryption.
|
|
69
|
+
*
|
|
70
|
+
* @param key 16, 24, or 32 bytes (AES-128 / 192 / 256)
|
|
71
|
+
* @param iv 1+ bytes; 12-byte (96-bit) IV is the recommended fast path
|
|
72
|
+
* @param aad any length up to 64 KiB; may be empty
|
|
73
|
+
* @param pt any length up to 2^36 - 32 bytes; may be empty
|
|
74
|
+
* @returns ciphertext concatenated with the 128-bit tag
|
|
75
|
+
* (length = pt.length + 16)
|
|
76
|
+
*
|
|
77
|
+
* @throws RangeError if key/iv/aad/pt lengths violate the spec or the
|
|
78
|
+
* buffer-bounded API.
|
|
79
|
+
*/
|
|
80
|
+
seal(key, iv, aad, pt) {
|
|
81
|
+
if (this._tok === undefined)
|
|
82
|
+
throw new Error('AESGCM: instance has been disposed');
|
|
83
|
+
this._validateInputs(key, iv, aad, pt.length);
|
|
84
|
+
try {
|
|
85
|
+
this._loadKey(key);
|
|
86
|
+
this._writeIv(iv);
|
|
87
|
+
this._writeAad(aad);
|
|
88
|
+
const startRc = this.x.gcmStart(iv.length, aad.length);
|
|
89
|
+
if (startRc !== 0)
|
|
90
|
+
throw new RangeError('invalid GCM input');
|
|
91
|
+
const output = new Uint8Array(pt.length + 16);
|
|
92
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
93
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
94
|
+
const tagOff = this.x.getTagOffset();
|
|
95
|
+
for (let off = 0; off < pt.length; off += PT_CHUNK_LIMIT) {
|
|
96
|
+
const chunkLen = Math.min(PT_CHUNK_LIMIT, pt.length - off);
|
|
97
|
+
this.mem.set(pt.subarray(off, off + chunkLen), ptOff);
|
|
98
|
+
const rc = this.x.gcmEncryptChunk(ptOff, ctOff, chunkLen);
|
|
99
|
+
if (rc !== 0)
|
|
100
|
+
throw new RangeError('GCM counter overflow');
|
|
101
|
+
output.set(this.mem.subarray(ctOff, ctOff + chunkLen), off);
|
|
102
|
+
}
|
|
103
|
+
this.x.gcmFinalize();
|
|
104
|
+
output.set(this.mem.subarray(tagOff, tagOff + 16), pt.length);
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.x.wipeBuffers();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Authenticated decryption.
|
|
113
|
+
*
|
|
114
|
+
* Performs verify-before-decrypt (SP 800-38D §7.2 permits the tag check
|
|
115
|
+
* to precede plaintext computation): the entire ciphertext is absorbed
|
|
116
|
+
* into GHASH, the tag is computed and constant-time-compared with the
|
|
117
|
+
* received tag, and only then is the ciphertext decrypted to plaintext.
|
|
118
|
+
* This avoids leaking decrypted bytes to higher layers when the tag
|
|
119
|
+
* fails to verify.
|
|
120
|
+
*
|
|
121
|
+
* @param key same constraints as `seal`
|
|
122
|
+
* @param iv same iv used during the matching `seal` call
|
|
123
|
+
* @param aad same aad used during the matching `seal` call
|
|
124
|
+
* @param sealed output of a previous `seal` call (ciphertext || tag)
|
|
125
|
+
* @returns plaintext (length = sealed.length - 16)
|
|
126
|
+
*
|
|
127
|
+
* @throws RangeError('authentication failed') if the tag fails to
|
|
128
|
+
* verify, or if the sealed input is too short, or any input
|
|
129
|
+
* length violates the spec. The same generic error covers all
|
|
130
|
+
* failure modes, no detail is leaked about which check failed.
|
|
131
|
+
*/
|
|
132
|
+
open(key, iv, aad, sealed) {
|
|
133
|
+
if (this._tok === undefined)
|
|
134
|
+
throw new Error('AESGCM: instance has been disposed');
|
|
135
|
+
if (sealed.length < 16)
|
|
136
|
+
throw new RangeError(AUTH_FAILED);
|
|
137
|
+
const ctLen = sealed.length - 16;
|
|
138
|
+
// Throw the generic auth-failed error rather than 'invalid input' on
|
|
139
|
+
// length validation, to keep failure modes indistinguishable.
|
|
140
|
+
try {
|
|
141
|
+
this._validateInputs(key, iv, aad, ctLen);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new RangeError(AUTH_FAILED);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
this._loadKey(key);
|
|
148
|
+
this._writeIv(iv);
|
|
149
|
+
this._writeAad(aad);
|
|
150
|
+
const startRc = this.x.gcmStart(iv.length, aad.length);
|
|
151
|
+
if (startRc !== 0)
|
|
152
|
+
throw new RangeError(AUTH_FAILED);
|
|
153
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
154
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
155
|
+
// Pass 1: absorb every CT chunk into GHASH (no decryption yet).
|
|
156
|
+
for (let off = 0; off < ctLen; off += PT_CHUNK_LIMIT) {
|
|
157
|
+
const chunkLen = Math.min(PT_CHUNK_LIMIT, ctLen - off);
|
|
158
|
+
this.mem.set(sealed.subarray(off, off + chunkLen), ctOff);
|
|
159
|
+
const rc = this.x.gcmAbsorbCtChunk(ctOff, chunkLen);
|
|
160
|
+
if (rc !== 0)
|
|
161
|
+
throw new RangeError(AUTH_FAILED);
|
|
162
|
+
}
|
|
163
|
+
this.x.gcmFinalize();
|
|
164
|
+
const tagOff = this.x.getTagOffset();
|
|
165
|
+
const expectedTag = this.mem.slice(tagOff, tagOff + 16);
|
|
166
|
+
const providedTag = sealed.slice(ctLen, ctLen + 16);
|
|
167
|
+
if (!constantTimeEqual(expectedTag, providedTag))
|
|
168
|
+
throw new RangeError(AUTH_FAILED);
|
|
169
|
+
// Pass 2: re-init counter, GCTR-decrypt every CT chunk → output.
|
|
170
|
+
this.x.gcmResetCtrToJ0Plus1();
|
|
171
|
+
const output = new Uint8Array(ctLen);
|
|
172
|
+
for (let off = 0; off < ctLen; off += PT_CHUNK_LIMIT) {
|
|
173
|
+
const chunkLen = Math.min(PT_CHUNK_LIMIT, ctLen - off);
|
|
174
|
+
this.mem.set(sealed.subarray(off, off + chunkLen), ctOff);
|
|
175
|
+
const rc = this.x.gcmDecryptChunk(ctOff, ptOff, chunkLen);
|
|
176
|
+
if (rc !== 0)
|
|
177
|
+
throw new RangeError(AUTH_FAILED);
|
|
178
|
+
output.set(this.mem.subarray(ptOff, ptOff + chunkLen), off);
|
|
179
|
+
}
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.x.wipeBuffers();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Wipe WASM state and release exclusive module access. Idempotent. */
|
|
187
|
+
dispose() {
|
|
188
|
+
if (this._tok === undefined)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
this.x.wipeBuffers();
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
_releaseModule('aes', this._tok);
|
|
195
|
+
this._tok = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ── Internal helpers ───────────────────────────────────────────────────
|
|
199
|
+
_validateInputs(key, iv, aad, dataLen) {
|
|
200
|
+
if (key.length !== 16 && key.length !== 24 && key.length !== 32)
|
|
201
|
+
throw new RangeError(`AES key must be 16, 24, or 32 bytes (got ${key.length})`);
|
|
202
|
+
if (iv.length < 1)
|
|
203
|
+
throw new RangeError('GCM IV must be ≥ 1 byte');
|
|
204
|
+
if (iv.length > MAX_IV_BYTES)
|
|
205
|
+
throw new RangeError(`GCM IV must be ≤ ${MAX_IV_BYTES} bytes (got ${iv.length})`);
|
|
206
|
+
if (aad.length > MAX_AAD_BYTES)
|
|
207
|
+
throw new RangeError(`GCM AAD must be ≤ ${MAX_AAD_BYTES} bytes (got ${aad.length})`);
|
|
208
|
+
if (dataLen > MAX_PT_BYTES)
|
|
209
|
+
throw new RangeError(`GCM plaintext must be ≤ 2^36 - 32 bytes (got ${dataLen})`);
|
|
210
|
+
}
|
|
211
|
+
_loadKey(key) {
|
|
212
|
+
this.mem.set(key, this.x.getKeyOffset());
|
|
213
|
+
if (this.x.loadKey(key.length) !== 0) {
|
|
214
|
+
this.x.wipeBuffers();
|
|
215
|
+
throw new Error('AESGCM: loadKey failed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
_writeIv(iv) {
|
|
219
|
+
this.mem.set(iv, this.x.getChunkPtOffset());
|
|
220
|
+
}
|
|
221
|
+
_writeAad(aad) {
|
|
222
|
+
if (aad.length === 0)
|
|
223
|
+
return;
|
|
224
|
+
this.mem.set(aad, this.x.getAadOffset());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CipherSuite } from '../stream/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `CipherSuite` implementation for the stream construction using
|
|
4
|
+
* AES-256-GCM-SIV (RFC 8452).
|
|
5
|
+
*
|
|
6
|
+
* Each chunk is encrypted with AES-256-GCM-SIV using a per-stream
|
|
7
|
+
* AES key derived via HKDF-SHA-256. Unlike `XChaCha20Cipher` there is
|
|
8
|
+
* no intermediate subkey derivation step, AES-GCM-SIV uses a 12-byte
|
|
9
|
+
* nonce natively, so the 32 bytes from HKDF feed straight into the
|
|
10
|
+
* cipher.
|
|
11
|
+
*
|
|
12
|
+
* Pass to `Seal` / `SealStream` / `OpenStream` / `SealStreamPool`
|
|
13
|
+
* instead of constructing this object directly. Use
|
|
14
|
+
* `AESGCMSIVCipher.keygen()` to generate a 32-byte master key.
|
|
15
|
+
*
|
|
16
|
+
* The cipher suite is AES-256 only; the standalone `AESGCMSIV` primitive
|
|
17
|
+
* class continues to support both AES-128 and AES-256 (RFC 8452 §6).
|
|
18
|
+
*/
|
|
19
|
+
export declare const AESGCMSIVCipher: CipherSuite & {
|
|
20
|
+
keygen(): Uint8Array;
|
|
21
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
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/cipher-suite.ts
|
|
23
|
+
//
|
|
24
|
+
// AESGCMSIVCipher, CipherSuite implementation for the STREAM construction.
|
|
25
|
+
// HKDF-SHA-256 key derivation → AES-256-GCM-SIV per chunk. Mirrors the
|
|
26
|
+
// XChaCha20Cipher HtE explicit-commitment construction byte-for-byte
|
|
27
|
+
// modulo the cipher backend, since AES-GCM-SIV's POLYVAL-based MAC is
|
|
28
|
+
// not key-committing on its own.
|
|
29
|
+
import { getInstance, _assertNotOwned } from '../init.js';
|
|
30
|
+
import { HKDF_SHA256 } from '../sha2/index.js';
|
|
31
|
+
import { sivAeadEncrypt, sivAeadDecrypt } from './ops.js';
|
|
32
|
+
import { wipe, randomBytes, concat } from '../utils.js';
|
|
33
|
+
import { HEADER_SIZE } from '../stream/constants.js';
|
|
34
|
+
import { WORKER_SOURCE } from '../embedded/aes-pool-worker.js';
|
|
35
|
+
const INFO = new TextEncoder().encode('aes-gcm-siv-sealstream-v3');
|
|
36
|
+
/** Returns the raw aes WASM export object. @internal */
|
|
37
|
+
function getExports() {
|
|
38
|
+
return getInstance('aes').exports;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `CipherSuite` implementation for the stream construction using
|
|
42
|
+
* AES-256-GCM-SIV (RFC 8452).
|
|
43
|
+
*
|
|
44
|
+
* Each chunk is encrypted with AES-256-GCM-SIV using a per-stream
|
|
45
|
+
* AES key derived via HKDF-SHA-256. Unlike `XChaCha20Cipher` there is
|
|
46
|
+
* no intermediate subkey derivation step, AES-GCM-SIV uses a 12-byte
|
|
47
|
+
* nonce natively, so the 32 bytes from HKDF feed straight into the
|
|
48
|
+
* cipher.
|
|
49
|
+
*
|
|
50
|
+
* Pass to `Seal` / `SealStream` / `OpenStream` / `SealStreamPool`
|
|
51
|
+
* instead of constructing this object directly. Use
|
|
52
|
+
* `AESGCMSIVCipher.keygen()` to generate a 32-byte master key.
|
|
53
|
+
*
|
|
54
|
+
* The cipher suite is AES-256 only; the standalone `AESGCMSIV` primitive
|
|
55
|
+
* class continues to support both AES-128 and AES-256 (RFC 8452 §6).
|
|
56
|
+
*/
|
|
57
|
+
export const AESGCMSIVCipher = {
|
|
58
|
+
formatEnum: 0x04,
|
|
59
|
+
formatName: 'aes-gcm-siv',
|
|
60
|
+
hkdfInfo: 'aes-gcm-siv-sealstream-v3',
|
|
61
|
+
keySize: 32,
|
|
62
|
+
kemCtSize: 0,
|
|
63
|
+
commitmentSize: 32,
|
|
64
|
+
tagSize: 16,
|
|
65
|
+
padded: false,
|
|
66
|
+
wasmChunkSize: 65536, // src/asm/aes/buffers.ts AES_CHUNK_SIZE
|
|
67
|
+
wasmModules: ['aes'],
|
|
68
|
+
/** Generate a random 32-byte master key suitable for use with `AESGCMSIVCipher`. @returns 32 cryptographically random bytes */
|
|
69
|
+
keygen() {
|
|
70
|
+
return randomBytes(32);
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Derive a 32-byte AES-256-GCM-SIV key and a 32-byte key commitment
|
|
74
|
+
* from `masterKey` and `nonce` via HKDF-SHA-256. The full 20-byte
|
|
75
|
+
* preamble header is appended to the HKDF info string, binding
|
|
76
|
+
* `formatEnum`, framed flag, nonce, and chunkSize into the derived
|
|
77
|
+
* material, header tampering causes derived keys to differ and AEAD
|
|
78
|
+
* fails on the first chunk.
|
|
79
|
+
*
|
|
80
|
+
* The 64-byte HKDF output is split: bytes 0..32 are the per-stream
|
|
81
|
+
* AES-GCM-SIV key, bytes 32..64 are the key commitment that ends up
|
|
82
|
+
* in the preamble. Verifying the commitment before any chunk is
|
|
83
|
+
* processed closes the Invisible Salamanders attack surface,
|
|
84
|
+
* AES-GCM-SIV's POLYVAL-based MAC is not key-committing on its own,
|
|
85
|
+
* so without this an adversary with control over two master keys
|
|
86
|
+
* could craft a single ciphertext + tag that decrypts validly under
|
|
87
|
+
* both.
|
|
88
|
+
*
|
|
89
|
+
* Unlike `XChaCha20Cipher` there is no subkey-derivation step:
|
|
90
|
+
* AES-GCM-SIV's nonce is 12 bytes, used directly per chunk; there
|
|
91
|
+
* is no HChaCha20 analog.
|
|
92
|
+
*
|
|
93
|
+
* @param masterKey 32-byte master key
|
|
94
|
+
* @param nonce Stream nonce (16 bytes)
|
|
95
|
+
* @param _kemCt Unused for symmetric AES-GCM-SIV; KEM wrappers pass it through
|
|
96
|
+
* @param header 20-byte preamble header, required (throws otherwise)
|
|
97
|
+
* @returns `DerivedKeys` holding the 32-byte AES-GCM-SIV key and 32-byte commitment
|
|
98
|
+
*/
|
|
99
|
+
deriveKeys(masterKey, nonce, _kemCt, header) {
|
|
100
|
+
if (!header || header.length !== HEADER_SIZE)
|
|
101
|
+
throw new Error(`AESGCMSIVCipher.deriveKeys: header binding required (got ${header?.length ?? 'undefined'} bytes)`);
|
|
102
|
+
_assertNotOwned('aes');
|
|
103
|
+
_assertNotOwned('sha2');
|
|
104
|
+
const hkdf = new HKDF_SHA256();
|
|
105
|
+
let okm;
|
|
106
|
+
try {
|
|
107
|
+
// INFO || header, binds formatEnum, framed flag, nonce, chunkSize into the KDF.
|
|
108
|
+
// Any header tampering produces different keys, AEAD fails on the first chunk.
|
|
109
|
+
const info = concat(INFO, header);
|
|
110
|
+
okm = hkdf.derive(masterKey, nonce, info, 64);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
hkdf.dispose();
|
|
114
|
+
}
|
|
115
|
+
// Bytes 0..32: per-stream AES-GCM-SIV key (no subkey derivation step).
|
|
116
|
+
// Bytes 32..64: key commitment for the seal preamble.
|
|
117
|
+
const aesKey = okm.slice(0, 32); // independent backing, survives okm wipe
|
|
118
|
+
const commitment = okm.slice(32, 64); // independent backing, survives okm wipe
|
|
119
|
+
wipe(okm);
|
|
120
|
+
return { bytes: aesKey, commitment };
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Encrypt and authenticate one stream chunk with AES-256-GCM-SIV.
|
|
124
|
+
* Output: ciphertext || 16-byte tag.
|
|
125
|
+
* @param keys Derived keys from `deriveKeys`
|
|
126
|
+
* @param counterNonce 12-byte per-chunk nonce (unique per chunk in the stream)
|
|
127
|
+
* @param chunk Plaintext chunk
|
|
128
|
+
* @param aad Optional additional authenticated data
|
|
129
|
+
* @returns Authenticated ciphertext
|
|
130
|
+
*/
|
|
131
|
+
sealChunk(keys, counterNonce, chunk, aad) {
|
|
132
|
+
_assertNotOwned('aes');
|
|
133
|
+
const x = getExports();
|
|
134
|
+
const { ciphertext, tag } = sivAeadEncrypt(x, keys.bytes, counterNonce, chunk, aad ?? new Uint8Array(0));
|
|
135
|
+
const out = new Uint8Array(ciphertext.length + 16);
|
|
136
|
+
out.set(ciphertext);
|
|
137
|
+
out.set(tag, ciphertext.length);
|
|
138
|
+
return out;
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Verify and decrypt one stream chunk. Throws `AuthenticationError('aes-gcm-siv')`
|
|
142
|
+
* on tag mismatch.
|
|
143
|
+
* @param keys Derived keys from `deriveKeys`
|
|
144
|
+
* @param counterNonce 12-byte per-chunk nonce, must match the value used by `sealChunk`
|
|
145
|
+
* @param chunk Ciphertext || 16-byte tag
|
|
146
|
+
* @param aad Optional additional authenticated data
|
|
147
|
+
* @returns Plaintext
|
|
148
|
+
*/
|
|
149
|
+
openChunk(keys, counterNonce, chunk, aad) {
|
|
150
|
+
_assertNotOwned('aes');
|
|
151
|
+
if (chunk.length < 16)
|
|
152
|
+
throw new RangeError(`chunk too short for 16-byte tag (got ${chunk.length})`);
|
|
153
|
+
const x = getExports();
|
|
154
|
+
const ct = chunk.subarray(0, chunk.length - 16);
|
|
155
|
+
const tag = chunk.subarray(chunk.length - 16);
|
|
156
|
+
return sivAeadDecrypt(x, keys.bytes, counterNonce, ct, tag, aad ?? new Uint8Array(0), 'aes-gcm-siv');
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Zero all derived key material in `keys`. Called by the stream
|
|
160
|
+
* layer on teardown and after auth failure.
|
|
161
|
+
* @param keys Derived keys to wipe
|
|
162
|
+
*/
|
|
163
|
+
wipeKeys(keys) {
|
|
164
|
+
wipe(keys.bytes);
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Spawn an AES pool worker from the embedded IIFE bundle. The worker
|
|
168
|
+
* holds its own aes WASM instance and derived AES-GCM-SIV key.
|
|
169
|
+
* @returns Newly constructed `Worker` instance
|
|
170
|
+
*/
|
|
171
|
+
createPoolWorker() {
|
|
172
|
+
// See docs/architecture.md#pool-worker-spawn-pattern.
|
|
173
|
+
const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });
|
|
174
|
+
const url = URL.createObjectURL(blob);
|
|
175
|
+
const w = new Worker(url);
|
|
176
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
177
|
+
return w;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WASM_GZ_BASE64 as aesWasm } from '../embedded/aes.js';
|
|
@@ -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/aes/embedded.ts
|
|
23
|
+
//
|
|
24
|
+
// Exports the gzip+base64 AES WASM blob for use as a WasmSource.
|
|
25
|
+
// Import via `leviathan-crypto/aes/embedded`.
|
|
26
|
+
export { WASM_GZ_BASE64 as aesWasm } from '../embedded/aes.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Generator } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* AES-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. This is the original Fortuna spec primitive,
|
|
8
|
+
* `SerpentGenerator` and `ChaCha20Generator` are deliberate spec
|
|
9
|
+
* deviations; `AESGenerator` is the canonical choice.
|
|
10
|
+
*
|
|
11
|
+
* Pass to `Fortuna.create({ generator: AESGenerator, ... })`, do not
|
|
12
|
+
* call `generate()` directly outside of Fortuna.
|
|
13
|
+
*/
|
|
14
|
+
export declare const AESGenerator: Generator;
|
|
@@ -0,0 +1,103 @@
|
|
|
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/generator.ts
|
|
23
|
+
//
|
|
24
|
+
// Practical Cryptography (Ferguson & Schneier, 2003) §9.4, generator
|
|
25
|
+
// AES-256 ECB counter-mode PRF for Fortuna's generator slot. This is the
|
|
26
|
+
// original Fortuna spec primitive; the library previously deviated by
|
|
27
|
+
// shipping only Serpent and ChaCha20 generators. AES restores the
|
|
28
|
+
// canonical construction.
|
|
29
|
+
import { _assertNotOwned, getInstance } from '../init.js';
|
|
30
|
+
import { wipe } from '../utils.js';
|
|
31
|
+
/**
|
|
32
|
+
* AES-256 ECB counter-mode PRF for Fortuna's generator slot.
|
|
33
|
+
*
|
|
34
|
+
* Each 16-byte counter value is encrypted as a plaintext block to produce
|
|
35
|
+
* one block of pseudorandom output. Practical Cryptography (Ferguson &
|
|
36
|
+
* Schneier, 2003) §9.4. This is the original Fortuna spec primitive,
|
|
37
|
+
* `SerpentGenerator` and `ChaCha20Generator` are deliberate spec
|
|
38
|
+
* deviations; `AESGenerator` is the canonical choice.
|
|
39
|
+
*
|
|
40
|
+
* Pass to `Fortuna.create({ generator: AESGenerator, ... })`, do not
|
|
41
|
+
* call `generate()` directly outside of Fortuna.
|
|
42
|
+
*/
|
|
43
|
+
export const AESGenerator = {
|
|
44
|
+
keySize: 32,
|
|
45
|
+
blockSize: 16,
|
|
46
|
+
counterSize: 16,
|
|
47
|
+
wasmModules: ['aes'],
|
|
48
|
+
/**
|
|
49
|
+
* Generate `n` pseudorandom bytes by encrypting successive 16-byte counter
|
|
50
|
+
* values in ECB mode. The counter is incremented as a 128-bit little-endian
|
|
51
|
+
* integer after each block.
|
|
52
|
+
* @param key 32-byte AES-256 key
|
|
53
|
+
* @param counter 16-byte initial counter value (little-endian)
|
|
54
|
+
* @param n Number of bytes to generate (0 ≤ n ≤ 2^30)
|
|
55
|
+
* @returns `n` pseudorandom bytes
|
|
56
|
+
*/
|
|
57
|
+
generate(key, counter, n) {
|
|
58
|
+
_assertNotOwned('aes');
|
|
59
|
+
if (key.length !== 32)
|
|
60
|
+
throw new RangeError(`AESGenerator: key must be 32 bytes (got ${key.length})`);
|
|
61
|
+
if (counter.length !== 16)
|
|
62
|
+
throw new RangeError(`AESGenerator: counter must be 16 bytes (got ${counter.length})`);
|
|
63
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 30)
|
|
64
|
+
throw new RangeError(`AESGenerator: n must be a non-negative safe integer <= 2^30 (got ${n})`);
|
|
65
|
+
const x = getInstance('aes').exports;
|
|
66
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
67
|
+
const c = counter.slice();
|
|
68
|
+
try {
|
|
69
|
+
mem.set(key, x.getKeyOffset());
|
|
70
|
+
if (x.loadKey(32) !== 0)
|
|
71
|
+
throw new Error('AESGenerator: loadKey failed');
|
|
72
|
+
const blocks = Math.ceil(n / 16);
|
|
73
|
+
const output = new Uint8Array(n);
|
|
74
|
+
const ptOff = x.getBlockPtOffset();
|
|
75
|
+
const ctOff = x.getBlockCtOffset();
|
|
76
|
+
for (let i = 0; i < blocks; i++) {
|
|
77
|
+
mem.set(c, ptOff);
|
|
78
|
+
x.encryptBlock();
|
|
79
|
+
// Last-block trim: copy only what the caller asked for. The
|
|
80
|
+
// unused tail stays in WASM memory (wiped in finally) instead
|
|
81
|
+
// of landing on the JS heap where callers could reach it via
|
|
82
|
+
// `result.buffer`. Mirrors SerpentGenerator/ChaCha20Generator
|
|
83
|
+
// exact-size output.
|
|
84
|
+
const offset = i * 16;
|
|
85
|
+
const writeLen = Math.min(16, n - offset);
|
|
86
|
+
output.set(mem.subarray(ctOff, ctOff + writeLen), offset);
|
|
87
|
+
// Increment c as a 16-byte little-endian integer
|
|
88
|
+
for (let j = 0; j < 16; j++) {
|
|
89
|
+
if (++c[j] !== 0)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
// Wipe WASM key/key-schedule/last-block scratch and the JS-heap
|
|
97
|
+
// counter copy so secret-derived state does not outlive this call
|
|
98
|
+
// in either the WASM linear memory or the JS heap.
|
|
99
|
+
x.wipeBuffers();
|
|
100
|
+
wipe(c);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|