@stvor/sdk 2.4.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/facade/app.d.ts +83 -76
- package/dist/facade/app.js +330 -195
- package/dist/facade/crypto-session.cjs +29 -0
- package/dist/facade/crypto-session.d.ts +71 -0
- package/dist/facade/crypto-session.js +152 -0
- package/dist/facade/errors.d.ts +29 -12
- package/dist/facade/errors.js +49 -8
- package/dist/facade/index.d.ts +27 -8
- package/dist/facade/index.js +23 -3
- package/dist/facade/local-storage-identity-store.cjs +29 -0
- package/dist/facade/local-storage-identity-store.d.ts +50 -0
- package/dist/facade/local-storage-identity-store.js +100 -0
- package/dist/facade/metrics-attestation.cjs +29 -0
- package/dist/facade/metrics-attestation.d.ts +209 -0
- package/dist/facade/metrics-attestation.js +333 -0
- package/dist/facade/metrics-engine.cjs +29 -0
- package/dist/facade/metrics-engine.d.ts +91 -0
- package/dist/facade/metrics-engine.js +170 -0
- package/dist/facade/redis-replay-cache.cjs +29 -0
- package/dist/facade/redis-replay-cache.d.ts +88 -0
- package/dist/facade/redis-replay-cache.js +60 -0
- package/dist/facade/relay-client.d.ts +22 -23
- package/dist/facade/relay-client.js +107 -128
- package/dist/facade/replay-manager.cjs +29 -0
- package/dist/facade/replay-manager.d.ts +51 -0
- package/dist/facade/replay-manager.js +150 -0
- package/dist/facade/sodium-singleton.cjs +29 -0
- package/dist/facade/sodium-singleton.d.ts +20 -0
- package/dist/facade/sodium-singleton.js +44 -0
- package/dist/facade/tofu-manager.cjs +29 -0
- package/dist/facade/tofu-manager.d.ts +82 -0
- package/dist/facade/tofu-manager.js +166 -0
- package/dist/facade/types.d.ts +2 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/legacy.d.ts +31 -1
- package/dist/legacy.js +90 -2
- package/dist/ratchet/core-production.cjs +29 -0
- package/dist/ratchet/core-production.d.ts +95 -0
- package/dist/ratchet/core-production.js +286 -0
- package/dist/{facade/crypto.cjs → ratchet/index.cjs} +1 -1
- package/dist/ratchet/index.d.ts +59 -0
- package/dist/ratchet/index.js +343 -0
- package/dist/ratchet/key-recovery.cjs +29 -0
- package/dist/ratchet/key-recovery.d.ts +45 -0
- package/dist/ratchet/key-recovery.js +148 -0
- package/dist/ratchet/replay-protection.cjs +29 -0
- package/dist/ratchet/replay-protection.d.ts +21 -0
- package/dist/ratchet/replay-protection.js +50 -0
- package/dist/{mock-relay-server.cjs → ratchet/tofu.cjs} +1 -1
- package/dist/ratchet/tofu.d.ts +27 -0
- package/dist/ratchet/tofu.js +62 -0
- package/dist/src/facade/app.cjs +29 -0
- package/dist/src/facade/app.d.ts +105 -0
- package/dist/src/facade/app.js +245 -0
- package/dist/src/facade/crypto.cjs +29 -0
- package/dist/src/facade/errors.cjs +29 -0
- package/dist/src/facade/errors.d.ts +19 -0
- package/dist/src/facade/errors.js +21 -0
- package/dist/src/facade/index.cjs +29 -0
- package/dist/src/facade/index.d.ts +8 -0
- package/dist/src/facade/index.js +5 -0
- package/dist/src/facade/relay-client.cjs +29 -0
- package/dist/src/facade/relay-client.d.ts +36 -0
- package/dist/src/facade/relay-client.js +154 -0
- package/dist/src/facade/types.cjs +29 -0
- package/dist/src/facade/types.d.ts +50 -0
- package/dist/src/facade/types.js +4 -0
- package/dist/src/index.cjs +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/legacy.cjs +29 -0
- package/dist/src/legacy.d.ts +0 -0
- package/dist/src/legacy.js +1 -0
- package/dist/src/mock-relay-server.cjs +29 -0
- package/package.json +16 -5
- /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
- /package/dist/{facade → src/facade}/crypto.js +0 -0
- /package/dist/{mock-relay-server.d.ts → src/mock-relay-server.d.ts} +0 -0
- /package/dist/{mock-relay-server.js → src/mock-relay-server.js} +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X3DH + Double Ratchet Implementation
|
|
3
|
+
* Uses ONLY Node.js built-in crypto module — zero external dependencies
|
|
4
|
+
*
|
|
5
|
+
* Implements the Signal Protocol Double Ratchet with deferred initialization:
|
|
6
|
+
* - First send → "initiator" DH ratchet (DH with peer's SPK)
|
|
7
|
+
* - First receive → "responder" DH ratchet (use own SPK, then fresh key)
|
|
8
|
+
* This allows either side to send first after symmetric X3DH key agreement.
|
|
9
|
+
*
|
|
10
|
+
* Provides:
|
|
11
|
+
* - X3DH key agreement (symmetric variant, both sides derive same SK)
|
|
12
|
+
* - Double Ratchet with DH ratchet + symmetric-key ratchet
|
|
13
|
+
* - AES-256-GCM AEAD encryption with header as AAD
|
|
14
|
+
* - ECDSA P-256 signing / verification
|
|
15
|
+
* - HKDF-SHA256 key derivation
|
|
16
|
+
* - HMAC-based chain-key ratchet (Signal-style)
|
|
17
|
+
*/
|
|
18
|
+
import crypto from 'crypto';
|
|
19
|
+
/* ================================================================
|
|
20
|
+
* Constants
|
|
21
|
+
* ================================================================ */
|
|
22
|
+
const CURVE = 'prime256v1';
|
|
23
|
+
const PUB_LEN = 65; // uncompressed P-256 public key
|
|
24
|
+
const HEADER_LEN = 85; // 65 + 4 + 4 + 12
|
|
25
|
+
const MAX_SKIP = 256;
|
|
26
|
+
/* ================================================================
|
|
27
|
+
* Init (no-op — Node.js crypto is always ready)
|
|
28
|
+
* ================================================================ */
|
|
29
|
+
export async function initializeCrypto() { }
|
|
30
|
+
/* ================================================================
|
|
31
|
+
* Key generation
|
|
32
|
+
* ================================================================ */
|
|
33
|
+
export function generateKeyPair() {
|
|
34
|
+
const ecdh = crypto.createECDH(CURVE);
|
|
35
|
+
ecdh.generateKeys();
|
|
36
|
+
return {
|
|
37
|
+
publicKey: Buffer.from(ecdh.getPublicKey()),
|
|
38
|
+
privateKey: Buffer.from(ecdh.getPrivateKey()),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/* ================================================================
|
|
42
|
+
* Low-level crypto helpers
|
|
43
|
+
* ================================================================ */
|
|
44
|
+
/** ECDH shared secret */
|
|
45
|
+
function ecdhSecret(priv, pub) {
|
|
46
|
+
const ecdh = crypto.createECDH(CURVE);
|
|
47
|
+
ecdh.setPrivateKey(priv);
|
|
48
|
+
return Buffer.from(ecdh.computeSecret(pub));
|
|
49
|
+
}
|
|
50
|
+
/** HKDF-SHA256 */
|
|
51
|
+
function hkdf(ikm, salt, info, len) {
|
|
52
|
+
return Buffer.from(crypto.hkdfSync('sha256', ikm, salt, info, len));
|
|
53
|
+
}
|
|
54
|
+
/** Root-key KDF → new rootKey + chainKey */
|
|
55
|
+
function kdfRK(rk, dhOut) {
|
|
56
|
+
const d = hkdf(dhOut, rk, 'stvor-rk', 64);
|
|
57
|
+
return {
|
|
58
|
+
rootKey: Buffer.from(d.subarray(0, 32)),
|
|
59
|
+
chainKey: Buffer.from(d.subarray(32, 64)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Chain-key KDF → new chainKey + messageKey */
|
|
63
|
+
function kdfCK(ck) {
|
|
64
|
+
return {
|
|
65
|
+
chainKey: Buffer.from(crypto.createHmac('sha256', ck).update('\x01').digest()),
|
|
66
|
+
messageKey: Buffer.from(crypto.createHmac('sha256', ck).update('\x02').digest()),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** AES-256-GCM encrypt with AAD */
|
|
70
|
+
function aeadEnc(key, pt, nonce, aad) {
|
|
71
|
+
const c = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
72
|
+
c.setAAD(aad);
|
|
73
|
+
const enc = Buffer.concat([c.update(pt), c.final()]);
|
|
74
|
+
return Buffer.concat([enc, c.getAuthTag()]); // ciphertext ‖ 16-byte tag
|
|
75
|
+
}
|
|
76
|
+
/** AES-256-GCM decrypt with AAD */
|
|
77
|
+
function aeadDec(key, ct, nonce, aad) {
|
|
78
|
+
const d = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
79
|
+
d.setAAD(aad);
|
|
80
|
+
d.setAuthTag(ct.subarray(-16));
|
|
81
|
+
return Buffer.concat([d.update(ct.subarray(0, -16)), d.final()]);
|
|
82
|
+
}
|
|
83
|
+
/* ================================================================
|
|
84
|
+
* ECDSA P-256 — sign / verify
|
|
85
|
+
* ================================================================ */
|
|
86
|
+
function toPrivKeyObj(pub, priv) {
|
|
87
|
+
return crypto.createPrivateKey({
|
|
88
|
+
key: {
|
|
89
|
+
kty: 'EC', crv: 'P-256',
|
|
90
|
+
x: pub.subarray(1, 33).toString('base64url'),
|
|
91
|
+
y: pub.subarray(33, 65).toString('base64url'),
|
|
92
|
+
d: priv.toString('base64url'),
|
|
93
|
+
},
|
|
94
|
+
format: 'jwk',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function toPubKeyObj(pub) {
|
|
98
|
+
return crypto.createPublicKey({
|
|
99
|
+
key: {
|
|
100
|
+
kty: 'EC', crv: 'P-256',
|
|
101
|
+
x: pub.subarray(1, 33).toString('base64url'),
|
|
102
|
+
y: pub.subarray(33, 65).toString('base64url'),
|
|
103
|
+
},
|
|
104
|
+
format: 'jwk',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** ECDSA-P256-SHA256 sign */
|
|
108
|
+
export function ecSign(data, kp) {
|
|
109
|
+
return Buffer.from(crypto.sign('sha256', data, toPrivKeyObj(kp.publicKey, kp.privateKey)));
|
|
110
|
+
}
|
|
111
|
+
/** ECDSA-P256-SHA256 verify */
|
|
112
|
+
export function ecVerify(data, sig, pub) {
|
|
113
|
+
return crypto.verify('sha256', data, toPubKeyObj(pub), sig);
|
|
114
|
+
}
|
|
115
|
+
/* ================================================================
|
|
116
|
+
* X3DH — Symmetric Variant
|
|
117
|
+
*
|
|
118
|
+
* Both sides independently derive the SAME shared secret from
|
|
119
|
+
* their own (IK, SPK) and the peer's (IK, SPK).
|
|
120
|
+
* Canonical ordering by comparing IK public keys.
|
|
121
|
+
* ================================================================ */
|
|
122
|
+
export function x3dhSymmetric(myIK, mySPK, peerIK, peerSPK) {
|
|
123
|
+
const iAmLower = Buffer.compare(myIK.publicKey, peerIK) < 0;
|
|
124
|
+
const d1 = iAmLower
|
|
125
|
+
? ecdhSecret(myIK.privateKey, peerSPK)
|
|
126
|
+
: ecdhSecret(mySPK.privateKey, peerIK);
|
|
127
|
+
const d2 = iAmLower
|
|
128
|
+
? ecdhSecret(mySPK.privateKey, peerIK)
|
|
129
|
+
: ecdhSecret(myIK.privateKey, peerSPK);
|
|
130
|
+
const d3 = ecdhSecret(mySPK.privateKey, peerSPK);
|
|
131
|
+
return hkdf(Buffer.concat([d1, d2, d3]), Buffer.alloc(32), 'X3DH', 32);
|
|
132
|
+
}
|
|
133
|
+
/* ================================================================
|
|
134
|
+
* Session Establishment
|
|
135
|
+
*
|
|
136
|
+
* Creates a "pending" session. The first send triggers an
|
|
137
|
+
* initiator DH ratchet; the first receive triggers a responder
|
|
138
|
+
* DH ratchet. Either side can go first.
|
|
139
|
+
* ================================================================ */
|
|
140
|
+
export function establishSession(myIK, mySPK, peerIK, peerSPK) {
|
|
141
|
+
const sk = x3dhSymmetric(myIK, mySPK, peerIK, peerSPK);
|
|
142
|
+
const ratchetKP = generateKeyPair();
|
|
143
|
+
return {
|
|
144
|
+
myIdentityPublicKey: Buffer.from(myIK.publicKey),
|
|
145
|
+
peerIdentityPublicKey: Buffer.from(peerIK),
|
|
146
|
+
rootKey: sk, // raw shared secret
|
|
147
|
+
sendingChainKey: Buffer.alloc(32), // set by first DH ratchet
|
|
148
|
+
receivingChainKey: Buffer.alloc(32),
|
|
149
|
+
myRatchetKeyPair: ratchetKP,
|
|
150
|
+
theirRatchetPublicKey: null,
|
|
151
|
+
sendCount: 0,
|
|
152
|
+
recvCount: 0,
|
|
153
|
+
prevSendCount: 0,
|
|
154
|
+
skippedKeys: new Map(),
|
|
155
|
+
isPostCompromise: false,
|
|
156
|
+
/* deferred init data */
|
|
157
|
+
peerSPK: Buffer.from(peerSPK),
|
|
158
|
+
mySPKPair: {
|
|
159
|
+
publicKey: Buffer.from(mySPK.publicKey),
|
|
160
|
+
privateKey: Buffer.from(mySPK.privateKey),
|
|
161
|
+
},
|
|
162
|
+
/* legacy compat */
|
|
163
|
+
identityKey: myIK.publicKey,
|
|
164
|
+
signedPreKey: mySPK.publicKey,
|
|
165
|
+
oneTimePreKey: new Uint8Array(0),
|
|
166
|
+
sendingChainMessageNumber: 0,
|
|
167
|
+
receivingChainMessageNumber: 0,
|
|
168
|
+
previousSendingChainLength: 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/* ================================================================
|
|
172
|
+
* Double Ratchet — Encrypt
|
|
173
|
+
*
|
|
174
|
+
* Header layout (85 bytes):
|
|
175
|
+
* [0..64] ratchet public key (65 B)
|
|
176
|
+
* [65..68] prev sending chain length (u32 BE)
|
|
177
|
+
* [69..72] message number (u32 BE)
|
|
178
|
+
* [73..84] AES-GCM nonce (12 B)
|
|
179
|
+
* ================================================================ */
|
|
180
|
+
export function encryptMessage(session, plaintext) {
|
|
181
|
+
/* ---- Deferred init: initiator DH ratchet on first send ---- */
|
|
182
|
+
if (!session.theirRatchetPublicKey && session.peerSPK) {
|
|
183
|
+
const dhOut = ecdhSecret(session.myRatchetKeyPair.privateKey, session.peerSPK);
|
|
184
|
+
const r = kdfRK(session.rootKey, dhOut);
|
|
185
|
+
session.rootKey = r.rootKey;
|
|
186
|
+
session.sendingChainKey = r.chainKey;
|
|
187
|
+
session.theirRatchetPublicKey = Buffer.from(session.peerSPK);
|
|
188
|
+
session.peerSPK = null; // consumed
|
|
189
|
+
session.mySPKPair = null;
|
|
190
|
+
}
|
|
191
|
+
/* ---- Symmetric ratchet ---- */
|
|
192
|
+
const { chainKey, messageKey } = kdfCK(session.sendingChainKey);
|
|
193
|
+
session.sendingChainKey = chainKey;
|
|
194
|
+
const nonce = crypto.randomBytes(12);
|
|
195
|
+
const header = Buffer.alloc(HEADER_LEN);
|
|
196
|
+
session.myRatchetKeyPair.publicKey.copy(header, 0);
|
|
197
|
+
header.writeUInt32BE(session.prevSendCount, PUB_LEN);
|
|
198
|
+
header.writeUInt32BE(session.sendCount, PUB_LEN + 4);
|
|
199
|
+
nonce.copy(header, PUB_LEN + 8);
|
|
200
|
+
const ct = aeadEnc(messageKey, plaintext, nonce, header);
|
|
201
|
+
session.sendCount++;
|
|
202
|
+
session.sendingChainMessageNumber = session.sendCount;
|
|
203
|
+
return { ciphertext: ct, header };
|
|
204
|
+
}
|
|
205
|
+
/* ================================================================
|
|
206
|
+
* Double Ratchet — Decrypt
|
|
207
|
+
* ================================================================ */
|
|
208
|
+
function skipKeys(s, until) {
|
|
209
|
+
if (until - s.recvCount > MAX_SKIP)
|
|
210
|
+
throw new Error('Too many skipped messages');
|
|
211
|
+
while (s.recvCount < until) {
|
|
212
|
+
const { chainKey, messageKey } = kdfCK(s.receivingChainKey);
|
|
213
|
+
s.receivingChainKey = chainKey;
|
|
214
|
+
s.skippedKeys.set(`${s.theirRatchetPublicKey.toString('hex')}:${s.recvCount}`, messageKey);
|
|
215
|
+
s.recvCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function dhRatchetStep(s, theirNewKey) {
|
|
219
|
+
s.prevSendCount = s.sendCount;
|
|
220
|
+
s.sendCount = 0;
|
|
221
|
+
s.recvCount = 0;
|
|
222
|
+
s.theirRatchetPublicKey = Buffer.from(theirNewKey);
|
|
223
|
+
// Derive receiving chain
|
|
224
|
+
const dh1 = ecdhSecret(s.myRatchetKeyPair.privateKey, theirNewKey);
|
|
225
|
+
const r1 = kdfRK(s.rootKey, dh1);
|
|
226
|
+
s.rootKey = r1.rootKey;
|
|
227
|
+
s.receivingChainKey = r1.chainKey;
|
|
228
|
+
// New ratchet key pair → derive sending chain
|
|
229
|
+
s.myRatchetKeyPair = generateKeyPair();
|
|
230
|
+
const dh2 = ecdhSecret(s.myRatchetKeyPair.privateKey, theirNewKey);
|
|
231
|
+
const r2 = kdfRK(s.rootKey, dh2);
|
|
232
|
+
s.rootKey = r2.rootKey;
|
|
233
|
+
s.sendingChainKey = r2.chainKey;
|
|
234
|
+
}
|
|
235
|
+
export function decryptMessage(session, ciphertext, header) {
|
|
236
|
+
const theirPub = header.subarray(0, PUB_LEN);
|
|
237
|
+
const prevChain = header.readUInt32BE(PUB_LEN);
|
|
238
|
+
const msgNum = header.readUInt32BE(PUB_LEN + 4);
|
|
239
|
+
const nonce = header.subarray(PUB_LEN + 8, HEADER_LEN);
|
|
240
|
+
/* ---- Deferred init: responder role on first receive ---- */
|
|
241
|
+
if (!session.theirRatchetPublicKey && session.mySPKPair) {
|
|
242
|
+
// Use our SPK as ratchet key for the first DH step
|
|
243
|
+
// so DH(mySPK, theirRatchetKey) matches initiator's DH(theirRatchetKey, mySPK)
|
|
244
|
+
session.myRatchetKeyPair = session.mySPKPair;
|
|
245
|
+
session.mySPKPair = null; // consumed
|
|
246
|
+
session.peerSPK = null;
|
|
247
|
+
}
|
|
248
|
+
// 1. Try skipped key
|
|
249
|
+
const skId = `${theirPub.toString('hex')}:${msgNum}`;
|
|
250
|
+
const skMK = session.skippedKeys.get(skId);
|
|
251
|
+
if (skMK) {
|
|
252
|
+
session.skippedKeys.delete(skId);
|
|
253
|
+
return aeadDec(skMK, ciphertext, nonce, header);
|
|
254
|
+
}
|
|
255
|
+
// 2. DH ratchet if new ratchet key
|
|
256
|
+
const needsRatchet = !session.theirRatchetPublicKey ||
|
|
257
|
+
Buffer.compare(session.theirRatchetPublicKey, theirPub) !== 0;
|
|
258
|
+
if (needsRatchet) {
|
|
259
|
+
if (session.theirRatchetPublicKey) {
|
|
260
|
+
skipKeys(session, prevChain);
|
|
261
|
+
}
|
|
262
|
+
dhRatchetStep(session, theirPub);
|
|
263
|
+
}
|
|
264
|
+
// 3. Skip to this message number
|
|
265
|
+
skipKeys(session, msgNum);
|
|
266
|
+
// 4. Derive message key
|
|
267
|
+
const { chainKey, messageKey } = kdfCK(session.receivingChainKey);
|
|
268
|
+
session.receivingChainKey = chainKey;
|
|
269
|
+
session.recvCount++;
|
|
270
|
+
session.receivingChainMessageNumber = session.recvCount;
|
|
271
|
+
return aeadDec(messageKey, ciphertext, nonce, header);
|
|
272
|
+
}
|
|
273
|
+
/* ================================================================
|
|
274
|
+
* Force Ratchet (post-compromise security)
|
|
275
|
+
* ================================================================ */
|
|
276
|
+
export function forceRatchet(session) {
|
|
277
|
+
session.myRatchetKeyPair = generateKeyPair();
|
|
278
|
+
session.sendCount = 0;
|
|
279
|
+
session.recvCount = 0;
|
|
280
|
+
session.prevSendCount = 0;
|
|
281
|
+
session.isPostCompromise = true;
|
|
282
|
+
}
|
|
283
|
+
/* ================================================================
|
|
284
|
+
* Serialisation / Deserialisation
|
|
285
|
+
* ================================================================ */
|
|
286
|
+
export function serializeSession(s) {
|
|
287
|
+
const sk = {};
|
|
288
|
+
for (const [k, v] of s.skippedKeys)
|
|
289
|
+
sk[k] = Array.from(v);
|
|
290
|
+
return Buffer.from(JSON.stringify({
|
|
291
|
+
myIK: Array.from(s.myIdentityPublicKey),
|
|
292
|
+
peerIK: Array.from(s.peerIdentityPublicKey),
|
|
293
|
+
rk: Array.from(s.rootKey),
|
|
294
|
+
sck: Array.from(s.sendingChainKey),
|
|
295
|
+
rck: Array.from(s.receivingChainKey),
|
|
296
|
+
mrk: {
|
|
297
|
+
pub: Array.from(s.myRatchetKeyPair.publicKey),
|
|
298
|
+
priv: Array.from(s.myRatchetKeyPair.privateKey),
|
|
299
|
+
},
|
|
300
|
+
trpk: s.theirRatchetPublicKey ? Array.from(s.theirRatchetPublicKey) : null,
|
|
301
|
+
sc: s.sendCount, rc: s.recvCount, psc: s.prevSendCount,
|
|
302
|
+
sk, ipc: s.isPostCompromise ? 1 : 0,
|
|
303
|
+
pspk: s.peerSPK ? Array.from(s.peerSPK) : null,
|
|
304
|
+
mspk: s.mySPKPair
|
|
305
|
+
? { pub: Array.from(s.mySPKPair.publicKey), priv: Array.from(s.mySPKPair.privateKey) }
|
|
306
|
+
: null,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
export function deserializeSession(data) {
|
|
310
|
+
const o = JSON.parse(data.toString());
|
|
311
|
+
const skipped = new Map();
|
|
312
|
+
if (o.sk) {
|
|
313
|
+
for (const [k, v] of Object.entries(o.sk)) {
|
|
314
|
+
skipped.set(k, Buffer.from(v));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const myIK = Buffer.from(o.myIK);
|
|
318
|
+
return {
|
|
319
|
+
myIdentityPublicKey: myIK,
|
|
320
|
+
peerIdentityPublicKey: Buffer.from(o.peerIK),
|
|
321
|
+
rootKey: Buffer.from(o.rk),
|
|
322
|
+
sendingChainKey: Buffer.from(o.sck),
|
|
323
|
+
receivingChainKey: Buffer.from(o.rck),
|
|
324
|
+
myRatchetKeyPair: {
|
|
325
|
+
publicKey: Buffer.from(o.mrk.pub),
|
|
326
|
+
privateKey: Buffer.from(o.mrk.priv),
|
|
327
|
+
},
|
|
328
|
+
theirRatchetPublicKey: o.trpk ? Buffer.from(o.trpk) : null,
|
|
329
|
+
sendCount: o.sc, recvCount: o.rc, prevSendCount: o.psc,
|
|
330
|
+
skippedKeys: skipped,
|
|
331
|
+
isPostCompromise: o.ipc === 1,
|
|
332
|
+
peerSPK: o.pspk ? Buffer.from(o.pspk) : null,
|
|
333
|
+
mySPKPair: o.mspk
|
|
334
|
+
? { publicKey: Buffer.from(o.mspk.pub), privateKey: Buffer.from(o.mspk.priv) }
|
|
335
|
+
: null,
|
|
336
|
+
identityKey: myIK,
|
|
337
|
+
signedPreKey: myIK,
|
|
338
|
+
oneTimePreKey: new Uint8Array(0),
|
|
339
|
+
sendingChainMessageNumber: o.sc,
|
|
340
|
+
receivingChainMessageNumber: o.rc,
|
|
341
|
+
previousSendingChainLength: o.psc,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for ratchet/key-recovery.js
|
|
4
|
+
// This allows `require('@stvor/sdk')` to work alongside ESM `import`.
|
|
5
|
+
|
|
6
|
+
const mod = require('module');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
|
|
9
|
+
// Use dynamic import to load the ESM module
|
|
10
|
+
let _cached;
|
|
11
|
+
async function _load() {
|
|
12
|
+
if (!_cached) {
|
|
13
|
+
_cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
|
|
14
|
+
}
|
|
15
|
+
return _cached;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// For simple CJS usage, expose a promise-based loader
|
|
19
|
+
module.exports = new Proxy({ load: _load }, {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
if (prop === '__esModule') return true;
|
|
22
|
+
if (prop === 'then') return undefined; // prevent treating as thenable
|
|
23
|
+
if (prop === 'load') return _load;
|
|
24
|
+
if (prop === 'default') {
|
|
25
|
+
return _load().then(m => m.default);
|
|
26
|
+
}
|
|
27
|
+
return _load().then(m => m[prop]);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate recovery key shares using Shamir's Secret Sharing.
|
|
3
|
+
* @param secret - The secret to split (e.g., a recovery key).
|
|
4
|
+
* @returns An array of two shares.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateRecoveryShares(secret: Uint8Array): Uint8Array[];
|
|
7
|
+
/**
|
|
8
|
+
* Combine recovery key shares to reconstruct the secret.
|
|
9
|
+
* @param shares - An array of shares.
|
|
10
|
+
* @returns The reconstructed secret.
|
|
11
|
+
*/
|
|
12
|
+
export declare function combineRecoveryShares(shares: Uint8Array[]): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Store recovery shares securely.
|
|
15
|
+
* @param userId - The user ID.
|
|
16
|
+
* @param shares - The recovery shares.
|
|
17
|
+
*/
|
|
18
|
+
export declare function storeRecoveryShares(userId: string, shares: Uint8Array[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Retrieve recovery shares for a user.
|
|
21
|
+
* @param userId - The user ID.
|
|
22
|
+
* @returns The recovery shares.
|
|
23
|
+
*/
|
|
24
|
+
export declare function retrieveRecoveryShares(userId: string): Uint8Array[];
|
|
25
|
+
/**
|
|
26
|
+
* Revoke recovery shares for a user.
|
|
27
|
+
* @param userId - The user ID.
|
|
28
|
+
*/
|
|
29
|
+
export declare function revokeRecoveryShares(userId: string): void;
|
|
30
|
+
export declare function verifyShareIntegrity(share: Uint8Array, expectedHash: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Formal Policy for Recovery Shares
|
|
33
|
+
*/
|
|
34
|
+
declare const recoveryPolicy: {
|
|
35
|
+
minAdmins: number;
|
|
36
|
+
tamperEvidence: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function getRecoveryPolicy(): typeof recoveryPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Example Admin Authentication Model
|
|
41
|
+
*/
|
|
42
|
+
export declare function authenticateAdmin(adminToken: string): boolean;
|
|
43
|
+
export declare function approveRecovery(userId: string, adminId: string): void;
|
|
44
|
+
export declare function revokeRecovery(userId: string, adminId: string): void;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { split, combine } from 'shamirs-secret-sharing';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { createSign, createVerify } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Generate recovery key shares using Shamir's Secret Sharing.
|
|
8
|
+
* @param secret - The secret to split (e.g., a recovery key).
|
|
9
|
+
* @returns An array of two shares.
|
|
10
|
+
*/
|
|
11
|
+
export function generateRecoveryShares(secret) {
|
|
12
|
+
return split(secret, { shares: 2, threshold: 2 });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Combine recovery key shares to reconstruct the secret.
|
|
16
|
+
* @param shares - An array of shares.
|
|
17
|
+
* @returns The reconstructed secret.
|
|
18
|
+
*/
|
|
19
|
+
export function combineRecoveryShares(shares) {
|
|
20
|
+
return combine(shares);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lifecycle Management for Recovery Shares
|
|
24
|
+
*/
|
|
25
|
+
const recoveryShares = new Map(); // Simulated storage
|
|
26
|
+
/**
|
|
27
|
+
* Store recovery shares securely.
|
|
28
|
+
* @param userId - The user ID.
|
|
29
|
+
* @param shares - The recovery shares.
|
|
30
|
+
*/
|
|
31
|
+
export function storeRecoveryShares(userId, shares) {
|
|
32
|
+
recoveryShares.set(userId, shares);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Retrieve recovery shares for a user.
|
|
36
|
+
* @param userId - The user ID.
|
|
37
|
+
* @returns The recovery shares.
|
|
38
|
+
*/
|
|
39
|
+
export function retrieveRecoveryShares(userId) {
|
|
40
|
+
const shares = recoveryShares.get(userId);
|
|
41
|
+
if (!shares) {
|
|
42
|
+
throw new Error('No recovery shares found for user');
|
|
43
|
+
}
|
|
44
|
+
return shares;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Revoke recovery shares for a user.
|
|
48
|
+
* @param userId - The user ID.
|
|
49
|
+
*/
|
|
50
|
+
export function revokeRecoveryShares(userId) {
|
|
51
|
+
recoveryShares.delete(userId);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Tamper-Evidence for Recovery Shares
|
|
55
|
+
*/
|
|
56
|
+
function generateShareHash(share) {
|
|
57
|
+
return createHash('sha256').update(share).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
export function verifyShareIntegrity(share, expectedHash) {
|
|
60
|
+
return generateShareHash(share) === expectedHash;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Honest 2-Man Rule Limitations
|
|
64
|
+
*
|
|
65
|
+
* 1. This is NOT enterprise-grade recovery.
|
|
66
|
+
* 2. No HSM or hardware-backed tamper resistance.
|
|
67
|
+
* 3. Software-based signatures are vulnerable to compromise.
|
|
68
|
+
*/
|
|
69
|
+
// Append-only log with software-based signing
|
|
70
|
+
const PRIVATE_KEY = process.env.RECOVERY_SIGNING_KEY || '';
|
|
71
|
+
const PUBLIC_KEY = process.env.RECOVERY_VERIFICATION_KEY || '';
|
|
72
|
+
function signRecoveryAction(action) {
|
|
73
|
+
const sign = createSign('SHA256');
|
|
74
|
+
sign.update(action);
|
|
75
|
+
sign.end();
|
|
76
|
+
return sign.sign(PRIVATE_KEY, 'hex');
|
|
77
|
+
}
|
|
78
|
+
function verifyRecoveryAction(action, signature) {
|
|
79
|
+
const verify = createVerify('SHA256');
|
|
80
|
+
verify.update(action);
|
|
81
|
+
verify.end();
|
|
82
|
+
return verify.verify(PUBLIC_KEY, signature, 'hex');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Formal Policy for Recovery Shares
|
|
86
|
+
*/
|
|
87
|
+
const recoveryPolicy = {
|
|
88
|
+
minAdmins: 2,
|
|
89
|
+
tamperEvidence: true,
|
|
90
|
+
};
|
|
91
|
+
export function getRecoveryPolicy() {
|
|
92
|
+
return recoveryPolicy;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Example Admin Authentication Model
|
|
96
|
+
*/
|
|
97
|
+
export function authenticateAdmin(adminToken) {
|
|
98
|
+
const validToken = process.env.ADMIN_TOKEN; // Ensure ADMIN_TOKEN is set in the environment
|
|
99
|
+
return adminToken === validToken;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Enhanced 2-Man Rule with audit trail and approval flow
|
|
103
|
+
*/
|
|
104
|
+
const AUDIT_LOG_PATH = './audit-log.json';
|
|
105
|
+
function logRecoveryAction(action, userId, adminId) {
|
|
106
|
+
const logEntry = {
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
action,
|
|
109
|
+
userId,
|
|
110
|
+
adminId,
|
|
111
|
+
};
|
|
112
|
+
const auditLog = existsSync(AUDIT_LOG_PATH)
|
|
113
|
+
? JSON.parse(readFileSync(AUDIT_LOG_PATH, 'utf-8'))
|
|
114
|
+
: [];
|
|
115
|
+
auditLog.push(logEntry);
|
|
116
|
+
writeFileSync(AUDIT_LOG_PATH, JSON.stringify(auditLog, null, 2));
|
|
117
|
+
}
|
|
118
|
+
export function approveRecovery(userId, adminId) {
|
|
119
|
+
logRecoveryAction('APPROVE_RECOVERY', userId, adminId);
|
|
120
|
+
}
|
|
121
|
+
export function revokeRecovery(userId, adminId) {
|
|
122
|
+
logRecoveryAction('REVOKE_RECOVERY', userId, adminId);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recommendations for enterprise-grade 2-Man Rule
|
|
126
|
+
*
|
|
127
|
+
* 1. Use HSM (Hardware Security Modules) for secure key storage.
|
|
128
|
+
* 2. Implement threshold cryptography for distributed key recovery.
|
|
129
|
+
* 3. Ensure tamper-proof audit logs with cryptographic integrity checks.
|
|
130
|
+
* 4. Define a formal compliance process for recovery actions.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* Example usage:
|
|
134
|
+
*/
|
|
135
|
+
(async () => {
|
|
136
|
+
// Generate a random recovery key
|
|
137
|
+
const recoveryKey = randomBytes(32);
|
|
138
|
+
console.log('Original Recovery Key:', recoveryKey.toString('hex'));
|
|
139
|
+
// Split the recovery key into two shares
|
|
140
|
+
const shares = generateRecoveryShares(recoveryKey);
|
|
141
|
+
console.log('Share 1:', shares[0].toString('hex'));
|
|
142
|
+
console.log('Share 2:', shares[1].toString('hex'));
|
|
143
|
+
// Store the shares securely
|
|
144
|
+
storeRecoveryShares('user1', shares);
|
|
145
|
+
// Combine the shares to reconstruct the recovery key
|
|
146
|
+
const reconstructedKey = combineRecoveryShares(shares);
|
|
147
|
+
console.log('Reconstructed Recovery Key:', reconstructedKey.toString('hex'));
|
|
148
|
+
})();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for ratchet/replay-protection.js
|
|
4
|
+
// This allows `require('@stvor/sdk')` to work alongside ESM `import`.
|
|
5
|
+
|
|
6
|
+
const mod = require('module');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
|
|
9
|
+
// Use dynamic import to load the ESM module
|
|
10
|
+
let _cached;
|
|
11
|
+
async function _load() {
|
|
12
|
+
if (!_cached) {
|
|
13
|
+
_cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
|
|
14
|
+
}
|
|
15
|
+
return _cached;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// For simple CJS usage, expose a promise-based loader
|
|
19
|
+
module.exports = new Proxy({ load: _load }, {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
if (prop === '__esModule') return true;
|
|
22
|
+
if (prop === 'then') return undefined; // prevent treating as thenable
|
|
23
|
+
if (prop === 'load') return _load;
|
|
24
|
+
if (prop === 'default') {
|
|
25
|
+
return _load().then(m => m.default);
|
|
26
|
+
}
|
|
27
|
+
return _load().then(m => m[prop]);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a message is a replay.
|
|
3
|
+
* @param userId - The user ID sending the message.
|
|
4
|
+
* @param nonce - The unique nonce for the message.
|
|
5
|
+
* @returns True if the message is a replay, false otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isReplay(userId: string, nonce: string): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Reject messages older than the allowed timestamp.
|
|
10
|
+
* @param timestamp - The message timestamp.
|
|
11
|
+
* @returns True if the message is too old, false otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isTooOld(timestamp: number): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Validate a message for replay protection.
|
|
16
|
+
* @param userId - The user ID sending the message.
|
|
17
|
+
* @param nonce - The unique nonce for the message.
|
|
18
|
+
* @param timestamp - The message timestamp.
|
|
19
|
+
* @throws Error if the message is a replay or too old.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateMessage(userId: string, nonce: string, timestamp: number): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createClient } from 'redis';
|
|
2
|
+
// Redis client setup
|
|
3
|
+
const redis = createClient({
|
|
4
|
+
url: process.env.REDIS_URL, // Ensure REDIS_URL is set in the environment
|
|
5
|
+
});
|
|
6
|
+
redis.connect();
|
|
7
|
+
const REPLAY_CACHE_PREFIX = 'replay:';
|
|
8
|
+
const MESSAGE_EXPIRY_SECONDS = 300; // 5 minutes
|
|
9
|
+
/**
|
|
10
|
+
* Check if a message is a replay.
|
|
11
|
+
* @param userId - The user ID sending the message.
|
|
12
|
+
* @param nonce - The unique nonce for the message.
|
|
13
|
+
* @returns True if the message is a replay, false otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export async function isReplay(userId, nonce) {
|
|
16
|
+
const key = `${REPLAY_CACHE_PREFIX}${userId}:${nonce}`;
|
|
17
|
+
const exists = await redis.exists(key);
|
|
18
|
+
if (exists) {
|
|
19
|
+
return true; // Replay detected
|
|
20
|
+
}
|
|
21
|
+
// Store the nonce with an expiry
|
|
22
|
+
await redis.set(key, '1', {
|
|
23
|
+
EX: MESSAGE_EXPIRY_SECONDS,
|
|
24
|
+
});
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reject messages older than the allowed timestamp.
|
|
29
|
+
* @param timestamp - The message timestamp.
|
|
30
|
+
* @returns True if the message is too old, false otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export function isTooOld(timestamp) {
|
|
33
|
+
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
34
|
+
return now - timestamp > MESSAGE_EXPIRY_SECONDS;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate a message for replay protection.
|
|
38
|
+
* @param userId - The user ID sending the message.
|
|
39
|
+
* @param nonce - The unique nonce for the message.
|
|
40
|
+
* @param timestamp - The message timestamp.
|
|
41
|
+
* @throws Error if the message is a replay or too old.
|
|
42
|
+
*/
|
|
43
|
+
export async function validateMessage(userId, nonce, timestamp) {
|
|
44
|
+
if (isTooOld(timestamp)) {
|
|
45
|
+
throw new Error('Message rejected: too old');
|
|
46
|
+
}
|
|
47
|
+
if (await isReplay(userId, nonce)) {
|
|
48
|
+
throw new Error('Message rejected: replay detected');
|
|
49
|
+
}
|
|
50
|
+
}
|