@bcts/crypto 1.0.0-alpha.10
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/LICENSE +48 -0
- package/README.md +65 -0
- package/dist/index.cjs +658 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +403 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +403 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +652 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +594 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +80 -0
- package/src/argon.ts +47 -0
- package/src/ecdsa-keys.ts +81 -0
- package/src/ecdsa-signing.ts +65 -0
- package/src/ed25519-signing.ts +72 -0
- package/src/error.ts +49 -0
- package/src/hash.ts +134 -0
- package/src/index.ts +115 -0
- package/src/memzero.ts +37 -0
- package/src/public-key-encryption.ts +68 -0
- package/src/schnorr-signing.ts +90 -0
- package/src/scrypt.ts +50 -0
- package/src/symmetric-encryption.ts +133 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/lib.rs
|
|
2
|
+
|
|
3
|
+
// Re-export all modules
|
|
4
|
+
|
|
5
|
+
// Error types
|
|
6
|
+
export { CryptoError, AeadError, type CryptoResult } from "./error.js";
|
|
7
|
+
|
|
8
|
+
// Memory zeroing
|
|
9
|
+
export { memzero, memzeroVecVecU8 } from "./memzero.js";
|
|
10
|
+
|
|
11
|
+
// Hash constants
|
|
12
|
+
export { CRC32_SIZE, SHA256_SIZE, SHA512_SIZE } from "./hash.js";
|
|
13
|
+
|
|
14
|
+
// Hash functions
|
|
15
|
+
export {
|
|
16
|
+
crc32,
|
|
17
|
+
crc32Data,
|
|
18
|
+
crc32DataOpt,
|
|
19
|
+
sha256,
|
|
20
|
+
doubleSha256,
|
|
21
|
+
sha512,
|
|
22
|
+
hmacSha256,
|
|
23
|
+
hmacSha512,
|
|
24
|
+
pbkdf2HmacSha256,
|
|
25
|
+
pbkdf2HmacSha512,
|
|
26
|
+
hkdfHmacSha256,
|
|
27
|
+
hkdfHmacSha512,
|
|
28
|
+
} from "./hash.js";
|
|
29
|
+
|
|
30
|
+
// Symmetric encryption constants
|
|
31
|
+
export {
|
|
32
|
+
SYMMETRIC_KEY_SIZE,
|
|
33
|
+
SYMMETRIC_NONCE_SIZE,
|
|
34
|
+
SYMMETRIC_AUTH_SIZE,
|
|
35
|
+
} from "./symmetric-encryption.js";
|
|
36
|
+
|
|
37
|
+
// Symmetric encryption functions
|
|
38
|
+
export {
|
|
39
|
+
aeadChaCha20Poly1305Encrypt,
|
|
40
|
+
aeadChaCha20Poly1305EncryptWithAad,
|
|
41
|
+
aeadChaCha20Poly1305Decrypt,
|
|
42
|
+
aeadChaCha20Poly1305DecryptWithAad,
|
|
43
|
+
} from "./symmetric-encryption.js";
|
|
44
|
+
|
|
45
|
+
// Public key encryption constants
|
|
46
|
+
export {
|
|
47
|
+
GENERIC_PRIVATE_KEY_SIZE,
|
|
48
|
+
GENERIC_PUBLIC_KEY_SIZE,
|
|
49
|
+
X25519_PRIVATE_KEY_SIZE,
|
|
50
|
+
X25519_PUBLIC_KEY_SIZE,
|
|
51
|
+
} from "./public-key-encryption.js";
|
|
52
|
+
|
|
53
|
+
// Public key encryption functions
|
|
54
|
+
export {
|
|
55
|
+
deriveAgreementPrivateKey,
|
|
56
|
+
deriveSigningPrivateKey,
|
|
57
|
+
x25519NewPrivateKeyUsing,
|
|
58
|
+
x25519PublicKeyFromPrivateKey,
|
|
59
|
+
x25519SharedKey,
|
|
60
|
+
} from "./public-key-encryption.js";
|
|
61
|
+
|
|
62
|
+
// ECDSA key constants
|
|
63
|
+
export {
|
|
64
|
+
ECDSA_PRIVATE_KEY_SIZE,
|
|
65
|
+
ECDSA_PUBLIC_KEY_SIZE,
|
|
66
|
+
ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE,
|
|
67
|
+
ECDSA_MESSAGE_HASH_SIZE,
|
|
68
|
+
ECDSA_SIGNATURE_SIZE,
|
|
69
|
+
SCHNORR_PUBLIC_KEY_SIZE,
|
|
70
|
+
} from "./ecdsa-keys.js";
|
|
71
|
+
|
|
72
|
+
// ECDSA key functions
|
|
73
|
+
export {
|
|
74
|
+
ecdsaNewPrivateKeyUsing,
|
|
75
|
+
ecdsaPublicKeyFromPrivateKey,
|
|
76
|
+
ecdsaDecompressPublicKey,
|
|
77
|
+
ecdsaCompressPublicKey,
|
|
78
|
+
ecdsaDerivePrivateKey,
|
|
79
|
+
schnorrPublicKeyFromPrivateKey,
|
|
80
|
+
} from "./ecdsa-keys.js";
|
|
81
|
+
|
|
82
|
+
// ECDSA signing functions
|
|
83
|
+
export { ecdsaSign, ecdsaVerify } from "./ecdsa-signing.js";
|
|
84
|
+
|
|
85
|
+
// Schnorr signing constants
|
|
86
|
+
export { SCHNORR_SIGNATURE_SIZE } from "./schnorr-signing.js";
|
|
87
|
+
|
|
88
|
+
// Schnorr signing functions
|
|
89
|
+
export {
|
|
90
|
+
schnorrSign,
|
|
91
|
+
schnorrSignUsing,
|
|
92
|
+
schnorrSignWithAuxRand,
|
|
93
|
+
schnorrVerify,
|
|
94
|
+
} from "./schnorr-signing.js";
|
|
95
|
+
|
|
96
|
+
// Ed25519 constants
|
|
97
|
+
export {
|
|
98
|
+
ED25519_PUBLIC_KEY_SIZE,
|
|
99
|
+
ED25519_PRIVATE_KEY_SIZE,
|
|
100
|
+
ED25519_SIGNATURE_SIZE,
|
|
101
|
+
} from "./ed25519-signing.js";
|
|
102
|
+
|
|
103
|
+
// Ed25519 functions
|
|
104
|
+
export {
|
|
105
|
+
ed25519NewPrivateKeyUsing,
|
|
106
|
+
ed25519PublicKeyFromPrivateKey,
|
|
107
|
+
ed25519Sign,
|
|
108
|
+
ed25519Verify,
|
|
109
|
+
} from "./ed25519-signing.js";
|
|
110
|
+
|
|
111
|
+
// Scrypt
|
|
112
|
+
export { scrypt, scryptOpt } from "./scrypt.js";
|
|
113
|
+
|
|
114
|
+
// Argon2id
|
|
115
|
+
export { argon2idHash, argon2idHashOpt } from "./argon.js";
|
package/src/memzero.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/memzero.rs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Securely zero out a typed array.
|
|
5
|
+
*
|
|
6
|
+
* **IMPORTANT: This is a best-effort implementation.** Unlike the Rust reference
|
|
7
|
+
* implementation which uses `std::ptr::write_volatile()` for guaranteed volatile
|
|
8
|
+
* writes, JavaScript engines and JIT compilers can still potentially optimize
|
|
9
|
+
* away these zeroing operations. The check at the end helps prevent optimization,
|
|
10
|
+
* but it is not foolproof.
|
|
11
|
+
*
|
|
12
|
+
* For truly sensitive cryptographic operations, consider using the Web Crypto API's
|
|
13
|
+
* `crypto.subtle` with non-extractable keys when possible, as it provides stronger
|
|
14
|
+
* guarantees than what can be achieved with pure JavaScript.
|
|
15
|
+
*
|
|
16
|
+
* This function attempts to prevent the compiler from optimizing away
|
|
17
|
+
* the zeroing operation by using a verification check after the zeroing loop.
|
|
18
|
+
*/
|
|
19
|
+
export function memzero(data: Uint8Array | Uint32Array): void {
|
|
20
|
+
const len = data.length;
|
|
21
|
+
for (let i = 0; i < len; i++) {
|
|
22
|
+
data[i] = 0;
|
|
23
|
+
}
|
|
24
|
+
// Force a side effect to prevent optimization
|
|
25
|
+
if (data.length > 0 && data[0] !== 0) {
|
|
26
|
+
throw new Error("memzero failed");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Securely zero out an array of Uint8Arrays.
|
|
32
|
+
*/
|
|
33
|
+
export function memzeroVecVecU8(arrays: Uint8Array[]): void {
|
|
34
|
+
for (const arr of arrays) {
|
|
35
|
+
memzero(arr);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/public_key_encryption.rs
|
|
2
|
+
|
|
3
|
+
import { x25519 } from "@noble/curves/ed25519.js";
|
|
4
|
+
import type { RandomNumberGenerator } from "@bcts/rand";
|
|
5
|
+
import { hkdfHmacSha256 } from "./hash.js";
|
|
6
|
+
|
|
7
|
+
// Constants
|
|
8
|
+
export const GENERIC_PRIVATE_KEY_SIZE = 32;
|
|
9
|
+
export const GENERIC_PUBLIC_KEY_SIZE = 32;
|
|
10
|
+
export const X25519_PRIVATE_KEY_SIZE = 32;
|
|
11
|
+
export const X25519_PUBLIC_KEY_SIZE = 32;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive an X25519 agreement private key from key material.
|
|
15
|
+
* Uses HKDF with "agreement" as domain separation salt.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveAgreementPrivateKey(keyMaterial: Uint8Array): Uint8Array {
|
|
18
|
+
const salt = new TextEncoder().encode("agreement");
|
|
19
|
+
return hkdfHmacSha256(keyMaterial, salt, X25519_PRIVATE_KEY_SIZE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Derive a signing private key from key material.
|
|
24
|
+
* Uses HKDF with "signing" as domain separation salt.
|
|
25
|
+
*/
|
|
26
|
+
export function deriveSigningPrivateKey(keyMaterial: Uint8Array): Uint8Array {
|
|
27
|
+
const salt = new TextEncoder().encode("signing");
|
|
28
|
+
return hkdfHmacSha256(keyMaterial, salt, 32);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a new random X25519 private key.
|
|
33
|
+
*/
|
|
34
|
+
export function x25519NewPrivateKeyUsing(rng: RandomNumberGenerator): Uint8Array {
|
|
35
|
+
return rng.randomData(X25519_PRIVATE_KEY_SIZE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive an X25519 public key from a private key.
|
|
40
|
+
*/
|
|
41
|
+
export function x25519PublicKeyFromPrivateKey(privateKey: Uint8Array): Uint8Array {
|
|
42
|
+
if (privateKey.length !== X25519_PRIVATE_KEY_SIZE) {
|
|
43
|
+
throw new Error(`Private key must be ${X25519_PRIVATE_KEY_SIZE} bytes`);
|
|
44
|
+
}
|
|
45
|
+
return x25519.getPublicKey(privateKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute a shared secret using X25519 key agreement (ECDH).
|
|
50
|
+
*
|
|
51
|
+
* **Security Note**: The resulting shared secret should be used with a KDF
|
|
52
|
+
* (like HKDF) before using it as an encryption key. Never use the raw
|
|
53
|
+
* shared secret directly for encryption.
|
|
54
|
+
*
|
|
55
|
+
* @param x25519Private - 32-byte X25519 private key
|
|
56
|
+
* @param x25519Public - 32-byte X25519 public key from the other party
|
|
57
|
+
* @returns 32-byte shared secret
|
|
58
|
+
* @throws {Error} If private key is not 32 bytes or public key is not 32 bytes
|
|
59
|
+
*/
|
|
60
|
+
export function x25519SharedKey(x25519Private: Uint8Array, x25519Public: Uint8Array): Uint8Array {
|
|
61
|
+
if (x25519Private.length !== X25519_PRIVATE_KEY_SIZE) {
|
|
62
|
+
throw new Error(`Private key must be ${X25519_PRIVATE_KEY_SIZE} bytes`);
|
|
63
|
+
}
|
|
64
|
+
if (x25519Public.length !== X25519_PUBLIC_KEY_SIZE) {
|
|
65
|
+
throw new Error(`Public key must be ${X25519_PUBLIC_KEY_SIZE} bytes`);
|
|
66
|
+
}
|
|
67
|
+
return x25519.getSharedSecret(x25519Private, x25519Public);
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/schnorr_signing.rs
|
|
2
|
+
|
|
3
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
4
|
+
import type { RandomNumberGenerator } from "@bcts/rand";
|
|
5
|
+
import { SecureRandomNumberGenerator } from "@bcts/rand";
|
|
6
|
+
import { ECDSA_PRIVATE_KEY_SIZE, SCHNORR_PUBLIC_KEY_SIZE } from "./ecdsa-keys.js";
|
|
7
|
+
|
|
8
|
+
// Constants
|
|
9
|
+
export const SCHNORR_SIGNATURE_SIZE = 64;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sign a message using Schnorr signature (BIP-340).
|
|
13
|
+
* Uses secure random auxiliary randomness.
|
|
14
|
+
*
|
|
15
|
+
* @param ecdsaPrivateKey - 32-byte private key
|
|
16
|
+
* @param message - Message to sign (not pre-hashed, per BIP-340)
|
|
17
|
+
* @returns 64-byte Schnorr signature
|
|
18
|
+
*/
|
|
19
|
+
export function schnorrSign(ecdsaPrivateKey: Uint8Array, message: Uint8Array): Uint8Array {
|
|
20
|
+
const rng = new SecureRandomNumberGenerator();
|
|
21
|
+
return schnorrSignUsing(ecdsaPrivateKey, message, rng);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sign a message using Schnorr signature with a custom RNG.
|
|
26
|
+
*
|
|
27
|
+
* @param ecdsaPrivateKey - 32-byte private key
|
|
28
|
+
* @param message - Message to sign
|
|
29
|
+
* @param rng - Random number generator for auxiliary randomness
|
|
30
|
+
* @returns 64-byte Schnorr signature
|
|
31
|
+
*/
|
|
32
|
+
export function schnorrSignUsing(
|
|
33
|
+
ecdsaPrivateKey: Uint8Array,
|
|
34
|
+
message: Uint8Array,
|
|
35
|
+
rng: RandomNumberGenerator,
|
|
36
|
+
): Uint8Array {
|
|
37
|
+
const auxRand = rng.randomData(32);
|
|
38
|
+
return schnorrSignWithAuxRand(ecdsaPrivateKey, message, auxRand);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sign a message using Schnorr signature with specific auxiliary randomness.
|
|
43
|
+
* This is useful for deterministic signing in tests.
|
|
44
|
+
*
|
|
45
|
+
* @param ecdsaPrivateKey - 32-byte private key
|
|
46
|
+
* @param message - Message to sign
|
|
47
|
+
* @param auxRand - 32-byte auxiliary randomness (per BIP-340)
|
|
48
|
+
* @returns 64-byte Schnorr signature
|
|
49
|
+
*/
|
|
50
|
+
export function schnorrSignWithAuxRand(
|
|
51
|
+
ecdsaPrivateKey: Uint8Array,
|
|
52
|
+
message: Uint8Array,
|
|
53
|
+
auxRand: Uint8Array,
|
|
54
|
+
): Uint8Array {
|
|
55
|
+
if (ecdsaPrivateKey.length !== ECDSA_PRIVATE_KEY_SIZE) {
|
|
56
|
+
throw new Error(`Private key must be ${ECDSA_PRIVATE_KEY_SIZE} bytes`);
|
|
57
|
+
}
|
|
58
|
+
if (auxRand.length !== 32) {
|
|
59
|
+
throw new Error("Auxiliary randomness must be 32 bytes");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return schnorr.sign(message, ecdsaPrivateKey, auxRand);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Verify a Schnorr signature (BIP-340).
|
|
67
|
+
*
|
|
68
|
+
* @param schnorrPublicKey - 32-byte x-only public key
|
|
69
|
+
* @param signature - 64-byte Schnorr signature
|
|
70
|
+
* @param message - Original message
|
|
71
|
+
* @returns true if signature is valid
|
|
72
|
+
*/
|
|
73
|
+
export function schnorrVerify(
|
|
74
|
+
schnorrPublicKey: Uint8Array,
|
|
75
|
+
signature: Uint8Array,
|
|
76
|
+
message: Uint8Array,
|
|
77
|
+
): boolean {
|
|
78
|
+
if (schnorrPublicKey.length !== SCHNORR_PUBLIC_KEY_SIZE) {
|
|
79
|
+
throw new Error(`Public key must be ${SCHNORR_PUBLIC_KEY_SIZE} bytes`);
|
|
80
|
+
}
|
|
81
|
+
if (signature.length !== SCHNORR_SIGNATURE_SIZE) {
|
|
82
|
+
throw new Error(`Signature must be ${SCHNORR_SIGNATURE_SIZE} bytes`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return schnorr.verify(signature, message, schnorrPublicKey);
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/scrypt.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/scrypt.rs
|
|
2
|
+
|
|
3
|
+
import { scrypt as nobleScrypt } from "@noble/hashes/scrypt.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive a key using Scrypt with recommended parameters.
|
|
7
|
+
* Uses N=2^15 (32768), r=8, p=1 as recommended defaults.
|
|
8
|
+
*
|
|
9
|
+
* @param password - Password or passphrase
|
|
10
|
+
* @param salt - Salt value
|
|
11
|
+
* @param outputLen - Desired output length
|
|
12
|
+
* @returns Derived key
|
|
13
|
+
*/
|
|
14
|
+
export function scrypt(password: Uint8Array, salt: Uint8Array, outputLen: number): Uint8Array {
|
|
15
|
+
return scryptOpt(password, salt, outputLen, 15, 8, 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derive a key using Scrypt with custom parameters.
|
|
20
|
+
*
|
|
21
|
+
* @param password - Password or passphrase
|
|
22
|
+
* @param salt - Salt value
|
|
23
|
+
* @param outputLen - Desired output length
|
|
24
|
+
* @param logN - Log2 of the CPU/memory cost parameter N (must be <64)
|
|
25
|
+
* @param r - Block size parameter (must be >0)
|
|
26
|
+
* @param p - Parallelization parameter (must be >0)
|
|
27
|
+
* @returns Derived key
|
|
28
|
+
*/
|
|
29
|
+
export function scryptOpt(
|
|
30
|
+
password: Uint8Array,
|
|
31
|
+
salt: Uint8Array,
|
|
32
|
+
outputLen: number,
|
|
33
|
+
logN: number,
|
|
34
|
+
r: number,
|
|
35
|
+
p: number,
|
|
36
|
+
): Uint8Array {
|
|
37
|
+
if (logN >= 64) {
|
|
38
|
+
throw new Error("logN must be <64");
|
|
39
|
+
}
|
|
40
|
+
if (r === 0) {
|
|
41
|
+
throw new Error("r must be >0");
|
|
42
|
+
}
|
|
43
|
+
if (p === 0) {
|
|
44
|
+
throw new Error("p must be >0");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const N = 1 << logN; // 2^logN
|
|
48
|
+
|
|
49
|
+
return nobleScrypt(password, salt, { N, r, p, dkLen: outputLen });
|
|
50
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Ported from bc-crypto-rust/src/symmetric_encryption.rs
|
|
2
|
+
|
|
3
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
4
|
+
import { CryptoError, AeadError } from "./error.js";
|
|
5
|
+
|
|
6
|
+
// Constants
|
|
7
|
+
export const SYMMETRIC_KEY_SIZE = 32;
|
|
8
|
+
export const SYMMETRIC_NONCE_SIZE = 12;
|
|
9
|
+
export const SYMMETRIC_AUTH_SIZE = 16;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Encrypt data using ChaCha20-Poly1305 AEAD cipher.
|
|
13
|
+
*
|
|
14
|
+
* **Security Warning**: The nonce MUST be unique for every encryption operation
|
|
15
|
+
* with the same key. Reusing a nonce completely breaks the security of the
|
|
16
|
+
* encryption scheme and can reveal plaintext.
|
|
17
|
+
*
|
|
18
|
+
* @param plaintext - The data to encrypt
|
|
19
|
+
* @param key - 32-byte encryption key
|
|
20
|
+
* @param nonce - 12-byte nonce (MUST be unique per encryption with the same key)
|
|
21
|
+
* @returns Tuple of [ciphertext, authTag] where authTag is 16 bytes
|
|
22
|
+
* @throws {CryptoError} If key is not 32 bytes or nonce is not 12 bytes
|
|
23
|
+
*/
|
|
24
|
+
export function aeadChaCha20Poly1305Encrypt(
|
|
25
|
+
plaintext: Uint8Array,
|
|
26
|
+
key: Uint8Array,
|
|
27
|
+
nonce: Uint8Array,
|
|
28
|
+
): [Uint8Array, Uint8Array] {
|
|
29
|
+
return aeadChaCha20Poly1305EncryptWithAad(plaintext, key, nonce, new Uint8Array(0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encrypt data using ChaCha20-Poly1305 AEAD cipher with additional authenticated data.
|
|
34
|
+
*
|
|
35
|
+
* **Security Warning**: The nonce MUST be unique for every encryption operation
|
|
36
|
+
* with the same key. Reusing a nonce completely breaks the security of the
|
|
37
|
+
* encryption scheme and can reveal plaintext.
|
|
38
|
+
*
|
|
39
|
+
* @param plaintext - The data to encrypt
|
|
40
|
+
* @param key - 32-byte encryption key
|
|
41
|
+
* @param nonce - 12-byte nonce (MUST be unique per encryption with the same key)
|
|
42
|
+
* @param aad - Additional authenticated data (not encrypted, but integrity-protected)
|
|
43
|
+
* @returns Tuple of [ciphertext, authTag] where authTag is 16 bytes
|
|
44
|
+
* @throws {CryptoError} If key is not 32 bytes or nonce is not 12 bytes
|
|
45
|
+
*/
|
|
46
|
+
export function aeadChaCha20Poly1305EncryptWithAad(
|
|
47
|
+
plaintext: Uint8Array,
|
|
48
|
+
key: Uint8Array,
|
|
49
|
+
nonce: Uint8Array,
|
|
50
|
+
aad: Uint8Array,
|
|
51
|
+
): [Uint8Array, Uint8Array] {
|
|
52
|
+
if (key.length !== SYMMETRIC_KEY_SIZE) {
|
|
53
|
+
throw CryptoError.invalidParameter(`Key must be ${SYMMETRIC_KEY_SIZE} bytes`);
|
|
54
|
+
}
|
|
55
|
+
if (nonce.length !== SYMMETRIC_NONCE_SIZE) {
|
|
56
|
+
throw CryptoError.invalidParameter(`Nonce must be ${SYMMETRIC_NONCE_SIZE} bytes`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
60
|
+
const sealed = cipher.encrypt(plaintext);
|
|
61
|
+
|
|
62
|
+
// The sealed output contains ciphertext + 16-byte auth tag
|
|
63
|
+
const ciphertext = sealed.slice(0, sealed.length - SYMMETRIC_AUTH_SIZE);
|
|
64
|
+
const authTag = sealed.slice(sealed.length - SYMMETRIC_AUTH_SIZE);
|
|
65
|
+
|
|
66
|
+
return [ciphertext, authTag];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decrypt data using ChaCha20-Poly1305 AEAD cipher.
|
|
71
|
+
*
|
|
72
|
+
* @param ciphertext - The encrypted data
|
|
73
|
+
* @param key - 32-byte encryption key (must match key used for encryption)
|
|
74
|
+
* @param nonce - 12-byte nonce (must match nonce used for encryption)
|
|
75
|
+
* @param authTag - 16-byte authentication tag from encryption
|
|
76
|
+
* @returns Decrypted plaintext
|
|
77
|
+
* @throws {CryptoError} If key/nonce/authTag sizes are invalid
|
|
78
|
+
* @throws {CryptoError} If authentication fails (tampered data or wrong key/nonce)
|
|
79
|
+
*/
|
|
80
|
+
export function aeadChaCha20Poly1305Decrypt(
|
|
81
|
+
ciphertext: Uint8Array,
|
|
82
|
+
key: Uint8Array,
|
|
83
|
+
nonce: Uint8Array,
|
|
84
|
+
authTag: Uint8Array,
|
|
85
|
+
): Uint8Array {
|
|
86
|
+
return aeadChaCha20Poly1305DecryptWithAad(ciphertext, key, nonce, new Uint8Array(0), authTag);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decrypt data using ChaCha20-Poly1305 AEAD cipher with additional authenticated data.
|
|
91
|
+
*
|
|
92
|
+
* @param ciphertext - The encrypted data
|
|
93
|
+
* @param key - 32-byte encryption key (must match key used for encryption)
|
|
94
|
+
* @param nonce - 12-byte nonce (must match nonce used for encryption)
|
|
95
|
+
* @param aad - Additional authenticated data (must exactly match AAD used for encryption)
|
|
96
|
+
* @param authTag - 16-byte authentication tag from encryption
|
|
97
|
+
* @returns Decrypted plaintext
|
|
98
|
+
* @throws {CryptoError} If key/nonce/authTag sizes are invalid
|
|
99
|
+
* @throws {CryptoError} If authentication fails (tampered data, wrong key/nonce, or AAD mismatch)
|
|
100
|
+
*/
|
|
101
|
+
export function aeadChaCha20Poly1305DecryptWithAad(
|
|
102
|
+
ciphertext: Uint8Array,
|
|
103
|
+
key: Uint8Array,
|
|
104
|
+
nonce: Uint8Array,
|
|
105
|
+
aad: Uint8Array,
|
|
106
|
+
authTag: Uint8Array,
|
|
107
|
+
): Uint8Array {
|
|
108
|
+
if (key.length !== SYMMETRIC_KEY_SIZE) {
|
|
109
|
+
throw CryptoError.invalidParameter(`Key must be ${SYMMETRIC_KEY_SIZE} bytes`);
|
|
110
|
+
}
|
|
111
|
+
if (nonce.length !== SYMMETRIC_NONCE_SIZE) {
|
|
112
|
+
throw CryptoError.invalidParameter(`Nonce must be ${SYMMETRIC_NONCE_SIZE} bytes`);
|
|
113
|
+
}
|
|
114
|
+
if (authTag.length !== SYMMETRIC_AUTH_SIZE) {
|
|
115
|
+
throw CryptoError.invalidParameter(`Auth tag must be ${SYMMETRIC_AUTH_SIZE} bytes`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Combine ciphertext and auth tag for decryption
|
|
119
|
+
const sealed = new Uint8Array(ciphertext.length + authTag.length);
|
|
120
|
+
sealed.set(ciphertext);
|
|
121
|
+
sealed.set(authTag, ciphertext.length);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
125
|
+
return cipher.decrypt(sealed);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Preserve the original error for debugging while wrapping in our error type
|
|
128
|
+
const aeadError = new AeadError(
|
|
129
|
+
`Decryption failed: ${error instanceof Error ? error.message : "authentication error"}`,
|
|
130
|
+
);
|
|
131
|
+
throw CryptoError.aead(aeadError);
|
|
132
|
+
}
|
|
133
|
+
}
|