@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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/replay-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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR Replay Protection Manager
|
|
3
|
+
* Implements persistent replay protection with fallback to in-memory
|
|
4
|
+
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
|
+
*
|
|
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
|
+
export interface IReplayCache {
|
|
14
|
+
addNonce(userId: string, nonce: string, timestamp: number): Promise<void>;
|
|
15
|
+
hasNonce(userId: string, nonce: string): Promise<boolean>;
|
|
16
|
+
cleanup(userId: string, maxAge: number): Promise<number>;
|
|
17
|
+
getStats(): Promise<{
|
|
18
|
+
size: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize replay protection with optional persistent storage
|
|
23
|
+
*/
|
|
24
|
+
export declare function initializeReplayProtection(customCache?: IReplayCache): void;
|
|
25
|
+
/**
|
|
26
|
+
* Check if message is a replay attack
|
|
27
|
+
*/
|
|
28
|
+
export declare function isReplay(userId: string, nonce: string, timestamp: number): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Validate message for replay protection
|
|
31
|
+
* Throws error if replay detected or message too old
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateMessage(userId: string, nonce: string, timestamp: number): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Validate message with Uint8Array nonce
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateMessageWithNonce(userId: string, nonce: Uint8Array, timestamp: number): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Cleanup expired nonces from cache
|
|
40
|
+
* Should be called periodically in production
|
|
41
|
+
*/
|
|
42
|
+
export declare function cleanupExpiredNonces(): Promise<number>;
|
|
43
|
+
/**
|
|
44
|
+
* Get cache statistics (for monitoring)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getCacheStats(): Promise<{
|
|
47
|
+
size: number;
|
|
48
|
+
maxSize: number;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function startAutoCleanup(): void;
|
|
51
|
+
export declare function stopAutoCleanup(): void;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR Replay Protection Manager
|
|
3
|
+
* Implements persistent replay protection with fallback to in-memory
|
|
4
|
+
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
|
+
*
|
|
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
|
|
59
|
+
*/
|
|
60
|
+
export function initializeReplayProtection(customCache) {
|
|
61
|
+
globalReplayCache = customCache || new InMemoryReplayCache();
|
|
62
|
+
console.log('[ReplayProtection] Initialized' + (customCache ? ' with persistent storage' : ' with in-memory fallback'));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if message is a replay attack
|
|
66
|
+
*/
|
|
67
|
+
export async function isReplay(userId, nonce, timestamp) {
|
|
68
|
+
if (!globalReplayCache) {
|
|
69
|
+
initializeReplayProtection();
|
|
70
|
+
}
|
|
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
|
+
}
|
|
78
|
+
// Check if nonce already seen
|
|
79
|
+
const isDuplicate = await globalReplayCache.hasNonce(userId, nonce);
|
|
80
|
+
if (isDuplicate) {
|
|
81
|
+
console.warn(`[ReplayProtection] Replay detected from user ${userId}`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// Store nonce with timestamp
|
|
85
|
+
await globalReplayCache.addNonce(userId, nonce, timestamp);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Validate message for replay protection
|
|
90
|
+
* Throws error if replay detected or message too old
|
|
91
|
+
*/
|
|
92
|
+
export async function validateMessage(userId, nonce, timestamp) {
|
|
93
|
+
const replay = await isReplay(userId, nonce, timestamp);
|
|
94
|
+
if (replay) {
|
|
95
|
+
throw new Error(`Replay attack detected from user ${userId}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate message with Uint8Array nonce
|
|
100
|
+
*/
|
|
101
|
+
export async function validateMessageWithNonce(userId, nonce, timestamp) {
|
|
102
|
+
const nonceHex = Buffer.from(nonce).toString('hex');
|
|
103
|
+
await validateMessage(userId, nonceHex, timestamp);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cleanup expired nonces from cache
|
|
107
|
+
* Should be called periodically in production
|
|
108
|
+
*/
|
|
109
|
+
export async function cleanupExpiredNonces() {
|
|
110
|
+
if (!globalReplayCache) {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const cleaned = await globalReplayCache.cleanup('all', NONCE_EXPIRY_MS);
|
|
114
|
+
if (cleaned > 0) {
|
|
115
|
+
console.log(`[ReplayProtection] Cleaned ${cleaned} expired nonces`);
|
|
116
|
+
}
|
|
117
|
+
return cleaned;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get cache statistics (for monitoring)
|
|
121
|
+
*/
|
|
122
|
+
export async function getCacheStats() {
|
|
123
|
+
const stats = await globalReplayCache?.getStats() || { size: 0 };
|
|
124
|
+
return {
|
|
125
|
+
size: stats.size,
|
|
126
|
+
maxSize: 10000,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
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');
|
|
141
|
+
}
|
|
142
|
+
export function stopAutoCleanup() {
|
|
143
|
+
if (cleanupInterval) {
|
|
144
|
+
clearInterval(cleanupInterval);
|
|
145
|
+
cleanupInterval = null;
|
|
146
|
+
console.log('[ReplayProtection] Auto-cleanup stopped');
|
|
147
|
+
}
|
|
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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR libsodium Singleton
|
|
3
|
+
* Ensures sodium.ready is called only ONCE globally
|
|
4
|
+
* Prevents race conditions during concurrent initialization
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Initialize libsodium ONCE globally
|
|
8
|
+
* Safe to call multiple times - returns same promise
|
|
9
|
+
*
|
|
10
|
+
* @throws Never throws - libsodium.ready is infallible
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureSodiumReady(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Check if libsodium is ready (synchronous)
|
|
15
|
+
*/
|
|
16
|
+
export declare function isSodiumReady(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Reset state (ONLY for testing)
|
|
19
|
+
*/
|
|
20
|
+
export declare function _resetSodiumState(): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR libsodium Singleton
|
|
3
|
+
* Ensures sodium.ready is called only ONCE globally
|
|
4
|
+
* Prevents race conditions during concurrent initialization
|
|
5
|
+
*/
|
|
6
|
+
import sodium from 'libsodium-wrappers';
|
|
7
|
+
let sodiumInitialized = false;
|
|
8
|
+
let sodiumInitPromise = null;
|
|
9
|
+
/**
|
|
10
|
+
* Initialize libsodium ONCE globally
|
|
11
|
+
* Safe to call multiple times - returns same promise
|
|
12
|
+
*
|
|
13
|
+
* @throws Never throws - libsodium.ready is infallible
|
|
14
|
+
*/
|
|
15
|
+
export async function ensureSodiumReady() {
|
|
16
|
+
// Already initialized - return immediately
|
|
17
|
+
if (sodiumInitialized) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Initialization in progress - return existing promise
|
|
21
|
+
if (sodiumInitPromise) {
|
|
22
|
+
return sodiumInitPromise;
|
|
23
|
+
}
|
|
24
|
+
// Start initialization
|
|
25
|
+
sodiumInitPromise = (async () => {
|
|
26
|
+
await sodium.ready;
|
|
27
|
+
sodiumInitialized = true;
|
|
28
|
+
console.log('[Crypto] libsodium initialized');
|
|
29
|
+
})();
|
|
30
|
+
return sodiumInitPromise;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if libsodium is ready (synchronous)
|
|
34
|
+
*/
|
|
35
|
+
export function isSodiumReady() {
|
|
36
|
+
return sodiumInitialized;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reset state (ONLY for testing)
|
|
40
|
+
*/
|
|
41
|
+
export function _resetSodiumState() {
|
|
42
|
+
sodiumInitialized = false;
|
|
43
|
+
sodiumInitPromise = null;
|
|
44
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR TOFU (Trust On First Use) Manager
|
|
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
|
|
12
|
+
*
|
|
13
|
+
* SEMANTICS:
|
|
14
|
+
* - Fingerprint = BLAKE2b(identity_public_key)
|
|
15
|
+
* - Binding: identity key ONLY (not bundle, not SPK)
|
|
16
|
+
* - Key rotation: requires manual re-trust via trustNewFingerprint()
|
|
17
|
+
* - Multi-device: NOT supported (each device = new identity)
|
|
18
|
+
*/
|
|
19
|
+
interface FingerprintRecord {
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
firstSeen: Date;
|
|
22
|
+
lastSeen: Date;
|
|
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[]>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Initialize TOFU manager with optional persistent storage
|
|
34
|
+
*/
|
|
35
|
+
export declare function initializeTofu(customStore?: ITofuStore): void;
|
|
36
|
+
/**
|
|
37
|
+
* Generate BLAKE2b-256 fingerprint from identity public key
|
|
38
|
+
*
|
|
39
|
+
* BINDING: Identity key ONLY
|
|
40
|
+
* - SPK rotation does NOT change fingerprint
|
|
41
|
+
* - OPK exhaustion does NOT change fingerprint
|
|
42
|
+
* - Only identity key rotation changes fingerprint
|
|
43
|
+
*/
|
|
44
|
+
export declare function generateFingerprint(identityPublicKey: Uint8Array): string;
|
|
45
|
+
/**
|
|
46
|
+
* Store fingerprint for user
|
|
47
|
+
*/
|
|
48
|
+
export declare function storeFingerprint(userId: string, fingerprint: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Verify fingerprint against stored value
|
|
51
|
+
*
|
|
52
|
+
* BEHAVIOR:
|
|
53
|
+
* - First use: stores fingerprint, returns true
|
|
54
|
+
* - Match: returns true
|
|
55
|
+
* - Mismatch: throws error (HARD FAILURE)
|
|
56
|
+
*/
|
|
57
|
+
export declare function verifyFingerprint(userId: string, identityPublicKey: Uint8Array): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Check if user fingerprint is trusted
|
|
60
|
+
*/
|
|
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>;
|
|
70
|
+
/**
|
|
71
|
+
* Re-trust a user after verifying their new fingerprint out-of-band
|
|
72
|
+
*/
|
|
73
|
+
export declare function trustNewFingerprint(userId: string, identityPublicKey: Uint8Array): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* List all trusted fingerprints
|
|
76
|
+
*/
|
|
77
|
+
export declare function listTrustedFingerprints(): Promise<string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Get detailed fingerprint info for debugging
|
|
80
|
+
*/
|
|
81
|
+
export declare function getFingerprintInfo(userId: string): Promise<FingerprintRecord | null>;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR TOFU (Trust On First Use) Manager
|
|
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
|
|
12
|
+
*
|
|
13
|
+
* SEMANTICS:
|
|
14
|
+
* - Fingerprint = BLAKE2b(identity_public_key)
|
|
15
|
+
* - Binding: identity key ONLY (not bundle, not SPK)
|
|
16
|
+
* - Key rotation: requires manual re-trust via trustNewFingerprint()
|
|
17
|
+
* - Multi-device: NOT supported (each device = new identity)
|
|
18
|
+
*/
|
|
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;
|
|
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
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate BLAKE2b-256 fingerprint from identity public key
|
|
50
|
+
*
|
|
51
|
+
* BINDING: Identity key ONLY
|
|
52
|
+
* - SPK rotation does NOT change fingerprint
|
|
53
|
+
* - OPK exhaustion does NOT change fingerprint
|
|
54
|
+
* - Only identity key rotation changes fingerprint
|
|
55
|
+
*/
|
|
56
|
+
export function generateFingerprint(identityPublicKey) {
|
|
57
|
+
const hash = createHash('sha256').update(identityPublicKey).digest();
|
|
58
|
+
return hash.toString('hex');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Store fingerprint for user
|
|
62
|
+
*/
|
|
63
|
+
export async function storeFingerprint(userId, fingerprint) {
|
|
64
|
+
if (!tofuStore) {
|
|
65
|
+
initializeTofu();
|
|
66
|
+
}
|
|
67
|
+
const record = {
|
|
68
|
+
fingerprint,
|
|
69
|
+
firstSeen: new Date(),
|
|
70
|
+
lastSeen: new Date(),
|
|
71
|
+
version: FINGERPRINT_VERSION,
|
|
72
|
+
trusted: true,
|
|
73
|
+
};
|
|
74
|
+
await tofuStore.saveFingerprint(userId, record);
|
|
75
|
+
console.log(`[TOFU] Stored fingerprint for user: ${userId}`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify fingerprint against stored value
|
|
79
|
+
*
|
|
80
|
+
* BEHAVIOR:
|
|
81
|
+
* - First use: stores fingerprint, returns true
|
|
82
|
+
* - Match: returns true
|
|
83
|
+
* - Mismatch: throws error (HARD FAILURE)
|
|
84
|
+
*/
|
|
85
|
+
export async function verifyFingerprint(userId, identityPublicKey) {
|
|
86
|
+
if (!tofuStore) {
|
|
87
|
+
initializeTofu();
|
|
88
|
+
}
|
|
89
|
+
const fingerprint = generateFingerprint(identityPublicKey);
|
|
90
|
+
const stored = await tofuStore.loadFingerprint(userId);
|
|
91
|
+
if (!stored) {
|
|
92
|
+
// First use - store and trust
|
|
93
|
+
await storeFingerprint(userId, fingerprint);
|
|
94
|
+
console.log(`[TOFU] First contact: ${userId} (fingerprint: ${fingerprint.slice(0, 16)}...)`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
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!`);
|
|
103
|
+
}
|
|
104
|
+
// Update last seen
|
|
105
|
+
stored.lastSeen = new Date();
|
|
106
|
+
await tofuStore.saveFingerprint(userId, stored);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if user fingerprint is trusted
|
|
111
|
+
*/
|
|
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;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Revoke trust for a user (after key rotation or suspected compromise)
|
|
131
|
+
*/
|
|
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}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Re-trust a user after verifying their new fingerprint out-of-band
|
|
141
|
+
*/
|
|
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}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* List all trusted fingerprints
|
|
149
|
+
*/
|
|
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);
|
|
164
|
+
}
|
|
165
|
+
// Default initialization
|
|
166
|
+
initializeTofu();
|
package/dist/facade/types.d.ts
CHANGED
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/legacy.d.ts
CHANGED
|
@@ -1 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* STVOR SDK - Legacy Core API
|
|
3
|
+
* Kept for backwards compatibility
|
|
4
|
+
*/
|
|
5
|
+
export interface StvorConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
serverUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Peer {
|
|
10
|
+
id: string;
|
|
11
|
+
publicKey: any;
|
|
12
|
+
}
|
|
13
|
+
export interface EncryptedMessage {
|
|
14
|
+
ciphertext: string;
|
|
15
|
+
nonce: string;
|
|
16
|
+
from: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class StvorClient {
|
|
19
|
+
private config;
|
|
20
|
+
private myKeyPair;
|
|
21
|
+
private myId;
|
|
22
|
+
private peers;
|
|
23
|
+
constructor(config: StvorConfig);
|
|
24
|
+
ready(): Promise<void>;
|
|
25
|
+
createPeer(name: string): Promise<Peer>;
|
|
26
|
+
send({ to, message }: {
|
|
27
|
+
to: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}): Promise<EncryptedMessage>;
|
|
30
|
+
receive(encrypted: EncryptedMessage): Promise<string>;
|
|
31
|
+
}
|