@brashkie/signalis-core 0.1.0 → 0.3.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/CHANGELOG.md +181 -0
- package/MIGRATION.md +256 -0
- package/NOTICE +107 -9
- package/README.es.md +332 -21
- package/README.md +345 -24
- package/SECURITY.md +191 -0
- package/dist/index.cjs +320 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +270 -3
- package/dist/index.d.ts +270 -3
- package/dist/index.mjs +302 -6
- package/dist/index.mjs.map +1 -1
- package/index.d.ts +35 -0
- package/index.js +19 -1
- package/package.json +41 -25
package/SECURITY.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# 🛡️ Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported | Status |
|
|
6
|
+
|---------|-----------|--------|
|
|
7
|
+
| 0.2.x | ✅ | Active development |
|
|
8
|
+
| 0.1.x | ⚠️ | Security fixes only (until 2026-08-01) |
|
|
9
|
+
| < 0.1 | ❌ | Not supported |
|
|
10
|
+
|
|
11
|
+
When a new minor version is released, the previous minor enters **3 months of security-only support**.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🚨 Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
**Please do NOT open public GitHub issues for security bugs.**
|
|
18
|
+
|
|
19
|
+
### Reporting Channels
|
|
20
|
+
|
|
21
|
+
Use one of these private channels:
|
|
22
|
+
|
|
23
|
+
1. **GitHub Security Advisories** (preferred):
|
|
24
|
+
- https://github.com/Brashkie/signalis-core/security/advisories/new
|
|
25
|
+
|
|
26
|
+
2. **Email**: brashkie@hepein.com
|
|
27
|
+
- PGP key: [download from GitHub profile]
|
|
28
|
+
- Subject: `[SECURITY] signalis-core: <brief description>`
|
|
29
|
+
|
|
30
|
+
### What to Include
|
|
31
|
+
|
|
32
|
+
To help us triage quickly, please provide:
|
|
33
|
+
|
|
34
|
+
- **Description** of the vulnerability
|
|
35
|
+
- **Affected versions** (if known)
|
|
36
|
+
- **Reproduction steps** (proof of concept welcome)
|
|
37
|
+
- **Impact assessment** (your view on severity)
|
|
38
|
+
- **Suggested fix** (if any)
|
|
39
|
+
- **Your contact info** (for follow-up)
|
|
40
|
+
|
|
41
|
+
### Response Timeline
|
|
42
|
+
|
|
43
|
+
| Severity | First response | Fix target |
|
|
44
|
+
|----------|---------------|------------|
|
|
45
|
+
| **Critical** (remote exploit, key recovery) | 24 hours | 7 days |
|
|
46
|
+
| **High** (timing leaks, DoS) | 3 days | 30 days |
|
|
47
|
+
| **Medium** (specific edge cases) | 7 days | Next minor release |
|
|
48
|
+
| **Low** (theoretical, hard to exploit) | 14 days | Best effort |
|
|
49
|
+
|
|
50
|
+
### Disclosure Process
|
|
51
|
+
|
|
52
|
+
1. We acknowledge receipt within the timeframes above
|
|
53
|
+
2. We investigate and confirm the issue
|
|
54
|
+
3. We develop a fix in a private branch
|
|
55
|
+
4. We prepare a security advisory
|
|
56
|
+
5. We coordinate a release (typically 7-30 days from confirmation)
|
|
57
|
+
6. **You are credited** in the advisory (unless you prefer anonymity)
|
|
58
|
+
|
|
59
|
+
### Bounty Program
|
|
60
|
+
|
|
61
|
+
We do **not** currently offer monetary bounties, but we:
|
|
62
|
+
- Publicly credit researchers (with permission)
|
|
63
|
+
- Provide signed acknowledgment letters
|
|
64
|
+
- Add you to the contributors hall of fame
|
|
65
|
+
|
|
66
|
+
If you're interested in funded research, please reach out.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🔒 Security Considerations
|
|
71
|
+
|
|
72
|
+
### Cryptographic Implementation
|
|
73
|
+
|
|
74
|
+
All primitives use **audited, widely-deployed Rust crates**:
|
|
75
|
+
|
|
76
|
+
| Primitive | Crate | Last audited |
|
|
77
|
+
|-----------|-------|--------------|
|
|
78
|
+
| Curve25519 | `curve25519-dalek` 4.x | 2023 (Cure53) |
|
|
79
|
+
| Ed25519 | `ed25519-dalek` 2.x | 2023 (Cure53) |
|
|
80
|
+
| AES-GCM | `aes-gcm` (RustCrypto) | 2022 |
|
|
81
|
+
| SHA-256/HMAC | `sha2`, `hmac` (RustCrypto) | 2022 |
|
|
82
|
+
| HKDF | `hkdf` (RustCrypto) | 2022 |
|
|
83
|
+
|
|
84
|
+
We do **not** implement primitives ourselves. Our work is:
|
|
85
|
+
- Glue code (NAPI bindings)
|
|
86
|
+
- Validation logic
|
|
87
|
+
- Type safety wrappers
|
|
88
|
+
- Test infrastructure
|
|
89
|
+
|
|
90
|
+
### Side-Channel Resistance
|
|
91
|
+
|
|
92
|
+
The underlying Rust crates provide:
|
|
93
|
+
- ✅ **Constant-time** scalar multiplication (Curve25519, Ed25519)
|
|
94
|
+
- ✅ **Constant-time** equality checks (HMAC verify, point equality)
|
|
95
|
+
- ✅ **AES-NI** hardware acceleration (when available)
|
|
96
|
+
- ✅ **Zeroization** of private key buffers on drop
|
|
97
|
+
|
|
98
|
+
### Known Limitations
|
|
99
|
+
|
|
100
|
+
#### XEd25519 (v0.2.0)
|
|
101
|
+
|
|
102
|
+
XEd25519 implementation follows the [Signal specification](https://signal.org/docs/specifications/xeddsa/). While Signal has deployed this in production for years, our implementation is **new** and has not been independently audited.
|
|
103
|
+
|
|
104
|
+
**Mitigation:** We use `curve25519-dalek`'s low-level primitives and `ed25519-dalek`'s `hazmat` API, both of which ARE audited. Only the high-level XEd25519 logic is ours.
|
|
105
|
+
|
|
106
|
+
If you require audited XEd25519, consider waiting for v1.0.0 (audited release).
|
|
107
|
+
|
|
108
|
+
#### Random Number Generation
|
|
109
|
+
|
|
110
|
+
We use `OsRng` from the `rand` crate, which sources entropy from:
|
|
111
|
+
- **Linux**: `getrandom()` syscall
|
|
112
|
+
- **macOS**: `SecRandomCopyBytes`
|
|
113
|
+
- **Windows**: `BCryptGenRandom`
|
|
114
|
+
- **iOS**: same as macOS
|
|
115
|
+
|
|
116
|
+
These are the standard, recommended sources. However:
|
|
117
|
+
- 🚨 **VMs with poor entropy** can produce weak keys
|
|
118
|
+
- 🚨 **Hibernation/snapshots** can reuse RNG state
|
|
119
|
+
|
|
120
|
+
Mitigation: Generate keys after the system has accumulated entropy, especially in VM contexts.
|
|
121
|
+
|
|
122
|
+
### Memory Safety
|
|
123
|
+
|
|
124
|
+
- **Rust side**: Compile-time memory safety, zeroization on drop
|
|
125
|
+
- **JS/Node side**: Buffers can be GC'd at any time
|
|
126
|
+
|
|
127
|
+
If you need guaranteed key wiping in JS:
|
|
128
|
+
```typescript
|
|
129
|
+
// Explicit overwrite (best effort)
|
|
130
|
+
function wipeKey(buf: Buffer) {
|
|
131
|
+
buf.fill(0);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Note:** Due to JS GC behavior, this is best-effort. For highest assurance, use HSM/TPM-backed keys.
|
|
136
|
+
|
|
137
|
+
### Network Security
|
|
138
|
+
|
|
139
|
+
This library provides **cryptographic primitives only**. It does NOT:
|
|
140
|
+
- ❌ Establish network connections
|
|
141
|
+
- ❌ Implement TLS/HTTPS
|
|
142
|
+
- ❌ Validate certificates
|
|
143
|
+
- ❌ Manage sessions
|
|
144
|
+
|
|
145
|
+
Your application is responsible for transport-layer security.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🚫 Out of Scope
|
|
150
|
+
|
|
151
|
+
The following are **not considered vulnerabilities**:
|
|
152
|
+
|
|
153
|
+
- 🔵 **Performance issues** (open as feature request)
|
|
154
|
+
- 🔵 **API ergonomics** (open as feature request)
|
|
155
|
+
- 🔵 **Documentation typos** (open as PR)
|
|
156
|
+
- 🔵 **Behavior on invalid inputs** that throws clear errors (working as intended)
|
|
157
|
+
- 🔵 **Side-channels in user code** (your code's responsibility)
|
|
158
|
+
- 🔵 **Issues in dependencies** (report upstream, but tell us too)
|
|
159
|
+
- 🔵 **Hypothetical attacks** without practical demonstration
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🏆 Hall of Fame
|
|
164
|
+
|
|
165
|
+
We thank the following security researchers for responsible disclosure:
|
|
166
|
+
|
|
167
|
+
_(Empty for now — be the first!)_
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📚 Security Resources
|
|
172
|
+
|
|
173
|
+
For learning about cryptographic security:
|
|
174
|
+
|
|
175
|
+
- 📘 [Cryptography Engineering](https://www.schneier.com/books/cryptography-engineering/) — Schneier, Ferguson, Kohno
|
|
176
|
+
- 📘 [Real-World Cryptography](https://www.manning.com/books/real-world-cryptography) — David Wong
|
|
177
|
+
- 📘 [Serious Cryptography](https://nostarch.com/seriouscrypto) — Jean-Philippe Aumasson
|
|
178
|
+
- 🌐 [Signal Protocol Documentation](https://signal.org/docs/)
|
|
179
|
+
- 🌐 [Cryptography Stack Exchange](https://crypto.stackexchange.com/)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 🔄 Policy Updates
|
|
184
|
+
|
|
185
|
+
This policy may be updated. Significant changes will be:
|
|
186
|
+
1. Committed to the repo with a clear commit message
|
|
187
|
+
2. Announced in the next release notes
|
|
188
|
+
3. Posted in GitHub Discussions
|
|
189
|
+
|
|
190
|
+
**Last updated:** May 2026
|
|
191
|
+
**Next scheduled review:** November 2026
|
package/dist/index.cjs
CHANGED
|
@@ -40,11 +40,20 @@ __export(index_exports, {
|
|
|
40
40
|
AES_GCM_MAX_PLAINTEXT_SIZE: () => AES_GCM_MAX_PLAINTEXT_SIZE,
|
|
41
41
|
AES_GCM_RECOMMENDED_MESSAGES_PER_KEY: () => AES_GCM_RECOMMENDED_MESSAGES_PER_KEY,
|
|
42
42
|
AuthenticationError: () => AuthenticationError,
|
|
43
|
+
CHACHA20_POLY1305_KEY_SIZE: () => CHACHA20_POLY1305_KEY_SIZE,
|
|
44
|
+
CHACHA20_POLY1305_NONCE_SIZE: () => CHACHA20_POLY1305_NONCE_SIZE,
|
|
45
|
+
CHACHA20_POLY1305_TAG_SIZE: () => CHACHA20_POLY1305_TAG_SIZE,
|
|
43
46
|
CURVE25519_PRIVATE_KEY_SIZE: () => CURVE25519_PRIVATE_KEY_SIZE,
|
|
44
47
|
CURVE25519_PUBLIC_KEY_SIZE: () => CURVE25519_PUBLIC_KEY_SIZE,
|
|
45
48
|
CURVE25519_SHARED_SECRET_SIZE: () => CURVE25519_SHARED_SECRET_SIZE,
|
|
49
|
+
ChaCha20Poly1305: () => ChaCha20Poly1305,
|
|
46
50
|
CryptoError: () => CryptoError,
|
|
47
51
|
Curve25519: () => Curve25519,
|
|
52
|
+
ED25519_PRIVATE_KEY_SIZE: () => ED25519_PRIVATE_KEY_SIZE,
|
|
53
|
+
ED25519_PUBLIC_KEY_SIZE: () => ED25519_PUBLIC_KEY_SIZE,
|
|
54
|
+
ED25519_SEED_SIZE: () => ED25519_SEED_SIZE,
|
|
55
|
+
ED25519_SIGNATURE_SIZE: () => ED25519_SIGNATURE_SIZE,
|
|
56
|
+
Ed25519: () => Ed25519,
|
|
48
57
|
HKDF: () => HKDF,
|
|
49
58
|
HKDF_MAX_OUTPUT_SIZE: () => HKDF_MAX_OUTPUT_SIZE,
|
|
50
59
|
HKDF_PRK_SIZE: () => HKDF_PRK_SIZE,
|
|
@@ -56,11 +65,18 @@ __export(index_exports, {
|
|
|
56
65
|
SHA256_BLOCK_SIZE: () => SHA256_BLOCK_SIZE,
|
|
57
66
|
SHA256_OUTPUT_SIZE: () => SHA256_OUTPUT_SIZE,
|
|
58
67
|
SignalisError: () => SignalisError,
|
|
68
|
+
SignatureError: () => SignatureError,
|
|
59
69
|
VERSION: () => VERSION,
|
|
60
70
|
ValidationError: () => ValidationError,
|
|
71
|
+
XED25519_PRIVATE_KEY_SIZE: () => XED25519_PRIVATE_KEY_SIZE,
|
|
72
|
+
XED25519_PUBLIC_KEY_SIZE: () => XED25519_PUBLIC_KEY_SIZE,
|
|
73
|
+
XED25519_RANDOM_SIZE: () => XED25519_RANDOM_SIZE,
|
|
74
|
+
XED25519_SIGNATURE_SIZE: () => XED25519_SIGNATURE_SIZE,
|
|
75
|
+
XEd25519: () => XEd25519,
|
|
61
76
|
asPrivateKey: () => asPrivateKey,
|
|
62
77
|
asPublicKey: () => asPublicKey,
|
|
63
78
|
asSharedSecret: () => asSharedSecret,
|
|
79
|
+
asSignature: () => asSignature,
|
|
64
80
|
assertBuffer: () => assertBuffer,
|
|
65
81
|
assertBufferLength: () => assertBufferLength,
|
|
66
82
|
assertBufferOfSize: () => assertBufferOfSize,
|
|
@@ -69,16 +85,18 @@ __export(index_exports, {
|
|
|
69
85
|
bufferToString: () => bufferToString,
|
|
70
86
|
buffersSameLength: () => buffersSameLength,
|
|
71
87
|
concat: () => concat,
|
|
88
|
+
constantTimeEq: () => constantTimeEq2,
|
|
72
89
|
constantTimeEqual: () => constantTimeEqual,
|
|
73
90
|
default: () => index_default,
|
|
74
91
|
fromBase64: () => fromBase64,
|
|
75
92
|
fromBase64Url: () => fromBase64Url,
|
|
76
93
|
fromHex: () => fromHex,
|
|
94
|
+
nativeSecureRandom: () => nativeSecureRandom,
|
|
77
95
|
nativeVersion: () => nativeVersion,
|
|
78
96
|
randomIv: () => randomIv,
|
|
79
97
|
randomKey: () => randomKey,
|
|
80
98
|
randomNonce: () => randomNonce,
|
|
81
|
-
secureRandom: () =>
|
|
99
|
+
secureRandom: () => secureRandom2,
|
|
82
100
|
stringToBuffer: () => stringToBuffer,
|
|
83
101
|
toBase64: () => toBase64,
|
|
84
102
|
toBase64Url: () => toBase64Url,
|
|
@@ -95,6 +113,14 @@ var native = __toESM(require("../index.js"));
|
|
|
95
113
|
var CURVE25519_PRIVATE_KEY_SIZE = 32;
|
|
96
114
|
var CURVE25519_PUBLIC_KEY_SIZE = 32;
|
|
97
115
|
var CURVE25519_SHARED_SECRET_SIZE = 32;
|
|
116
|
+
var ED25519_PRIVATE_KEY_SIZE = 32;
|
|
117
|
+
var ED25519_PUBLIC_KEY_SIZE = 32;
|
|
118
|
+
var ED25519_SIGNATURE_SIZE = 64;
|
|
119
|
+
var ED25519_SEED_SIZE = 32;
|
|
120
|
+
var XED25519_PRIVATE_KEY_SIZE = 32;
|
|
121
|
+
var XED25519_PUBLIC_KEY_SIZE = 32;
|
|
122
|
+
var XED25519_SIGNATURE_SIZE = 64;
|
|
123
|
+
var XED25519_RANDOM_SIZE = 64;
|
|
98
124
|
var HKDF_PRK_SIZE = 32;
|
|
99
125
|
var HKDF_MAX_OUTPUT_SIZE = 8160;
|
|
100
126
|
var AES_256_KEY_SIZE = 32;
|
|
@@ -157,6 +183,12 @@ var KeyDerivationError = class extends CryptoError {
|
|
|
157
183
|
this.name = "KeyDerivationError";
|
|
158
184
|
}
|
|
159
185
|
};
|
|
186
|
+
var SignatureError = class extends CryptoError {
|
|
187
|
+
constructor(message = "Signature verification failed") {
|
|
188
|
+
super(message, "verify_signature");
|
|
189
|
+
this.name = "SignatureError";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
160
192
|
var LengthError = class extends ValidationError {
|
|
161
193
|
constructor(message, options = {}) {
|
|
162
194
|
super(message, options);
|
|
@@ -436,7 +468,53 @@ var AES_GCM = Object.freeze({
|
|
|
436
468
|
/** Nonce size in bytes (12). */
|
|
437
469
|
NONCE_SIZE: AES_256_GCM_NONCE_SIZE,
|
|
438
470
|
/** Tag size in bytes (16). */
|
|
439
|
-
TAG_SIZE: AES_256_GCM_TAG_SIZE
|
|
471
|
+
TAG_SIZE: AES_256_GCM_TAG_SIZE,
|
|
472
|
+
/**
|
|
473
|
+
* Encrypt with AES-256-GCM and Additional Authenticated Data (NEW in v0.2.0).
|
|
474
|
+
*
|
|
475
|
+
* AAD is authenticated but NOT encrypted. Useful for binding metadata
|
|
476
|
+
* (like message headers) to the ciphertext.
|
|
477
|
+
*
|
|
478
|
+
* @param key - 32-byte symmetric key
|
|
479
|
+
* @param nonce - 12-byte unique nonce
|
|
480
|
+
* @param plaintext - Data to encrypt
|
|
481
|
+
* @param aad - Additional authenticated data (any length, can be empty)
|
|
482
|
+
* @returns Ciphertext + auth tag
|
|
483
|
+
* @throws {ValidationError} On invalid sizes.
|
|
484
|
+
*/
|
|
485
|
+
encryptWithAad(key, nonce, plaintext, aad) {
|
|
486
|
+
assertBufferOfSize(key, AES_256_KEY_SIZE, "key");
|
|
487
|
+
assertBufferOfSize(nonce, AES_256_GCM_NONCE_SIZE, "nonce");
|
|
488
|
+
assertBuffer(plaintext, "plaintext");
|
|
489
|
+
assertBuffer(aad, "aad");
|
|
490
|
+
return native.aes256GcmEncryptWithAad(key, nonce, plaintext, aad);
|
|
491
|
+
},
|
|
492
|
+
/**
|
|
493
|
+
* Decrypt with AES-256-GCM and AAD (NEW in v0.2.0).
|
|
494
|
+
*
|
|
495
|
+
* The same AAD used during encryption must be provided. Mismatch = failure.
|
|
496
|
+
*
|
|
497
|
+
* @throws {AuthenticationError} If tag verification fails (incl. AAD mismatch).
|
|
498
|
+
*/
|
|
499
|
+
decryptWithAad(key, nonce, ciphertext, aad) {
|
|
500
|
+
assertBufferOfSize(key, AES_256_KEY_SIZE, "key");
|
|
501
|
+
assertBufferOfSize(nonce, AES_256_GCM_NONCE_SIZE, "nonce");
|
|
502
|
+
assertBuffer(ciphertext, "ciphertext");
|
|
503
|
+
assertBuffer(aad, "aad");
|
|
504
|
+
if (ciphertext.length < AES_256_GCM_TAG_SIZE) {
|
|
505
|
+
throw new CryptoError(
|
|
506
|
+
`Ciphertext too short: must be at least ${AES_256_GCM_TAG_SIZE} bytes (tag size)`,
|
|
507
|
+
"aes_gcm_decrypt_with_aad"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
return native.aes256GcmDecryptWithAad(key, nonce, ciphertext, aad);
|
|
512
|
+
} catch (e) {
|
|
513
|
+
throw new AuthenticationError(
|
|
514
|
+
`AES-256-GCM authentication failed: ${e.message}`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
440
518
|
});
|
|
441
519
|
var AES_CBC = Object.freeze({
|
|
442
520
|
/**
|
|
@@ -541,7 +619,220 @@ var SHA256 = Object.freeze({
|
|
|
541
619
|
/** Output size in bytes (32). */
|
|
542
620
|
OUTPUT_SIZE: SHA256_OUTPUT_SIZE
|
|
543
621
|
});
|
|
622
|
+
var Ed25519 = Object.freeze({
|
|
623
|
+
/**
|
|
624
|
+
* Generate a new random Ed25519 keypair.
|
|
625
|
+
*/
|
|
626
|
+
generateKeyPair() {
|
|
627
|
+
const kp = native.ed25519GenerateKeypair();
|
|
628
|
+
return Object.freeze({
|
|
629
|
+
privateKey: kp.private,
|
|
630
|
+
publicKey: kp.public
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
/**
|
|
634
|
+
* Derive a deterministic Ed25519 keypair from a 32-byte seed.
|
|
635
|
+
*/
|
|
636
|
+
keyPairFromSeed(seed) {
|
|
637
|
+
assertBufferOfSize(seed, ED25519_SEED_SIZE, "seed");
|
|
638
|
+
const kp = native.ed25519KeypairFromSeed(seed);
|
|
639
|
+
return Object.freeze({
|
|
640
|
+
privateKey: kp.private,
|
|
641
|
+
publicKey: kp.public
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
/**
|
|
645
|
+
* Derive the public key from a private key.
|
|
646
|
+
*/
|
|
647
|
+
publicFromPrivate(privateKey) {
|
|
648
|
+
assertBufferOfSize(privateKey, ED25519_PRIVATE_KEY_SIZE, "privateKey");
|
|
649
|
+
return native.ed25519PublicFromPrivate(privateKey);
|
|
650
|
+
},
|
|
651
|
+
/**
|
|
652
|
+
* Sign a message. Ed25519 signatures are deterministic (RFC 8032).
|
|
653
|
+
*/
|
|
654
|
+
sign(privateKey, message) {
|
|
655
|
+
assertBufferOfSize(privateKey, ED25519_PRIVATE_KEY_SIZE, "privateKey");
|
|
656
|
+
assertBuffer(message, "message");
|
|
657
|
+
return native.ed25519Sign(privateKey, message);
|
|
658
|
+
},
|
|
659
|
+
/**
|
|
660
|
+
* Verify a signature. Throws on failure.
|
|
661
|
+
*
|
|
662
|
+
* @throws {SignatureError} If signature is invalid.
|
|
663
|
+
*/
|
|
664
|
+
verify(publicKey, message, signature) {
|
|
665
|
+
assertBufferOfSize(publicKey, ED25519_PUBLIC_KEY_SIZE, "publicKey");
|
|
666
|
+
assertBuffer(message, "message");
|
|
667
|
+
assertBufferOfSize(signature, ED25519_SIGNATURE_SIZE, "signature");
|
|
668
|
+
try {
|
|
669
|
+
native.ed25519Verify(publicKey, message, signature);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
throw new SignatureError(e.message);
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
/**
|
|
675
|
+
* Verify a signature. Returns boolean (does not throw).
|
|
676
|
+
*/
|
|
677
|
+
verifyBool(publicKey, message, signature) {
|
|
678
|
+
if (!Buffer.isBuffer(publicKey) || publicKey.length !== ED25519_PUBLIC_KEY_SIZE) return false;
|
|
679
|
+
if (!Buffer.isBuffer(message)) return false;
|
|
680
|
+
if (!Buffer.isBuffer(signature) || signature.length !== ED25519_SIGNATURE_SIZE) return false;
|
|
681
|
+
return native.ed25519VerifyBool(publicKey, message, signature);
|
|
682
|
+
},
|
|
683
|
+
/** Private key size in bytes (32). */
|
|
684
|
+
PRIVATE_KEY_SIZE: ED25519_PRIVATE_KEY_SIZE,
|
|
685
|
+
/** Public key size in bytes (32). */
|
|
686
|
+
PUBLIC_KEY_SIZE: ED25519_PUBLIC_KEY_SIZE,
|
|
687
|
+
/** Signature size in bytes (64). */
|
|
688
|
+
SIGNATURE_SIZE: ED25519_SIGNATURE_SIZE,
|
|
689
|
+
/** Seed size for deterministic keypair derivation (32). */
|
|
690
|
+
SEED_SIZE: ED25519_SEED_SIZE
|
|
691
|
+
});
|
|
692
|
+
var XEd25519 = Object.freeze({
|
|
693
|
+
/**
|
|
694
|
+
* Sign a message using a Curve25519 private key. Uses OS RNG.
|
|
695
|
+
* Signatures are NOT deterministic (different each call).
|
|
696
|
+
*/
|
|
697
|
+
sign(privateKey, message) {
|
|
698
|
+
assertBufferOfSize(privateKey, XED25519_PRIVATE_KEY_SIZE, "privateKey");
|
|
699
|
+
assertBuffer(message, "message");
|
|
700
|
+
return native.xed25519Sign(privateKey, message);
|
|
701
|
+
},
|
|
702
|
+
/**
|
|
703
|
+
* Sign with explicit 64-byte random nonce (for testing/reproducibility).
|
|
704
|
+
*/
|
|
705
|
+
signWithRandom(privateKey, message, random) {
|
|
706
|
+
assertBufferOfSize(privateKey, XED25519_PRIVATE_KEY_SIZE, "privateKey");
|
|
707
|
+
assertBuffer(message, "message");
|
|
708
|
+
assertBufferOfSize(random, XED25519_RANDOM_SIZE, "random");
|
|
709
|
+
return native.xed25519SignWithRandom(privateKey, message, random);
|
|
710
|
+
},
|
|
711
|
+
/**
|
|
712
|
+
* Verify a XEd25519 signature. Throws on failure.
|
|
713
|
+
*
|
|
714
|
+
* @throws {SignatureError} If signature is invalid.
|
|
715
|
+
*/
|
|
716
|
+
verify(publicKey, message, signature) {
|
|
717
|
+
assertBufferOfSize(publicKey, XED25519_PUBLIC_KEY_SIZE, "publicKey");
|
|
718
|
+
assertBuffer(message, "message");
|
|
719
|
+
assertBufferOfSize(signature, XED25519_SIGNATURE_SIZE, "signature");
|
|
720
|
+
try {
|
|
721
|
+
native.xed25519Verify(publicKey, message, signature);
|
|
722
|
+
} catch (e) {
|
|
723
|
+
throw new SignatureError(e.message);
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
/**
|
|
727
|
+
* Verify a XEd25519 signature. Returns boolean (does not throw).
|
|
728
|
+
*/
|
|
729
|
+
verifyBool(publicKey, message, signature) {
|
|
730
|
+
if (!Buffer.isBuffer(publicKey) || publicKey.length !== XED25519_PUBLIC_KEY_SIZE) return false;
|
|
731
|
+
if (!Buffer.isBuffer(message)) return false;
|
|
732
|
+
if (!Buffer.isBuffer(signature) || signature.length !== XED25519_SIGNATURE_SIZE) return false;
|
|
733
|
+
return native.xed25519VerifyBool(publicKey, message, signature);
|
|
734
|
+
},
|
|
735
|
+
/** Private key size in bytes (32, same as Curve25519). */
|
|
736
|
+
PRIVATE_KEY_SIZE: XED25519_PRIVATE_KEY_SIZE,
|
|
737
|
+
/** Public key size in bytes (32, same as Curve25519). */
|
|
738
|
+
PUBLIC_KEY_SIZE: XED25519_PUBLIC_KEY_SIZE,
|
|
739
|
+
/** Signature size in bytes (64). */
|
|
740
|
+
SIGNATURE_SIZE: XED25519_SIGNATURE_SIZE,
|
|
741
|
+
/** Random nonce size for signing (64). */
|
|
742
|
+
RANDOM_SIZE: XED25519_RANDOM_SIZE
|
|
743
|
+
});
|
|
544
744
|
var nativeVersion = native.version();
|
|
745
|
+
var CHACHA20_POLY1305_KEY_SIZE = 32;
|
|
746
|
+
var CHACHA20_POLY1305_NONCE_SIZE = 12;
|
|
747
|
+
var CHACHA20_POLY1305_TAG_SIZE = 16;
|
|
748
|
+
var ChaCha20Poly1305 = Object.freeze({
|
|
749
|
+
/**
|
|
750
|
+
* Encrypt + authenticate `plaintext`.
|
|
751
|
+
*
|
|
752
|
+
* @param key 32-byte key
|
|
753
|
+
* @param nonce 12-byte nonce — MUST be unique per (key, plaintext)
|
|
754
|
+
* @param plaintext data to encrypt
|
|
755
|
+
* @returns ciphertext || tag (16 bytes appended)
|
|
756
|
+
*/
|
|
757
|
+
encrypt(key, nonce, plaintext) {
|
|
758
|
+
if (!Buffer.isBuffer(key) || key.length !== CHACHA20_POLY1305_KEY_SIZE) {
|
|
759
|
+
throw new RangeError(`key must be ${CHACHA20_POLY1305_KEY_SIZE} bytes`);
|
|
760
|
+
}
|
|
761
|
+
if (!Buffer.isBuffer(nonce) || nonce.length !== CHACHA20_POLY1305_NONCE_SIZE) {
|
|
762
|
+
throw new RangeError(`nonce must be ${CHACHA20_POLY1305_NONCE_SIZE} bytes`);
|
|
763
|
+
}
|
|
764
|
+
if (!Buffer.isBuffer(plaintext)) {
|
|
765
|
+
throw new TypeError("plaintext must be a Buffer");
|
|
766
|
+
}
|
|
767
|
+
return native.chacha20Poly1305Encrypt(key, nonce, plaintext);
|
|
768
|
+
},
|
|
769
|
+
/**
|
|
770
|
+
* Verify-then-decrypt. Returns plaintext on success, throws on auth failure.
|
|
771
|
+
*/
|
|
772
|
+
decrypt(key, nonce, ciphertext) {
|
|
773
|
+
if (!Buffer.isBuffer(key) || key.length !== CHACHA20_POLY1305_KEY_SIZE) {
|
|
774
|
+
throw new RangeError(`key must be ${CHACHA20_POLY1305_KEY_SIZE} bytes`);
|
|
775
|
+
}
|
|
776
|
+
if (!Buffer.isBuffer(nonce) || nonce.length !== CHACHA20_POLY1305_NONCE_SIZE) {
|
|
777
|
+
throw new RangeError(`nonce must be ${CHACHA20_POLY1305_NONCE_SIZE} bytes`);
|
|
778
|
+
}
|
|
779
|
+
if (!Buffer.isBuffer(ciphertext)) {
|
|
780
|
+
throw new TypeError("ciphertext must be a Buffer");
|
|
781
|
+
}
|
|
782
|
+
return native.chacha20Poly1305Decrypt(key, nonce, ciphertext);
|
|
783
|
+
},
|
|
784
|
+
/**
|
|
785
|
+
* Encrypt + authenticate with Additional Authenticated Data.
|
|
786
|
+
*
|
|
787
|
+
* AAD is NOT encrypted but IS authenticated. Use for plaintext metadata
|
|
788
|
+
* (e.g., message headers) that must not be tampered with.
|
|
789
|
+
*/
|
|
790
|
+
encryptWithAad(key, nonce, plaintext, aad) {
|
|
791
|
+
if (!Buffer.isBuffer(key) || key.length !== CHACHA20_POLY1305_KEY_SIZE) {
|
|
792
|
+
throw new RangeError(`key must be ${CHACHA20_POLY1305_KEY_SIZE} bytes`);
|
|
793
|
+
}
|
|
794
|
+
if (!Buffer.isBuffer(nonce) || nonce.length !== CHACHA20_POLY1305_NONCE_SIZE) {
|
|
795
|
+
throw new RangeError(`nonce must be ${CHACHA20_POLY1305_NONCE_SIZE} bytes`);
|
|
796
|
+
}
|
|
797
|
+
if (!Buffer.isBuffer(plaintext) || !Buffer.isBuffer(aad)) {
|
|
798
|
+
throw new TypeError("plaintext and aad must be Buffers");
|
|
799
|
+
}
|
|
800
|
+
return native.chacha20Poly1305EncryptWithAad(key, nonce, plaintext, aad);
|
|
801
|
+
},
|
|
802
|
+
/**
|
|
803
|
+
* Verify (key + nonce + ciphertext + AAD) and decrypt.
|
|
804
|
+
*/
|
|
805
|
+
decryptWithAad(key, nonce, ciphertext, aad) {
|
|
806
|
+
if (!Buffer.isBuffer(key) || key.length !== CHACHA20_POLY1305_KEY_SIZE) {
|
|
807
|
+
throw new RangeError(`key must be ${CHACHA20_POLY1305_KEY_SIZE} bytes`);
|
|
808
|
+
}
|
|
809
|
+
if (!Buffer.isBuffer(nonce) || nonce.length !== CHACHA20_POLY1305_NONCE_SIZE) {
|
|
810
|
+
throw new RangeError(`nonce must be ${CHACHA20_POLY1305_NONCE_SIZE} bytes`);
|
|
811
|
+
}
|
|
812
|
+
if (!Buffer.isBuffer(ciphertext) || !Buffer.isBuffer(aad)) {
|
|
813
|
+
throw new TypeError("ciphertext and aad must be Buffers");
|
|
814
|
+
}
|
|
815
|
+
return native.chacha20Poly1305DecryptWithAad(key, nonce, ciphertext, aad);
|
|
816
|
+
},
|
|
817
|
+
/** Key size in bytes (32). */
|
|
818
|
+
KEY_SIZE: CHACHA20_POLY1305_KEY_SIZE,
|
|
819
|
+
/** Nonce size in bytes (12). */
|
|
820
|
+
NONCE_SIZE: CHACHA20_POLY1305_NONCE_SIZE,
|
|
821
|
+
/** Authentication tag size in bytes (16), appended to ciphertext. */
|
|
822
|
+
TAG_SIZE: CHACHA20_POLY1305_TAG_SIZE
|
|
823
|
+
});
|
|
824
|
+
function constantTimeEq2(a, b) {
|
|
825
|
+
if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
|
|
826
|
+
throw new TypeError("constantTimeEq: both arguments must be Buffers");
|
|
827
|
+
}
|
|
828
|
+
return native.constantTimeEq(a, b);
|
|
829
|
+
}
|
|
830
|
+
function nativeSecureRandom(size) {
|
|
831
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
832
|
+
throw new RangeError(`size must be a positive integer, got ${size}`);
|
|
833
|
+
}
|
|
834
|
+
return native.secureRandom(size);
|
|
835
|
+
}
|
|
545
836
|
|
|
546
837
|
// src/types.ts
|
|
547
838
|
function asPublicKey(buf) {
|
|
@@ -553,10 +844,13 @@ function asPrivateKey(buf) {
|
|
|
553
844
|
function asSharedSecret(buf) {
|
|
554
845
|
return buf;
|
|
555
846
|
}
|
|
847
|
+
function asSignature(buf) {
|
|
848
|
+
return buf;
|
|
849
|
+
}
|
|
556
850
|
|
|
557
851
|
// src/utils.ts
|
|
558
852
|
var import_node_crypto = require("crypto");
|
|
559
|
-
function
|
|
853
|
+
function secureRandom2(length) {
|
|
560
854
|
if (!Number.isInteger(length) || length < 0) {
|
|
561
855
|
throw new RangeError(`length must be a non-negative integer, got ${length}`);
|
|
562
856
|
}
|
|
@@ -626,17 +920,19 @@ function xor(a, b) {
|
|
|
626
920
|
}
|
|
627
921
|
|
|
628
922
|
// src/index.ts
|
|
629
|
-
var VERSION = "0.
|
|
923
|
+
var VERSION = "0.2.0";
|
|
630
924
|
var SignalisCore = Object.freeze({
|
|
631
925
|
// Crypto primitives
|
|
632
926
|
Curve25519,
|
|
927
|
+
Ed25519,
|
|
928
|
+
XEd25519,
|
|
633
929
|
HKDF,
|
|
634
930
|
AES_GCM,
|
|
635
931
|
AES_CBC,
|
|
636
932
|
HMAC,
|
|
637
933
|
SHA256,
|
|
638
934
|
// Random
|
|
639
|
-
secureRandom,
|
|
935
|
+
secureRandom: secureRandom2,
|
|
640
936
|
randomNonce,
|
|
641
937
|
randomIv,
|
|
642
938
|
randomKey,
|
|
@@ -648,7 +944,7 @@ var SignalisCore = Object.freeze({
|
|
|
648
944
|
// Security
|
|
649
945
|
constantTimeEqual,
|
|
650
946
|
// Version
|
|
651
|
-
VERSION: "0.
|
|
947
|
+
VERSION: "0.2.0",
|
|
652
948
|
nativeVersion
|
|
653
949
|
});
|
|
654
950
|
var index_default = SignalisCore;
|
|
@@ -664,11 +960,20 @@ var index_default = SignalisCore;
|
|
|
664
960
|
AES_GCM_MAX_PLAINTEXT_SIZE,
|
|
665
961
|
AES_GCM_RECOMMENDED_MESSAGES_PER_KEY,
|
|
666
962
|
AuthenticationError,
|
|
963
|
+
CHACHA20_POLY1305_KEY_SIZE,
|
|
964
|
+
CHACHA20_POLY1305_NONCE_SIZE,
|
|
965
|
+
CHACHA20_POLY1305_TAG_SIZE,
|
|
667
966
|
CURVE25519_PRIVATE_KEY_SIZE,
|
|
668
967
|
CURVE25519_PUBLIC_KEY_SIZE,
|
|
669
968
|
CURVE25519_SHARED_SECRET_SIZE,
|
|
969
|
+
ChaCha20Poly1305,
|
|
670
970
|
CryptoError,
|
|
671
971
|
Curve25519,
|
|
972
|
+
ED25519_PRIVATE_KEY_SIZE,
|
|
973
|
+
ED25519_PUBLIC_KEY_SIZE,
|
|
974
|
+
ED25519_SEED_SIZE,
|
|
975
|
+
ED25519_SIGNATURE_SIZE,
|
|
976
|
+
Ed25519,
|
|
672
977
|
HKDF,
|
|
673
978
|
HKDF_MAX_OUTPUT_SIZE,
|
|
674
979
|
HKDF_PRK_SIZE,
|
|
@@ -680,11 +985,18 @@ var index_default = SignalisCore;
|
|
|
680
985
|
SHA256_BLOCK_SIZE,
|
|
681
986
|
SHA256_OUTPUT_SIZE,
|
|
682
987
|
SignalisError,
|
|
988
|
+
SignatureError,
|
|
683
989
|
VERSION,
|
|
684
990
|
ValidationError,
|
|
991
|
+
XED25519_PRIVATE_KEY_SIZE,
|
|
992
|
+
XED25519_PUBLIC_KEY_SIZE,
|
|
993
|
+
XED25519_RANDOM_SIZE,
|
|
994
|
+
XED25519_SIGNATURE_SIZE,
|
|
995
|
+
XEd25519,
|
|
685
996
|
asPrivateKey,
|
|
686
997
|
asPublicKey,
|
|
687
998
|
asSharedSecret,
|
|
999
|
+
asSignature,
|
|
688
1000
|
assertBuffer,
|
|
689
1001
|
assertBufferLength,
|
|
690
1002
|
assertBufferOfSize,
|
|
@@ -693,10 +1005,12 @@ var index_default = SignalisCore;
|
|
|
693
1005
|
bufferToString,
|
|
694
1006
|
buffersSameLength,
|
|
695
1007
|
concat,
|
|
1008
|
+
constantTimeEq,
|
|
696
1009
|
constantTimeEqual,
|
|
697
1010
|
fromBase64,
|
|
698
1011
|
fromBase64Url,
|
|
699
1012
|
fromHex,
|
|
1013
|
+
nativeSecureRandom,
|
|
700
1014
|
nativeVersion,
|
|
701
1015
|
randomIv,
|
|
702
1016
|
randomKey,
|