@stvor/sdk 2.2.1 → 2.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.
@@ -0,0 +1,318 @@
1
+ import sodium from 'libsodium-wrappers';
2
+ import { ensureSodiumReady } from '../facade/sodium-singleton.js';
3
+ // Initialize libsodium (safe to call multiple times)
4
+ export async function initializeCrypto() {
5
+ await ensureSodiumReady();
6
+ }
7
+ /**
8
+ * X3DH Session Establishment
9
+ * @param identityKeyPair - The user's identity key pair
10
+ * @param signedPreKeyPair - The user's signed pre-key pair
11
+ * @param oneTimePreKey - A one-time pre-key
12
+ * @param recipientIdentityKey - The recipient's identity key
13
+ * @param recipientSignedPreKey - The recipient's signed pre-key
14
+ * @param recipientOneTimePreKey - The recipient's one-time pre-key
15
+ * @param recipientSPKSignature - Signature of SPK by recipient's identity key
16
+ * @param protocolVersion - The protocol version
17
+ * @param cipherSuite - The cipher suite
18
+ * @returns SessionState
19
+ */
20
+ export function establishSession(identityKeyPair, signedPreKeyPair, oneTimePreKey, recipientIdentityKey, recipientSignedPreKey, recipientOneTimePreKey, recipientSPKSignature, protocolVersion, cipherSuite) {
21
+ // Validate protocol version and cipher suite
22
+ validateProtocolVersion(protocolVersion);
23
+ validateCipherSuite(cipherSuite);
24
+ // Verify SPK signature
25
+ verifySPKSignature(recipientSignedPreKey, recipientSPKSignature, recipientIdentityKey);
26
+ // Derive shared secret with cryptographic binding
27
+ const sharedSecret = deriveSharedSecret(identityKeyPair.publicKey, recipientSignedPreKey, recipientOneTimePreKey, protocolVersion, cipherSuite);
28
+ if (!sharedSecret) {
29
+ throw new Error('Failed to derive shared secret');
30
+ }
31
+ // Derive root key
32
+ const rootKey = deriveKey(sharedSecret, 'x3dh-root-key', sodium.from_string(protocolVersion));
33
+ return {
34
+ identityKey: identityKeyPair.publicKey,
35
+ signedPreKey: signedPreKeyPair.publicKey,
36
+ oneTimePreKey,
37
+ rootKey,
38
+ sendingChainKey: rootKey,
39
+ receivingChainKey: rootKey,
40
+ skippedMessageKeys: new Map(),
41
+ isPostCompromise: false,
42
+ };
43
+ }
44
+ /**
45
+ * Double Ratchet Encryption
46
+ * @param plaintext - The message to encrypt
47
+ * @param session - The current session state
48
+ * @returns { ciphertext: Uint8Array; header: { publicKey: Uint8Array; nonce: Uint8Array } }
49
+ */
50
+ export function encryptMessage(plaintext, session) {
51
+ // Generate a new ratchet key pair
52
+ const ratchetKeyPair = sodium.crypto_kx_keypair();
53
+ // Perform a Diffie-Hellman exchange with the recipient's public key
54
+ const sharedSecret = sodium.crypto_kx_client_session_keys(ratchetKeyPair.publicKey, ratchetKeyPair.privateKey, session.identityKey);
55
+ // Update root key and derive new sending chain key
56
+ const newRootKey = sodium.crypto_generichash(32, new Uint8Array([...session.rootKey, ...sharedSecret.sharedTx]));
57
+ const newSendingChainKey = sodium.crypto_generichash(32, newRootKey);
58
+ // Derive a message key
59
+ const messageKey = sodium.crypto_generichash(32, newSendingChainKey);
60
+ // Encrypt the message
61
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
62
+ const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(sodium.from_string(plaintext), null, // No additional data
63
+ null, nonce, messageKey);
64
+ // Update session state
65
+ session.rootKey = newRootKey;
66
+ session.sendingChainKey = newSendingChainKey;
67
+ return {
68
+ ciphertext,
69
+ header: {
70
+ publicKey: ratchetKeyPair.publicKey,
71
+ nonce,
72
+ },
73
+ };
74
+ }
75
+ /**
76
+ * Double Ratchet Decryption
77
+ * @param ciphertext - The encrypted message
78
+ * @param header - The message header containing the sender's public key and nonce
79
+ * @param session - The current session state
80
+ * @returns The decrypted plaintext
81
+ */
82
+ export function decryptMessage(ciphertext, header, session) {
83
+ // Check for skipped message keys
84
+ const skippedKey = session.skippedMessageKeys.get(header.nonce.toString());
85
+ if (skippedKey) {
86
+ session.skippedMessageKeys.delete(header.nonce.toString());
87
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, null, // No additional data
88
+ header.nonce, skippedKey);
89
+ return sodium.to_string(plaintext);
90
+ }
91
+ // Perform a Diffie-Hellman exchange with the sender's public key
92
+ const sharedSecret = sodium.crypto_kx_client_session_keys(session.identityKey, session.signedPreKey, header.publicKey);
93
+ // Update root key and derive new receiving chain key
94
+ const newRootKey = sodium.crypto_generichash(32, new Uint8Array([...session.rootKey, ...sharedSecret.sharedTx]));
95
+ const newReceivingChainKey = sodium.crypto_generichash(32, newRootKey);
96
+ // Derive the message key
97
+ const messageKey = sodium.crypto_generichash(32, newReceivingChainKey);
98
+ // Decrypt the message
99
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, null, // No additional data
100
+ header.nonce, messageKey);
101
+ // Update session state
102
+ session.rootKey = newRootKey;
103
+ session.receivingChainKey = newReceivingChainKey;
104
+ return sodium.to_string(plaintext);
105
+ }
106
+ // Enhanced KDF with explicit domain separation and transcript binding
107
+ function deriveKey(inputKey, context, transcript) {
108
+ const label = sodium.from_string(context);
109
+ return sodium.crypto_generichash(32, new Uint8Array([...label, ...inputKey, ...transcript]));
110
+ }
111
+ function hkdfExtract(salt, inputKeyMaterial) {
112
+ return sodium.crypto_generichash(32, new Uint8Array([...salt, ...inputKeyMaterial]));
113
+ }
114
+ function hkdfExpand(prk, info, length) {
115
+ const infoBytes = sodium.from_string(info);
116
+ return sodium.crypto_generichash(length, new Uint8Array([...prk, ...infoBytes]));
117
+ }
118
+ function deriveRootKey(oldRootKey, dhOutput, transcript) {
119
+ const salt = deriveKey(oldRootKey, 'DR:dh', transcript);
120
+ const prk = hkdfExtract(salt, dhOutput);
121
+ return hkdfExpand(prk, 'DR:root', 32);
122
+ }
123
+ function deriveChainKey(rootKey, transcript) {
124
+ const prk = hkdfExtract(rootKey, sodium.from_string('DR:chain'));
125
+ return hkdfExpand(prk, 'DR:chain', 32);
126
+ }
127
+ function deriveMessageKey(chainKey, transcript) {
128
+ const prk = hkdfExtract(chainKey, sodium.from_string('DR:message'));
129
+ return hkdfExpand(prk, 'DR:message', 32);
130
+ }
131
+ // Updated skipped keys handling with bounds
132
+ const MAX_SKIPPED_KEYS = 50; // Limit to prevent DoS
133
+ // Enhanced skipped keys handling with state exhaustion protection
134
+ const MAX_TOTAL_SKIPPED_KEYS = 500; // Global limit across sessions
135
+ let totalSkippedKeys = 0;
136
+ export function addSkippedKey(session, header, messageKey) {
137
+ const keyId = `${header.publicKey.toString()}:${header.nonce.toString()}`;
138
+ if (session.skippedMessageKeys.size >= MAX_SKIPPED_KEYS) {
139
+ throw new Error('Skipped keys limit exceeded for session');
140
+ }
141
+ if (totalSkippedKeys >= MAX_TOTAL_SKIPPED_KEYS) {
142
+ throw new Error('Global skipped keys limit exceeded');
143
+ }
144
+ session.skippedMessageKeys.set(keyId, messageKey);
145
+ totalSkippedKeys++;
146
+ }
147
+ export function removeSkippedKey(session, header) {
148
+ const keyId = `${header.publicKey.toString()}:${header.nonce.toString()}`;
149
+ if (session.skippedMessageKeys.delete(keyId)) {
150
+ totalSkippedKeys--;
151
+ }
152
+ }
153
+ // Improved skipped keys eviction policy
154
+ function cleanUpSkippedKeys(session) {
155
+ const currentTime = Date.now();
156
+ session.skippedMessageKeys.forEach((_, keyId) => {
157
+ const [timestamp] = keyId.split(':');
158
+ if (currentTime - parseInt(timestamp, 10) > 300000) { // Evict keys older than 5 minutes
159
+ session.skippedMessageKeys.delete(keyId);
160
+ totalSkippedKeys--;
161
+ }
162
+ });
163
+ }
164
+ export function processSkippedKeys(session) {
165
+ cleanUpSkippedKeys(session);
166
+ }
167
+ // Updated simultaneous send handling
168
+ export function handleSimultaneousSend(session, isInitiator) {
169
+ if (isInitiator) {
170
+ // Initiator ratchets forward
171
+ session.sendingChainKey = deriveChainKey(session.sendingChainKey, sodium.from_string('initiator'));
172
+ }
173
+ else {
174
+ // Responder ratchets forward
175
+ session.receivingChainKey = deriveChainKey(session.receivingChainKey, sodium.from_string('responder'));
176
+ }
177
+ }
178
+ // Updated SPK signature verification with downgrade protection
179
+ function verifySPKSignature(spk, spkSignature, identityKey) {
180
+ const isValid = sodium.crypto_sign_verify_detached(spkSignature, spk, identityKey);
181
+ if (!isValid) {
182
+ throw new Error('Invalid SPK signature');
183
+ }
184
+ }
185
+ // Updated OPK exhaustion handling
186
+ const OPK_POOL_SIZE = 100; // Example pool size
187
+ let opkPool = [];
188
+ export function generateOPKPool() {
189
+ opkPool = Array.from({ length: OPK_POOL_SIZE }, () => sodium.crypto_kx_keypair().publicKey);
190
+ }
191
+ // Improved X3DH race safety and OPK handling
192
+ const OPK_LOCK = new Map(); // Lock for atomic OPK consumption
193
+ export function consumeOPKAtomically(userId) {
194
+ if (OPK_LOCK.get(userId)) {
195
+ throw new Error('OPK consumption in progress');
196
+ }
197
+ OPK_LOCK.set(userId, true);
198
+ try {
199
+ const opk = consumeOPK();
200
+ return opk;
201
+ }
202
+ finally {
203
+ OPK_LOCK.delete(userId);
204
+ }
205
+ }
206
+ // Enhanced X3DH with cryptographic binding and explicit abort semantics
207
+ function deriveSharedSecret(ik, spk, opk, protocolVersion, cipherSuite) {
208
+ const context = sodium.from_string(`${protocolVersion}:${cipherSuite}`);
209
+ return sodium.crypto_generichash(32, new Uint8Array([...ik, ...spk, ...opk, ...context]));
210
+ }
211
+ // Final improvements for X3DH
212
+ function validateProtocolVersion(version) {
213
+ const supportedVersions = ['1.0'];
214
+ if (!supportedVersions.includes(version)) {
215
+ throw new Error(`Unsupported protocol version: ${version}`);
216
+ }
217
+ }
218
+ function validateCipherSuite(cipherSuite) {
219
+ const supportedSuites = ['AES-GCM'];
220
+ if (!supportedSuites.includes(cipherSuite)) {
221
+ throw new Error(`Unsupported cipher suite: ${cipherSuite}`);
222
+ }
223
+ }
224
+ /**
225
+ * Policy for forced DH rotation.
226
+ * Triggers a DH ratchet step based on:
227
+ * - Number of messages sent.
228
+ * - Time elapsed since the last ratchet.
229
+ * - Explicit compromise flag.
230
+ */
231
+ const DH_RATCHET_POLICY = {
232
+ maxMessages: 50, // Trigger after 50 messages
233
+ maxTime: 10 * 60 * 1000, // Trigger after 10 minutes
234
+ };
235
+ let lastRatchetTime = Date.now();
236
+ let messageCounter = 0;
237
+ export function enforceDHRatchetPolicy(session, remotePublicKey, suspectedCompromise = false) {
238
+ const currentTime = Date.now();
239
+ // Check if policy conditions are met
240
+ if (messageCounter >= DH_RATCHET_POLICY.maxMessages ||
241
+ currentTime - lastRatchetTime >= DH_RATCHET_POLICY.maxTime ||
242
+ suspectedCompromise) {
243
+ forceDHRatchet(session, remotePublicKey);
244
+ // Reset counters
245
+ lastRatchetTime = currentTime;
246
+ messageCounter = 0;
247
+ }
248
+ }
249
+ /**
250
+ * Increment message counter and enforce policy.
251
+ */
252
+ export function incrementMessageCounter(session, remotePublicKey) {
253
+ messageCounter++;
254
+ enforceDHRatchetPolicy(session, remotePublicKey);
255
+ }
256
+ /**
257
+ * Force a DH ratchet step to enable PCS.
258
+ * @param session - The current session state.
259
+ * @param remotePublicKey - The remote party's ephemeral public key.
260
+ */
261
+ export function forceDHRatchet(session, remotePublicKey) {
262
+ // Generate a new ephemeral key pair
263
+ const ephemeralKeyPair = sodium.crypto_kx_keypair();
264
+ // Perform a Diffie-Hellman exchange
265
+ const dhOutput = sodium.crypto_kx_client_session_keys(ephemeralKeyPair.publicKey, ephemeralKeyPair.privateKey, remotePublicKey);
266
+ // Update the root key
267
+ const newRootKey = deriveRootKey(session.rootKey, dhOutput.sharedTx, sodium.from_string('dh-ratchet-recovery'));
268
+ // Clear compromised keys
269
+ session.sendingChainKey = newRootKey;
270
+ session.receivingChainKey = newRootKey;
271
+ session.skippedMessageKeys.clear();
272
+ // Update session state
273
+ session.rootKey = newRootKey;
274
+ }
275
+ /**
276
+ * Ensure rootKey updates only occur through DH ratchet.
277
+ * @param session - The current session state.
278
+ * @param dhOutput - The DH output used to update the root key.
279
+ */
280
+ function enforceDHRatchetOnly(session, dhOutput) {
281
+ if (!dhOutput) {
282
+ throw new Error('Root key updates must occur through DH ratchet');
283
+ }
284
+ // Update the root key
285
+ const newRootKey = deriveRootKey(session.rootKey, dhOutput, sodium.from_string('dh-ratchet-only'));
286
+ session.rootKey = newRootKey;
287
+ }
288
+ /**
289
+ * Trigger PCS recovery only after receiving a new DH public key.
290
+ * @param session - The current session state.
291
+ * @param remotePublicKey - The new DH public key from the remote party.
292
+ */
293
+ export function receiveNewDHPublicKey(session, remotePublicKey) {
294
+ // Generate a new ephemeral key pair
295
+ const ephemeralKeyPair = sodium.crypto_kx_keypair();
296
+ // Perform a Diffie-Hellman exchange
297
+ const dhOutput = sodium.crypto_kx_client_session_keys(ephemeralKeyPair.publicKey, ephemeralKeyPair.privateKey, remotePublicKey);
298
+ // Update the root key
299
+ const newRootKey = deriveRootKey(session.rootKey, dhOutput, sodium.from_string('dh-ratchet-recovery'));
300
+ // Clear compromised keys
301
+ session.sendingChainKey = newRootKey;
302
+ session.receivingChainKey = newRootKey;
303
+ session.skippedMessageKeys.clear();
304
+ // Update session state
305
+ session.rootKey = newRootKey;
306
+ }
307
+ /**
308
+ * Define and enforce state transitions between epochs.
309
+ * @param session - The current session state.
310
+ */
311
+ function transitionToPostCompromiseEpoch(session) {
312
+ // Clear all pre-compromise state
313
+ session.sendingChainKey = null;
314
+ session.receivingChainKey = null;
315
+ session.skippedMessageKeys.clear();
316
+ // Mark the session as post-compromise
317
+ session.isPostCompromise = true;
318
+ }
@@ -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,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
+ }
@@ -0,0 +1 @@
1
+ export {};