@learncard/sss-key-manager 0.1.23 → 0.2.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/README.md +12 -95
- package/dist/api-client.d.ts +44 -7
- package/dist/api-client.d.ts.map +1 -1
- package/dist/atomic-operations.d.ts +10 -2
- package/dist/atomic-operations.d.ts.map +1 -1
- package/dist/email-relay-crypto.d.ts +49 -0
- package/dist/email-relay-crypto.d.ts.map +1 -0
- package/dist/escrow-attestation.d.ts +8 -0
- package/dist/escrow-attestation.d.ts.map +1 -0
- package/dist/escrow-crypto.d.ts +52 -0
- package/dist/escrow-crypto.d.ts.map +1 -0
- package/dist/escrow-pin.d.ts +22 -0
- package/dist/escrow-pin.d.ts.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/key-manager.d.ts +2 -0
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/qr-login.d.ts.map +1 -1
- package/dist/recovery-phrase.d.ts +6 -0
- package/dist/recovery-phrase.d.ts.map +1 -1
- package/dist/sss-key-manager.cjs.development.js +3163 -440
- package/dist/sss-key-manager.cjs.development.js.map +4 -4
- package/dist/sss-key-manager.cjs.production.min.js +33 -33
- package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
- package/dist/sss-key-manager.esm.js +3163 -440
- package/dist/sss-key-manager.esm.js.map +4 -4
- package/dist/sss-strategy.d.ts +74 -4
- package/dist/sss-strategy.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/types.d.ts +87 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api-client.test.ts +122 -0
- package/src/api-client.ts +107 -116
- package/src/atomic-operations.test.ts +56 -16
- package/src/atomic-operations.ts +55 -19
- package/src/critical-paths.test.ts +51 -1
- package/src/email-relay-crypto.test.ts +87 -0
- package/src/email-relay-crypto.ts +293 -0
- package/src/escrow-attestation.test.ts +85 -0
- package/src/escrow-attestation.ts +64 -0
- package/src/escrow-crypto.test.ts +381 -0
- package/src/escrow-crypto.ts +319 -0
- package/src/escrow-pin.test.ts +119 -0
- package/src/escrow-pin.ts +83 -0
- package/src/index.ts +63 -1
- package/src/key-manager.ts +4 -9
- package/src/qr-login.ts +22 -15
- package/src/recovery-phrase.ts +40 -0
- package/src/sss-session-writes.test.ts +109 -0
- package/src/sss-strategy.test.ts +2642 -110
- package/src/sss-strategy.ts +2186 -224
- package/src/storage.test.ts +66 -8
- package/src/storage.ts +41 -29
- package/src/types.ts +107 -6
package/dist/sss-strategy.d.ts
CHANGED
|
@@ -12,28 +12,42 @@
|
|
|
12
12
|
* - Recovery method execution and setup
|
|
13
13
|
* - Storage cleanup knowledge
|
|
14
14
|
*/
|
|
15
|
-
import type { SSSKeyDerivationStrategy } from './types';
|
|
15
|
+
import type { SSSKeyDerivationStrategy, EscrowAttestationPolicy } from './types';
|
|
16
|
+
import type { EmailRelayBranding } from './email-relay-crypto';
|
|
16
17
|
export interface SSSStorageFunctions {
|
|
17
18
|
storeDeviceShare: (share: string, id?: string) => Promise<void>;
|
|
18
19
|
getDeviceShare: (id?: string) => Promise<string | null>;
|
|
19
20
|
hasDeviceShare: (id?: string) => Promise<boolean>;
|
|
20
21
|
clearAllShares: (id?: string) => Promise<void>;
|
|
22
|
+
deleteDeviceShare: (id?: string) => Promise<void>;
|
|
21
23
|
storeShareVersion: (version: number, id?: string) => Promise<void>;
|
|
22
24
|
getShareVersion: (id?: string) => Promise<number | null>;
|
|
23
25
|
}
|
|
24
26
|
export interface SSSStrategyConfig {
|
|
27
|
+
/** Enable automatic enrollment under an explicit enclave trust policy. */
|
|
28
|
+
escrow?: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
attestation: EscrowAttestationPolicy;
|
|
31
|
+
};
|
|
32
|
+
/** Report enrollment failures without logging recovery material. */
|
|
33
|
+
onEscrowError?: (err: unknown) => void;
|
|
25
34
|
/** Server URL for key share operations */
|
|
26
35
|
serverUrl: string;
|
|
27
36
|
/** Custom storage functions (defaults to IndexedDB) */
|
|
28
37
|
storage?: SSSStorageFunctions;
|
|
29
38
|
/**
|
|
30
|
-
* Whether
|
|
31
|
-
*
|
|
32
|
-
* but never persisted — fire-and-forget.
|
|
39
|
+
* Whether email recovery enrollment is enabled. Delivery is synchronous
|
|
40
|
+
* and must be confirmed with the code included in the recovery email.
|
|
33
41
|
*
|
|
34
42
|
* Defaults to false. Controlled by VITE_ENABLE_EMAIL_BACKUP_SHARE env var.
|
|
35
43
|
*/
|
|
36
44
|
enableEmailBackupShare?: boolean;
|
|
45
|
+
/** Base64 DER/SPKI P-256 public key pinned for the isolated email relay. */
|
|
46
|
+
escrowRelayPublicKey?: string;
|
|
47
|
+
/** Identifier selecting the matching relay private key during rotation. */
|
|
48
|
+
escrowRelayKeyId?: string;
|
|
49
|
+
/** Tenant branding encrypted into the relay payload with the recovery key. */
|
|
50
|
+
emailBranding?: EmailRelayBranding;
|
|
37
51
|
/**
|
|
38
52
|
* Tenant identifier forwarded as `X-Tenant-Id` on every server request.
|
|
39
53
|
* The lca-api uses this to brand recovery / OTP emails for the active
|
|
@@ -41,6 +55,22 @@ export interface SSSStrategyConfig {
|
|
|
41
55
|
*/
|
|
42
56
|
tenantId?: string;
|
|
43
57
|
}
|
|
58
|
+
export declare const SERVER_REQUEST_TIMEOUT_MS = 30000;
|
|
59
|
+
/** HTTP failure metadata without potentially sensitive server response text. */
|
|
60
|
+
export declare class EscrowRequestError extends Error {
|
|
61
|
+
readonly status: number;
|
|
62
|
+
constructor(status: number);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Signals that an identity-recovery attempt failed after the one-shot server
|
|
66
|
+
* session was submitted and must be restarted with a new verification code.
|
|
67
|
+
*/
|
|
68
|
+
export declare class IdentityRecoverySessionConsumedError extends Error {
|
|
69
|
+
constructor(message: string);
|
|
70
|
+
}
|
|
71
|
+
/** Two tap-to-confirm questions with six choices each: a 1-in-36 guess. */
|
|
72
|
+
export declare const RECOVERY_PHRASE_CHALLENGE_COUNT = 2;
|
|
73
|
+
export declare const selectRecoveryPhraseChallengeIndices: (wordCount: number, challengeCount?: number) => number[];
|
|
44
74
|
/**
|
|
45
75
|
* Format an email share with a 4-character hex version prefix.
|
|
46
76
|
*
|
|
@@ -61,6 +91,46 @@ export declare const parseVersionedEmailShare: (input: string) => {
|
|
|
61
91
|
share: string;
|
|
62
92
|
version: number | undefined;
|
|
63
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Serialize every share rotation (device + server write pair). Two rotations
|
|
96
|
+
* interleaving would leave the device share and the server auth share on
|
|
97
|
+
* different polynomials, which locks the account out on the next reconstruct.
|
|
98
|
+
*/
|
|
99
|
+
export declare const withRotationLock: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
100
|
+
/** DID_CHALLENGE_TTL_SECS (5 minutes) plus a processing margin for auth-share PUTs. */
|
|
101
|
+
export declare const PENDING_WRITE_EXPIRY_MS: number;
|
|
102
|
+
export interface PendingShareCandidate {
|
|
103
|
+
share: string;
|
|
104
|
+
createdAt: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read device-only candidates from independent splits; never persist an auth,
|
|
108
|
+
* recovery, or email share here (two shares from one split reveal the key).
|
|
109
|
+
* Legacy raw shares have unknown age, but must still be checked before expiry.
|
|
110
|
+
* Mutating helpers must run under the account's share-update lock.
|
|
111
|
+
*/
|
|
112
|
+
export declare const readPendingShareCandidates: (storage: SSSStorageFunctions, storageId?: string) => Promise<PendingShareCandidate[]>;
|
|
113
|
+
/** Clear only an empty/resolved queue or when explicitly forgetting this device. */
|
|
114
|
+
export declare const clearPendingShareCandidates: (storage: SSSStorageFunctions, storageId?: string) => Promise<void>;
|
|
115
|
+
/** Persist the versioned device-only queue without storing another share of any split. */
|
|
116
|
+
export declare const writePendingShareCandidates: (storage: SSSStorageFunctions, candidates: PendingShareCandidate[], storageId?: string) => Promise<void>;
|
|
117
|
+
/** Append a candidate; never evict a live write to make room for another one. */
|
|
118
|
+
export declare const addPendingShareCandidate: (storage: SSSStorageFunctions, candidate: PendingShareCandidate, storageId?: string) => Promise<void>;
|
|
119
|
+
/** Roll back only the definitively rejected operation, preserving earlier in-flight writes. */
|
|
120
|
+
export declare const removePendingShareCandidate: (storage: SSSStorageFunctions, share: string, storageId?: string) => Promise<void>;
|
|
121
|
+
interface CurrentShareSnapshot {
|
|
122
|
+
currentVersion: number | null;
|
|
123
|
+
authShare: string | null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check current and retained history before expiry: an expired nonce does not
|
|
127
|
+
* prove a write never committed. Keep unrelated live writes even after promotion,
|
|
128
|
+
* since older servers may still commit them after this read.
|
|
129
|
+
*/
|
|
130
|
+
export declare const resolvePendingShareCandidates: (storage: SSSStorageFunctions, storageId: string | undefined, fetchCurrent: () => Promise<CurrentShareSnapshot>, expectedDid: string, didFromPrivateKey: (key: string) => Promise<string>, assertActiveUser?: () => void, onPromoted?: (snapshot: CurrentShareSnapshot) => void, fetchHistorical?: (version: number) => Promise<string | null>) => Promise<{
|
|
131
|
+
snapshot: CurrentShareSnapshot;
|
|
132
|
+
privateKey: string | null;
|
|
133
|
+
}>;
|
|
64
134
|
/**
|
|
65
135
|
* Create an SSS key derivation strategy.
|
|
66
136
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sss-strategy.d.ts","sourceRoot":"","sources":["../src/sss-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACR,wBAAwB,
|
|
1
|
+
{"version":3,"file":"sss-strategy.d.ts","sourceRoot":"","sources":["../src/sss-strategy.ts"],"names":[],"mappings":"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"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare function createAdaptiveStorage(): {
|
|
|
39
39
|
getDeviceShare: (id?: string) => Promise<string | null>;
|
|
40
40
|
hasDeviceShare: (id?: string) => Promise<boolean>;
|
|
41
41
|
clearAllShares: (id?: string) => Promise<void>;
|
|
42
|
+
deleteDeviceShare: (id?: string) => Promise<void>;
|
|
42
43
|
storeShareVersion: (version: number, id?: string) => Promise<void>;
|
|
43
44
|
getShareVersion: (id?: string) => Promise<number | null>;
|
|
44
45
|
};
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+IH,wBAAsB,gBAAgB,CAClC,KAAK,EAAE,MAAM,EACb,EAAE,GAAE,MAAgC,GACrC,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACnC,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,MAAgC,GACrC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACjC,EAAE,GAAE,MAAgC,GACrC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYxB;AAED,wBAAsB,cAAc,CAAC,EAAE,GAAE,MAAgC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCjG;AAED,wBAAsB,cAAc,CAAC,EAAE,GAAE,MAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3F;AAED,wBAAsB,iBAAiB,CAAC,EAAE,GAAE,MAAgC,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3F;AAED,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsDvE;AAYD,wBAAgB,oBAAoB,IAAI,OAAO,CAS9C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAU5D;AAyDD;;;;GAIG;AACH,wBAAgB,qBAAqB;8BAEH,MAAM,OAAO,MAAM;0BAKvB,MAAM;0BAGN,MAAM;0BAGN,MAAM;6BAGH,MAAM;iCAGF,MAAM,OAAO,MAAM;2BAKzB,MAAM;EAGpC;AAED,wBAAsB,cAAc,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC/D"}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Re-exports provider-agnostic interfaces from @learncard/types
|
|
5
5
|
* and defines SSS-specific types (recovery shapes, backup files, etc.).
|
|
6
6
|
*/
|
|
7
|
-
export { AuthSessionError, type AuthProviderType, type AuthUser, type AuthProvider, type RecoveryMethodInfo, type RecoveryResult, type ServerKeyStatus, type KeyDerivationStrategy, } from '@learncard/types';
|
|
8
|
-
import type { AuthProvider, AuthProviderType, KeyDerivationStrategy, RecoveryMethodInfo } from '@learncard/types';
|
|
7
|
+
export { AuthSessionError, type AuthProviderType, type AuthUser, type AuthProvider, type RecoveryMethodInfo, type RecoveryResult, type IdentityRecoverySession, type ServerKeyStatus, type SssActivationState, type KeyDerivationStrategy, type DidAuthVpSigner, type EscrowEnrollmentOptions, type EscrowEnrollmentState, type EscrowPinStatus, } from '@learncard/types';
|
|
8
|
+
import type { AuthProvider, AuthProviderType, KeyDerivationStrategy, RecoveryMethodInfo, SssActivationState } from '@learncard/types';
|
|
9
9
|
export type ContactMethodType = 'email' | 'phone';
|
|
10
10
|
export interface ContactMethod {
|
|
11
11
|
type: ContactMethodType;
|
|
@@ -21,7 +21,56 @@ export declare const SecurityLevels: readonly SecurityLevel[];
|
|
|
21
21
|
* SSS recovery method type identifiers.
|
|
22
22
|
* These are the specific recovery methods supported by the SSS strategy.
|
|
23
23
|
*/
|
|
24
|
-
export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email';
|
|
24
|
+
export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email' | 'escrow';
|
|
25
|
+
/** Trust policy for the enclave that receives recovery material. */
|
|
26
|
+
export type EscrowAttestationPolicy = {
|
|
27
|
+
mode: 'software';
|
|
28
|
+
pinnedPublicKeys: string[];
|
|
29
|
+
} | {
|
|
30
|
+
mode: 'nitro';
|
|
31
|
+
pinnedMeasurements: {
|
|
32
|
+
imageSha384: string;
|
|
33
|
+
}[];
|
|
34
|
+
rootCertificatePem?: string;
|
|
35
|
+
};
|
|
36
|
+
/** Public status of an escrow recovery waiting period. */
|
|
37
|
+
export interface EscrowHoldStatus {
|
|
38
|
+
holdId: string;
|
|
39
|
+
releasePolicy: 'hold' | 'pin';
|
|
40
|
+
status: 'pending' | 'cancelled' | 'completed' | 'expired';
|
|
41
|
+
requestedAt: string;
|
|
42
|
+
releaseAfter: string;
|
|
43
|
+
cancelledAt?: string;
|
|
44
|
+
completedAt?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Persist these secrets securely until completion; a null token means an existing hold. */
|
|
47
|
+
export type EscrowRecoveryStart = EscrowHoldStatus & {
|
|
48
|
+
resumeToken: string | null;
|
|
49
|
+
clientEphemeralPrivateKey: string;
|
|
50
|
+
pinSalt?: string;
|
|
51
|
+
};
|
|
52
|
+
/** The account has no available PIN release policy. */
|
|
53
|
+
export declare class EscrowPinUnavailableError extends Error {
|
|
54
|
+
constructor();
|
|
55
|
+
}
|
|
56
|
+
/** A failed PIN attempt consumes its hold; another attempt must start a fresh hold. */
|
|
57
|
+
export declare class EscrowPinMismatchError extends Error {
|
|
58
|
+
readonly attemptsRemaining: number;
|
|
59
|
+
constructor(attemptsRemaining: number);
|
|
60
|
+
}
|
|
61
|
+
/** Temporary throttle: retry after cooldown with a fresh hold, never retry /complete. */
|
|
62
|
+
export declare class EscrowPinThrottledError extends Error {
|
|
63
|
+
constructor();
|
|
64
|
+
}
|
|
65
|
+
/** A pending waiting-period request is too recent to restart. */
|
|
66
|
+
export declare class EscrowHoldRestartThrottledError extends Error {
|
|
67
|
+
readonly retryAfter?: string | undefined;
|
|
68
|
+
constructor(retryAfter?: string | undefined);
|
|
69
|
+
}
|
|
70
|
+
/** PIN recovery is exhausted; delayed escrow recovery remains available. */
|
|
71
|
+
export declare class EscrowPinLockedError extends Error {
|
|
72
|
+
constructor();
|
|
73
|
+
}
|
|
25
74
|
export interface PasskeyRecoveryMethod {
|
|
26
75
|
type: 'passkey';
|
|
27
76
|
credentialId?: string;
|
|
@@ -41,6 +90,14 @@ export type RecoveryMethod = PasskeyRecoveryMethod | BackupFileRecoveryMethod |
|
|
|
41
90
|
* SSS-specific recovery input — what the user provides to recover their key.
|
|
42
91
|
*/
|
|
43
92
|
export type RecoveryInput = {
|
|
93
|
+
method: 'escrow-pin';
|
|
94
|
+
pin: string;
|
|
95
|
+
} | {
|
|
96
|
+
method: 'escrow';
|
|
97
|
+
holdId: string;
|
|
98
|
+
resumeToken: string;
|
|
99
|
+
clientEphemeralPrivateKey: string;
|
|
100
|
+
} | {
|
|
44
101
|
method: 'passkey';
|
|
45
102
|
credentialId: string;
|
|
46
103
|
} | {
|
|
@@ -58,6 +115,9 @@ export type RecoveryInput = {
|
|
|
58
115
|
* SSS-specific recovery setup input — what the user provides to set up a method.
|
|
59
116
|
*/
|
|
60
117
|
export type RecoverySetupInput = {
|
|
118
|
+
method: 'escrow';
|
|
119
|
+
pin?: string;
|
|
120
|
+
} | {
|
|
61
121
|
method: 'passkey';
|
|
62
122
|
} | {
|
|
63
123
|
method: 'phrase';
|
|
@@ -67,22 +127,41 @@ export type RecoverySetupInput = {
|
|
|
67
127
|
did: string;
|
|
68
128
|
} | {
|
|
69
129
|
method: 'email';
|
|
130
|
+
email: string;
|
|
70
131
|
};
|
|
71
132
|
/**
|
|
72
133
|
* SSS-specific recovery setup result.
|
|
73
134
|
*/
|
|
74
135
|
export type RecoverySetupResult = {
|
|
136
|
+
method: 'escrow';
|
|
137
|
+
shareVersion: number;
|
|
138
|
+
} | {
|
|
75
139
|
method: 'passkey';
|
|
76
140
|
credentialId: string;
|
|
77
141
|
} | {
|
|
78
142
|
method: 'phrase';
|
|
79
143
|
phrase: string;
|
|
144
|
+
challengeWordIndices: number[];
|
|
145
|
+
/** Shuffled choices per challenged word (the answer plus decoys not in the phrase). */
|
|
146
|
+
challengeWordOptions: string[][];
|
|
80
147
|
} | {
|
|
81
148
|
method: 'backup';
|
|
82
149
|
backupFile: BackupFile;
|
|
83
150
|
} | {
|
|
84
151
|
method: 'email';
|
|
85
152
|
};
|
|
153
|
+
/** Proof supplied after a pending recovery method has been created. */
|
|
154
|
+
export type RecoveryConfirmationInput = {
|
|
155
|
+
method: 'phrase';
|
|
156
|
+
challengeWords: string[];
|
|
157
|
+
} | {
|
|
158
|
+
method: 'backup';
|
|
159
|
+
fileContents: string;
|
|
160
|
+
password: string;
|
|
161
|
+
} | {
|
|
162
|
+
method: 'email';
|
|
163
|
+
code: string;
|
|
164
|
+
};
|
|
86
165
|
export interface EncryptedShare {
|
|
87
166
|
encryptedData: string;
|
|
88
167
|
iv: string;
|
|
@@ -104,6 +183,8 @@ export interface UserKeyRecord {
|
|
|
104
183
|
recoveryMethods: RecoveryMethodInfo[];
|
|
105
184
|
migratedFromWeb3Auth: boolean;
|
|
106
185
|
migratedAt?: Date;
|
|
186
|
+
sssActivationState?: SssActivationState;
|
|
187
|
+
provisionalCreatedAt?: Date;
|
|
107
188
|
createdAt: Date;
|
|
108
189
|
updatedAt: Date;
|
|
109
190
|
}
|
|
@@ -112,6 +193,8 @@ export interface BackupFile {
|
|
|
112
193
|
createdAt: string;
|
|
113
194
|
primaryDid: string;
|
|
114
195
|
shareVersion?: number;
|
|
196
|
+
/** SHA-256 checksum of the decrypted share, used to verify a just-written file. */
|
|
197
|
+
shareChecksum?: string;
|
|
115
198
|
encryptedShare: {
|
|
116
199
|
ciphertext: string;
|
|
117
200
|
iv: string;
|
|
@@ -133,7 +216,7 @@ export interface SSSKeyManagerConfig {
|
|
|
133
216
|
* The SSS key derivation strategy — a KeyDerivationStrategy narrowed
|
|
134
217
|
* with SSS-specific recovery input/output types.
|
|
135
218
|
*/
|
|
136
|
-
export type SSSKeyDerivationStrategy = KeyDerivationStrategy<RecoveryInput, RecoverySetupInput, RecoverySetupResult>;
|
|
219
|
+
export type SSSKeyDerivationStrategy = KeyDerivationStrategy<RecoveryInput, RecoverySetupInput, RecoverySetupResult, RecoveryConfirmationInput>;
|
|
137
220
|
/** @deprecated Use KeyDerivationStrategy instead. Kept for legacy SSSKeyManager class. */
|
|
138
221
|
export interface KeyDerivationProvider {
|
|
139
222
|
readonly name: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EACH,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,qBAAqB,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/sss-key-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shamir Secret Sharing key manager for LearnCard - replaces Web3Auth SFA",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/sss-key-manager.esm.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"vitest": "4.1.11"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@learncard/types": "5.
|
|
41
|
+
"@learncard/types": "5.22.0",
|
|
42
42
|
"@noble/ed25519": "^2.0.0",
|
|
43
43
|
"@scure/bip39": "^1.2.1",
|
|
44
44
|
"hash-wasm": "^4.11.0",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SSSApiClient } from './api-client';
|
|
4
|
+
|
|
5
|
+
import type { AuthProvider } from './types';
|
|
6
|
+
|
|
7
|
+
const SENTINEL_TOKEN = 'SENTINEL_TOKEN_XYZ';
|
|
8
|
+
const DID_CHALLENGE_ERROR = 'This operation requires the DID-challenge flow; use createSSSStrategy';
|
|
9
|
+
|
|
10
|
+
const createMockAuthProvider = (): AuthProvider => ({
|
|
11
|
+
getIdToken: vi.fn().mockResolvedValue(SENTINEL_TOKEN),
|
|
12
|
+
getCurrentUser: vi.fn().mockResolvedValue({
|
|
13
|
+
id: 'user-123',
|
|
14
|
+
email: 'sentinel-user@example.com',
|
|
15
|
+
providerType: 'firebase' as const,
|
|
16
|
+
}),
|
|
17
|
+
getProviderType: vi.fn().mockReturnValue('firebase' as const),
|
|
18
|
+
signOut: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('SSSApiClient', () => {
|
|
22
|
+
let client: SSSApiClient;
|
|
23
|
+
let calls: { url: string; init?: RequestInit }[];
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
calls = [];
|
|
27
|
+
|
|
28
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
29
|
+
calls.push({ url: url.toString(), init });
|
|
30
|
+
return new Response(
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
authShare: null,
|
|
33
|
+
primaryDid: null,
|
|
34
|
+
securityLevel: 'basic',
|
|
35
|
+
recoveryMethods: [],
|
|
36
|
+
keyProvider: 'sss',
|
|
37
|
+
shareVersion: 1,
|
|
38
|
+
sssActivationState: 'active',
|
|
39
|
+
recoverySessionToken: 'recovery-session-token',
|
|
40
|
+
rebindSessionToken: 'rebind-session-token',
|
|
41
|
+
recoveryMethodsRequireConfirmation: [],
|
|
42
|
+
}),
|
|
43
|
+
{ status: 200 }
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
client = new SSSApiClient({
|
|
48
|
+
serverUrl: 'http://test-server:5100/api',
|
|
49
|
+
authProvider: createMockAuthProvider(),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.restoreAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('uses X-Auth-Token rather than Authorization for provider-authenticated reads', async () => {
|
|
58
|
+
await client.getAuthShare();
|
|
59
|
+
|
|
60
|
+
const headers = calls[0]!.init?.headers as Record<string, string>;
|
|
61
|
+
expect(headers['X-Auth-Token']).toBe(SENTINEL_TOKEN);
|
|
62
|
+
expect(headers.Authorization).toBeUndefined();
|
|
63
|
+
expect(calls[0]!.url).not.toContain(SENTINEL_TOKEN);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps recovery read credentials out of the URL', async () => {
|
|
67
|
+
await client.getRecoveryShare('passkey', 'credential-1');
|
|
68
|
+
|
|
69
|
+
const headers = calls[0]!.init?.headers as Record<string, string>;
|
|
70
|
+
expect(headers['X-Auth-Token']).toBe(SENTINEL_TOKEN);
|
|
71
|
+
expect(calls[0]!.url).not.toContain(SENTINEL_TOKEN);
|
|
72
|
+
expect(calls[0]!.url).not.toMatch(/[?&](token|share)=/i);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('retains the valid unauthenticated identity-recovery helpers', async () => {
|
|
76
|
+
await client.startIdentityRecovery('recovery@example.com');
|
|
77
|
+
await client.verifyIdentityRecovery('recovery@example.com', '123456');
|
|
78
|
+
await client.useIdentityRecoverySession('recovery-session-token', 'phrase');
|
|
79
|
+
|
|
80
|
+
expect(calls.map(call => call.url)).toEqual([
|
|
81
|
+
'http://test-server:5100/api/keys/recovery-session/start',
|
|
82
|
+
'http://test-server:5100/api/keys/recovery-session/verify',
|
|
83
|
+
'http://test-server:5100/api/keys/recovery-session/recover',
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('retains identity rebind because it supplies both DID proof and the auth token', async () => {
|
|
88
|
+
await client.completeIdentityRecoveryRebind({
|
|
89
|
+
recoverySessionToken: 'rebind-session-token',
|
|
90
|
+
newAuthToken: SENTINEL_TOKEN,
|
|
91
|
+
providerType: 'firebase',
|
|
92
|
+
primaryDid: 'did:key:zTest',
|
|
93
|
+
authShare: { encryptedData: 'encrypted-share', encryptedDek: '', iv: '' },
|
|
94
|
+
didAuthVp: 'did-auth-vp',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const headers = calls[0]!.init?.headers as Record<string, string>;
|
|
98
|
+
expect(headers.Authorization).toBe('Bearer did-auth-vp');
|
|
99
|
+
expect(headers['X-Auth-Token']).toBe(SENTINEL_TOKEN);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
[
|
|
104
|
+
'storeAuthShare',
|
|
105
|
+
() =>
|
|
106
|
+
client.storeAuthShare({
|
|
107
|
+
authShare: { encryptedData: 'share', encryptedDek: '', iv: '' },
|
|
108
|
+
primaryDid: 'did:key:zTest',
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
['addRecoveryMethod', () => client.addRecoveryMethod({ type: 'phrase' })],
|
|
112
|
+
['markMigrated', () => client.markMigrated()],
|
|
113
|
+
['activate', () => client.activate()],
|
|
114
|
+
['sendEmailBackupShare', () => client.sendEmailBackupShare('email-share')],
|
|
115
|
+
['addRecoveryEmail', () => client.addRecoveryEmail('recovery@example.com')],
|
|
116
|
+
['verifyRecoveryEmail', () => client.verifyRecoveryEmail('123456')],
|
|
117
|
+
['deleteUserKey', () => client.deleteUserKey()],
|
|
118
|
+
])('%s fails closed instead of calling a route without DID proof', async (_name, call) => {
|
|
119
|
+
await expect(call()).rejects.toThrow(DID_CHALLENGE_ERROR);
|
|
120
|
+
expect(calls).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
});
|