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
package/dist/chacha20/ops.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// src/ts/chacha20/ops.ts
|
|
2
2
|
//
|
|
3
|
-
// Raw XChaCha20-Poly1305 operations
|
|
3
|
+
// Raw XChaCha20-Poly1305 operations, standalone functions that take
|
|
4
4
|
// ChaChaExports explicitly. Used by both the class wrappers (index.ts)
|
|
5
5
|
// and the pool worker (pool.worker.ts), eliminating duplication.
|
|
6
|
-
import { constantTimeEqual } from '../utils.js';
|
|
6
|
+
import { constantTimeEqual, wipe } from '../utils.js';
|
|
7
7
|
import { AuthenticationError } from '../errors.js';
|
|
8
|
-
// ── Module-private helpers
|
|
8
|
+
// ── Module-private helpers ──────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Feed `data` into the active Poly1305 accumulator via the WASM message buffer.
|
|
11
|
+
* No-op when `data` is empty.
|
|
12
|
+
* @param x ChaCha20 WASM exports
|
|
13
|
+
* @param data Bytes to absorb
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
9
16
|
function polyFeed(x, data) {
|
|
10
17
|
if (data.length === 0)
|
|
11
18
|
return;
|
|
@@ -19,11 +26,19 @@ function polyFeed(x, data) {
|
|
|
19
26
|
pos += chunk;
|
|
20
27
|
}
|
|
21
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the 16-byte Poly1305 length footer from AAD and ciphertext lengths.
|
|
31
|
+
* Both lengths are encoded as 64-bit little-endian integers (RFC 8439 §2.8).
|
|
32
|
+
* @param aadLen AAD byte length
|
|
33
|
+
* @param ctLen Ciphertext byte length
|
|
34
|
+
* @returns 16-byte length block
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
22
37
|
function lenBlock(aadLen, ctLen) {
|
|
23
38
|
const b = new Uint8Array(16);
|
|
24
39
|
const dv = new DataView(b.buffer);
|
|
25
|
-
// RFC 8439 §2.8
|
|
26
|
-
// JS numbers are f64
|
|
40
|
+
// RFC 8439 §2.8, 64-bit LE lengths.
|
|
41
|
+
// JS numbers are f64, write low 32 bits directly, high bits via
|
|
27
42
|
// Math.floor(n / 2^32). Safe for n ≤ Number.MAX_SAFE_INTEGER.
|
|
28
43
|
dv.setUint32(0, aadLen >>> 0, true);
|
|
29
44
|
dv.setUint32(4, Math.floor(aadLen / 0x100000000) >>> 0, true);
|
|
@@ -31,12 +46,20 @@ function lenBlock(aadLen, ctLen) {
|
|
|
31
46
|
dv.setUint32(12, Math.floor(ctLen / 0x100000000) >>> 0, true);
|
|
32
47
|
return b;
|
|
33
48
|
}
|
|
34
|
-
// ── Inner AEAD (12-byte nonce)
|
|
35
|
-
/**
|
|
49
|
+
// ── Inner AEAD (12-byte nonce) ──────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8).
|
|
52
|
+
* @param x ChaCha20 WASM exports
|
|
53
|
+
* @param key 32-byte key
|
|
54
|
+
* @param nonce 12-byte nonce, must be unique per (key, message)
|
|
55
|
+
* @param plaintext Data to encrypt (must be ≤ WASM CHUNK_SIZE)
|
|
56
|
+
* @param aad Additional authenticated data
|
|
57
|
+
* @returns `{ ciphertext, tag }`, tag is 16 bytes
|
|
58
|
+
*/
|
|
36
59
|
export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
37
60
|
const maxChunk = x.getChunkSize();
|
|
38
61
|
if (plaintext.length > maxChunk)
|
|
39
|
-
throw new RangeError(`plaintext exceeds ${maxChunk} bytes
|
|
62
|
+
throw new RangeError(`plaintext exceeds ${maxChunk} bytes, split into smaller chunks`);
|
|
40
63
|
const mem = new Uint8Array(x.memory.buffer);
|
|
41
64
|
// Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
|
|
42
65
|
mem.set(key, x.getKeyOffset());
|
|
@@ -50,9 +73,8 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
50
73
|
const aadPad = (16 - aad.length % 16) % 16;
|
|
51
74
|
if (aadPad > 0)
|
|
52
75
|
polyFeed(x, new Uint8Array(aadPad));
|
|
53
|
-
// Step 4:
|
|
76
|
+
// Step 4: counter=1 (state intact from chachaGenPolyKey; no reload needed).
|
|
54
77
|
x.chachaSetCounter(1);
|
|
55
|
-
x.chachaLoadKey();
|
|
56
78
|
// Step 5: Encrypt
|
|
57
79
|
mem.set(plaintext, x.getChunkPtOffset());
|
|
58
80
|
x.chachaEncryptChunk_simd(plaintext.length);
|
|
@@ -71,11 +93,22 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
71
93
|
const tag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
72
94
|
return { ciphertext, tag };
|
|
73
95
|
}
|
|
74
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* ChaCha20-Poly1305 AEAD decrypt with constant-time tag comparison (RFC 8439 §2.8).
|
|
98
|
+
* Throws `AuthenticationError` on tag mismatch; never returns plaintext on failure.
|
|
99
|
+
* @param x ChaCha20 WASM exports
|
|
100
|
+
* @param key 32-byte key
|
|
101
|
+
* @param nonce 12-byte nonce, must match the value used to encrypt
|
|
102
|
+
* @param ciphertext Ciphertext bytes (must be ≤ WASM CHUNK_SIZE)
|
|
103
|
+
* @param tag 16-byte Poly1305 tag
|
|
104
|
+
* @param aad Additional authenticated data
|
|
105
|
+
* @param cipherName Error label for `AuthenticationError` (default 'chacha20-poly1305')
|
|
106
|
+
* @returns Plaintext
|
|
107
|
+
*/
|
|
75
108
|
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'chacha20-poly1305') {
|
|
76
109
|
const maxChunk = x.getChunkSize();
|
|
77
110
|
if (ciphertext.length > maxChunk)
|
|
78
|
-
throw new RangeError(`ciphertext exceeds ${maxChunk} bytes
|
|
111
|
+
throw new RangeError(`ciphertext exceeds ${maxChunk} bytes, split into smaller chunks`);
|
|
79
112
|
const mem = new Uint8Array(x.memory.buffer);
|
|
80
113
|
// Compute expected tag
|
|
81
114
|
mem.set(key, x.getKeyOffset());
|
|
@@ -97,9 +130,14 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'c
|
|
|
97
130
|
const tagOff = x.getPolyTagOffset();
|
|
98
131
|
const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
99
132
|
if (!constantTimeEqual(expectedTag, tag)) {
|
|
100
|
-
//
|
|
133
|
+
// Defense-in-depth wipe on auth fail: chunk ct, chacha block (keystream),
|
|
134
|
+
// Poly1305 one-time subkey copy at POLY_KEY_OFFSET.
|
|
101
135
|
const ctOff = x.getChunkCtOffset();
|
|
102
136
|
mem.fill(0, ctOff, ctOff + maxChunk);
|
|
137
|
+
const blockOff = x.getChachaBlockOffset();
|
|
138
|
+
mem.fill(0, blockOff, blockOff + 64);
|
|
139
|
+
const polyKeyOff = x.getPolyKeyOffset();
|
|
140
|
+
mem.fill(0, polyKeyOff, polyKeyOff + 32);
|
|
103
141
|
throw new AuthenticationError(cipherName);
|
|
104
142
|
}
|
|
105
143
|
// Decrypt only after authentication succeeds
|
|
@@ -110,8 +148,15 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'c
|
|
|
110
148
|
const ptOff = x.getChunkCtOffset();
|
|
111
149
|
return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
|
|
112
150
|
}
|
|
113
|
-
// ── XChaCha20 helpers
|
|
114
|
-
/**
|
|
151
|
+
// ── XChaCha20 helpers ───────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Derive a 32-byte HChaCha20 subkey from `key` and the first 16 bytes of `nonce`.
|
|
154
|
+
* Used as the inner key for XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha §2.3).
|
|
155
|
+
* @param x ChaCha20 WASM exports
|
|
156
|
+
* @param key 32-byte master key
|
|
157
|
+
* @param nonce 24-byte XChaCha20 nonce (only bytes 0-15 are used)
|
|
158
|
+
* @returns 32-byte HChaCha20 subkey
|
|
159
|
+
*/
|
|
115
160
|
export function deriveSubkey(x, key, nonce) {
|
|
116
161
|
const mem = new Uint8Array(x.memory.buffer);
|
|
117
162
|
mem.set(key, x.getKeyOffset());
|
|
@@ -120,28 +165,63 @@ export function deriveSubkey(x, key, nonce) {
|
|
|
120
165
|
const off = x.getXChaChaSubkeyOffset();
|
|
121
166
|
return new Uint8Array(x.memory.buffer).slice(off, off + 32);
|
|
122
167
|
}
|
|
123
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Build the inner 12-byte ChaCha20 nonce for XChaCha20 from bytes 16-23 of the
|
|
170
|
+
* 24-byte XChaCha nonce (draft-irtf-cfrg-xchacha §2.3).
|
|
171
|
+
* @param nonce 24-byte XChaCha20 nonce
|
|
172
|
+
* @returns 12-byte inner nonce (bytes 0-3 are zero, bytes 4-11 are nonce[16:24])
|
|
173
|
+
*/
|
|
124
174
|
export function innerNonce(nonce) {
|
|
125
175
|
const n = new Uint8Array(12);
|
|
126
176
|
n.set(nonce.subarray(16, 24), 4);
|
|
127
177
|
return n;
|
|
128
178
|
}
|
|
129
|
-
// ── Full XChaCha20-Poly1305
|
|
130
|
-
/**
|
|
179
|
+
// ── Full XChaCha20-Poly1305 ─────────────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* XChaCha20-Poly1305 encrypt (draft-irtf-cfrg-xchacha).
|
|
182
|
+
* Derives HChaCha20 subkey from `key` + nonce[0:16], then runs
|
|
183
|
+
* ChaCha20-Poly1305 with a 12-byte inner nonce (nonce[16:24]).
|
|
184
|
+
* @param x ChaCha20 WASM exports
|
|
185
|
+
* @param key 32-byte key
|
|
186
|
+
* @param nonce 24-byte nonce
|
|
187
|
+
* @param plaintext Data to encrypt
|
|
188
|
+
* @param aad Additional authenticated data
|
|
189
|
+
* @returns Ciphertext || 16-byte Poly1305 tag
|
|
190
|
+
*/
|
|
131
191
|
export function xcEncrypt(x, key, nonce, plaintext, aad) {
|
|
132
192
|
const subkey = deriveSubkey(x, key, nonce);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
193
|
+
try {
|
|
194
|
+
const inner = innerNonce(nonce);
|
|
195
|
+
const { ciphertext, tag } = aeadEncrypt(x, subkey, inner, plaintext, aad);
|
|
196
|
+
const result = new Uint8Array(ciphertext.length + 16);
|
|
197
|
+
result.set(ciphertext);
|
|
198
|
+
result.set(tag, ciphertext.length);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
wipe(subkey);
|
|
203
|
+
}
|
|
139
204
|
}
|
|
140
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* XChaCha20-Poly1305 decrypt (draft-irtf-cfrg-xchacha).
|
|
207
|
+
* Derives HChaCha20 subkey, verifies the Poly1305 tag, then decrypts.
|
|
208
|
+
* Throws `AuthenticationError` on tag mismatch.
|
|
209
|
+
* @param x ChaCha20 WASM exports
|
|
210
|
+
* @param key 32-byte key
|
|
211
|
+
* @param nonce 24-byte nonce, must match the value used to encrypt
|
|
212
|
+
* @param ciphertext Ciphertext || 16-byte tag (combined format from `xcEncrypt`)
|
|
213
|
+
* @param aad Additional authenticated data
|
|
214
|
+
* @returns Plaintext
|
|
215
|
+
*/
|
|
141
216
|
export function xcDecrypt(x, key, nonce, ciphertext, aad) {
|
|
142
217
|
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
143
218
|
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
144
219
|
const subkey = deriveSubkey(x, key, nonce);
|
|
145
|
-
|
|
146
|
-
|
|
220
|
+
try {
|
|
221
|
+
const inner = innerNonce(nonce);
|
|
222
|
+
return aeadDecrypt(x, subkey, inner, ct, tag, aad, 'xchacha20-poly1305');
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
wipe(subkey);
|
|
226
|
+
}
|
|
147
227
|
}
|
|
@@ -8,6 +8,17 @@ import { aeadEncrypt, aeadDecrypt } from './ops.js';
|
|
|
8
8
|
import { AuthenticationError } from '../errors.js';
|
|
9
9
|
let x;
|
|
10
10
|
let subkey;
|
|
11
|
+
/**
|
|
12
|
+
* Message handler for the XChaCha20 pool worker.
|
|
13
|
+
*
|
|
14
|
+
* Accepts three message types:
|
|
15
|
+
* - `'init'` , instantiate the chacha20 WASM module and store the derived subkey
|
|
16
|
+
* - `'wipe'` , zero subkey and WASM buffers, then post `{ type: 'wiped' }`
|
|
17
|
+
* - `{ op: 'seal' | 'open', ... }`, encrypt or decrypt one chunk
|
|
18
|
+
*
|
|
19
|
+
* Replies with `{ type: 'result', id, data }` on success or
|
|
20
|
+
* `{ type: 'error', id, message, isAuthError }` on failure.
|
|
21
|
+
*/
|
|
11
22
|
self.onmessage = async (e) => {
|
|
12
23
|
const msg = e.data;
|
|
13
24
|
if (msg.type === 'init') {
|
|
@@ -23,6 +34,8 @@ self.onmessage = async (e) => {
|
|
|
23
34
|
self.postMessage({ type: 'ready' });
|
|
24
35
|
}
|
|
25
36
|
catch (err) {
|
|
37
|
+
if (msg.derivedKeyBytes)
|
|
38
|
+
msg.derivedKeyBytes.fill(0);
|
|
26
39
|
self.postMessage({ type: 'error', id: -1, message: err.message, isAuthError: false });
|
|
27
40
|
}
|
|
28
41
|
return;
|
|
@@ -34,6 +47,7 @@ self.onmessage = async (e) => {
|
|
|
34
47
|
if (x)
|
|
35
48
|
x.wipeBuffers();
|
|
36
49
|
x = undefined;
|
|
50
|
+
self.postMessage({ type: 'wiped' });
|
|
37
51
|
return;
|
|
38
52
|
}
|
|
39
53
|
if (!x || !subkey) {
|
package/dist/chacha20/types.d.ts
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export interface ChaChaExports {
|
|
3
|
-
memory: WebAssembly.Memory;
|
|
4
|
-
getModuleId(): number;
|
|
5
|
-
getKeyOffset(): number;
|
|
6
|
-
getChachaNonceOffset(): number;
|
|
7
|
-
getChachaCtrOffset(): number;
|
|
8
|
-
getChachaBlockOffset(): number;
|
|
9
|
-
getChachaStateOffset(): number;
|
|
10
|
-
getChunkPtOffset(): number;
|
|
11
|
-
getChunkCtOffset(): number;
|
|
12
|
-
getChunkSize(): number;
|
|
13
|
-
getPolyKeyOffset(): number;
|
|
14
|
-
getPolyMsgOffset(): number;
|
|
15
|
-
getPolyTagOffset(): number;
|
|
16
|
-
getPolyBufLenOffset(): number;
|
|
17
|
-
getXChaChaNonceOffset(): number;
|
|
18
|
-
getXChaChaSubkeyOffset(): number;
|
|
19
|
-
chachaLoadKey(): void;
|
|
20
|
-
chachaSetCounter(n: number): void;
|
|
21
|
-
chachaResetCounter(): void;
|
|
22
|
-
chachaEncryptChunk(n: number): number;
|
|
23
|
-
chachaDecryptChunk(n: number): number;
|
|
24
|
-
chachaEncryptChunk_simd(n: number): number;
|
|
25
|
-
chachaDecryptChunk_simd(n: number): number;
|
|
26
|
-
chachaGenPolyKey(): void;
|
|
27
|
-
hchacha20(): void;
|
|
28
|
-
polyInit(): void;
|
|
29
|
-
polyUpdate(n: number): void;
|
|
30
|
-
polyFinal(): void;
|
|
31
|
-
wipeBuffers(): void;
|
|
32
|
-
}
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CTE_WASM: Uint8Array<ArrayBuffer>;
|
package/dist/cte-wasm.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// auto-generated, do not edit
|
|
2
|
+
// raw WASM bytes for constant-time equality comparison module
|
|
3
|
+
export const CTE_WASM = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 1, 96, 3, 127, 127, 127, 1, 127, 3, 2, 1, 0, 5, 4, 1, 1, 1, 1, 7, 20, 2, 7, 99, 111, 109, 112, 97, 114, 101, 0, 0, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 133, 1, 1, 130, 1, 3, 2, 127, 1, 126, 1, 123, 3, 64, 32, 3, 65, 16, 106, 34, 4, 32, 2, 76, 4, 64, 32, 6, 32, 0, 32, 3, 106, 253, 0, 4, 0, 32, 1, 32, 3, 106, 253, 0, 4, 0, 253, 81, 253, 80, 33, 6, 32, 4, 33, 3, 12, 1, 11, 11, 3, 64, 32, 2, 32, 3, 74, 4, 64, 32, 5, 32, 0, 32, 3, 106, 49, 0, 0, 32, 1, 32, 3, 106, 49, 0, 0, 133, 132, 33, 5, 32, 3, 65, 1, 106, 33, 3, 12, 1, 11, 11, 66, 0, 32, 5, 32, 6, 253, 29, 0, 32, 6, 253, 29, 1, 132, 132, 34, 5, 125, 32, 5, 132, 66, 63, 135, 66, 127, 133, 167, 65, 1, 113, 11]);
|
package/dist/cte.wasm
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a 64-byte raw r || s signature to DER per RFC 3279 §2.2.3.
|
|
3
|
+
* Output length is variable: 8 bytes minimum (r = s = 1 byte each, no
|
|
4
|
+
* sign-pad), 72 bytes maximum (both components 32 bytes with high bit
|
|
5
|
+
* set, each picking up a 0x00 sign-pad).
|
|
6
|
+
*
|
|
7
|
+
* @throws TypeError if `sig` is not a Uint8Array
|
|
8
|
+
* @throws RangeError if `sig.length !== 64`
|
|
9
|
+
*/
|
|
10
|
+
export declare function ecdsaSignatureToDer(sig: Uint8Array): Uint8Array;
|
|
11
|
+
/**
|
|
12
|
+
* Convert a DER ECDSA-P256 signature to 64-byte raw r || s. Rejects
|
|
13
|
+
* any DER syntax violation via SigningError('sig-malformed-input'):
|
|
14
|
+
* see the file-level header for the rejection rules.
|
|
15
|
+
*
|
|
16
|
+
* Semantic value rejections (r = 0, s = 0, high-s, off-range) are
|
|
17
|
+
* deferred to the WASM verify path; this function only enforces DER
|
|
18
|
+
* structure.
|
|
19
|
+
*
|
|
20
|
+
* @throws TypeError if `der` is not a Uint8Array
|
|
21
|
+
* @throws SigningError('sig-malformed-input') on any DER syntax error
|
|
22
|
+
*/
|
|
23
|
+
export declare function ecdsaSignatureFromDer(der: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,192 @@
|
|
|
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/ecdsa/der.ts
|
|
23
|
+
//
|
|
24
|
+
// ECDSA signature DER ↔ raw r||s conversion utility, RFC 3279 §2.2.3,
|
|
25
|
+
// ECDSA Signature Algorithm.
|
|
26
|
+
//
|
|
27
|
+
// Ecdsa-Sig-Value ::= SEQUENCE {
|
|
28
|
+
// r INTEGER,
|
|
29
|
+
// s INTEGER
|
|
30
|
+
// }
|
|
31
|
+
//
|
|
32
|
+
// The library's WASM ABI consumes and produces raw 64-byte r || s
|
|
33
|
+
// signatures. DER is a side utility for callers who need X.509 / JWS /
|
|
34
|
+
// TLS interop. The encoder / decoder is hand-rolled against X.690 §8.3
|
|
35
|
+
// (INTEGER) and §8.9 (SEQUENCE); leviathan-crypto is zero-dependency
|
|
36
|
+
// so no external ASN.1 parser is used.
|
|
37
|
+
//
|
|
38
|
+
// Strict DER (not BER). The decoder rejects:
|
|
39
|
+
// - non-minimal length encodings (long-form length when short-form
|
|
40
|
+
// would suffice; X.690 §10.1, definite-length encoding)
|
|
41
|
+
// - excess leading-zero bytes inside INTEGER content (X.690 §8.3.2)
|
|
42
|
+
// - negative INTEGERs (sign bit set on the leading content octet
|
|
43
|
+
// without a 0x00 sign-pad; ECDSA r, s ∈ [1, n-1] are positive)
|
|
44
|
+
// - INTEGER content longer than 33 bytes (32-byte BE scalar plus an
|
|
45
|
+
// optional 0x00 sign-pad)
|
|
46
|
+
// - trailing bytes after the outer SEQUENCE
|
|
47
|
+
// - wrong tags (outer 0x30 SEQUENCE, inner 0x02 INTEGER)
|
|
48
|
+
//
|
|
49
|
+
// The from-DER path never throws on a semantic value problem
|
|
50
|
+
// (r = 0, s = 0, high-s, off-range); those are verify-time rejections
|
|
51
|
+
// in the WASM. Only DER syntax violations throw.
|
|
52
|
+
import { SigningError } from '../errors.js';
|
|
53
|
+
const SEQUENCE_TAG = 0x30;
|
|
54
|
+
const INTEGER_TAG = 0x02;
|
|
55
|
+
function reject(detail) {
|
|
56
|
+
throw new SigningError('sig-malformed-input', `leviathan-crypto: ecdsa-p256 DER signature ${detail}`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Encode a positive 32-byte BE scalar (r or s component) as a DER
|
|
60
|
+
* INTEGER TLV. Strips leading zero bytes to the minimal encoding per
|
|
61
|
+
* X.690 §8.3.2, then prepends a single 0x00 if the high bit of the
|
|
62
|
+
* resulting first content byte is set, so the INTEGER remains positive.
|
|
63
|
+
*
|
|
64
|
+
* The component length is at most 33 bytes (32-byte scalar + optional
|
|
65
|
+
* sign-pad), so the DER length octet always fits in short-form.
|
|
66
|
+
*/
|
|
67
|
+
function encodeInteger(scalarBE) {
|
|
68
|
+
let start = 0;
|
|
69
|
+
while (start < scalarBE.length - 1 && scalarBE[start] === 0)
|
|
70
|
+
start++;
|
|
71
|
+
const stripped = scalarBE.subarray(start);
|
|
72
|
+
const needsPad = (stripped[0] & 0x80) !== 0;
|
|
73
|
+
const contentLen = stripped.length + (needsPad ? 1 : 0);
|
|
74
|
+
const out = new Uint8Array(2 + contentLen);
|
|
75
|
+
out[0] = INTEGER_TAG;
|
|
76
|
+
out[1] = contentLen;
|
|
77
|
+
if (needsPad) {
|
|
78
|
+
out[2] = 0;
|
|
79
|
+
out.set(stripped, 3);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
out.set(stripped, 2);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decode one DER INTEGER TLV starting at `start`. Returns the strict-DER
|
|
88
|
+
* minimal content bytes (with any leading 0x00 sign-pad stripped) and
|
|
89
|
+
* the offset immediately past the INTEGER. Throws SigningError on any
|
|
90
|
+
* DER syntax violation.
|
|
91
|
+
*/
|
|
92
|
+
function decodeInteger(der, start) {
|
|
93
|
+
if (start + 2 > der.length)
|
|
94
|
+
reject(`has a truncated INTEGER header at offset ${start}`);
|
|
95
|
+
if (der[start] !== INTEGER_TAG)
|
|
96
|
+
reject(`INTEGER tag at offset ${start} is 0x${der[start].toString(16).padStart(2, '0')}, expected 0x02`);
|
|
97
|
+
const lenByte = der[start + 1];
|
|
98
|
+
// Strict DER: ECDSA-P256 INTEGER content is at most 33 bytes, so the
|
|
99
|
+
// length octet is always short-form (high bit clear, value 1..127).
|
|
100
|
+
if (lenByte & 0x80)
|
|
101
|
+
reject(`INTEGER at offset ${start} uses long-form length encoding (forbidden for content < 128 bytes)`);
|
|
102
|
+
// Zero-length INTEGER content has no representable value; ASN.1
|
|
103
|
+
// requires at least one content octet (X.690 §8.3.1).
|
|
104
|
+
if (lenByte === 0)
|
|
105
|
+
reject(`INTEGER at offset ${start} has zero-length content`);
|
|
106
|
+
const contentStart = start + 2;
|
|
107
|
+
const contentEnd = contentStart + lenByte;
|
|
108
|
+
if (contentEnd > der.length)
|
|
109
|
+
reject(`INTEGER at offset ${start} extends past the outer SEQUENCE end`);
|
|
110
|
+
const content = der.subarray(contentStart, contentEnd);
|
|
111
|
+
// Minimal encoding: a leading 0x00 octet is permitted only when the
|
|
112
|
+
// next byte's high bit is set (sign-pad). Otherwise the 0x00 is
|
|
113
|
+
// excess per X.690 §8.3.2.
|
|
114
|
+
if (content[0] === 0x00 && content.length > 1 && (content[1] & 0x80) === 0)
|
|
115
|
+
reject(`INTEGER at offset ${start} has excess leading zero byte (non-minimal DER)`);
|
|
116
|
+
// ECDSA r, s ∈ [1, n-1] are positive integers. A first content
|
|
117
|
+
// octet with the high bit set means the ASN.1 INTEGER decodes as a
|
|
118
|
+
// negative two's-complement value; reject.
|
|
119
|
+
if ((content[0] & 0x80) !== 0)
|
|
120
|
+
reject(`INTEGER at offset ${start} is negative (high bit set on first content byte); ECDSA r, s are positive`);
|
|
121
|
+
const value = (content[0] === 0x00) ? content.subarray(1) : content;
|
|
122
|
+
return { value, next: contentEnd };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Convert a 64-byte raw r || s signature to DER per RFC 3279 §2.2.3.
|
|
126
|
+
* Output length is variable: 8 bytes minimum (r = s = 1 byte each, no
|
|
127
|
+
* sign-pad), 72 bytes maximum (both components 32 bytes with high bit
|
|
128
|
+
* set, each picking up a 0x00 sign-pad).
|
|
129
|
+
*
|
|
130
|
+
* @throws TypeError if `sig` is not a Uint8Array
|
|
131
|
+
* @throws RangeError if `sig.length !== 64`
|
|
132
|
+
*/
|
|
133
|
+
export function ecdsaSignatureToDer(sig) {
|
|
134
|
+
if (!(sig instanceof Uint8Array))
|
|
135
|
+
throw new TypeError('leviathan-crypto: ecdsa-p256 raw signature must be a Uint8Array');
|
|
136
|
+
if (sig.length !== 64)
|
|
137
|
+
throw new RangeError(`leviathan-crypto: ecdsa-p256 raw signature must be 64 bytes r||s (got ${sig.length})`);
|
|
138
|
+
const r = encodeInteger(sig.subarray(0, 32));
|
|
139
|
+
const s = encodeInteger(sig.subarray(32, 64));
|
|
140
|
+
const contentLen = r.length + s.length;
|
|
141
|
+
// SEQUENCE content is at most 2 * 35 = 70 bytes, so the SEQUENCE
|
|
142
|
+
// length octet is always short-form.
|
|
143
|
+
const out = new Uint8Array(2 + contentLen);
|
|
144
|
+
out[0] = SEQUENCE_TAG;
|
|
145
|
+
out[1] = contentLen;
|
|
146
|
+
out.set(r, 2);
|
|
147
|
+
out.set(s, 2 + r.length);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Convert a DER ECDSA-P256 signature to 64-byte raw r || s. Rejects
|
|
152
|
+
* any DER syntax violation via SigningError('sig-malformed-input'):
|
|
153
|
+
* see the file-level header for the rejection rules.
|
|
154
|
+
*
|
|
155
|
+
* Semantic value rejections (r = 0, s = 0, high-s, off-range) are
|
|
156
|
+
* deferred to the WASM verify path; this function only enforces DER
|
|
157
|
+
* structure.
|
|
158
|
+
*
|
|
159
|
+
* @throws TypeError if `der` is not a Uint8Array
|
|
160
|
+
* @throws SigningError('sig-malformed-input') on any DER syntax error
|
|
161
|
+
*/
|
|
162
|
+
export function ecdsaSignatureFromDer(der) {
|
|
163
|
+
if (!(der instanceof Uint8Array))
|
|
164
|
+
throw new TypeError('leviathan-crypto: ecdsa-p256 DER signature must be a Uint8Array');
|
|
165
|
+
// 8 bytes is the absolute minimum: SEQUENCE(2) + INTEGER(0x01 0x01)
|
|
166
|
+
// + INTEGER(0x01 0x01) = 8. Anything shorter cannot represent two
|
|
167
|
+
// non-empty INTEGER components.
|
|
168
|
+
if (der.length < 8)
|
|
169
|
+
reject(`is shorter than the 8-byte minimum (got ${der.length} bytes)`);
|
|
170
|
+
if (der[0] !== SEQUENCE_TAG)
|
|
171
|
+
reject(`outer tag is 0x${der[0].toString(16).padStart(2, '0')}, expected 0x30 (SEQUENCE)`);
|
|
172
|
+
const seqLen = der[1];
|
|
173
|
+
// ECDSA-P256 SEQUENCE content is at most 70 bytes; strict DER
|
|
174
|
+
// requires short-form length encoding when < 128.
|
|
175
|
+
if (seqLen & 0x80)
|
|
176
|
+
reject('uses long-form length encoding for the outer SEQUENCE (forbidden for content < 128 bytes)');
|
|
177
|
+
if (2 + seqLen !== der.length)
|
|
178
|
+
reject(`outer SEQUENCE length ${seqLen} does not match input size (${der.length} bytes total)`);
|
|
179
|
+
const { value: r, next: afterR } = decodeInteger(der, 2);
|
|
180
|
+
const { value: s, next: end } = decodeInteger(der, afterR);
|
|
181
|
+
if (end !== der.length)
|
|
182
|
+
reject('has trailing bytes after the second INTEGER');
|
|
183
|
+
// Each component must fit in 32 bytes BE (P-256 scalar size).
|
|
184
|
+
if (r.length > 32)
|
|
185
|
+
reject(`r component is ${r.length} bytes, exceeds the 32-byte scalar size`);
|
|
186
|
+
if (s.length > 32)
|
|
187
|
+
reject(`s component is ${s.length} bytes, exceeds the 32-byte scalar size`);
|
|
188
|
+
const out = new Uint8Array(64);
|
|
189
|
+
out.set(r, 32 - r.length);
|
|
190
|
+
out.set(s, 64 - s.length);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a 32-byte P-256 secret scalar as DER `ECPrivateKey` per
|
|
3
|
+
* RFC 5915 §3. Output length is exactly 51 bytes; version is 1, the
|
|
4
|
+
* named-curve OID for secp256r1 (`1.2.840.10045.3.1.7`,
|
|
5
|
+
* SP 800-186 §3.2.1.3) is included in `parameters [0]`, and the
|
|
6
|
+
* `publicKey [1]` field is omitted.
|
|
7
|
+
*
|
|
8
|
+
* Byte-stable: same input scalar produces byte-identical output. No
|
|
9
|
+
* canonicalisation pass needed because every field has a single legal
|
|
10
|
+
* DER encoding under the strict-DER rules of X.690 §10.
|
|
11
|
+
*
|
|
12
|
+
* @throws TypeError if `scalar` is not a Uint8Array
|
|
13
|
+
* @throws RangeError if `scalar.length !== 32`
|
|
14
|
+
*/
|
|
15
|
+
export declare function encodeEcPrivateKey(scalar: Uint8Array): Uint8Array;
|
|
16
|
+
/**
|
|
17
|
+
* Decode a DER `ECPrivateKey` (RFC 5915 §3) and return the 32-byte
|
|
18
|
+
* raw P-256 secret scalar.
|
|
19
|
+
*
|
|
20
|
+
* Strict-DER per X.690 §10 (Restrictions on the BER). Rejects the
|
|
21
|
+
* cases enumerated in the file-level header. Accepts (and ignores)
|
|
22
|
+
* an optional `publicKey [1]` field per RFC 5915 §3 lenient input;
|
|
23
|
+
* the scalar is the only return value.
|
|
24
|
+
*
|
|
25
|
+
* Any curve OID inside `parameters [0]` other than secp256r1
|
|
26
|
+
* (`1.2.840.10045.3.1.7`) is rejected. Decoders that need other
|
|
27
|
+
* curves must use a different parser; this library is P-256 only.
|
|
28
|
+
*
|
|
29
|
+
* @throws TypeError if `der` is not a Uint8Array
|
|
30
|
+
* @throws Error on any DER syntax violation or unsupported parameter
|
|
31
|
+
*/
|
|
32
|
+
export declare function decodeEcPrivateKey(der: Uint8Array): Uint8Array;
|