@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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="media/logo.png" alt="Signalis Core" width="200" />
|
|
4
4
|
|
|
5
5
|
# π Signalis Core
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](https://www.rust-lang.org/)
|
|
14
14
|
[](https://nodejs.org/)
|
|
15
15
|
[](#testing)
|
|
16
|
-
[](#testing)
|
|
17
17
|
|
|
18
18
|
[**English**](./README.md) Β· [**EspaΓ±ol**](./README.es.md) Β· [Docs](./docs) Β· [Roadmap](./ROADMAP.md) Β· [Changelog](./CHANGELOG.md)
|
|
19
19
|
|
|
@@ -32,10 +32,96 @@ Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## π What's New in v0.3.0
|
|
36
|
+
|
|
37
|
+
**v0.3.0 ships Android support + ChaCha20-Poly1305 β fully backwards compatible with v0.2.0.**
|
|
38
|
+
|
|
39
|
+
| New | Description |
|
|
40
|
+
|-----|-------------|
|
|
41
|
+
| π **Android arm64-v8a** | Native binary for modern Android phones (React Native, Termux) |
|
|
42
|
+
| π **Android armv7** | Native binary for older Android devices (Android 4.4+) |
|
|
43
|
+
| π **ChaCha20-Poly1305** | RFC 8439 AEAD β 2-3Γ faster than AES-GCM on ARM without AES-NI |
|
|
44
|
+
| π **`constantTimeEq()`** | Timing-safe Buffer comparison (for MAC/signature checking) |
|
|
45
|
+
| π **`nativeSecureRandom()`** | OS-backed CSPRNG via Rust side (alternative to JS `secureRandom`) |
|
|
46
|
+
| π **`sc-utils` crate** | Public utility helpers (random, constant_time_eq, secure_zeroize) |
|
|
47
|
+
| π **CI: cargo-audit gate** | Vulnerable transitive dependencies now fail PRs automatically |
|
|
48
|
+
|
|
49
|
+
### Quick Example: ChaCha20-Poly1305
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { ChaCha20Poly1305, secureRandom } from '@brashkie/signalis-core';
|
|
53
|
+
|
|
54
|
+
const key = secureRandom(32);
|
|
55
|
+
const nonce = secureRandom(12);
|
|
56
|
+
|
|
57
|
+
const ct = ChaCha20Poly1305.encrypt(key, nonce, Buffer.from('Hello!'));
|
|
58
|
+
const pt = ChaCha20Poly1305.decrypt(key, nonce, ct); // β "Hello!"
|
|
59
|
+
|
|
60
|
+
// With Additional Authenticated Data
|
|
61
|
+
const aad = Buffer.from('header metadata');
|
|
62
|
+
const ct2 = ChaCha20Poly1305.encryptWithAad(key, nonce, plaintext, aad);
|
|
63
|
+
const pt2 = ChaCha20Poly1305.decryptWithAad(key, nonce, ct2, aad);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Why Should I Use ChaCha20-Poly1305 Instead of AES-GCM?
|
|
67
|
+
|
|
68
|
+
Use **AES-GCM** on:
|
|
69
|
+
- x86_64 servers (Intel/AMD with AES-NI hardware)
|
|
70
|
+
- Modern desktops
|
|
71
|
+
- Newer ARM CPUs with ARMv8 Crypto Extensions
|
|
72
|
+
|
|
73
|
+
Use **ChaCha20-Poly1305** on:
|
|
74
|
+
- Android phones (most don't have AES-NI)
|
|
75
|
+
- Embedded / IoT devices
|
|
76
|
+
- Anything without hardware AES support
|
|
77
|
+
|
|
78
|
+
Both have **identical security guarantees**. The difference is purely performance, depending on whether your target has AES-NI hardware.
|
|
79
|
+
|
|
80
|
+
See [MIGRATION.md](./MIGRATION.md) for upgrade details (it's a drop-in replacement).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## π Supported Platforms
|
|
85
|
+
|
|
86
|
+
`@brashkie/signalis-core` ships prebuilt native binaries for **9 platforms** via npm `optionalDependencies`. The right binary downloads automatically based on your host OS + arch.
|
|
87
|
+
|
|
88
|
+
| OS | Architecture | Sub-package | Status |
|
|
89
|
+
|----|--------------|-------------|--------|
|
|
90
|
+
| π§ Linux | x64 (glibc) | `signalis-core-linux-x64-gnu` | β
|
|
|
91
|
+
| π§ Linux | x64 (musl) | `signalis-core-linux-x64-musl` | β
|
|
|
92
|
+
| π§ Linux | arm64 (glibc) | `signalis-core-linux-arm64-gnu` | β
|
|
|
93
|
+
| π macOS | x64 (Intel) | `signalis-core-darwin-x64` | β
|
|
|
94
|
+
| π macOS | arm64 (Apple Silicon) | `signalis-core-darwin-arm64` | β
|
|
|
95
|
+
| πͺ Windows | x64 | `signalis-core-win32-x64-msvc` | β
|
|
|
96
|
+
| πͺ Windows | arm64 | `signalis-core-win32-arm64-msvc` | β
|
|
|
97
|
+
| π€ Android | arm64-v8a | `signalis-core-android-arm64` | π v0.3.0 |
|
|
98
|
+
| π€ Android | armv7 | `signalis-core-android-arm-eabi` | π v0.3.0 |
|
|
99
|
+
|
|
100
|
+
Coming in v0.4.0: **iOS arm64**, **WASM (browsers)**, **FreeBSD x64**.
|
|
101
|
+
|
|
102
|
+
### Android Installation
|
|
103
|
+
|
|
104
|
+
Same `npm install` works in any Node.js environment running on Android, including:
|
|
105
|
+
- **Termux** on Android phones (`pkg install nodejs`)
|
|
106
|
+
- **React Native** with Android target
|
|
107
|
+
- **NodeJS-Mobile** apps
|
|
108
|
+
- Custom embedded Node builds for IoT
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# On Android (Termux for example):
|
|
112
|
+
pkg install nodejs
|
|
113
|
+
npm install @brashkie/signalis-core
|
|
114
|
+
# β npm automatically downloads signalis-core-android-arm64 sub-package
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
35
119
|
## π Table of Contents
|
|
36
120
|
|
|
37
121
|
- [π Signalis Core](#-signalis-core)
|
|
38
122
|
- [β¨ What is Signalis Core?](#-what-is-signalis-core)
|
|
123
|
+
- [π What's New in v0.3.0](#-whats-new-in-v030)
|
|
124
|
+
- [π Supported Platforms](#-supported-platforms)
|
|
39
125
|
- [π Table of Contents](#-table-of-contents)
|
|
40
126
|
- [π Features](#-features)
|
|
41
127
|
- [π€ Why Signalis Core?](#-why-signalis-core)
|
|
@@ -49,6 +135,8 @@ Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https
|
|
|
49
135
|
- [Default Import](#default-import)
|
|
50
136
|
- [π API Reference](#-api-reference)
|
|
51
137
|
- [Curve25519 / X25519](#curve25519--x25519)
|
|
138
|
+
- [Ed25519](#ed25519)
|
|
139
|
+
- [XEd25519](#xed25519)
|
|
52
140
|
- [HKDF-SHA256](#hkdf-sha256)
|
|
53
141
|
- [AES-256-GCM](#aes-256-gcm)
|
|
54
142
|
- [AES-256-CBC](#aes-256-cbc)
|
|
@@ -59,6 +147,8 @@ Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https
|
|
|
59
147
|
- [π‘ Examples](#-examples)
|
|
60
148
|
- [Example: Secure file encryption](#example-secure-file-encryption)
|
|
61
149
|
- [Example: Signal-style "Triple DH"](#example-signal-style-triple-dh)
|
|
150
|
+
- [Example: Signing identity assertions (NEW v0.2.0)](#example-signing-identity-assertions-new-v020)
|
|
151
|
+
- [Example: Encrypted messages with authenticated headers (NEW v0.2.0)](#example-encrypted-messages-with-authenticated-headers-new-v020)
|
|
62
152
|
- [ποΈ Architecture](#οΈ-architecture)
|
|
63
153
|
- [Build Output](#build-output)
|
|
64
154
|
- [π‘οΈ Security](#οΈ-security)
|
|
@@ -82,14 +172,16 @@ Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https
|
|
|
82
172
|
| Feature | Description |
|
|
83
173
|
|---------|-------------|
|
|
84
174
|
| π₯ **Blazing Fast** | Native Rust implementation via napi-rs (10-100x faster than pure JS) |
|
|
85
|
-
| π‘οΈ **Audited Crypto** | Built on `curve25519-dalek`, RustCrypto suite β battle-tested libraries |
|
|
175
|
+
| π‘οΈ **Audited Crypto** | Built on `curve25519-dalek`, `ed25519-dalek`, RustCrypto suite β battle-tested libraries |
|
|
176
|
+
| βοΈ **Digital Signatures** | Ed25519 (RFC 8032) and XEd25519 (Signal-style) β **NEW v0.2.0** |
|
|
177
|
+
| π **AEAD with AAD** | AES-256-GCM with Additional Authenticated Data β **NEW v0.2.0** |
|
|
86
178
|
| π¦ **Dual Package** | Works in CommonJS, ESM, and TypeScript projects |
|
|
87
179
|
| π― **Type-Safe** | Full TypeScript definitions with branded types and rich error classes |
|
|
88
|
-
| β
**Test Vectors** | Validated against RFC 5869, RFC 7748, RFC 4231, and NIST vectors |
|
|
89
|
-
| π **Cross-Platform** | Prebuilt binaries for Windows, macOS, Linux (x64, ARM) |
|
|
180
|
+
| β
**Test Vectors** | Validated against RFC 5869, RFC 7748, RFC 8032, RFC 4231, and NIST vectors |
|
|
181
|
+
| π **Cross-Platform** | Prebuilt binaries for Windows, macOS, Linux (x64, ARM) + **Android arm64/armv7** |
|
|
90
182
|
| π **Constant-Time** | Side-channel resistant comparisons via `subtle` crate |
|
|
91
183
|
| π§Ή **Auto-Zeroization** | Secrets are wiped from memory automatically |
|
|
92
|
-
| π **99%+ Coverage** | Comprehensive test suite with
|
|
184
|
+
| π **99%+ Coverage** | Comprehensive test suite with 269+ assertions |
|
|
93
185
|
| π **Well Documented** | Complete JSDoc + inline examples for every function |
|
|
94
186
|
|
|
95
187
|
---
|
|
@@ -234,6 +326,86 @@ Curve25519.PUBLIC_KEY_SIZE; // 32
|
|
|
234
326
|
Curve25519.SHARED_SECRET_SIZE; // 32
|
|
235
327
|
```
|
|
236
328
|
|
|
329
|
+
### Ed25519
|
|
330
|
+
|
|
331
|
+
**NEW in v0.2.0.** Standard Ed25519 digital signatures (RFC 8032). Deterministic β same input always produces the same signature.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { Ed25519, type KeyPair, type Signature } from '@brashkie/signalis-core';
|
|
335
|
+
|
|
336
|
+
// Generate a new signing keypair
|
|
337
|
+
const keys: KeyPair = Ed25519.generateKeyPair();
|
|
338
|
+
// β { privateKey: Buffer(32), publicKey: Buffer(32) }
|
|
339
|
+
|
|
340
|
+
// Deterministic from a 32-byte seed
|
|
341
|
+
const fromSeed = Ed25519.keyPairFromSeed(seed);
|
|
342
|
+
|
|
343
|
+
// Derive public from private
|
|
344
|
+
const pub = Ed25519.publicFromPrivate(privateKey);
|
|
345
|
+
|
|
346
|
+
// Sign a message β 64-byte signature
|
|
347
|
+
const sig: Signature = Ed25519.sign(privateKey, message);
|
|
348
|
+
|
|
349
|
+
// Verify (throws SignatureError on failure)
|
|
350
|
+
Ed25519.verify(publicKey, message, sig);
|
|
351
|
+
|
|
352
|
+
// Verify (returns boolean, no throw)
|
|
353
|
+
const ok = Ed25519.verifyBool(publicKey, message, sig);
|
|
354
|
+
|
|
355
|
+
// Constants
|
|
356
|
+
Ed25519.PRIVATE_KEY_SIZE; // 32
|
|
357
|
+
Ed25519.PUBLIC_KEY_SIZE; // 32
|
|
358
|
+
Ed25519.SIGNATURE_SIZE; // 64
|
|
359
|
+
Ed25519.SEED_SIZE; // 32
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### XEd25519
|
|
363
|
+
|
|
364
|
+
**NEW in v0.2.0.** Sign with the **SAME** Curve25519 keypair used for ECDH. This is what the Signal Protocol uses for identity keys.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { Curve25519, XEd25519 } from '@brashkie/signalis-core';
|
|
368
|
+
|
|
369
|
+
// ONE keypair for both ECDH and signing
|
|
370
|
+
const identity = Curve25519.generateKeyPair();
|
|
371
|
+
|
|
372
|
+
// Use for ECDH:
|
|
373
|
+
const shared = Curve25519.diffieHellman(identity.privateKey, peerPublic);
|
|
374
|
+
|
|
375
|
+
// Use the SAME key to sign:
|
|
376
|
+
const sig = XEd25519.sign(identity.privateKey, message);
|
|
377
|
+
|
|
378
|
+
// Verify with the SAME Curve25519 public key
|
|
379
|
+
XEd25519.verify(identity.publicKey, message, sig);
|
|
380
|
+
|
|
381
|
+
// XEd25519 signatures are NOT deterministic (use OS RNG)
|
|
382
|
+
const sig1 = XEd25519.sign(identity.privateKey, message);
|
|
383
|
+
const sig2 = XEd25519.sign(identity.privateKey, message);
|
|
384
|
+
// sig1.equals(sig2) β false (intentionally probabilistic)
|
|
385
|
+
|
|
386
|
+
// For deterministic signing (testing), provide explicit 64-byte random:
|
|
387
|
+
const random = secureRandom(64);
|
|
388
|
+
const detSig = XEd25519.signWithRandom(identity.privateKey, message, random);
|
|
389
|
+
|
|
390
|
+
// Verify (boolean, no throw)
|
|
391
|
+
const ok = XEd25519.verifyBool(identity.publicKey, message, sig);
|
|
392
|
+
|
|
393
|
+
// Constants
|
|
394
|
+
XEd25519.PRIVATE_KEY_SIZE; // 32 (same as Curve25519)
|
|
395
|
+
XEd25519.PUBLIC_KEY_SIZE; // 32 (same as Curve25519)
|
|
396
|
+
XEd25519.SIGNATURE_SIZE; // 64
|
|
397
|
+
XEd25519.RANDOM_SIZE; // 64
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**When to use Ed25519 vs XEd25519:**
|
|
401
|
+
|
|
402
|
+
| Need | Use |
|
|
403
|
+
|------|-----|
|
|
404
|
+
| Standard Ed25519, deterministic, RFC 8032 compliant | **Ed25519** |
|
|
405
|
+
| Single identity key for ECDH + signing (Signal style) | **XEd25519** |
|
|
406
|
+
| Reproducible signatures from seed | **Ed25519** |
|
|
407
|
+
| Compatibility with Signal Protocol semantics | **XEd25519** |
|
|
408
|
+
|
|
237
409
|
### HKDF-SHA256
|
|
238
410
|
|
|
239
411
|
Key derivation per RFC 5869.
|
|
@@ -283,6 +455,26 @@ const pt = AES_GCM.decrypt(key, nonce, ct);
|
|
|
283
455
|
// Throws AuthenticationError if tampered
|
|
284
456
|
```
|
|
285
457
|
|
|
458
|
+
**With Additional Authenticated Data (AAD) β NEW in v0.2.0:**
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { AES_GCM } from '@brashkie/signalis-core';
|
|
462
|
+
|
|
463
|
+
// AAD is authenticated but NOT encrypted β useful for headers/metadata
|
|
464
|
+
const header = Buffer.from('msg_id=42|sender=alice');
|
|
465
|
+
const body = Buffer.from('encrypted body content');
|
|
466
|
+
|
|
467
|
+
const ct = AES_GCM.encryptWithAad(key, nonce, body, header);
|
|
468
|
+
|
|
469
|
+
// Decrypt β MUST pass same AAD, or AuthenticationError
|
|
470
|
+
const pt = AES_GCM.decryptWithAad(key, nonce, ct, header);
|
|
471
|
+
|
|
472
|
+
// Tampering with header (AAD) β fails
|
|
473
|
+
const tampered = Buffer.from(header);
|
|
474
|
+
tampered[0] ^= 0xff;
|
|
475
|
+
AES_GCM.decryptWithAad(key, nonce, ct, tampered); // throws AuthenticationError
|
|
476
|
+
```
|
|
477
|
+
|
|
286
478
|
> **β οΈ CRITICAL:** Never reuse a `(key, nonce)` pair. Use `randomNonce()` for every message, or use a deterministic counter under the same key (max 2Β³Β² messages).
|
|
287
479
|
|
|
288
480
|
### AES-256-CBC
|
|
@@ -303,6 +495,43 @@ if (HMAC.verifySha256(macKey, concat([iv, ct]), tag)) {
|
|
|
303
495
|
}
|
|
304
496
|
```
|
|
305
497
|
|
|
498
|
+
### ChaCha20-Poly1305 π
|
|
499
|
+
|
|
500
|
+
RFC 8439 authenticated encryption. Alternative to AES-GCM, faster on ARM without AES-NI.
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { ChaCha20Poly1305, secureRandom } from '@brashkie/signalis-core';
|
|
504
|
+
|
|
505
|
+
const key = secureRandom(32); // ChaCha20Poly1305.KEY_SIZE
|
|
506
|
+
const nonce = secureRandom(12); // ChaCha20Poly1305.NONCE_SIZE
|
|
507
|
+
|
|
508
|
+
// Basic encrypt/decrypt (no AAD)
|
|
509
|
+
const ct = ChaCha20Poly1305.encrypt(key, nonce, plaintext);
|
|
510
|
+
const pt = ChaCha20Poly1305.decrypt(key, nonce, ct);
|
|
511
|
+
|
|
512
|
+
// With Additional Authenticated Data
|
|
513
|
+
const aad = Buffer.from('public header');
|
|
514
|
+
const ct2 = ChaCha20Poly1305.encryptWithAad(key, nonce, plaintext, aad);
|
|
515
|
+
const pt2 = ChaCha20Poly1305.decryptWithAad(key, nonce, ct2, aad);
|
|
516
|
+
|
|
517
|
+
// Constants
|
|
518
|
+
ChaCha20Poly1305.KEY_SIZE; // 32
|
|
519
|
+
ChaCha20Poly1305.NONCE_SIZE; // 12
|
|
520
|
+
ChaCha20Poly1305.TAG_SIZE; // 16 (appended to ciphertext)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Performance vs AES-GCM:**
|
|
524
|
+
|
|
525
|
+
| Target | Winner | Why |
|
|
526
|
+
|--------|--------|-----|
|
|
527
|
+
| Server x86_64 (AES-NI) | AES-GCM | Hardware acceleration |
|
|
528
|
+
| Modern desktop (AES-NI) | AES-GCM | Hardware acceleration |
|
|
529
|
+
| Android arm64 (no ARMv8 crypto) | **ChaCha20-Poly1305** | Pure CPU, ChaCha is faster |
|
|
530
|
+
| IoT / embedded | **ChaCha20-Poly1305** | No AES hardware needed |
|
|
531
|
+
| Apple Silicon (M-series) | Either (close) | Both well-optimized |
|
|
532
|
+
|
|
533
|
+
**Security:** ChaCha20-Poly1305 and AES-GCM have equivalent security properties. The choice is purely about performance on your target.
|
|
534
|
+
|
|
306
535
|
### HMAC-SHA256
|
|
307
536
|
|
|
308
537
|
Message authentication.
|
|
@@ -344,7 +573,9 @@ import {
|
|
|
344
573
|
fromBase64Url,
|
|
345
574
|
|
|
346
575
|
// Security
|
|
347
|
-
constantTimeEqual,
|
|
576
|
+
constantTimeEqual, // (a, b) β boolean (timing-safe, JS-side)
|
|
577
|
+
constantTimeEq, // π v0.3.0 β same but routed via Rust side
|
|
578
|
+
nativeSecureRandom, // π v0.3.0 β OS CSPRNG via Rust (vs node:crypto)
|
|
348
579
|
|
|
349
580
|
// Buffer ops
|
|
350
581
|
concat, // (buffers[]) β Buffer
|
|
@@ -353,6 +584,14 @@ import {
|
|
|
353
584
|
} from '@brashkie/signalis-core';
|
|
354
585
|
```
|
|
355
586
|
|
|
587
|
+
#### When to use `constantTimeEq` vs `constantTimeEqual`?
|
|
588
|
+
|
|
589
|
+
Both compare in constant time. `constantTimeEqual` uses JavaScript's `Buffer.compare` semantics (the JS-side implementation). `constantTimeEq` (v0.3.0) routes through Rust's audited `subtle::ConstantTimeEq` crate. Either is safe β use `constantTimeEq` if you want to ensure your timing-safe code path matches what the rest of the library uses internally.
|
|
590
|
+
|
|
591
|
+
#### When to use `nativeSecureRandom` vs `secureRandom`?
|
|
592
|
+
|
|
593
|
+
`secureRandom(n)` uses Node's `crypto.randomBytes()`. `nativeSecureRandom(n)` (v0.3.0) routes through Rust's `OsRng` (which calls `getrandom`/`BCryptGenRandom`/`SecRandomCopyBytes` directly). Both are cryptographically secure. Use `nativeSecureRandom` if you want a single audit surface for entropy across your whole stack.
|
|
594
|
+
|
|
356
595
|
### Errors
|
|
357
596
|
|
|
358
597
|
All errors extend `SignalisError`:
|
|
@@ -364,6 +603,7 @@ import {
|
|
|
364
603
|
CryptoError, // Crypto op failed
|
|
365
604
|
AuthenticationError, // Tag/MAC verification failed (extends CryptoError)
|
|
366
605
|
KeyDerivationError, // HKDF or similar failed (extends CryptoError)
|
|
606
|
+
SignatureError, // Ed25519/XEd25519 verify failed (extends CryptoError) β NEW v0.2.0
|
|
367
607
|
LengthError, // Output length out of bounds (extends ValidationError)
|
|
368
608
|
} from '@brashkie/signalis-core';
|
|
369
609
|
|
|
@@ -374,6 +614,8 @@ try {
|
|
|
374
614
|
console.error('Tampering detected!');
|
|
375
615
|
} else if (e instanceof ValidationError) {
|
|
376
616
|
console.error(`Invalid parameter: ${e.parameter}`);
|
|
617
|
+
} else if (e instanceof SignatureError) {
|
|
618
|
+
console.error('Invalid signature!');
|
|
377
619
|
}
|
|
378
620
|
}
|
|
379
621
|
```
|
|
@@ -385,10 +627,13 @@ try {
|
|
|
385
627
|
The `examples/` directory contains complete working demos:
|
|
386
628
|
|
|
387
629
|
```bash
|
|
388
|
-
npm run example:cjs
|
|
389
|
-
npm run example:esm
|
|
390
|
-
npm run example:ts
|
|
391
|
-
npm run
|
|
630
|
+
npm run example:cjs # CommonJS (10 demos)
|
|
631
|
+
npm run example:esm # ESM (Alice β Bob channel)
|
|
632
|
+
npm run example:ts # TypeScript (type-safe patterns)
|
|
633
|
+
npm run example:signing # Ed25519 + XEd25519 β NEW v0.2.0
|
|
634
|
+
npm run example:aad # AES-GCM with AAD β NEW v0.2.0
|
|
635
|
+
npm run example:e2e # Complete E2E channel β NEW v0.2.0
|
|
636
|
+
npm run examples # Run all
|
|
392
637
|
```
|
|
393
638
|
|
|
394
639
|
### Example: Secure file encryption
|
|
@@ -438,6 +683,66 @@ function tripleDH(IK_A_priv: Buffer, EK_A_priv: Buffer,
|
|
|
438
683
|
}
|
|
439
684
|
```
|
|
440
685
|
|
|
686
|
+
### Example: Signing identity assertions (NEW v0.2.0)
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
import { Curve25519, XEd25519 } from '@brashkie/signalis-core';
|
|
690
|
+
|
|
691
|
+
// Alice's long-term identity key (Curve25519/XEd25519)
|
|
692
|
+
const aliceIdentity = Curve25519.generateKeyPair();
|
|
693
|
+
|
|
694
|
+
// Alice generates an ephemeral session key
|
|
695
|
+
const aliceEphemeral = Curve25519.generateKeyPair();
|
|
696
|
+
|
|
697
|
+
// Alice signs her ephemeral with her identity β proves "this ephemeral is mine"
|
|
698
|
+
const authProof = XEd25519.sign(
|
|
699
|
+
aliceIdentity.privateKey,
|
|
700
|
+
aliceEphemeral.publicKey,
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// Bob verifies the authorization:
|
|
704
|
+
// 1. Got Alice's identity public key from a trusted source
|
|
705
|
+
// 2. Receives ephemeral + signature
|
|
706
|
+
try {
|
|
707
|
+
XEd25519.verify(aliceIdentity.publicKey, aliceEphemeral.publicKey, authProof);
|
|
708
|
+
// β
Bob now trusts the ephemeral key belongs to Alice
|
|
709
|
+
} catch {
|
|
710
|
+
// β Mallory tried to MITM with her own ephemeral
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Example: Encrypted messages with authenticated headers (NEW v0.2.0)
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
import { AES_GCM, secureRandom } from '@brashkie/signalis-core';
|
|
718
|
+
|
|
719
|
+
const sessionKey = derivedFromECDHandHKDF;
|
|
720
|
+
|
|
721
|
+
function sendMessage(body: Buffer, msgId: number) {
|
|
722
|
+
const nonce = secureRandom(12);
|
|
723
|
+
const header = Buffer.from(JSON.stringify({
|
|
724
|
+
msg_id: msgId,
|
|
725
|
+
timestamp: Date.now(),
|
|
726
|
+
sender: 'alice',
|
|
727
|
+
}));
|
|
728
|
+
|
|
729
|
+
// Header is authenticated but NOT encrypted (receiver needs it as plaintext)
|
|
730
|
+
const ciphertext = AES_GCM.encryptWithAad(sessionKey, nonce, body, header);
|
|
731
|
+
|
|
732
|
+
return { header, nonce, ciphertext };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function receiveMessage(packet: { header: Buffer; nonce: Buffer; ciphertext: Buffer }) {
|
|
736
|
+
// Decryption fails if EITHER ciphertext OR header was tampered
|
|
737
|
+
return AES_GCM.decryptWithAad(
|
|
738
|
+
sessionKey,
|
|
739
|
+
packet.nonce,
|
|
740
|
+
packet.ciphertext,
|
|
741
|
+
packet.header,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
441
746
|
---
|
|
442
747
|
|
|
443
748
|
## ποΈ Architecture
|
|
@@ -445,18 +750,20 @@ function tripleDH(IK_A_priv: Buffer, EK_A_priv: Buffer,
|
|
|
445
750
|
```
|
|
446
751
|
@brashkie/signalis-core
|
|
447
752
|
β
|
|
448
|
-
βββ π¦ Rust Workspace (
|
|
753
|
+
βββ π¦ Rust Workspace (8 crates)
|
|
449
754
|
β βββ sc-curve25519 β X25519 ECDH operations
|
|
755
|
+
β βββ sc-ed25519 β Ed25519 signatures (RFC 8032) β NEW v0.2.0
|
|
756
|
+
β βββ sc-xed25519 β XEd25519 Signal-style signatures β NEW v0.2.0
|
|
450
757
|
β βββ sc-hkdf β HKDF-SHA256 derivation
|
|
451
|
-
β βββ sc-aes β AES-256-GCM & CBC
|
|
758
|
+
β βββ sc-aes β AES-256-GCM (with AAD) & CBC
|
|
452
759
|
β βββ sc-hmac β HMAC-SHA256 with constant-time verify
|
|
453
760
|
β βββ sc-sha256 β SHA-256 hashing
|
|
454
761
|
β βββ sc-node β NAPI-RS bindings (cdylib)
|
|
455
762
|
β
|
|
456
763
|
βββ π¦ TypeScript Layer
|
|
457
764
|
βββ core.ts β Crypto wrappers with validation
|
|
458
|
-
βββ types.ts β Type definitions (KeyPair, etc.)
|
|
459
|
-
βββ errors.ts β Typed error classes
|
|
765
|
+
βββ types.ts β Type definitions (KeyPair, Signature, etc.)
|
|
766
|
+
βββ errors.ts β Typed error classes (incl. SignatureError)
|
|
460
767
|
βββ validators.ts β Input assertions
|
|
461
768
|
βββ utils.ts β Encoding + random + buffer helpers
|
|
462
769
|
βββ constants.ts β Public constants (sizes, limits)
|
|
@@ -487,8 +794,10 @@ dist/
|
|
|
487
794
|
| Primitive | Spec | Implementation |
|
|
488
795
|
|-----------|------|----------------|
|
|
489
796
|
| X25519 | [RFC 7748](https://datatracker.ietf.org/doc/html/rfc7748) | [`curve25519-dalek`](https://github.com/dalek-cryptography/curve25519-dalek) (audited by NCC Group) |
|
|
797
|
+
| Ed25519 | [RFC 8032](https://datatracker.ietf.org/doc/html/rfc8032) | [`ed25519-dalek`](https://github.com/dalek-cryptography/ed25519-dalek) (audited) β **NEW v0.2.0** |
|
|
798
|
+
| XEd25519 | [Signal Spec](https://signal.org/docs/specifications/xeddsa/) | Custom impl over `curve25519-dalek` β **NEW v0.2.0** |
|
|
490
799
|
| HKDF-SHA256 | [RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869) | [`hkdf`](https://github.com/RustCrypto/KDFs) (RustCrypto) |
|
|
491
|
-
| AES-256-GCM | [NIST SP 800-38D](https://csrc.nist.gov/publications/detail/sp/800-38d/final) | [`aes-gcm`](https://github.com/RustCrypto/AEADs) (RustCrypto) |
|
|
800
|
+
| AES-256-GCM | [NIST SP 800-38D](https://csrc.nist.gov/publications/detail/sp/800-38d/final) | [`aes-gcm`](https://github.com/RustCrypto/AEADs) (RustCrypto, AAD support) |
|
|
492
801
|
| AES-256-CBC | [NIST SP 800-38A](https://csrc.nist.gov/publications/detail/sp/800-38a/final) | [`aes`](https://github.com/RustCrypto/block-ciphers) (RustCrypto) |
|
|
493
802
|
| HMAC-SHA256 | [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) | [`hmac`](https://github.com/RustCrypto/MACs) (RustCrypto) |
|
|
494
803
|
| SHA-256 | [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) | [`sha2`](https://github.com/RustCrypto/hashes) (RustCrypto) |
|
|
@@ -521,12 +830,17 @@ Benchmarks (Node 22, x86_64):
|
|
|
521
830
|
|-----------|------------|-------------|
|
|
522
831
|
| Curve25519 keygen | ~50,000 ops/sec | **15Γ** faster than tweetnacl |
|
|
523
832
|
| X25519 ECDH | ~25,000 ops/sec | **20Γ** faster |
|
|
833
|
+
| Ed25519 sign | ~25,000 ops/sec | **20Γ** faster β NEW v0.2.0 |
|
|
834
|
+
| Ed25519 verify | ~10,000 ops/sec | **15Γ** faster β NEW v0.2.0 |
|
|
835
|
+
| XEd25519 sign | ~20,000 ops/sec | β β NEW v0.2.0 |
|
|
836
|
+
| XEd25519 verify | ~10,000 ops/sec | β β NEW v0.2.0 |
|
|
524
837
|
| HKDF derive (32 bytes) | ~500,000 ops/sec | **30Γ** faster |
|
|
525
838
|
| AES-256-GCM encrypt (1 KB) | ~2 GB/sec | **80Γ** faster |
|
|
839
|
+
| AES-GCM with AAD | <5% overhead vs no AAD | β NEW v0.2.0 |
|
|
526
840
|
| SHA-256 (1 KB) | ~3 GB/sec | **50Γ** faster |
|
|
527
841
|
| HMAC-SHA256 (1 KB) | ~2.5 GB/sec | **40Γ** faster |
|
|
528
842
|
|
|
529
|
-
*Run `npm run bench` to benchmark on your machine (coming in v0.
|
|
843
|
+
*Run `npm run bench` to benchmark on your machine (coming in v0.3).*
|
|
530
844
|
|
|
531
845
|
---
|
|
532
846
|
|
|
@@ -562,15 +876,20 @@ npm run test:coverage:ui
|
|
|
562
876
|
|
|
563
877
|
### What's Tested
|
|
564
878
|
|
|
565
|
-
- β
All RFC test vectors (RFC 5869, 7748, 4231)
|
|
879
|
+
- β
All RFC test vectors (RFC 5869, 7748, 8032, 4231)
|
|
566
880
|
- β
All NIST test vectors (AES, SHA-256)
|
|
881
|
+
- β
Ed25519 RFC 8032 vector 1 (empty message) β NEW v0.2.0
|
|
882
|
+
- β
XEd25519 round-trips with deterministic + probabilistic signing β NEW v0.2.0
|
|
883
|
+
- β
AES-GCM AAD authentication (tampered AAD fails decryption) β NEW v0.2.0
|
|
567
884
|
- β
Input validation for every public function
|
|
568
885
|
- β
Error handling for every code path
|
|
569
886
|
- β
Round-trip encryption / decryption
|
|
570
|
-
- β
Tampering detection (AES-GCM tag failures)
|
|
887
|
+
- β
Tampering detection (AES-GCM tag failures, signature failures)
|
|
571
888
|
- β
Both CommonJS and ESM consumption paths
|
|
572
889
|
- β
Default export and named exports
|
|
573
890
|
|
|
891
|
+
**Test count: 269+ assertions across Rust (48) + Vitest (172) + CJS (12) + ESM (15) + new v0.2.0 tests (49).**
|
|
892
|
+
|
|
574
893
|
---
|
|
575
894
|
|
|
576
895
|
## π¨ Building from Source
|
|
@@ -611,9 +930,10 @@ npm run examples
|
|
|
611
930
|
See [ROADMAP.md](./ROADMAP.md) for detailed plans.
|
|
612
931
|
|
|
613
932
|
**TL;DR:**
|
|
614
|
-
- **v0.1** β
β Cryptographic primitives (
|
|
615
|
-
- **v0.2** β
|
|
616
|
-
- **
|
|
933
|
+
- **v0.1** β
β Cryptographic primitives (Curve25519, HKDF, AES, HMAC, SHA-256)
|
|
934
|
+
- **v0.2** β
β Ed25519, XEd25519, AES-GCM with AAD (current release)
|
|
935
|
+
- **v0.3** β Benchmarks, X448 support, more test vectors
|
|
936
|
+
- **v1.0** β Stable API, external audit
|
|
617
937
|
- **Then:** [@brashkie/signalis](https://github.com/Brashkie/signalis) (X3DH + Double Ratchet)
|
|
618
938
|
- **Then:** [@brashkie/waproto](https://github.com/Brashkie/waproto) (WhatsApp Protocol)
|
|
619
939
|
- **Then:** HepeinBaileys 2.0 (full WhatsApp client from scratch)
|
|
@@ -638,9 +958,10 @@ Please also read our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
|
638
958
|
Built on the shoulders of giants:
|
|
639
959
|
|
|
640
960
|
- **[curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek)** β Curve25519 in pure Rust
|
|
961
|
+
- **[ed25519-dalek](https://github.com/dalek-cryptography/ed25519-dalek)** β Ed25519 in pure Rust (added in v0.2.0)
|
|
641
962
|
- **[RustCrypto](https://github.com/RustCrypto)** β `aes`, `hkdf`, `hmac`, `sha2`
|
|
642
963
|
- **[napi-rs](https://napi.rs)** β Rust β Node bindings
|
|
643
|
-
- **Signal Foundation** β [Protocol specifications](https://signal.org/docs/)
|
|
964
|
+
- **Signal Foundation** β [Protocol specifications](https://signal.org/docs/) (including [XEd25519](https://signal.org/docs/specifications/xeddsa/))
|
|
644
965
|
- **[tsup](https://tsup.egoist.dev/)** β Dual ESM/CJS bundler
|
|
645
966
|
- **[Vitest](https://vitest.dev/)** β Modern test runner
|
|
646
967
|
|
|
@@ -662,4 +983,4 @@ See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for full details.
|
|
|
662
983
|
|
|
663
984
|
[GitHub](https://github.com/Brashkie) Β· [npm](https://www.npmjs.com/~brashkie)
|
|
664
985
|
|
|
665
|
-
</div>
|
|
986
|
+
</div>
|