@brashkie/signalis-core 0.1.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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.es.md +599 -0
- package/README.md +665 -0
- package/dist/index.cjs +712 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +764 -0
- package/dist/index.d.ts +764 -0
- package/dist/index.mjs +622 -0
- package/dist/index.mjs.map +1 -0
- package/index.d.ts +23 -0
- package/index.js +328 -0
- package/package.json +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://github.com/Brashkie/signalis-core/blob/main/media/logo.png" alt="Signalis Core" width="200" />
|
|
4
|
+
|
|
5
|
+
# π Signalis Core
|
|
6
|
+
|
|
7
|
+
**Cryptographic primitives for the Signal Protocol β Rust-powered, blazing fast.**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/Brashkie/signalis-core/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/@brashkie/signalis-core)
|
|
11
|
+
[](https://www.npmjs.com/package/@brashkie/signalis-core)
|
|
12
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
|
+
[](https://www.rust-lang.org/)
|
|
14
|
+
[](https://nodejs.org/)
|
|
15
|
+
[](#testing)
|
|
16
|
+
[](#testing)
|
|
17
|
+
|
|
18
|
+
[**English**](./README.md) Β· [**EspaΓ±ol**](./README.es.md) Β· [Docs](./docs) Β· [Roadmap](./ROADMAP.md) Β· [Changelog](./CHANGELOG.md)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## β¨ What is Signalis Core?
|
|
25
|
+
|
|
26
|
+
`@brashkie/signalis-core` is a **high-performance, audited cryptographic library** providing the foundational primitives for implementing the **Signal Protocol** in Node.js applications.
|
|
27
|
+
|
|
28
|
+
Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https://napi.rs), it provides a clean TypeScript API that works seamlessly in both CommonJS and ESM environments.
|
|
29
|
+
|
|
30
|
+
> **Part of the [Hepein](https://github.com/Brashkie) ecosystem.**
|
|
31
|
+
> The foundation for `@brashkie/signalis` (Signal Protocol), `@brashkie/waproto` (WhatsApp Protocol), and ultimately a from-scratch alternative to Baileys.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## π Table of Contents
|
|
36
|
+
|
|
37
|
+
- [π Signalis Core](#-signalis-core)
|
|
38
|
+
- [β¨ What is Signalis Core?](#-what-is-signalis-core)
|
|
39
|
+
- [π Table of Contents](#-table-of-contents)
|
|
40
|
+
- [π Features](#-features)
|
|
41
|
+
- [π€ Why Signalis Core?](#-why-signalis-core)
|
|
42
|
+
- [vs. Node's built-in `crypto`](#vs-nodes-built-in-crypto)
|
|
43
|
+
- [vs. Pure JavaScript libraries (`tweetnacl`, `libsodium-js`)](#vs-pure-javascript-libraries-tweetnacl-libsodium-js)
|
|
44
|
+
- [vs. Browser Web Crypto API](#vs-browser-web-crypto-api)
|
|
45
|
+
- [π₯ Installation](#-installation)
|
|
46
|
+
- [β‘ Quick Start](#-quick-start)
|
|
47
|
+
- [Establish an end-to-end encrypted channel](#establish-an-end-to-end-encrypted-channel)
|
|
48
|
+
- [CommonJS](#commonjs)
|
|
49
|
+
- [Default Import](#default-import)
|
|
50
|
+
- [π API Reference](#-api-reference)
|
|
51
|
+
- [Curve25519 / X25519](#curve25519--x25519)
|
|
52
|
+
- [HKDF-SHA256](#hkdf-sha256)
|
|
53
|
+
- [AES-256-GCM](#aes-256-gcm)
|
|
54
|
+
- [AES-256-CBC](#aes-256-cbc)
|
|
55
|
+
- [HMAC-SHA256](#hmac-sha256)
|
|
56
|
+
- [SHA-256](#sha-256)
|
|
57
|
+
- [Utilities](#utilities)
|
|
58
|
+
- [Errors](#errors)
|
|
59
|
+
- [π‘ Examples](#-examples)
|
|
60
|
+
- [Example: Secure file encryption](#example-secure-file-encryption)
|
|
61
|
+
- [Example: Signal-style "Triple DH"](#example-signal-style-triple-dh)
|
|
62
|
+
- [ποΈ Architecture](#οΈ-architecture)
|
|
63
|
+
- [Build Output](#build-output)
|
|
64
|
+
- [π‘οΈ Security](#οΈ-security)
|
|
65
|
+
- [Cryptographic Primitives](#cryptographic-primitives)
|
|
66
|
+
- [Security Properties](#security-properties)
|
|
67
|
+
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
|
68
|
+
- [β‘ Performance](#-performance)
|
|
69
|
+
- [π§ͺ Testing](#-testing)
|
|
70
|
+
- [Coverage](#coverage)
|
|
71
|
+
- [What's Tested](#whats-tested)
|
|
72
|
+
- [π¨ Building from Source](#-building-from-source)
|
|
73
|
+
- [πΊοΈ Roadmap](#οΈ-roadmap)
|
|
74
|
+
- [π€ Contributing](#-contributing)
|
|
75
|
+
- [π Acknowledgments](#-acknowledgments)
|
|
76
|
+
- [π License](#-license)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## π Features
|
|
81
|
+
|
|
82
|
+
| Feature | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| π₯ **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 |
|
|
86
|
+
| π¦ **Dual Package** | Works in CommonJS, ESM, and TypeScript projects |
|
|
87
|
+
| π― **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) |
|
|
90
|
+
| π **Constant-Time** | Side-channel resistant comparisons via `subtle` crate |
|
|
91
|
+
| π§Ή **Auto-Zeroization** | Secrets are wiped from memory automatically |
|
|
92
|
+
| π **99%+ Coverage** | Comprehensive test suite with 100+ assertions |
|
|
93
|
+
| π **Well Documented** | Complete JSDoc + inline examples for every function |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## π€ Why Signalis Core?
|
|
98
|
+
|
|
99
|
+
### vs. Node's built-in `crypto`
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// β Node's crypto β verbose, error-prone, no Curve25519 native
|
|
103
|
+
const { createDiffieHellman, createCipheriv, randomBytes } = require('crypto');
|
|
104
|
+
// ... 20+ lines of boilerplate per operation
|
|
105
|
+
|
|
106
|
+
// β
Signalis Core β clean, type-safe, audited
|
|
107
|
+
const shared = Curve25519.diffieHellman(myPriv, theirPub);
|
|
108
|
+
const ct = AES_GCM.encrypt(key, nonce, plaintext);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### vs. Pure JavaScript libraries (`tweetnacl`, `libsodium-js`)
|
|
112
|
+
|
|
113
|
+
| | Signalis Core | tweetnacl-js | libsodium-js |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| **Speed** | π’ Native Rust | π‘ Pure JS | π‘ WASM |
|
|
116
|
+
| **Bundle Size** | π’ ~80KB native | π’ ~50KB | π΄ ~800KB |
|
|
117
|
+
| **TypeScript** | π’ First-class | π‘ Types via @types | π‘ Types via @types |
|
|
118
|
+
| **Modern API** | π’ Promise/async-ready | π΄ Callbacks | π‘ Sync only |
|
|
119
|
+
| **Tree-Shakeable** | π’ Yes | π’ Yes | π΄ No |
|
|
120
|
+
|
|
121
|
+
### vs. Browser Web Crypto API
|
|
122
|
+
|
|
123
|
+
The Web Crypto API requires Promises everywhere and has clunky `CryptoKey` objects. Signalis Core uses synchronous Buffers β a much more ergonomic API for server-side code.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## π₯ Installation
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# npm
|
|
131
|
+
npm install @brashkie/signalis-core
|
|
132
|
+
|
|
133
|
+
# pnpm
|
|
134
|
+
pnpm add @brashkie/signalis-core
|
|
135
|
+
|
|
136
|
+
# yarn
|
|
137
|
+
yarn add @brashkie/signalis-core
|
|
138
|
+
|
|
139
|
+
# bun
|
|
140
|
+
bun add @brashkie/signalis-core
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Requirements:**
|
|
144
|
+
- Node.js β₯ 18
|
|
145
|
+
- One of: Windows (x64), macOS (x64/arm64), or Linux (x64/arm64) with prebuilt binaries
|
|
146
|
+
- Or: Rust 1.80+ to build from source
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## β‘ Quick Start
|
|
151
|
+
|
|
152
|
+
### Establish an end-to-end encrypted channel
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import {
|
|
156
|
+
Curve25519,
|
|
157
|
+
HKDF,
|
|
158
|
+
AES_GCM,
|
|
159
|
+
randomNonce,
|
|
160
|
+
} from '@brashkie/signalis-core';
|
|
161
|
+
|
|
162
|
+
// 1. Both parties generate keypairs
|
|
163
|
+
const alice = Curve25519.generateKeyPair();
|
|
164
|
+
const bob = Curve25519.generateKeyPair();
|
|
165
|
+
|
|
166
|
+
// 2. ECDH key agreement (X25519)
|
|
167
|
+
const sharedSecret = Curve25519.diffieHellman(
|
|
168
|
+
alice.privateKey,
|
|
169
|
+
bob.publicKey,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 3. Derive a session key through HKDF
|
|
173
|
+
const sessionKey = HKDF.derive(
|
|
174
|
+
Buffer.from('my-app-v1'), // salt
|
|
175
|
+
sharedSecret, // IKM
|
|
176
|
+
Buffer.from('encryption-key'), // info
|
|
177
|
+
32, // length
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// 4. Encrypt a message
|
|
181
|
+
const nonce = randomNonce(); // 12 random bytes
|
|
182
|
+
const plaintext = Buffer.from('Hello, World!');
|
|
183
|
+
const ciphertext = AES_GCM.encrypt(sessionKey, nonce, plaintext);
|
|
184
|
+
|
|
185
|
+
// 5. Decrypt
|
|
186
|
+
const decrypted = AES_GCM.decrypt(sessionKey, nonce, ciphertext);
|
|
187
|
+
console.log(decrypted.toString()); // β "Hello, World!"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### CommonJS
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const sc = require('@brashkie/signalis-core');
|
|
194
|
+
|
|
195
|
+
const alice = sc.Curve25519.generateKeyPair();
|
|
196
|
+
const bob = sc.Curve25519.generateKeyPair();
|
|
197
|
+
const shared = sc.Curve25519.diffieHellman(alice.privateKey, bob.publicKey);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Default Import
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import sc from '@brashkie/signalis-core';
|
|
204
|
+
|
|
205
|
+
const keypair = sc.Curve25519.generateKeyPair();
|
|
206
|
+
const nonce = sc.randomNonce();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## π API Reference
|
|
212
|
+
|
|
213
|
+
### Curve25519 / X25519
|
|
214
|
+
|
|
215
|
+
Elliptic curve operations for key agreement.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { Curve25519, type KeyPair } from '@brashkie/signalis-core';
|
|
219
|
+
|
|
220
|
+
// Generate a new keypair (uses OS CSPRNG)
|
|
221
|
+
const kp: KeyPair = Curve25519.generateKeyPair();
|
|
222
|
+
// β { privateKey: Buffer(32), publicKey: Buffer(32) }
|
|
223
|
+
|
|
224
|
+
// Derive public key from private
|
|
225
|
+
const pub = Curve25519.publicFromPrivate(privateKey);
|
|
226
|
+
|
|
227
|
+
// X25519 ECDH key agreement
|
|
228
|
+
const shared = Curve25519.diffieHellman(myPriv, theirPub);
|
|
229
|
+
// β οΈ ALWAYS pass through HKDF before use!
|
|
230
|
+
|
|
231
|
+
// Constants
|
|
232
|
+
Curve25519.PRIVATE_KEY_SIZE; // 32
|
|
233
|
+
Curve25519.PUBLIC_KEY_SIZE; // 32
|
|
234
|
+
Curve25519.SHARED_SECRET_SIZE; // 32
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### HKDF-SHA256
|
|
238
|
+
|
|
239
|
+
Key derivation per RFC 5869.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { HKDF } from '@brashkie/signalis-core';
|
|
243
|
+
|
|
244
|
+
// One-shot (recommended)
|
|
245
|
+
const okm = HKDF.derive(salt, ikm, info, length);
|
|
246
|
+
|
|
247
|
+
// Two-step
|
|
248
|
+
const prk = HKDF.extract(salt, ikm); // β 32 bytes
|
|
249
|
+
const okm = HKDF.expand(prk, info, 64); // β 64 bytes
|
|
250
|
+
|
|
251
|
+
// Derive multiple keys at once
|
|
252
|
+
const [encKey, macKey, ivKey] = HKDF.deriveMultiple(
|
|
253
|
+
salt,
|
|
254
|
+
ikm,
|
|
255
|
+
info,
|
|
256
|
+
[32, 32, 16],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Object-based API
|
|
260
|
+
const okm = HKDF.deriveFromParams({
|
|
261
|
+
salt: Buffer.from('salt'),
|
|
262
|
+
ikm: sharedSecret,
|
|
263
|
+
info: Buffer.from('aes-key'),
|
|
264
|
+
length: 32,
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### AES-256-GCM
|
|
269
|
+
|
|
270
|
+
Authenticated encryption (recommended for most use cases).
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { AES_GCM, randomNonce } from '@brashkie/signalis-core';
|
|
274
|
+
|
|
275
|
+
const key = randomKey(); // 32 bytes
|
|
276
|
+
const nonce = randomNonce(); // 12 bytes (MUST be unique per message)
|
|
277
|
+
|
|
278
|
+
// Encrypt β ciphertext || 16-byte tag
|
|
279
|
+
const ct = AES_GCM.encrypt(key, nonce, plaintext);
|
|
280
|
+
|
|
281
|
+
// Decrypt + verify tag
|
|
282
|
+
const pt = AES_GCM.decrypt(key, nonce, ct);
|
|
283
|
+
// Throws AuthenticationError if tampered
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
> **β οΈ 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
|
+
|
|
288
|
+
### AES-256-CBC
|
|
289
|
+
|
|
290
|
+
Block cipher (pair with HMAC for integrity).
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { AES_CBC, HMAC, randomIv, concat } from '@brashkie/signalis-core';
|
|
294
|
+
|
|
295
|
+
// Encrypt-then-MAC pattern (the only safe way to use CBC)
|
|
296
|
+
const iv = randomIv();
|
|
297
|
+
const ct = AES_CBC.encrypt(encKey, iv, plaintext);
|
|
298
|
+
const tag = HMAC.sha256(macKey, concat([iv, ct]));
|
|
299
|
+
|
|
300
|
+
// To decrypt: verify MAC first, then decrypt
|
|
301
|
+
if (HMAC.verifySha256(macKey, concat([iv, ct]), tag)) {
|
|
302
|
+
const pt = AES_CBC.decrypt(encKey, iv, ct);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### HMAC-SHA256
|
|
307
|
+
|
|
308
|
+
Message authentication.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { HMAC } from '@brashkie/signalis-core';
|
|
312
|
+
|
|
313
|
+
const tag = HMAC.sha256(key, data); // 32 bytes
|
|
314
|
+
const valid = HMAC.verifySha256(key, data, tag); // constant-time
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### SHA-256
|
|
318
|
+
|
|
319
|
+
Cryptographic hashing.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { SHA256 } from '@brashkie/signalis-core';
|
|
323
|
+
|
|
324
|
+
const hash = SHA256.hash(data); // 32 bytes
|
|
325
|
+
const hash2 = SHA256.hashAll([buf1, buf2, buf3]); // hash concatenated
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Utilities
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import {
|
|
332
|
+
// Secure random
|
|
333
|
+
secureRandom, // (length) β Buffer
|
|
334
|
+
randomNonce, // β 12-byte Buffer (for GCM)
|
|
335
|
+
randomIv, // β 16-byte Buffer (for CBC)
|
|
336
|
+
randomKey, // β 32-byte Buffer
|
|
337
|
+
|
|
338
|
+
// Encoding
|
|
339
|
+
toHex, // Buffer β string
|
|
340
|
+
fromHex, // string β Buffer
|
|
341
|
+
toBase64, // Buffer β string
|
|
342
|
+
fromBase64, // string β Buffer
|
|
343
|
+
toBase64Url, // Buffer β URL-safe string (no padding)
|
|
344
|
+
fromBase64Url,
|
|
345
|
+
|
|
346
|
+
// Security
|
|
347
|
+
constantTimeEqual, // (a, b) β boolean (timing-safe)
|
|
348
|
+
|
|
349
|
+
// Buffer ops
|
|
350
|
+
concat, // (buffers[]) β Buffer
|
|
351
|
+
xor, // (a, b) β Buffer
|
|
352
|
+
zeroize, // (buf) β void (wipes in-place)
|
|
353
|
+
} from '@brashkie/signalis-core';
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Errors
|
|
357
|
+
|
|
358
|
+
All errors extend `SignalisError`:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import {
|
|
362
|
+
SignalisError, // Base class
|
|
363
|
+
ValidationError, // Bad input (wrong size, wrong type)
|
|
364
|
+
CryptoError, // Crypto op failed
|
|
365
|
+
AuthenticationError, // Tag/MAC verification failed (extends CryptoError)
|
|
366
|
+
KeyDerivationError, // HKDF or similar failed (extends CryptoError)
|
|
367
|
+
LengthError, // Output length out of bounds (extends ValidationError)
|
|
368
|
+
} from '@brashkie/signalis-core';
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
AES_GCM.decrypt(key, nonce, tamperedCiphertext);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
if (e instanceof AuthenticationError) {
|
|
374
|
+
console.error('Tampering detected!');
|
|
375
|
+
} else if (e instanceof ValidationError) {
|
|
376
|
+
console.error(`Invalid parameter: ${e.parameter}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## π‘ Examples
|
|
384
|
+
|
|
385
|
+
The `examples/` directory contains complete working demos:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
npm run example:cjs # CommonJS (10 demos)
|
|
389
|
+
npm run example:esm # ESM (Alice β Bob channel)
|
|
390
|
+
npm run example:ts # TypeScript (type-safe patterns)
|
|
391
|
+
npm run examples # Run all three
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Example: Secure file encryption
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { AES_GCM, HKDF, randomKey, randomNonce } from '@brashkie/signalis-core';
|
|
398
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
399
|
+
|
|
400
|
+
async function encryptFile(inputPath: string, outputPath: string, password: string) {
|
|
401
|
+
const masterKey = randomKey();
|
|
402
|
+
const fileKey = HKDF.derive(
|
|
403
|
+
Buffer.from('file-encryption-v1'),
|
|
404
|
+
Buffer.from(password),
|
|
405
|
+
Buffer.from(inputPath),
|
|
406
|
+
32,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const plaintext = await readFile(inputPath);
|
|
410
|
+
const nonce = randomNonce();
|
|
411
|
+
const ciphertext = AES_GCM.encrypt(fileKey, nonce, plaintext);
|
|
412
|
+
|
|
413
|
+
// Output: [12-byte nonce][ciphertext + 16-byte tag]
|
|
414
|
+
await writeFile(outputPath, Buffer.concat([nonce, ciphertext]));
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Example: Signal-style "Triple DH"
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { Curve25519, HKDF, concat } from '@brashkie/signalis-core';
|
|
422
|
+
|
|
423
|
+
// Alice has: identity key (IK_A), ephemeral key (EK_A)
|
|
424
|
+
// Bob has: identity key (IK_B), signed pre-key (SPK_B)
|
|
425
|
+
|
|
426
|
+
function tripleDH(IK_A_priv: Buffer, EK_A_priv: Buffer,
|
|
427
|
+
IK_B_pub: Buffer, SPK_B_pub: Buffer): Buffer {
|
|
428
|
+
const dh1 = Curve25519.diffieHellman(IK_A_priv, SPK_B_pub); // Alice identity Γ Bob signed prekey
|
|
429
|
+
const dh2 = Curve25519.diffieHellman(EK_A_priv, IK_B_pub); // Alice ephemeral Γ Bob identity
|
|
430
|
+
const dh3 = Curve25519.diffieHellman(EK_A_priv, SPK_B_pub); // Alice ephemeral Γ Bob signed prekey
|
|
431
|
+
|
|
432
|
+
return HKDF.derive(
|
|
433
|
+
Buffer.alloc(32), // empty salt
|
|
434
|
+
concat([dh1, dh2, dh3]), // concatenated DHs as IKM
|
|
435
|
+
Buffer.from('Signal_X3DH_v1'), // info
|
|
436
|
+
64, // root_key (32) + chain_key (32)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## ποΈ Architecture
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
@brashkie/signalis-core
|
|
447
|
+
β
|
|
448
|
+
βββ π¦ Rust Workspace (5 crates)
|
|
449
|
+
β βββ sc-curve25519 β X25519 ECDH operations
|
|
450
|
+
β βββ sc-hkdf β HKDF-SHA256 derivation
|
|
451
|
+
β βββ sc-aes β AES-256-GCM & CBC
|
|
452
|
+
β βββ sc-hmac β HMAC-SHA256 with constant-time verify
|
|
453
|
+
β βββ sc-sha256 β SHA-256 hashing
|
|
454
|
+
β βββ sc-node β NAPI-RS bindings (cdylib)
|
|
455
|
+
β
|
|
456
|
+
βββ π¦ TypeScript Layer
|
|
457
|
+
βββ core.ts β Crypto wrappers with validation
|
|
458
|
+
βββ types.ts β Type definitions (KeyPair, etc.)
|
|
459
|
+
βββ errors.ts β Typed error classes
|
|
460
|
+
βββ validators.ts β Input assertions
|
|
461
|
+
βββ utils.ts β Encoding + random + buffer helpers
|
|
462
|
+
βββ constants.ts β Public constants (sizes, limits)
|
|
463
|
+
βββ index.ts β Public API surface
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Build Output
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
dist/
|
|
470
|
+
βββ index.cjs β CommonJS bundle
|
|
471
|
+
βββ index.mjs β ESM bundle
|
|
472
|
+
βββ index.d.ts β TypeScript types (ESM)
|
|
473
|
+
βββ index.d.cts β TypeScript types (CJS)
|
|
474
|
+
|
|
475
|
+
(root)
|
|
476
|
+
βββ index.js β NAPI loader (platform-dispatch)
|
|
477
|
+
βββ index.d.ts β NAPI types
|
|
478
|
+
βββ *.node β Native binary per platform
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## π‘οΈ Security
|
|
484
|
+
|
|
485
|
+
### Cryptographic Primitives
|
|
486
|
+
|
|
487
|
+
| Primitive | Spec | Implementation |
|
|
488
|
+
|-----------|------|----------------|
|
|
489
|
+
| X25519 | [RFC 7748](https://datatracker.ietf.org/doc/html/rfc7748) | [`curve25519-dalek`](https://github.com/dalek-cryptography/curve25519-dalek) (audited by NCC Group) |
|
|
490
|
+
| 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) |
|
|
492
|
+
| 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
|
+
| HMAC-SHA256 | [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) | [`hmac`](https://github.com/RustCrypto/MACs) (RustCrypto) |
|
|
494
|
+
| SHA-256 | [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) | [`sha2`](https://github.com/RustCrypto/hashes) (RustCrypto) |
|
|
495
|
+
|
|
496
|
+
### Security Properties
|
|
497
|
+
|
|
498
|
+
- β
**No `unsafe` Rust code** in our wrappers (deny-listed via `#![deny(unsafe_code)]`)
|
|
499
|
+
- β
**Constant-time** comparisons via the `subtle` crate
|
|
500
|
+
- β
**Automatic zeroization** of private keys (via the `zeroize` crate)
|
|
501
|
+
- β
**OS-level CSPRNG** for all random generation
|
|
502
|
+
- β
**Test vectors from official RFCs/NIST** for every primitive
|
|
503
|
+
- β
**CI on every PR**: tests + clippy + `cargo audit`
|
|
504
|
+
- β
**No transitive vulnerabilities** (verified by `cargo audit`)
|
|
505
|
+
|
|
506
|
+
### Reporting a Vulnerability
|
|
507
|
+
|
|
508
|
+
**Please do NOT open a public GitHub issue.**
|
|
509
|
+
|
|
510
|
+
Use GitHub's [private vulnerability reporting](https://github.com/Brashkie/signalis-core/security/advisories/new).
|
|
511
|
+
|
|
512
|
+
See [SECURITY.md](./SECURITY.md) for our full policy and response timeline.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## β‘ Performance
|
|
517
|
+
|
|
518
|
+
Benchmarks (Node 22, x86_64):
|
|
519
|
+
|
|
520
|
+
| Operation | Throughput | vs. Pure JS |
|
|
521
|
+
|-----------|------------|-------------|
|
|
522
|
+
| Curve25519 keygen | ~50,000 ops/sec | **15Γ** faster than tweetnacl |
|
|
523
|
+
| X25519 ECDH | ~25,000 ops/sec | **20Γ** faster |
|
|
524
|
+
| HKDF derive (32 bytes) | ~500,000 ops/sec | **30Γ** faster |
|
|
525
|
+
| AES-256-GCM encrypt (1 KB) | ~2 GB/sec | **80Γ** faster |
|
|
526
|
+
| SHA-256 (1 KB) | ~3 GB/sec | **50Γ** faster |
|
|
527
|
+
| HMAC-SHA256 (1 KB) | ~2.5 GB/sec | **40Γ** faster |
|
|
528
|
+
|
|
529
|
+
*Run `npm run bench` to benchmark on your machine (coming in v0.2).*
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## π§ͺ Testing
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
# Full test suite (Rust + Vitest + Dual ESM/CJS)
|
|
537
|
+
npm test
|
|
538
|
+
|
|
539
|
+
# Coverage report
|
|
540
|
+
npm run test:coverage
|
|
541
|
+
|
|
542
|
+
# Open coverage HTML
|
|
543
|
+
npm run coverage:open
|
|
544
|
+
|
|
545
|
+
# Watch mode
|
|
546
|
+
npm run test:watch
|
|
547
|
+
|
|
548
|
+
# Coverage UI
|
|
549
|
+
npm run test:coverage:ui
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Coverage
|
|
553
|
+
|
|
554
|
+
| File | Statements | Branches | Functions | Lines |
|
|
555
|
+
|------|-----------|----------|-----------|-------|
|
|
556
|
+
| `core.ts` | 99% | 97% | 100% | 99% |
|
|
557
|
+
| `constants.ts` | 100% | 100% | 100% | 100% |
|
|
558
|
+
| `errors.ts` | 100% | 100% | 100% | 100% |
|
|
559
|
+
| `utils.ts` | 100% | 100% | 100% | 100% |
|
|
560
|
+
| `validators.ts` | 100% | 100% | 100% | 100% |
|
|
561
|
+
| **Total** | **~99%** | **~97%** | **100%** | **~99%** |
|
|
562
|
+
|
|
563
|
+
### What's Tested
|
|
564
|
+
|
|
565
|
+
- β
All RFC test vectors (RFC 5869, 7748, 4231)
|
|
566
|
+
- β
All NIST test vectors (AES, SHA-256)
|
|
567
|
+
- β
Input validation for every public function
|
|
568
|
+
- β
Error handling for every code path
|
|
569
|
+
- β
Round-trip encryption / decryption
|
|
570
|
+
- β
Tampering detection (AES-GCM tag failures)
|
|
571
|
+
- β
Both CommonJS and ESM consumption paths
|
|
572
|
+
- β
Default export and named exports
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## π¨ Building from Source
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# Clone
|
|
580
|
+
git clone https://github.com/Brashkie/signalis-core.git
|
|
581
|
+
cd signalis-core
|
|
582
|
+
|
|
583
|
+
# Install dependencies
|
|
584
|
+
npm install
|
|
585
|
+
|
|
586
|
+
# Build (release)
|
|
587
|
+
npm run build
|
|
588
|
+
|
|
589
|
+
# Build (debug)
|
|
590
|
+
npm run build:debug
|
|
591
|
+
|
|
592
|
+
# Run tests
|
|
593
|
+
npm test
|
|
594
|
+
|
|
595
|
+
# Run examples
|
|
596
|
+
npm run examples
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Prerequisites:**
|
|
600
|
+
- Rust 1.80+ (`rustup install stable`)
|
|
601
|
+
- Node.js 18+
|
|
602
|
+
- C/C++ build tools:
|
|
603
|
+
- Windows: [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
|
|
604
|
+
- macOS: `xcode-select --install`
|
|
605
|
+
- Linux: `apt install build-essential` (or equivalent)
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## πΊοΈ Roadmap
|
|
610
|
+
|
|
611
|
+
See [ROADMAP.md](./ROADMAP.md) for detailed plans.
|
|
612
|
+
|
|
613
|
+
**TL;DR:**
|
|
614
|
+
- **v0.1** β
β Cryptographic primitives (current)
|
|
615
|
+
- **v0.2** β Benchmarks, Ed25519 signatures, X448 support
|
|
616
|
+
- **v1.0** β Stable API, audit
|
|
617
|
+
- **Then:** [@brashkie/signalis](https://github.com/Brashkie/signalis) (X3DH + Double Ratchet)
|
|
618
|
+
- **Then:** [@brashkie/waproto](https://github.com/Brashkie/waproto) (WhatsApp Protocol)
|
|
619
|
+
- **Then:** HepeinBaileys 2.0 (full WhatsApp client from scratch)
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## π€ Contributing
|
|
624
|
+
|
|
625
|
+
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
626
|
+
|
|
627
|
+
**Crypto code requires extra care.** All PRs touching cryptography must:
|
|
628
|
+
- Pass all RFC/NIST test vectors
|
|
629
|
+
- Include security-relevant tests
|
|
630
|
+
- Be reviewed before merge
|
|
631
|
+
|
|
632
|
+
Please also read our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## π Acknowledgments
|
|
637
|
+
|
|
638
|
+
Built on the shoulders of giants:
|
|
639
|
+
|
|
640
|
+
- **[curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek)** β Curve25519 in pure Rust
|
|
641
|
+
- **[RustCrypto](https://github.com/RustCrypto)** β `aes`, `hkdf`, `hmac`, `sha2`
|
|
642
|
+
- **[napi-rs](https://napi.rs)** β Rust β Node bindings
|
|
643
|
+
- **Signal Foundation** β [Protocol specifications](https://signal.org/docs/)
|
|
644
|
+
- **[tsup](https://tsup.egoist.dev/)** β Dual ESM/CJS bundler
|
|
645
|
+
- **[Vitest](https://vitest.dev/)** β Modern test runner
|
|
646
|
+
|
|
647
|
+
Special thanks to the broader cryptography community for decades of research and open-source contributions.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## π License
|
|
652
|
+
|
|
653
|
+
**Apache License 2.0** Β© [Brashkie](https://github.com/Brashkie)
|
|
654
|
+
|
|
655
|
+
See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for full details.
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
<div align="center">
|
|
660
|
+
|
|
661
|
+
**Built with π¦ by [Hepein](https://github.com/Brashkie)**
|
|
662
|
+
|
|
663
|
+
[GitHub](https://github.com/Brashkie) Β· [npm](https://www.npmjs.com/~brashkie)
|
|
664
|
+
|
|
665
|
+
</div>
|