@stvor/sdk 2.4.0 → 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.cjs +29 -0
- 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 +49 -54
- package/dist/facade/crypto-session.js +117 -140
- package/dist/facade/errors.cjs +29 -0
- package/dist/facade/errors.d.ts +29 -12
- package/dist/facade/errors.js +49 -8
- package/dist/facade/index.cjs +29 -0
- 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.cjs +29 -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 +28 -35
- package/dist/facade/replay-manager.js +102 -69
- package/dist/facade/sodium-singleton.cjs +29 -0
- package/dist/facade/tofu-manager.cjs +29 -0
- package/dist/facade/tofu-manager.d.ts +38 -36
- package/dist/facade/tofu-manager.js +109 -77
- package/dist/facade/types.cjs +29 -0
- package/dist/facade/types.d.ts +2 -0
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/legacy.cjs +29 -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/ratchet/index.cjs +29 -0
- package/dist/ratchet/index.d.ts +49 -78
- package/dist/ratchet/index.js +313 -288
- package/dist/ratchet/key-recovery.cjs +29 -0
- package/dist/ratchet/replay-protection.cjs +29 -0
- package/dist/ratchet/tofu.cjs +29 -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/dist/src/mock-relay-server.d.ts +30 -0
- package/dist/src/mock-relay-server.js +236 -0
- package/package.json +37 -11
- package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
- package/dist/ratchet/tests/ratchet.test.js +0 -160
- /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
- /package/dist/{facade → src/facade}/crypto.js +0 -0
|
@@ -1,66 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR Replay Protection Manager
|
|
3
|
-
*
|
|
3
|
+
* Implements persistent replay protection with fallback to in-memory
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
* Features:
|
|
8
|
+
* - Persistent storage interface (Redis, PostgreSQL, etc.)
|
|
9
|
+
* - In-memory fallback for development
|
|
10
|
+
* - Proper cleanup of expired entries
|
|
11
|
+
* - Cache statistics
|
|
12
|
+
*/
|
|
13
|
+
// In-memory replay cache (fallback)
|
|
14
|
+
class InMemoryReplayCache {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.cache = new Map();
|
|
17
|
+
this.maxSize = 10000;
|
|
18
|
+
}
|
|
19
|
+
async addNonce(userId, nonce, timestamp) {
|
|
20
|
+
const key = `${userId}:${nonce}`;
|
|
21
|
+
this.cache.set(key, { timestamp: Date.now(), userId });
|
|
22
|
+
// Prevent memory exhaustion
|
|
23
|
+
if (this.cache.size > this.maxSize) {
|
|
24
|
+
await this.cleanupOldest(1000);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async hasNonce(userId, nonce) {
|
|
28
|
+
return this.cache.has(`${userId}:${nonce}`);
|
|
29
|
+
}
|
|
30
|
+
async cleanup(userId, maxAge) {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
let cleaned = 0;
|
|
33
|
+
for (const [key, value] of this.cache.entries()) {
|
|
34
|
+
if (value.userId === userId && now - value.timestamp > maxAge) {
|
|
35
|
+
this.cache.delete(key);
|
|
36
|
+
cleaned++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return cleaned;
|
|
40
|
+
}
|
|
41
|
+
async getStats() {
|
|
42
|
+
return { size: this.cache.size };
|
|
43
|
+
}
|
|
44
|
+
async cleanupOldest(count) {
|
|
45
|
+
const entries = Array.from(this.cache.entries());
|
|
46
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
47
|
+
for (let i = 0; i < Math.min(count, entries.length); i++) {
|
|
48
|
+
this.cache.delete(entries[i][0]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Global in-memory cache instance
|
|
53
|
+
let globalReplayCache = null;
|
|
54
|
+
// Configuration
|
|
55
|
+
const NONCE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes - production should be shorter
|
|
56
|
+
const MESSAGE_EXPIRY_MS = 60 * 1000; // Messages older than 1 minute are rejected
|
|
57
|
+
/**
|
|
58
|
+
* Initialize replay protection with optional persistent storage
|
|
30
59
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const NONCE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
36
|
-
const MAX_CACHE_SIZE = 10000; // Prevent memory exhaustion
|
|
60
|
+
export function initializeReplayProtection(customCache) {
|
|
61
|
+
globalReplayCache = customCache || new InMemoryReplayCache();
|
|
62
|
+
console.log('[ReplayProtection] Initialized' + (customCache ? ' with persistent storage' : ' with in-memory fallback'));
|
|
63
|
+
}
|
|
37
64
|
/**
|
|
38
65
|
* Check if message is a replay attack
|
|
39
|
-
* @param userId - Sender's user ID
|
|
40
|
-
* @param nonce - Message nonce (base64 or hex)
|
|
41
|
-
* @param timestamp - Message timestamp (Unix seconds)
|
|
42
|
-
* @returns true if replay detected
|
|
43
66
|
*/
|
|
44
67
|
export async function isReplay(userId, nonce, timestamp) {
|
|
45
|
-
|
|
68
|
+
if (!globalReplayCache) {
|
|
69
|
+
initializeReplayProtection();
|
|
70
|
+
}
|
|
46
71
|
const now = Date.now();
|
|
72
|
+
// CRITICAL: Check if message is too old FIRST (before checking cache)
|
|
73
|
+
// This prevents replay of old messages that have expired
|
|
74
|
+
const messageAge = now - timestamp * 1000;
|
|
75
|
+
if (messageAge > MESSAGE_EXPIRY_MS) {
|
|
76
|
+
throw new Error(`Message rejected: too old (${Math.round(messageAge / 1000)}s)`);
|
|
77
|
+
}
|
|
47
78
|
// Check if nonce already seen
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
79
|
+
const isDuplicate = await globalReplayCache.hasNonce(userId, nonce);
|
|
80
|
+
if (isDuplicate) {
|
|
81
|
+
console.warn(`[ReplayProtection] Replay detected from user ${userId}`);
|
|
51
82
|
return true;
|
|
52
83
|
}
|
|
53
|
-
// Check if message is too old
|
|
54
|
-
const messageAge = now - timestamp * 1000;
|
|
55
|
-
if (messageAge > NONCE_EXPIRY_MS) {
|
|
56
|
-
throw new Error('Message rejected: too old');
|
|
57
|
-
}
|
|
58
84
|
// Store nonce with timestamp
|
|
59
|
-
|
|
60
|
-
// Cleanup old entries if cache is too large
|
|
61
|
-
if (nonceCache.size > MAX_CACHE_SIZE) {
|
|
62
|
-
await cleanupExpiredNonces();
|
|
63
|
-
}
|
|
85
|
+
await globalReplayCache.addNonce(userId, nonce, timestamp);
|
|
64
86
|
return false;
|
|
65
87
|
}
|
|
66
88
|
/**
|
|
@@ -77,41 +99,52 @@ export async function validateMessage(userId, nonce, timestamp) {
|
|
|
77
99
|
* Validate message with Uint8Array nonce
|
|
78
100
|
*/
|
|
79
101
|
export async function validateMessageWithNonce(userId, nonce, timestamp) {
|
|
80
|
-
const nonceHex =
|
|
102
|
+
const nonceHex = Buffer.from(nonce).toString('hex');
|
|
81
103
|
await validateMessage(userId, nonceHex, timestamp);
|
|
82
104
|
}
|
|
83
105
|
/**
|
|
84
106
|
* Cleanup expired nonces from cache
|
|
107
|
+
* Should be called periodically in production
|
|
85
108
|
*/
|
|
86
|
-
async function cleanupExpiredNonces() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const [key, value] of nonceCache.entries()) {
|
|
90
|
-
if (now - value.timestamp > NONCE_EXPIRY_MS) {
|
|
91
|
-
nonceCache.delete(key);
|
|
92
|
-
cleaned++;
|
|
93
|
-
}
|
|
109
|
+
export async function cleanupExpiredNonces() {
|
|
110
|
+
if (!globalReplayCache) {
|
|
111
|
+
return 0;
|
|
94
112
|
}
|
|
113
|
+
const cleaned = await globalReplayCache.cleanup('all', NONCE_EXPIRY_MS);
|
|
95
114
|
if (cleaned > 0) {
|
|
96
115
|
console.log(`[ReplayProtection] Cleaned ${cleaned} expired nonces`);
|
|
97
116
|
}
|
|
117
|
+
return cleaned;
|
|
98
118
|
}
|
|
99
119
|
/**
|
|
100
120
|
* Get cache statistics (for monitoring)
|
|
101
121
|
*/
|
|
102
|
-
export function getCacheStats() {
|
|
122
|
+
export async function getCacheStats() {
|
|
123
|
+
const stats = await globalReplayCache?.getStats() || { size: 0 };
|
|
103
124
|
return {
|
|
104
|
-
size:
|
|
105
|
-
maxSize:
|
|
125
|
+
size: stats.size,
|
|
126
|
+
maxSize: 10000,
|
|
106
127
|
};
|
|
107
128
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
// Auto-cleanup interval (every 1 minute)
|
|
130
|
+
let cleanupInterval = null;
|
|
131
|
+
export function startAutoCleanup() {
|
|
132
|
+
if (cleanupInterval) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
cleanupInterval = setInterval(() => {
|
|
136
|
+
cleanupExpiredNonces().catch(err => {
|
|
137
|
+
console.error('[ReplayProtection] Cleanup error:', err);
|
|
138
|
+
});
|
|
139
|
+
}, 60000);
|
|
140
|
+
console.log('[ReplayProtection] Auto-cleanup started');
|
|
113
141
|
}
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
142
|
+
export function stopAutoCleanup() {
|
|
143
|
+
if (cleanupInterval) {
|
|
144
|
+
clearInterval(cleanupInterval);
|
|
145
|
+
cleanupInterval = null;
|
|
146
|
+
console.log('[ReplayProtection] Auto-cleanup stopped');
|
|
147
|
+
}
|
|
117
148
|
}
|
|
149
|
+
// Default initialization
|
|
150
|
+
initializeReplayProtection();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/sodium-singleton.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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/tofu-manager.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
|
+
});
|
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR TOFU (Trust On First Use) Manager
|
|
3
|
-
*
|
|
3
|
+
* Implements persistent TOFU with fallback to in-memory
|
|
4
|
+
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Persistent storage interface
|
|
9
|
+
* - In-memory fallback for development
|
|
10
|
+
* - Fingerprint verification
|
|
11
|
+
* - Key rotation support
|
|
4
12
|
*
|
|
5
13
|
* SEMANTICS:
|
|
6
14
|
* - Fingerprint = BLAKE2b(identity_public_key)
|
|
7
15
|
* - Binding: identity key ONLY (not bundle, not SPK)
|
|
8
16
|
* - Key rotation: requires manual re-trust via trustNewFingerprint()
|
|
9
17
|
* - Multi-device: NOT supported (each device = new identity)
|
|
10
|
-
* - Reinstall: fingerprint lost (in-memory only)
|
|
11
|
-
*
|
|
12
|
-
* LIMITATIONS:
|
|
13
|
-
* - First-use MITM vulnerability (standard TOFU)
|
|
14
|
-
* - No persistence (keys lost on restart)
|
|
15
|
-
* - No out-of-band verification UX
|
|
16
|
-
*
|
|
17
|
-
* TODO:
|
|
18
|
-
* - Add persistent storage (IndexedDB/localStorage)
|
|
19
|
-
* - Add manual verification UI (compare fingerprints)
|
|
20
|
-
* - Add key rotation notification system
|
|
21
18
|
*/
|
|
22
19
|
interface FingerprintRecord {
|
|
23
20
|
fingerprint: string;
|
|
24
21
|
firstSeen: Date;
|
|
22
|
+
lastSeen: Date;
|
|
25
23
|
version: number;
|
|
24
|
+
trusted: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface ITofuStore {
|
|
27
|
+
saveFingerprint(userId: string, record: FingerprintRecord): Promise<void>;
|
|
28
|
+
loadFingerprint(userId: string): Promise<FingerprintRecord | null>;
|
|
29
|
+
deleteFingerprint(userId: string): Promise<void>;
|
|
30
|
+
listFingerprints(): Promise<string[]>;
|
|
26
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Initialize TOFU manager with optional persistent storage
|
|
34
|
+
*/
|
|
35
|
+
export declare function initializeTofu(customStore?: ITofuStore): void;
|
|
27
36
|
/**
|
|
28
37
|
* Generate BLAKE2b-256 fingerprint from identity public key
|
|
29
38
|
*
|
|
@@ -34,7 +43,7 @@ interface FingerprintRecord {
|
|
|
34
43
|
*/
|
|
35
44
|
export declare function generateFingerprint(identityPublicKey: Uint8Array): string;
|
|
36
45
|
/**
|
|
37
|
-
* Store fingerprint for user
|
|
46
|
+
* Store fingerprint for user
|
|
38
47
|
*/
|
|
39
48
|
export declare function storeFingerprint(userId: string, fingerprint: string): Promise<void>;
|
|
40
49
|
/**
|
|
@@ -44,37 +53,30 @@ export declare function storeFingerprint(userId: string, fingerprint: string): P
|
|
|
44
53
|
* - First use: stores fingerprint, returns true
|
|
45
54
|
* - Match: returns true
|
|
46
55
|
* - Mismatch: throws error (HARD FAILURE)
|
|
47
|
-
*
|
|
48
|
-
* KEY ROTATION:
|
|
49
|
-
* - Automatic rotation NOT supported
|
|
50
|
-
* - Requires manual trustNewFingerprint() call
|
|
51
|
-
* - Otherwise connection fails on mismatch
|
|
52
|
-
*
|
|
53
|
-
* @throws Error on fingerprint mismatch (possible MITM or key rotation)
|
|
54
56
|
*/
|
|
55
57
|
export declare function verifyFingerprint(userId: string, identityPublicKey: Uint8Array): Promise<boolean>;
|
|
56
58
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* USE CASES:
|
|
60
|
-
* - User reinstalled app and lost keys
|
|
61
|
-
* - Legitimate key rotation after compromise
|
|
62
|
-
* - Migration from old device
|
|
63
|
-
*
|
|
64
|
-
* SECURITY: Should be called ONLY after out-of-band verification
|
|
59
|
+
* Check if user fingerprint is trusted
|
|
65
60
|
*/
|
|
66
|
-
export declare function
|
|
61
|
+
export declare function isFingerprintTrusted(userId: string): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Get fingerprint for user
|
|
64
|
+
*/
|
|
65
|
+
export declare function getFingerprint(userId: string): Promise<string | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Revoke trust for a user (after key rotation or suspected compromise)
|
|
68
|
+
*/
|
|
69
|
+
export declare function revokeTrust(userId: string): Promise<void>;
|
|
67
70
|
/**
|
|
68
|
-
*
|
|
71
|
+
* Re-trust a user after verifying their new fingerprint out-of-band
|
|
69
72
|
*/
|
|
70
|
-
export declare function
|
|
73
|
+
export declare function trustNewFingerprint(userId: string, identityPublicKey: Uint8Array): Promise<void>;
|
|
71
74
|
/**
|
|
72
|
-
*
|
|
73
|
-
* Example: "a3f2 d8c1 5e90 7b4a ..."
|
|
75
|
+
* List all trusted fingerprints
|
|
74
76
|
*/
|
|
75
|
-
export declare function
|
|
77
|
+
export declare function listTrustedFingerprints(): Promise<string[]>;
|
|
76
78
|
/**
|
|
77
|
-
*
|
|
79
|
+
* Get detailed fingerprint info for debugging
|
|
78
80
|
*/
|
|
79
|
-
export declare function
|
|
81
|
+
export declare function getFingerprintInfo(userId: string): Promise<FingerprintRecord | null>;
|
|
80
82
|
export {};
|
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR TOFU (Trust On First Use) Manager
|
|
3
|
-
*
|
|
3
|
+
* Implements persistent TOFU with fallback to in-memory
|
|
4
|
+
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Persistent storage interface
|
|
9
|
+
* - In-memory fallback for development
|
|
10
|
+
* - Fingerprint verification
|
|
11
|
+
* - Key rotation support
|
|
4
12
|
*
|
|
5
13
|
* SEMANTICS:
|
|
6
14
|
* - Fingerprint = BLAKE2b(identity_public_key)
|
|
7
15
|
* - Binding: identity key ONLY (not bundle, not SPK)
|
|
8
16
|
* - Key rotation: requires manual re-trust via trustNewFingerprint()
|
|
9
17
|
* - Multi-device: NOT supported (each device = new identity)
|
|
10
|
-
* - Reinstall: fingerprint lost (in-memory only)
|
|
11
|
-
*
|
|
12
|
-
* LIMITATIONS:
|
|
13
|
-
* - First-use MITM vulnerability (standard TOFU)
|
|
14
|
-
* - No persistence (keys lost on restart)
|
|
15
|
-
* - No out-of-band verification UX
|
|
16
|
-
*
|
|
17
|
-
* TODO:
|
|
18
|
-
* - Add persistent storage (IndexedDB/localStorage)
|
|
19
|
-
* - Add manual verification UI (compare fingerprints)
|
|
20
|
-
* - Add key rotation notification system
|
|
21
18
|
*/
|
|
22
|
-
import
|
|
23
|
-
// In-memory
|
|
24
|
-
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
// In-memory TOFU store (fallback)
|
|
21
|
+
class InMemoryTofuStore {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.store = new Map();
|
|
24
|
+
}
|
|
25
|
+
async saveFingerprint(userId, record) {
|
|
26
|
+
this.store.set(userId, record);
|
|
27
|
+
}
|
|
28
|
+
async loadFingerprint(userId) {
|
|
29
|
+
return this.store.get(userId) || null;
|
|
30
|
+
}
|
|
31
|
+
async deleteFingerprint(userId) {
|
|
32
|
+
this.store.delete(userId);
|
|
33
|
+
}
|
|
34
|
+
async listFingerprints() {
|
|
35
|
+
return Array.from(this.store.keys());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Global store instance
|
|
39
|
+
let tofuStore = null;
|
|
25
40
|
const FINGERPRINT_VERSION = 1; // Increment on breaking changes
|
|
41
|
+
/**
|
|
42
|
+
* Initialize TOFU manager with optional persistent storage
|
|
43
|
+
*/
|
|
44
|
+
export function initializeTofu(customStore) {
|
|
45
|
+
tofuStore = customStore || new InMemoryTofuStore();
|
|
46
|
+
console.log('[TOFU] Initialized' + (customStore ? ' with persistent storage' : ' with in-memory fallback'));
|
|
47
|
+
}
|
|
26
48
|
/**
|
|
27
49
|
* Generate BLAKE2b-256 fingerprint from identity public key
|
|
28
50
|
*
|
|
@@ -32,29 +54,25 @@ const FINGERPRINT_VERSION = 1; // Increment on breaking changes
|
|
|
32
54
|
* - Only identity key rotation changes fingerprint
|
|
33
55
|
*/
|
|
34
56
|
export function generateFingerprint(identityPublicKey) {
|
|
35
|
-
const hash =
|
|
36
|
-
return
|
|
57
|
+
const hash = createHash('sha256').update(identityPublicKey).digest();
|
|
58
|
+
return hash.toString('hex');
|
|
37
59
|
}
|
|
38
60
|
/**
|
|
39
|
-
* Store fingerprint for user
|
|
61
|
+
* Store fingerprint for user
|
|
40
62
|
*/
|
|
41
63
|
export async function storeFingerprint(userId, fingerprint) {
|
|
64
|
+
if (!tofuStore) {
|
|
65
|
+
initializeTofu();
|
|
66
|
+
}
|
|
42
67
|
const record = {
|
|
43
68
|
fingerprint,
|
|
44
69
|
firstSeen: new Date(),
|
|
70
|
+
lastSeen: new Date(),
|
|
45
71
|
version: FINGERPRINT_VERSION,
|
|
72
|
+
trusted: true,
|
|
46
73
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// try {
|
|
50
|
-
// await pool.query(
|
|
51
|
-
// 'INSERT INTO fingerprints (user_id, fingerprint, first_seen, version) VALUES ($1, $2, $3, $4) ON CONFLICT (user_id) DO UPDATE SET fingerprint = $2',
|
|
52
|
-
// [userId, fingerprint, record.firstSeen, record.version]
|
|
53
|
-
// );
|
|
54
|
-
// } catch (error) {
|
|
55
|
-
// // Fallback to in-memory storage
|
|
56
|
-
// fingerprintCache.set(userId, record);
|
|
57
|
-
// }
|
|
74
|
+
await tofuStore.saveFingerprint(userId, record);
|
|
75
|
+
console.log(`[TOFU] Stored fingerprint for user: ${userId}`);
|
|
58
76
|
}
|
|
59
77
|
/**
|
|
60
78
|
* Verify fingerprint against stored value
|
|
@@ -63,72 +81,86 @@ export async function storeFingerprint(userId, fingerprint) {
|
|
|
63
81
|
* - First use: stores fingerprint, returns true
|
|
64
82
|
* - Match: returns true
|
|
65
83
|
* - Mismatch: throws error (HARD FAILURE)
|
|
66
|
-
*
|
|
67
|
-
* KEY ROTATION:
|
|
68
|
-
* - Automatic rotation NOT supported
|
|
69
|
-
* - Requires manual trustNewFingerprint() call
|
|
70
|
-
* - Otherwise connection fails on mismatch
|
|
71
|
-
*
|
|
72
|
-
* @throws Error on fingerprint mismatch (possible MITM or key rotation)
|
|
73
84
|
*/
|
|
74
85
|
export async function verifyFingerprint(userId, identityPublicKey) {
|
|
86
|
+
if (!tofuStore) {
|
|
87
|
+
initializeTofu();
|
|
88
|
+
}
|
|
75
89
|
const fingerprint = generateFingerprint(identityPublicKey);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// First use - store fingerprint
|
|
90
|
+
const stored = await tofuStore.loadFingerprint(userId);
|
|
91
|
+
if (!stored) {
|
|
92
|
+
// First use - store and trust
|
|
80
93
|
await storeFingerprint(userId, fingerprint);
|
|
81
|
-
console.log(`[TOFU]
|
|
94
|
+
console.log(`[TOFU] First contact: ${userId} (fingerprint: ${fingerprint.slice(0, 16)}...)`);
|
|
82
95
|
return true;
|
|
83
96
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`POSSIBLE CAUSES:\n` +
|
|
91
|
-
` 1. MITM attack (key substitution)\n` +
|
|
92
|
-
` 2. User reinstalled app (legitimate key rotation)\n` +
|
|
93
|
-
` 3. Multi-device not supported (different keys)\n\n` +
|
|
94
|
-
`ACTION: Verify out-of-band or call trustNewFingerprint()`);
|
|
97
|
+
if (stored.fingerprint !== fingerprint) {
|
|
98
|
+
// Fingerprint mismatch - potential MITM!
|
|
99
|
+
console.error(`[TOFU] FINGERPRINT MISMATCH for ${userId}!`);
|
|
100
|
+
console.error(`[TOFU] Expected: ${stored.fingerprint.slice(0, 16)}...`);
|
|
101
|
+
console.error(`[TOFU] Received: ${fingerprint.slice(0, 16)}...`);
|
|
102
|
+
throw new Error(`FINGERPRINT MISMATCH for user ${userId} - possible MITM attack!`);
|
|
95
103
|
}
|
|
104
|
+
// Update last seen
|
|
105
|
+
stored.lastSeen = new Date();
|
|
106
|
+
await tofuStore.saveFingerprint(userId, stored);
|
|
96
107
|
return true;
|
|
97
108
|
}
|
|
98
109
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* USE CASES:
|
|
102
|
-
* - User reinstalled app and lost keys
|
|
103
|
-
* - Legitimate key rotation after compromise
|
|
104
|
-
* - Migration from old device
|
|
105
|
-
*
|
|
106
|
-
* SECURITY: Should be called ONLY after out-of-band verification
|
|
110
|
+
* Check if user fingerprint is trusted
|
|
107
111
|
*/
|
|
108
|
-
export async function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
export async function isFingerprintTrusted(userId) {
|
|
113
|
+
if (!tofuStore) {
|
|
114
|
+
initializeTofu();
|
|
115
|
+
}
|
|
116
|
+
const stored = await tofuStore.loadFingerprint(userId);
|
|
117
|
+
return stored?.trusted || false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get fingerprint for user
|
|
121
|
+
*/
|
|
122
|
+
export async function getFingerprint(userId) {
|
|
123
|
+
if (!tofuStore) {
|
|
124
|
+
initializeTofu();
|
|
125
|
+
}
|
|
126
|
+
const stored = await tofuStore.loadFingerprint(userId);
|
|
127
|
+
return stored?.fingerprint || null;
|
|
115
128
|
}
|
|
116
129
|
/**
|
|
117
|
-
*
|
|
130
|
+
* Revoke trust for a user (after key rotation or suspected compromise)
|
|
118
131
|
*/
|
|
119
|
-
export function
|
|
120
|
-
|
|
132
|
+
export async function revokeTrust(userId) {
|
|
133
|
+
if (!tofuStore) {
|
|
134
|
+
initializeTofu();
|
|
135
|
+
}
|
|
136
|
+
await tofuStore.deleteFingerprint(userId);
|
|
137
|
+
console.log(`[TOFU] Revoked trust for user: ${userId}`);
|
|
121
138
|
}
|
|
122
139
|
/**
|
|
123
|
-
*
|
|
124
|
-
* Example: "a3f2 d8c1 5e90 7b4a ..."
|
|
140
|
+
* Re-trust a user after verifying their new fingerprint out-of-band
|
|
125
141
|
*/
|
|
126
|
-
export function
|
|
127
|
-
|
|
142
|
+
export async function trustNewFingerprint(userId, identityPublicKey) {
|
|
143
|
+
const fingerprint = generateFingerprint(identityPublicKey);
|
|
144
|
+
await storeFingerprint(userId, fingerprint);
|
|
145
|
+
console.log(`[TOFU] Re-trusted user: ${userId}`);
|
|
128
146
|
}
|
|
129
147
|
/**
|
|
130
|
-
*
|
|
148
|
+
* List all trusted fingerprints
|
|
131
149
|
*/
|
|
132
|
-
export function
|
|
133
|
-
|
|
150
|
+
export async function listTrustedFingerprints() {
|
|
151
|
+
if (!tofuStore) {
|
|
152
|
+
initializeTofu();
|
|
153
|
+
}
|
|
154
|
+
return tofuStore.listFingerprints();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get detailed fingerprint info for debugging
|
|
158
|
+
*/
|
|
159
|
+
export async function getFingerprintInfo(userId) {
|
|
160
|
+
if (!tofuStore) {
|
|
161
|
+
initializeTofu();
|
|
162
|
+
}
|
|
163
|
+
return tofuStore.loadFingerprint(userId);
|
|
134
164
|
}
|
|
165
|
+
// Default initialization
|
|
166
|
+
initializeTofu();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/types.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
|
+
});
|