@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
package/dist/facade/index.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* STVOR DX Facade SDK
|
|
3
|
+
* High-level developer experience layer for STVOR E2E encryption
|
|
4
|
+
*
|
|
5
|
+
* Design goals:
|
|
6
|
+
* - Minimal API surface
|
|
7
|
+
* - Zero crypto knowledge required
|
|
8
|
+
* - Secure by default
|
|
9
|
+
* - Opinionated (no configuration)
|
|
10
|
+
*/
|
|
4
11
|
export { StvorError } from './errors.js';
|
|
12
|
+
// Re-export classes and functions
|
|
5
13
|
export { StvorApp, StvorFacadeClient, Stvor, init, createApp } from './app.js';
|
|
14
|
+
export { ErrorCode as STVOR_ERRORS } from './errors.js';
|
|
15
|
+
// Re-export metrics verification for Dashboard
|
|
16
|
+
export { verifyMetricsSignature, MetricsEngine } from './metrics-engine.js';
|
|
17
|
+
// Re-export crypto session management
|
|
18
|
+
export { CryptoSessionManager } from './crypto-session.js';
|
|
19
|
+
export { LocalStorageIdentityStore, LocalStorageSessionStore } from './local-storage-identity-store.js';
|
|
20
|
+
// Re-export replay protection
|
|
21
|
+
export { isReplay, validateMessage, validateMessageWithNonce, getCacheStats, cleanupExpiredNonces, initializeReplayProtection, startAutoCleanup, stopAutoCleanup, } from './replay-manager.js';
|
|
22
|
+
// Re-export Redis replay cache for production
|
|
23
|
+
export { RedisReplayCache } from './redis-replay-cache.js';
|
|
24
|
+
// Re-export TOFU management
|
|
25
|
+
export { generateFingerprint, storeFingerprint, verifyFingerprint, isFingerprintTrusted, getFingerprint, revokeTrust, trustNewFingerprint, listTrustedFingerprints, getFingerprintInfo, initializeTofu, } from './tofu-manager.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/local-storage-identity-store.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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage-based Identity Store for browser environments
|
|
3
|
+
* Implements IIdentityStore for persistent identity key storage
|
|
4
|
+
*/
|
|
5
|
+
import { IIdentityStore } from './crypto-session.js';
|
|
6
|
+
/**
|
|
7
|
+
* Browser-based identity storage using localStorage
|
|
8
|
+
* CRITICAL: Keys are stored in base64url — in production, use encrypted storage
|
|
9
|
+
*/
|
|
10
|
+
export declare class LocalStorageIdentityStore implements IIdentityStore {
|
|
11
|
+
private storageKey;
|
|
12
|
+
constructor(userId: string, storageKeyPrefix?: string);
|
|
13
|
+
saveIdentityKeys(userId: string, keys: {
|
|
14
|
+
identityKeyPair: {
|
|
15
|
+
publicKey: string;
|
|
16
|
+
privateKey: string;
|
|
17
|
+
};
|
|
18
|
+
signedPreKeyPair: {
|
|
19
|
+
publicKey: string;
|
|
20
|
+
privateKey: string;
|
|
21
|
+
};
|
|
22
|
+
signedPreKeySignature: string;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
loadIdentityKeys(userId: string): Promise<{
|
|
25
|
+
identityKeyPair: {
|
|
26
|
+
publicKey: string;
|
|
27
|
+
privateKey: string;
|
|
28
|
+
};
|
|
29
|
+
signedPreKeyPair: {
|
|
30
|
+
publicKey: string;
|
|
31
|
+
privateKey: string;
|
|
32
|
+
};
|
|
33
|
+
signedPreKeySignature: string;
|
|
34
|
+
} | null>;
|
|
35
|
+
/** Delete stored keys (for logout / account reset) */
|
|
36
|
+
deleteIdentityKeys(userId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Session storage implementation for browser environments
|
|
40
|
+
*/
|
|
41
|
+
export declare class LocalStorageSessionStore {
|
|
42
|
+
private storageKey;
|
|
43
|
+
constructor(userId: string, storageKeyPrefix?: string);
|
|
44
|
+
saveSession(userId: string, peerId: string, _session: unknown): Promise<void>;
|
|
45
|
+
loadSession(userId: string, peerId: string): Promise<unknown | null>;
|
|
46
|
+
deleteSession(userId: string, peerId: string): Promise<void>;
|
|
47
|
+
listSessions(userId: string): Promise<string[]>;
|
|
48
|
+
private getAllSessions;
|
|
49
|
+
}
|
|
50
|
+
export default LocalStorageIdentityStore;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage-based Identity Store for browser environments
|
|
3
|
+
* Implements IIdentityStore for persistent identity key storage
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Browser-based identity storage using localStorage
|
|
7
|
+
* CRITICAL: Keys are stored in base64url — in production, use encrypted storage
|
|
8
|
+
*/
|
|
9
|
+
export class LocalStorageIdentityStore {
|
|
10
|
+
constructor(userId, storageKeyPrefix = 'stvor_identity_') {
|
|
11
|
+
this.storageKey = `${storageKeyPrefix}${userId}`;
|
|
12
|
+
}
|
|
13
|
+
async saveIdentityKeys(userId, keys) {
|
|
14
|
+
try {
|
|
15
|
+
const data = {
|
|
16
|
+
identityKeyPair: keys.identityKeyPair,
|
|
17
|
+
signedPreKeyPair: keys.signedPreKeyPair,
|
|
18
|
+
signedPreKeySignature: keys.signedPreKeySignature,
|
|
19
|
+
};
|
|
20
|
+
localStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
21
|
+
console.log(`[LocalStorageIdentityStore] Keys saved for user: ${userId}`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('[LocalStorageIdentityStore] Failed to save keys:', error);
|
|
25
|
+
throw new Error('Failed to save identity keys to localStorage');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async loadIdentityKeys(userId) {
|
|
29
|
+
try {
|
|
30
|
+
const data = localStorage.getItem(this.storageKey);
|
|
31
|
+
if (!data) {
|
|
32
|
+
console.log(`[LocalStorageIdentityStore] No keys found for user: ${userId}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const parsed = JSON.parse(data);
|
|
36
|
+
console.log(`[LocalStorageIdentityStore] Keys loaded for user: ${userId}`);
|
|
37
|
+
return {
|
|
38
|
+
identityKeyPair: parsed.identityKeyPair,
|
|
39
|
+
signedPreKeyPair: parsed.signedPreKeyPair,
|
|
40
|
+
signedPreKeySignature: parsed.signedPreKeySignature,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('[LocalStorageIdentityStore] Failed to load keys:', error);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Delete stored keys (for logout / account reset) */
|
|
49
|
+
async deleteIdentityKeys(userId) {
|
|
50
|
+
localStorage.removeItem(this.storageKey);
|
|
51
|
+
console.log(`[LocalStorageIdentityStore] Keys deleted for user: ${userId}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Session storage implementation for browser environments
|
|
56
|
+
*/
|
|
57
|
+
export class LocalStorageSessionStore {
|
|
58
|
+
constructor(userId, storageKeyPrefix = 'stvor_session_') {
|
|
59
|
+
this.storageKey = `${storageKeyPrefix}${userId}`;
|
|
60
|
+
}
|
|
61
|
+
async saveSession(userId, peerId, _session) {
|
|
62
|
+
try {
|
|
63
|
+
const allSessions = this.getAllSessions();
|
|
64
|
+
allSessions[peerId] = { savedAt: Date.now() };
|
|
65
|
+
localStorage.setItem(this.storageKey, JSON.stringify(allSessions));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('[LocalStorageSessionStore] Failed to save session:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async loadSession(userId, peerId) {
|
|
72
|
+
try {
|
|
73
|
+
const allSessions = this.getAllSessions();
|
|
74
|
+
return allSessions[peerId] || null;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error('[LocalStorageSessionStore] Failed to load session:', error);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async deleteSession(userId, peerId) {
|
|
82
|
+
try {
|
|
83
|
+
const allSessions = this.getAllSessions();
|
|
84
|
+
delete allSessions[peerId];
|
|
85
|
+
localStorage.setItem(this.storageKey, JSON.stringify(allSessions));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('[LocalStorageSessionStore] Failed to delete session:', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async listSessions(userId) {
|
|
92
|
+
const allSessions = this.getAllSessions();
|
|
93
|
+
return Object.keys(allSessions);
|
|
94
|
+
}
|
|
95
|
+
getAllSessions() {
|
|
96
|
+
const data = localStorage.getItem(this.storageKey);
|
|
97
|
+
return data ? JSON.parse(data) : {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export default LocalStorageIdentityStore;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/metrics-attestation.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,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR v2.4.0 - Metrics Attestation Engine
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ CRITICAL SECURITY MODEL:
|
|
5
|
+
*
|
|
6
|
+
* This module ONLY records real E2EE events and creates attestations.
|
|
7
|
+
* It does NOT verify attestations (that's backend's job).
|
|
8
|
+
*
|
|
9
|
+
* Trust boundary:
|
|
10
|
+
* ┌─────────────────────────────────────────┐
|
|
11
|
+
* │ SDK (Trusted) - Record + Sign │
|
|
12
|
+
* │ ┌─────────────────────────────────────┐ │
|
|
13
|
+
* │ │ MetricsAttestationEngine │ │
|
|
14
|
+
* │ └─────────────────────────────────────┘ │
|
|
15
|
+
* └──────────────┬──────────────────────────┘
|
|
16
|
+
* │ POST /api/metrics/attest
|
|
17
|
+
* ▼
|
|
18
|
+
* ┌─────────────────────────────────────────┐
|
|
19
|
+
* │ BACKEND (Trusted) - Verify + Store │
|
|
20
|
+
* │ ┌─────────────────────────────────────┐ │
|
|
21
|
+
* │ │ MetricsVerificationService │ │
|
|
22
|
+
* │ │ - Check signature │ │
|
|
23
|
+
* │ │ - Check monotonicity │ │
|
|
24
|
+
* │ │ - Check anti-replay │ │
|
|
25
|
+
* │ └─────────────────────────────────────┘ │
|
|
26
|
+
* └──────────────┬──────────────────────────┘
|
|
27
|
+
* │ Verified metrics in DB
|
|
28
|
+
* ▼
|
|
29
|
+
* ┌─────────────────────────────────────────┐
|
|
30
|
+
* │ Dashboard (Untrusted) - Display Only │
|
|
31
|
+
* │ - No crypto verification in browser │
|
|
32
|
+
* │ - No calculations │
|
|
33
|
+
* │ - No fallback numbers │
|
|
34
|
+
* │ - Only: fetch from /api/metrics │
|
|
35
|
+
* └─────────────────────────────────────────┘
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Raw metrics from SDK (before attestation)
|
|
39
|
+
* INVARIANT: Only incremented after crypto success
|
|
40
|
+
*/
|
|
41
|
+
export interface RawMetrics {
|
|
42
|
+
messagesEncrypted: number;
|
|
43
|
+
messagesDecrypted: number;
|
|
44
|
+
messagesRejected: number;
|
|
45
|
+
replayAttempts: number;
|
|
46
|
+
authFailures: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Attestation = metrics + proof that can be sent to backend
|
|
50
|
+
* Backend will verify this, not Dashboard
|
|
51
|
+
*/
|
|
52
|
+
export interface MetricsAttestation {
|
|
53
|
+
metrics: RawMetrics;
|
|
54
|
+
attestationId: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
sessionId: string;
|
|
57
|
+
sequenceNumber: number;
|
|
58
|
+
proof: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* MetricsAttestationEngine
|
|
62
|
+
*
|
|
63
|
+
* RESPONSIBILITY: Record real events + create attestations
|
|
64
|
+
* NOT RESPONSIBLE: Verify attestations (backend does that)
|
|
65
|
+
*/
|
|
66
|
+
export declare class MetricsAttestationEngine {
|
|
67
|
+
private metrics;
|
|
68
|
+
private sessionId;
|
|
69
|
+
private sequenceNumber;
|
|
70
|
+
private attestationKey;
|
|
71
|
+
constructor(appToken: string);
|
|
72
|
+
/**
|
|
73
|
+
* Record real event: Successful encryption with AEAD
|
|
74
|
+
* INVARIANT: Only called after cryptoSession.encryptForPeer() succeeds
|
|
75
|
+
*/
|
|
76
|
+
recordMessageEncrypted(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Record real event: Successful decryption with AAD verification
|
|
79
|
+
* INVARIANT: Only called after cryptoSession.decryptFromPeer() succeeds
|
|
80
|
+
*/
|
|
81
|
+
recordMessageDecrypted(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Record real event: AAD verification failed (auth failure)
|
|
84
|
+
* INVARIANT: Only called when AEAD auth tag is invalid
|
|
85
|
+
*/
|
|
86
|
+
recordMessageRejected(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Record real event: Replay attack detected
|
|
89
|
+
* INVARIANT: Only called when nonce is duplicate
|
|
90
|
+
*/
|
|
91
|
+
recordReplayAttempt(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Record real event: Signature verification failed
|
|
94
|
+
* INVARIANT: Only called on crypto auth failure
|
|
95
|
+
*/
|
|
96
|
+
recordAuthFailure(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Create attestation that can be sent to backend
|
|
99
|
+
*
|
|
100
|
+
* Backend MUST verify:
|
|
101
|
+
* - Signature is valid
|
|
102
|
+
* - Metrics are monotonic
|
|
103
|
+
* - Timestamp is within acceptable window
|
|
104
|
+
* - sessionId matches
|
|
105
|
+
* - sequenceNumber hasn't been seen before
|
|
106
|
+
*/
|
|
107
|
+
createAttestation(): MetricsAttestation;
|
|
108
|
+
/**
|
|
109
|
+
* Get current metrics snapshot (immutable)
|
|
110
|
+
* Used for monitoring/debugging, NOT for Dashboard display
|
|
111
|
+
*/
|
|
112
|
+
getMetrics(): Readonly<RawMetrics>;
|
|
113
|
+
/**
|
|
114
|
+
* Internal: Create proof for attestation
|
|
115
|
+
*
|
|
116
|
+
* Format:
|
|
117
|
+
* proof = HMAC-SHA256(
|
|
118
|
+
* JSON.stringify({metrics, sessionId, sequenceNumber, timestamp}),
|
|
119
|
+
* attestationKey
|
|
120
|
+
* )
|
|
121
|
+
*
|
|
122
|
+
* Backend will recompute this with the appToken sent by SDK.
|
|
123
|
+
* If proof matches, backend knows:
|
|
124
|
+
* - Metrics came from this SDK instance
|
|
125
|
+
* - Metrics haven't been tampered
|
|
126
|
+
* - This is the correct sequence number
|
|
127
|
+
*/
|
|
128
|
+
private createProof;
|
|
129
|
+
/**
|
|
130
|
+
* Derive attestation key from appToken
|
|
131
|
+
*
|
|
132
|
+
* HKDF-SHA256 with:
|
|
133
|
+
* - IKM: appToken
|
|
134
|
+
* - salt: 32 zero bytes
|
|
135
|
+
* - info: "stvor-metrics-attestation-v1"
|
|
136
|
+
*
|
|
137
|
+
* Result is deterministic: same appToken → same key
|
|
138
|
+
*
|
|
139
|
+
* This key is sent to backend for verification.
|
|
140
|
+
* Backend computes same key from appToken and verifies proof.
|
|
141
|
+
*/
|
|
142
|
+
private deriveAttestationKey;
|
|
143
|
+
/**
|
|
144
|
+
* Generate unique session ID
|
|
145
|
+
* Used to distinguish different SDK instances
|
|
146
|
+
*/
|
|
147
|
+
private generateSessionId;
|
|
148
|
+
/**
|
|
149
|
+
* Generate unique attestation ID
|
|
150
|
+
* Used for anti-replay detection
|
|
151
|
+
*/
|
|
152
|
+
private generateAttestationId;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Backend Verification Service (Pseudo-code)
|
|
156
|
+
*
|
|
157
|
+
* This runs on BACKEND, not in browser or SDK
|
|
158
|
+
*/
|
|
159
|
+
export declare class MetricsVerificationService {
|
|
160
|
+
/**
|
|
161
|
+
* Verify attestation received from SDK
|
|
162
|
+
*
|
|
163
|
+
* RETURN: VerificationResult
|
|
164
|
+
*/
|
|
165
|
+
verifyAttestation(attestation: MetricsAttestation, appToken: string, lastSequenceNumber: number): VerificationResult;
|
|
166
|
+
/**
|
|
167
|
+
* Verify proof signature (backend-side)
|
|
168
|
+
*/
|
|
169
|
+
private verifyProof;
|
|
170
|
+
private deriveAttestationKey;
|
|
171
|
+
private constantTimeCompare;
|
|
172
|
+
private hasSeenAttestationId;
|
|
173
|
+
private getLastVerifiedMetrics;
|
|
174
|
+
}
|
|
175
|
+
export interface VerificationResult {
|
|
176
|
+
valid: boolean;
|
|
177
|
+
reason: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* SECURITY INVARIANTS (MUST BE ENFORCED)
|
|
181
|
+
*
|
|
182
|
+
* I1: Dashboard NEVER generates metric numbers
|
|
183
|
+
* ✓ MetricsAttestationEngine records only on crypto success
|
|
184
|
+
* ✓ Dashboard only fetches from /api/metrics
|
|
185
|
+
*
|
|
186
|
+
* I2: Metrics without backend verification are discarded
|
|
187
|
+
* ✓ Backend verifies proof before storing
|
|
188
|
+
* ✓ Only verified metrics go to DB
|
|
189
|
+
* ✓ Dashboard reads DB, not SDK
|
|
190
|
+
*
|
|
191
|
+
* I3: Metric counters are monotonic
|
|
192
|
+
* ✓ SDK: counters only increment (never set)
|
|
193
|
+
* ✓ Backend: checks sequenceNumber is sequential
|
|
194
|
+
* ✓ Backend: checks metrics don't roll back
|
|
195
|
+
*
|
|
196
|
+
* I4: Metrics replay is impossible
|
|
197
|
+
* ✓ SDK: each attestation has unique attestationId
|
|
198
|
+
* ✓ Backend: stores all seen attestationIds
|
|
199
|
+
* ✓ Backend: rejects duplicate attestationIds
|
|
200
|
+
*
|
|
201
|
+
* I5: Different SDK instances cannot forge each other
|
|
202
|
+
* ✓ Each SDK has unique sessionId
|
|
203
|
+
* ✓ Backend checks sessionId matches appToken
|
|
204
|
+
*
|
|
205
|
+
* I6: appToken compromise ≠ metrics forgery
|
|
206
|
+
* ✓ appToken only derives the signing key
|
|
207
|
+
* ✓ Backend verifies timestamp is recent
|
|
208
|
+
* ✓ Backend checks monotonicity constraints
|
|
209
|
+
*/
|