decane-connect-kit 2.11.0 → 2.12.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/index.cjs +355 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -1
- package/dist/index.d.ts +66 -1
- package/dist/index.js +349 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ __export(errors_exports, {
|
|
|
43
43
|
NetworkError: () => NetworkError,
|
|
44
44
|
NewDeviceError: () => NewDeviceError,
|
|
45
45
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
46
|
+
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
46
47
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
47
48
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
48
49
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
@@ -52,11 +53,13 @@ __export(errors_exports, {
|
|
|
52
53
|
ShareCorruptedError: () => ShareCorruptedError,
|
|
53
54
|
StaleDeviceShareError: () => StaleDeviceShareError,
|
|
54
55
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
56
|
+
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
55
57
|
UserCancelledError: () => UserCancelledError,
|
|
56
58
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
57
|
-
WrongPasswordError: () => WrongPasswordError
|
|
59
|
+
WrongPasswordError: () => WrongPasswordError,
|
|
60
|
+
WrongUnlockPasswordError: () => WrongUnlockPasswordError
|
|
58
61
|
});
|
|
59
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
62
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
60
63
|
var init_errors = __esm({
|
|
61
64
|
"src/social/errors.ts"() {
|
|
62
65
|
"use strict";
|
|
@@ -151,6 +154,21 @@ var init_errors = __esm({
|
|
|
151
154
|
super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
|
|
152
155
|
}
|
|
153
156
|
};
|
|
157
|
+
PromptPasswordNotConfiguredError = class extends DecaneConnectError {
|
|
158
|
+
constructor() {
|
|
159
|
+
super("PROMPT_PASSWORD_NOT_CONFIGURED", "This device unlocks with a password and no promptUnlockPassword callback was provided in config");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
WrongUnlockPasswordError = class extends DecaneConnectError {
|
|
163
|
+
constructor() {
|
|
164
|
+
super("WRONG_UNLOCK_PASSWORD", "That password did not unlock this device");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
UnlockTokenRejectedError = class extends DecaneConnectError {
|
|
168
|
+
constructor() {
|
|
169
|
+
super("UNLOCK_TOKEN_REJECTED", "This device's unlock token is no longer valid \u2014 sign in normally to re-enable password unlock");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
154
172
|
AttestationError = class extends DecaneConnectError {
|
|
155
173
|
constructor(msg) {
|
|
156
174
|
super("ATTESTATION_FAILED", msg);
|
|
@@ -206,9 +224,11 @@ __export(index_exports, {
|
|
|
206
224
|
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
207
225
|
InvalidPinError: () => InvalidPinError,
|
|
208
226
|
InvalidRecoveryFileError: () => InvalidRecoveryFileError,
|
|
227
|
+
MIN_PASSWORD_LENGTH: () => MIN_PASSWORD_LENGTH,
|
|
209
228
|
NetworkError: () => NetworkError,
|
|
210
229
|
NewDeviceError: () => NewDeviceError,
|
|
211
230
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
231
|
+
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
212
232
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
213
233
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
214
234
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
@@ -219,10 +239,13 @@ __export(index_exports, {
|
|
|
219
239
|
SocialWalletModal: () => SocialWalletModal,
|
|
220
240
|
StaleDeviceShareError: () => StaleDeviceShareError,
|
|
221
241
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
242
|
+
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
222
243
|
UserCancelledError: () => UserCancelledError,
|
|
223
244
|
WalletSelector: () => WalletSelector,
|
|
245
|
+
WeakPasswordError: () => WeakPasswordError,
|
|
224
246
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
225
247
|
WrongPasswordError: () => WrongPasswordError,
|
|
248
|
+
WrongUnlockPasswordError: () => WrongUnlockPasswordError,
|
|
226
249
|
createDecane: () => createDecane,
|
|
227
250
|
createDecaneConnect: () => createDecaneConnect,
|
|
228
251
|
createEthereumProvider: () => createEthereumProvider,
|
|
@@ -238,6 +261,7 @@ __export(index_exports, {
|
|
|
238
261
|
useSocialBalance: () => useSocialBalance,
|
|
239
262
|
useSocialWallet: () => useSocialWallet,
|
|
240
263
|
useWalletSelector: () => useWalletSelector,
|
|
264
|
+
validateUnlockPassword: () => validateUnlockPassword,
|
|
241
265
|
verifyEmailCode: () => verifyEmailCode
|
|
242
266
|
});
|
|
243
267
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2184,6 +2208,32 @@ var ApiClient = class {
|
|
|
2184
2208
|
body: JSON.stringify({ userId, response, origin })
|
|
2185
2209
|
});
|
|
2186
2210
|
}
|
|
2211
|
+
// ── Device unlock token (the "unlock with your password" credential) ──────
|
|
2212
|
+
/**
|
|
2213
|
+
* Issue this device an unlock token. Returned in the clear once and never
|
|
2214
|
+
* again — the backend stores only its hash — so the caller must encrypt and
|
|
2215
|
+
* persist it before the value is dropped.
|
|
2216
|
+
*/
|
|
2217
|
+
async issueUnlockToken(jwt) {
|
|
2218
|
+
return this._request("/auth/unlock-token", {
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
/** Redeem a decrypted unlock token for a session. */
|
|
2224
|
+
async exchangeUnlockToken(token) {
|
|
2225
|
+
return this._request("/auth/unlock-token/exchange", {
|
|
2226
|
+
method: "POST",
|
|
2227
|
+
body: JSON.stringify({ token })
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
/** Turn the password path off account-wide, so a lost device stops working. */
|
|
2231
|
+
async revokeUnlockTokens(jwt) {
|
|
2232
|
+
await this._request("/auth/unlock-token", {
|
|
2233
|
+
method: "DELETE",
|
|
2234
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2187
2237
|
async revoke(jwt) {
|
|
2188
2238
|
await this._request("/auth/revoke", {
|
|
2189
2239
|
method: "POST",
|
|
@@ -2832,16 +2882,86 @@ async function aesGcmDecrypt(key, ciphertext, iv) {
|
|
|
2832
2882
|
return new Uint8Array(pt);
|
|
2833
2883
|
}
|
|
2834
2884
|
|
|
2835
|
-
// src/social/crypto/
|
|
2885
|
+
// src/social/crypto/password.ts
|
|
2836
2886
|
var PBKDF2_ITERATIONS = 6e5;
|
|
2837
2887
|
function buf2(u) {
|
|
2838
2888
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2839
2889
|
}
|
|
2890
|
+
var MIN_PASSWORD_LENGTH = 12;
|
|
2891
|
+
var TOO_PREDICTABLE = [
|
|
2892
|
+
/^(.)\1+$/,
|
|
2893
|
+
// one repeated character
|
|
2894
|
+
/^0?123456789?0?1?2?$/,
|
|
2895
|
+
// ascending runs
|
|
2896
|
+
/^(?:password|passw0rd)/i,
|
|
2897
|
+
/^(?:qwerty|azerty|asdfgh)/i,
|
|
2898
|
+
/^(?:letmein|welcome|iloveyou|admin)/i
|
|
2899
|
+
];
|
|
2900
|
+
var WeakPasswordError = class extends Error {
|
|
2901
|
+
constructor(message) {
|
|
2902
|
+
super(message);
|
|
2903
|
+
this.name = "WeakPasswordError";
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
function validateUnlockPassword(password) {
|
|
2907
|
+
if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
|
|
2908
|
+
throw new WeakPasswordError(
|
|
2909
|
+
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
|
2910
|
+
);
|
|
2911
|
+
}
|
|
2912
|
+
if (/^\d+$/.test(password)) {
|
|
2913
|
+
throw new WeakPasswordError("Password cannot be only digits \u2014 add letters or symbols");
|
|
2914
|
+
}
|
|
2915
|
+
if (/^\s|\s$/.test(password)) {
|
|
2916
|
+
throw new WeakPasswordError("Password cannot start or end with a space");
|
|
2917
|
+
}
|
|
2918
|
+
if (TOO_PREDICTABLE.some((re) => re.test(password))) {
|
|
2919
|
+
throw new WeakPasswordError("Password is too easy to guess \u2014 choose something less common");
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
async function derivePasswordKeys(password, salt) {
|
|
2923
|
+
const base = await crypto.subtle.importKey(
|
|
2924
|
+
"raw",
|
|
2925
|
+
buf2(new TextEncoder().encode(password)),
|
|
2926
|
+
"PBKDF2",
|
|
2927
|
+
false,
|
|
2928
|
+
["deriveBits"]
|
|
2929
|
+
);
|
|
2930
|
+
const master = await crypto.subtle.deriveBits(
|
|
2931
|
+
{ name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
2932
|
+
base,
|
|
2933
|
+
256
|
|
2934
|
+
);
|
|
2935
|
+
const hkdfKey = await crypto.subtle.importKey("raw", master, "HKDF", false, ["deriveKey"]);
|
|
2936
|
+
const expand = (label) => crypto.subtle.deriveKey(
|
|
2937
|
+
{
|
|
2938
|
+
name: "HKDF",
|
|
2939
|
+
hash: "SHA-256",
|
|
2940
|
+
salt: buf2(salt),
|
|
2941
|
+
info: buf2(new TextEncoder().encode(label))
|
|
2942
|
+
},
|
|
2943
|
+
hkdfKey,
|
|
2944
|
+
{ name: "AES-GCM", length: 256 },
|
|
2945
|
+
false,
|
|
2946
|
+
["encrypt", "decrypt"]
|
|
2947
|
+
);
|
|
2948
|
+
const [shareKey, tokenKey] = await Promise.all([
|
|
2949
|
+
expand("decane:unlock:share"),
|
|
2950
|
+
expand("decane:unlock:token")
|
|
2951
|
+
]);
|
|
2952
|
+
return { shareKey, tokenKey };
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/social/crypto/pin.ts
|
|
2956
|
+
var PBKDF2_ITERATIONS2 = 6e5;
|
|
2957
|
+
function buf3(u) {
|
|
2958
|
+
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2959
|
+
}
|
|
2840
2960
|
async function deriveKeyFromPin(pin, salt) {
|
|
2841
2961
|
const pinBytes = new TextEncoder().encode(pin);
|
|
2842
|
-
const base = await crypto.subtle.importKey("raw",
|
|
2962
|
+
const base = await crypto.subtle.importKey("raw", buf3(pinBytes), "PBKDF2", false, ["deriveKey"]);
|
|
2843
2963
|
return crypto.subtle.deriveKey(
|
|
2844
|
-
{ name: "PBKDF2", salt:
|
|
2964
|
+
{ name: "PBKDF2", salt: buf3(salt), iterations: PBKDF2_ITERATIONS2, hash: "SHA-256" },
|
|
2845
2965
|
base,
|
|
2846
2966
|
{ name: "AES-GCM", length: 256 },
|
|
2847
2967
|
false,
|
|
@@ -2858,7 +2978,7 @@ init_errors();
|
|
|
2858
2978
|
function fromB64u(s) {
|
|
2859
2979
|
return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
2860
2980
|
}
|
|
2861
|
-
function
|
|
2981
|
+
function buf4(u) {
|
|
2862
2982
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2863
2983
|
}
|
|
2864
2984
|
function isPasskeySupported() {
|
|
@@ -2889,7 +3009,7 @@ async function authenticateWithPasskey(opts) {
|
|
|
2889
3009
|
...opts.optionsJSON,
|
|
2890
3010
|
extensions: {
|
|
2891
3011
|
...opts.optionsJSON.extensions,
|
|
2892
|
-
prf: { eval: { first:
|
|
3012
|
+
prf: { eval: { first: buf4(opts.prfSalt) } }
|
|
2893
3013
|
}
|
|
2894
3014
|
};
|
|
2895
3015
|
let response;
|
|
@@ -2913,11 +3033,11 @@ async function getPrfOutput(opts) {
|
|
|
2913
3033
|
try {
|
|
2914
3034
|
cred = await navigator.credentials.get({
|
|
2915
3035
|
publicKey: {
|
|
2916
|
-
challenge:
|
|
3036
|
+
challenge: buf4(challenge),
|
|
2917
3037
|
rpId: opts.rpId,
|
|
2918
|
-
allowCredentials: [{ id:
|
|
3038
|
+
allowCredentials: [{ id: buf4(opts.credentialId), type: "public-key" }],
|
|
2919
3039
|
userVerification: "required",
|
|
2920
|
-
extensions: { prf: { eval: { first:
|
|
3040
|
+
extensions: { prf: { eval: { first: buf4(opts.prfSalt) } } }
|
|
2921
3041
|
}
|
|
2922
3042
|
});
|
|
2923
3043
|
} catch (e) {
|
|
@@ -3284,7 +3404,7 @@ async function fetchWrappedShareBackup(api, jwt) {
|
|
|
3284
3404
|
|
|
3285
3405
|
// src/social/recovery/recoveryShareFile.ts
|
|
3286
3406
|
init_errors();
|
|
3287
|
-
var
|
|
3407
|
+
var PBKDF2_ITERATIONS3 = 6e5;
|
|
3288
3408
|
var FILE_VERSION = 2;
|
|
3289
3409
|
function b642(u) {
|
|
3290
3410
|
let s = "";
|
|
@@ -3294,7 +3414,7 @@ function b642(u) {
|
|
|
3294
3414
|
function unb642(s) {
|
|
3295
3415
|
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
3296
3416
|
}
|
|
3297
|
-
function
|
|
3417
|
+
function buf5(u) {
|
|
3298
3418
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
3299
3419
|
}
|
|
3300
3420
|
async function deriveFileKey(password, salt, iterations) {
|
|
@@ -3306,7 +3426,7 @@ async function deriveFileKey(password, salt, iterations) {
|
|
|
3306
3426
|
["deriveKey"]
|
|
3307
3427
|
);
|
|
3308
3428
|
return crypto.subtle.deriveKey(
|
|
3309
|
-
{ name: "PBKDF2", salt:
|
|
3429
|
+
{ name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
|
|
3310
3430
|
base,
|
|
3311
3431
|
{ name: "AES-GCM", length: 256 },
|
|
3312
3432
|
false,
|
|
@@ -3321,16 +3441,16 @@ async function buildRecoveryShareFile(share, password, addresses, shareSetVersio
|
|
|
3321
3441
|
}
|
|
3322
3442
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
3323
3443
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3324
|
-
const key = await deriveFileKey(password, salt,
|
|
3444
|
+
const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS3);
|
|
3325
3445
|
const ct = new Uint8Array(
|
|
3326
|
-
await crypto.subtle.encrypt({ name: "AES-GCM", iv:
|
|
3446
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(share))
|
|
3327
3447
|
);
|
|
3328
3448
|
return {
|
|
3329
3449
|
version: FILE_VERSION,
|
|
3330
3450
|
type: "decane-recovery-share",
|
|
3331
3451
|
shareSetVersion,
|
|
3332
3452
|
kdf: "pbkdf2-sha256",
|
|
3333
|
-
kdfParams: { iterations:
|
|
3453
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b642(salt) },
|
|
3334
3454
|
cipher: "aes-256-gcm",
|
|
3335
3455
|
iv: b642(iv),
|
|
3336
3456
|
ciphertext: b642(ct),
|
|
@@ -3361,7 +3481,7 @@ async function decryptRecoveryShareFile(file, password) {
|
|
|
3361
3481
|
let share;
|
|
3362
3482
|
try {
|
|
3363
3483
|
share = new Uint8Array(
|
|
3364
|
-
await crypto.subtle.decrypt({ name: "AES-GCM", iv:
|
|
3484
|
+
await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(ct))
|
|
3365
3485
|
);
|
|
3366
3486
|
} catch {
|
|
3367
3487
|
throw new WrongPasswordError();
|
|
@@ -3436,14 +3556,14 @@ async function recoverDeviceShare(input) {
|
|
|
3436
3556
|
|
|
3437
3557
|
// src/social/recovery/portableBackup.ts
|
|
3438
3558
|
init_errors();
|
|
3439
|
-
var
|
|
3559
|
+
var PBKDF2_ITERATIONS4 = 6e5;
|
|
3440
3560
|
var FILE_VERSION2 = 1;
|
|
3441
3561
|
function b643(u) {
|
|
3442
3562
|
let s = "";
|
|
3443
3563
|
for (const b of u) s += String.fromCharCode(b);
|
|
3444
3564
|
return btoa(s);
|
|
3445
3565
|
}
|
|
3446
|
-
function
|
|
3566
|
+
function buf6(u) {
|
|
3447
3567
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
3448
3568
|
}
|
|
3449
3569
|
async function deriveFileKey2(password, salt, iterations) {
|
|
@@ -3455,7 +3575,7 @@ async function deriveFileKey2(password, salt, iterations) {
|
|
|
3455
3575
|
["deriveKey"]
|
|
3456
3576
|
);
|
|
3457
3577
|
return crypto.subtle.deriveKey(
|
|
3458
|
-
{ name: "PBKDF2", salt:
|
|
3578
|
+
{ name: "PBKDF2", salt: buf6(salt), iterations, hash: "SHA-256" },
|
|
3459
3579
|
base,
|
|
3460
3580
|
{ name: "AES-GCM", length: 256 },
|
|
3461
3581
|
false,
|
|
@@ -3475,16 +3595,16 @@ async function buildPortableBackup(deviceShare, recoveryShare, password, address
|
|
|
3475
3595
|
);
|
|
3476
3596
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
3477
3597
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3478
|
-
const key = await deriveFileKey2(password, salt,
|
|
3598
|
+
const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS4);
|
|
3479
3599
|
const ct = new Uint8Array(
|
|
3480
|
-
await crypto.subtle.encrypt({ name: "AES-GCM", iv:
|
|
3600
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf6(iv) }, key, buf6(plaintext))
|
|
3481
3601
|
);
|
|
3482
3602
|
return {
|
|
3483
3603
|
version: FILE_VERSION2,
|
|
3484
3604
|
type: "decane-portable-backup",
|
|
3485
3605
|
shareSetVersion,
|
|
3486
3606
|
kdf: "pbkdf2-sha256",
|
|
3487
|
-
kdfParams: { iterations:
|
|
3607
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS4, salt: b643(salt) },
|
|
3488
3608
|
cipher: "aes-256-gcm",
|
|
3489
3609
|
iv: b643(iv),
|
|
3490
3610
|
ciphertext: b643(ct),
|
|
@@ -3691,7 +3811,24 @@ async function createDecaneConnect(config) {
|
|
|
3691
3811
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3692
3812
|
await storage.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3693
3813
|
}
|
|
3814
|
+
function hasPasswordUnlock(stored) {
|
|
3815
|
+
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3816
|
+
}
|
|
3817
|
+
async function passwordDeviceCandidate() {
|
|
3818
|
+
if (persisted?.userId) {
|
|
3819
|
+
const stored = await storage.getShare(persisted.userId);
|
|
3820
|
+
return hasPasswordUnlock(stored) ? stored : null;
|
|
3821
|
+
}
|
|
3822
|
+
const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
|
|
3823
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
3824
|
+
}
|
|
3694
3825
|
async function getWrappingKey(stored) {
|
|
3826
|
+
if (stored.unlockMethod === "password") {
|
|
3827
|
+
if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
|
|
3828
|
+
const password = await config.promptUnlockPassword({ kind: "unlock" });
|
|
3829
|
+
const { shareKey } = await derivePasswordKeys(password, stored.passwordSalt);
|
|
3830
|
+
return shareKey;
|
|
3831
|
+
}
|
|
3695
3832
|
if (stored.unlockMethod === "passkey") {
|
|
3696
3833
|
const prfOutput = await getPrfOutput({
|
|
3697
3834
|
rpId,
|
|
@@ -4222,6 +4359,127 @@ async function createDecaneConnect(config) {
|
|
|
4222
4359
|
if (!candidate) throw new NewDeviceError();
|
|
4223
4360
|
return passkeyLogin(candidate);
|
|
4224
4361
|
},
|
|
4362
|
+
/**
|
|
4363
|
+
* Turn on "unlock with your password" for this device.
|
|
4364
|
+
*
|
|
4365
|
+
* Re-wraps the device share under the password and stores an unlock token
|
|
4366
|
+
* encrypted beside it, replacing whatever wrapping was there. Requires a
|
|
4367
|
+
* live session, because issuing the token needs a JWT.
|
|
4368
|
+
*
|
|
4369
|
+
* The share is only overwritten once the new ciphertext is in hand, so a
|
|
4370
|
+
* failure part-way leaves the device exactly as it was rather than holding
|
|
4371
|
+
* a share nothing can open.
|
|
4372
|
+
*/
|
|
4373
|
+
async setUnlockPassword(password) {
|
|
4374
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4375
|
+
validateUnlockPassword(password);
|
|
4376
|
+
const stored = await storage.getShare(auth.userId);
|
|
4377
|
+
if (!stored) throw new NewDeviceError();
|
|
4378
|
+
const deviceShare = await aesGcmDecrypt(
|
|
4379
|
+
await getWrappingKey(stored),
|
|
4380
|
+
stored.wrappedDeviceShare,
|
|
4381
|
+
stored.iv
|
|
4382
|
+
);
|
|
4383
|
+
try {
|
|
4384
|
+
const passwordSalt = generateRandomBytes(16);
|
|
4385
|
+
const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
|
|
4386
|
+
const { token } = await api.issueUnlockToken(auth.jwt);
|
|
4387
|
+
const unlockTokenIv = generateRandomBytes(12);
|
|
4388
|
+
const wrappedUnlockToken = await aesGcmEncrypt(
|
|
4389
|
+
tokenKey,
|
|
4390
|
+
new TextEncoder().encode(token),
|
|
4391
|
+
unlockTokenIv
|
|
4392
|
+
);
|
|
4393
|
+
const iv = generateRandomBytes(12);
|
|
4394
|
+
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4395
|
+
await storage.putShare({
|
|
4396
|
+
...stored,
|
|
4397
|
+
unlockMethod: "password",
|
|
4398
|
+
wrappedDeviceShare,
|
|
4399
|
+
iv,
|
|
4400
|
+
passwordSalt,
|
|
4401
|
+
passwordIterations: PBKDF2_ITERATIONS,
|
|
4402
|
+
wrappedUnlockToken,
|
|
4403
|
+
unlockTokenIv,
|
|
4404
|
+
// The old wrapping's metadata would otherwise sit there describing a
|
|
4405
|
+
// key that no longer opens anything.
|
|
4406
|
+
credentialId: void 0,
|
|
4407
|
+
prfSalt: void 0,
|
|
4408
|
+
pinSalt: void 0,
|
|
4409
|
+
pinIterations: void 0
|
|
4410
|
+
});
|
|
4411
|
+
} finally {
|
|
4412
|
+
zeroize(deviceShare);
|
|
4413
|
+
}
|
|
4414
|
+
},
|
|
4415
|
+
/** True when this device can open a session from a password alone. */
|
|
4416
|
+
async canUnlockWithPassword() {
|
|
4417
|
+
return await passwordDeviceCandidate() !== null;
|
|
4418
|
+
},
|
|
4419
|
+
/**
|
|
4420
|
+
* Open a session from the password alone — no identity provider, no passkey.
|
|
4421
|
+
*
|
|
4422
|
+
* The password decrypts the unlock token, the token buys a JWT, and the
|
|
4423
|
+
* same password unwraps the device share. Both halves come from one
|
|
4424
|
+
* derivation, so one prompt does the whole job.
|
|
4425
|
+
*/
|
|
4426
|
+
async unlockWithPassword(password) {
|
|
4427
|
+
const stored = await passwordDeviceCandidate();
|
|
4428
|
+
if (!stored) throw new NewDeviceError();
|
|
4429
|
+
events.emit("wallet-unlocking");
|
|
4430
|
+
const { shareKey, tokenKey } = await derivePasswordKeys(password, stored.passwordSalt);
|
|
4431
|
+
let token;
|
|
4432
|
+
try {
|
|
4433
|
+
token = new TextDecoder().decode(
|
|
4434
|
+
await aesGcmDecrypt(tokenKey, stored.wrappedUnlockToken, stored.unlockTokenIv)
|
|
4435
|
+
);
|
|
4436
|
+
} catch {
|
|
4437
|
+
throw new WrongUnlockPasswordError();
|
|
4438
|
+
}
|
|
4439
|
+
let jwt;
|
|
4440
|
+
try {
|
|
4441
|
+
({ jwt } = await api.exchangeUnlockToken(token));
|
|
4442
|
+
} catch (e) {
|
|
4443
|
+
if (e instanceof DecaneConnectError && e.code === "NETWORK") throw e;
|
|
4444
|
+
throw new UnlockTokenRejectedError();
|
|
4445
|
+
}
|
|
4446
|
+
const deviceShare = await aesGcmDecrypt(shareKey, stored.wrappedDeviceShare, stored.iv);
|
|
4447
|
+
const rawAddresses = await api.getAddresses(jwt);
|
|
4448
|
+
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? {
|
|
4449
|
+
evm: rawAddresses.evm,
|
|
4450
|
+
solana: rawAddresses.solana,
|
|
4451
|
+
tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm)
|
|
4452
|
+
} : null;
|
|
4453
|
+
const addresses = await session.start(deviceShare, jwt, recordedAddresses);
|
|
4454
|
+
zeroize(deviceShare);
|
|
4455
|
+
auth = { jwt, userId: stored.userId, addresses };
|
|
4456
|
+
persisted = { userId: stored.userId, addresses };
|
|
4457
|
+
savePersisted(PERSIST_KEY, persisted);
|
|
4458
|
+
events.emit("connected");
|
|
4459
|
+
persistLiveSession();
|
|
4460
|
+
return {
|
|
4461
|
+
addresses,
|
|
4462
|
+
isNewUser: false,
|
|
4463
|
+
...stored.profile ? { profile: stored.profile } : {}
|
|
4464
|
+
};
|
|
4465
|
+
},
|
|
4466
|
+
/**
|
|
4467
|
+
* Turn the password path off.
|
|
4468
|
+
*
|
|
4469
|
+
* Revokes the token account-wide first — that is what makes a lost device
|
|
4470
|
+
* safe, and it must happen even if this device's local cleanup fails.
|
|
4471
|
+
*/
|
|
4472
|
+
async removeUnlockPassword() {
|
|
4473
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4474
|
+
await api.revokeUnlockTokens(auth.jwt);
|
|
4475
|
+
const stored = await storage.getShare(auth.userId);
|
|
4476
|
+
if (!stored || stored.unlockMethod !== "password") return;
|
|
4477
|
+
await storage.putShare({
|
|
4478
|
+
...stored,
|
|
4479
|
+
wrappedUnlockToken: void 0,
|
|
4480
|
+
unlockTokenIv: void 0
|
|
4481
|
+
});
|
|
4482
|
+
},
|
|
4225
4483
|
async hasPasskey() {
|
|
4226
4484
|
if (!auth) return false;
|
|
4227
4485
|
const stored = await storage.getShare(auth.userId);
|
|
@@ -4565,8 +4823,8 @@ async function createDecaneConnect(config) {
|
|
|
4565
4823
|
};
|
|
4566
4824
|
return client;
|
|
4567
4825
|
}
|
|
4568
|
-
function bufferToB64u(
|
|
4569
|
-
return btoa(String.fromCharCode(...
|
|
4826
|
+
function bufferToB64u(buf7) {
|
|
4827
|
+
return btoa(String.fromCharCode(...buf7)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
4570
4828
|
}
|
|
4571
4829
|
function b64uToBuffer(b64u) {
|
|
4572
4830
|
const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -7023,6 +7281,26 @@ function DecaneKit({ config, children }) {
|
|
|
7023
7281
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
7024
7282
|
return sdk.addPasskey();
|
|
7025
7283
|
}, []);
|
|
7284
|
+
const socialSetUnlockPassword = (0, import_react5.useCallback)(async (password) => {
|
|
7285
|
+
const sdk = socialRef.current;
|
|
7286
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7287
|
+
return sdk.setUnlockPassword(password);
|
|
7288
|
+
}, []);
|
|
7289
|
+
const socialCanUnlockWithPassword = (0, import_react5.useCallback)(async () => {
|
|
7290
|
+
const sdk = socialRef.current;
|
|
7291
|
+
if (!sdk) return false;
|
|
7292
|
+
return sdk.canUnlockWithPassword();
|
|
7293
|
+
}, []);
|
|
7294
|
+
const socialUnlockWithPassword = (0, import_react5.useCallback)(async (password) => {
|
|
7295
|
+
const sdk = socialRef.current;
|
|
7296
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7297
|
+
applyResult(await sdk.unlockWithPassword(password));
|
|
7298
|
+
}, []);
|
|
7299
|
+
const socialRemoveUnlockPassword = (0, import_react5.useCallback)(async () => {
|
|
7300
|
+
const sdk = socialRef.current;
|
|
7301
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7302
|
+
return sdk.removeUnlockPassword();
|
|
7303
|
+
}, []);
|
|
7026
7304
|
const socialHasRecoveryShare = (0, import_react5.useCallback)(async () => {
|
|
7027
7305
|
const sdk = socialRef.current;
|
|
7028
7306
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7086,6 +7364,10 @@ function DecaneKit({ config, children }) {
|
|
|
7086
7364
|
socialHasRecoveryShare,
|
|
7087
7365
|
socialHasPasskey,
|
|
7088
7366
|
socialAddPasskey,
|
|
7367
|
+
socialSetUnlockPassword,
|
|
7368
|
+
socialCanUnlockWithPassword,
|
|
7369
|
+
socialUnlockWithPassword,
|
|
7370
|
+
socialRemoveUnlockPassword,
|
|
7089
7371
|
socialShouldPromptRecovery,
|
|
7090
7372
|
socialDismissRecoveryPrompt,
|
|
7091
7373
|
socialModalOpen,
|
|
@@ -7372,6 +7654,10 @@ function useSocialWallet() {
|
|
|
7372
7654
|
socialHasRecoveryShare,
|
|
7373
7655
|
socialHasPasskey,
|
|
7374
7656
|
socialAddPasskey,
|
|
7657
|
+
socialSetUnlockPassword,
|
|
7658
|
+
socialCanUnlockWithPassword,
|
|
7659
|
+
socialUnlockWithPassword,
|
|
7660
|
+
socialRemoveUnlockPassword,
|
|
7375
7661
|
socialShouldPromptRecovery,
|
|
7376
7662
|
socialDismissRecoveryPrompt,
|
|
7377
7663
|
openSocialModal,
|
|
@@ -7469,6 +7755,37 @@ function useSocialWallet() {
|
|
|
7469
7755
|
throw e;
|
|
7470
7756
|
}
|
|
7471
7757
|
}, [socialAddPasskey]);
|
|
7758
|
+
const setUnlockPassword = (0, import_react6.useCallback)(async (password) => {
|
|
7759
|
+
setError(null);
|
|
7760
|
+
try {
|
|
7761
|
+
await socialSetUnlockPassword(password);
|
|
7762
|
+
} catch (e) {
|
|
7763
|
+
setError(e.message);
|
|
7764
|
+
throw e;
|
|
7765
|
+
}
|
|
7766
|
+
}, [socialSetUnlockPassword]);
|
|
7767
|
+
const canUnlockWithPassword = (0, import_react6.useCallback)(
|
|
7768
|
+
() => socialCanUnlockWithPassword(),
|
|
7769
|
+
[socialCanUnlockWithPassword]
|
|
7770
|
+
);
|
|
7771
|
+
const unlockWithPassword = (0, import_react6.useCallback)(async (password) => {
|
|
7772
|
+
setError(null);
|
|
7773
|
+
try {
|
|
7774
|
+
await socialUnlockWithPassword(password);
|
|
7775
|
+
} catch (e) {
|
|
7776
|
+
setError(e.message);
|
|
7777
|
+
throw e;
|
|
7778
|
+
}
|
|
7779
|
+
}, [socialUnlockWithPassword]);
|
|
7780
|
+
const removeUnlockPassword = (0, import_react6.useCallback)(async () => {
|
|
7781
|
+
setError(null);
|
|
7782
|
+
try {
|
|
7783
|
+
await socialRemoveUnlockPassword();
|
|
7784
|
+
} catch (e) {
|
|
7785
|
+
setError(e.message);
|
|
7786
|
+
throw e;
|
|
7787
|
+
}
|
|
7788
|
+
}, [socialRemoveUnlockPassword]);
|
|
7472
7789
|
const hasRecoveryShare = (0, import_react6.useCallback)(
|
|
7473
7790
|
() => run(() => socialHasRecoveryShare()),
|
|
7474
7791
|
[socialHasRecoveryShare]
|
|
@@ -7525,6 +7842,13 @@ function useSocialWallet() {
|
|
|
7525
7842
|
// Passkey management for THIS device
|
|
7526
7843
|
hasPasskey,
|
|
7527
7844
|
addPasskey,
|
|
7845
|
+
// "Unlock with your password": setUnlockPassword needs a live session;
|
|
7846
|
+
// canUnlockWithPassword and unlockWithPassword are the signed-out pair that
|
|
7847
|
+
// open one without an identity provider or a passkey.
|
|
7848
|
+
setUnlockPassword,
|
|
7849
|
+
canUnlockWithPassword,
|
|
7850
|
+
unlockWithPassword,
|
|
7851
|
+
removeUnlockPassword,
|
|
7528
7852
|
// Recovery (2-of-3 Shamir share set)
|
|
7529
7853
|
rotateShares,
|
|
7530
7854
|
exportPortableBackup,
|
|
@@ -7834,9 +8158,11 @@ function useSignTypedData() {
|
|
|
7834
8158
|
InvalidApiKeyError,
|
|
7835
8159
|
InvalidPinError,
|
|
7836
8160
|
InvalidRecoveryFileError,
|
|
8161
|
+
MIN_PASSWORD_LENGTH,
|
|
7837
8162
|
NetworkError,
|
|
7838
8163
|
NewDeviceError,
|
|
7839
8164
|
OriginNotAllowedError,
|
|
8165
|
+
PromptPasswordNotConfiguredError,
|
|
7840
8166
|
PromptPinNotConfiguredError,
|
|
7841
8167
|
ProvisioningTimeoutError,
|
|
7842
8168
|
ReconnectRequiredError,
|
|
@@ -7847,10 +8173,13 @@ function useSignTypedData() {
|
|
|
7847
8173
|
SocialWalletModal,
|
|
7848
8174
|
StaleDeviceShareError,
|
|
7849
8175
|
UnlockRequiredError,
|
|
8176
|
+
UnlockTokenRejectedError,
|
|
7850
8177
|
UserCancelledError,
|
|
7851
8178
|
WalletSelector,
|
|
8179
|
+
WeakPasswordError,
|
|
7852
8180
|
WebAuthnNotSupportedError,
|
|
7853
8181
|
WrongPasswordError,
|
|
8182
|
+
WrongUnlockPasswordError,
|
|
7854
8183
|
createDecane,
|
|
7855
8184
|
createDecaneConnect,
|
|
7856
8185
|
createEthereumProvider,
|
|
@@ -7866,6 +8195,7 @@ function useSignTypedData() {
|
|
|
7866
8195
|
useSocialBalance,
|
|
7867
8196
|
useSocialWallet,
|
|
7868
8197
|
useWalletSelector,
|
|
8198
|
+
validateUnlockPassword,
|
|
7869
8199
|
verifyEmailCode
|
|
7870
8200
|
});
|
|
7871
8201
|
//# sourceMappingURL=index.cjs.map
|