decane-connect-kit 2.10.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 +376 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -4
- package/dist/index.d.ts +75 -4
- package/dist/index.js +370 -26
- 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,
|
|
@@ -4041,6 +4178,16 @@ async function createDecaneConnect(config) {
|
|
|
4041
4178
|
const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4042
4179
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4043
4180
|
}
|
|
4181
|
+
async function rememberProfile(userId, profile) {
|
|
4182
|
+
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4183
|
+
if (!Object.keys(incoming).length) return;
|
|
4184
|
+
try {
|
|
4185
|
+
const stored = await storage.getShare(userId);
|
|
4186
|
+
if (!stored) return;
|
|
4187
|
+
await storage.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4188
|
+
} catch {
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4044
4191
|
function usablePasskeyShare(stored) {
|
|
4045
4192
|
if (!stored || stored.unlockMethod !== "passkey") return null;
|
|
4046
4193
|
if (!stored.credentialId || !stored.prfSalt) return null;
|
|
@@ -4078,7 +4225,11 @@ async function createDecaneConnect(config) {
|
|
|
4078
4225
|
savePersisted(PERSIST_KEY, persisted);
|
|
4079
4226
|
events.emit("connected");
|
|
4080
4227
|
persistLiveSession();
|
|
4081
|
-
return {
|
|
4228
|
+
return {
|
|
4229
|
+
addresses,
|
|
4230
|
+
isNewUser: false,
|
|
4231
|
+
...stored.profile ? { profile: stored.profile } : {}
|
|
4232
|
+
};
|
|
4082
4233
|
}
|
|
4083
4234
|
async function handleAuthResult(result) {
|
|
4084
4235
|
let addresses;
|
|
@@ -4094,6 +4245,7 @@ async function createDecaneConnect(config) {
|
|
|
4094
4245
|
savePersisted(PERSIST_KEY, persisted);
|
|
4095
4246
|
events.emit("connected");
|
|
4096
4247
|
persistLiveSession();
|
|
4248
|
+
if (result.profile) await rememberProfile(result.userId, result.profile);
|
|
4097
4249
|
return {
|
|
4098
4250
|
addresses,
|
|
4099
4251
|
isNewUser: result.isNewUser,
|
|
@@ -4207,6 +4359,127 @@ async function createDecaneConnect(config) {
|
|
|
4207
4359
|
if (!candidate) throw new NewDeviceError();
|
|
4208
4360
|
return passkeyLogin(candidate);
|
|
4209
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
|
+
},
|
|
4210
4483
|
async hasPasskey() {
|
|
4211
4484
|
if (!auth) return false;
|
|
4212
4485
|
const stored = await storage.getShare(auth.userId);
|
|
@@ -4550,8 +4823,8 @@ async function createDecaneConnect(config) {
|
|
|
4550
4823
|
};
|
|
4551
4824
|
return client;
|
|
4552
4825
|
}
|
|
4553
|
-
function bufferToB64u(
|
|
4554
|
-
return btoa(String.fromCharCode(...
|
|
4826
|
+
function bufferToB64u(buf7) {
|
|
4827
|
+
return btoa(String.fromCharCode(...buf7)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
4555
4828
|
}
|
|
4556
4829
|
function b64uToBuffer(b64u) {
|
|
4557
4830
|
const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -7008,6 +7281,26 @@ function DecaneKit({ config, children }) {
|
|
|
7008
7281
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
7009
7282
|
return sdk.addPasskey();
|
|
7010
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
|
+
}, []);
|
|
7011
7304
|
const socialHasRecoveryShare = (0, import_react5.useCallback)(async () => {
|
|
7012
7305
|
const sdk = socialRef.current;
|
|
7013
7306
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7071,6 +7364,10 @@ function DecaneKit({ config, children }) {
|
|
|
7071
7364
|
socialHasRecoveryShare,
|
|
7072
7365
|
socialHasPasskey,
|
|
7073
7366
|
socialAddPasskey,
|
|
7367
|
+
socialSetUnlockPassword,
|
|
7368
|
+
socialCanUnlockWithPassword,
|
|
7369
|
+
socialUnlockWithPassword,
|
|
7370
|
+
socialRemoveUnlockPassword,
|
|
7074
7371
|
socialShouldPromptRecovery,
|
|
7075
7372
|
socialDismissRecoveryPrompt,
|
|
7076
7373
|
socialModalOpen,
|
|
@@ -7300,6 +7597,11 @@ function useSocialAuth() {
|
|
|
7300
7597
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
7301
7598
|
// True when this device can skip the full sign-in and resume with a passkey.
|
|
7302
7599
|
canUsePasskey: socialAuth.canUsePasskey,
|
|
7600
|
+
// Who the provider said the user is — { name?, email?, picture? }, or null.
|
|
7601
|
+
// A passkey sign-in returns the profile recorded on this device when the
|
|
7602
|
+
// user first signed in, so it carries the same name and email a Google
|
|
7603
|
+
// sign-in does rather than coming back anonymous.
|
|
7604
|
+
profile: socialAuth.profile,
|
|
7303
7605
|
// Long-running work in progress, for driving your own status UI.
|
|
7304
7606
|
phase: socialAuth.phase,
|
|
7305
7607
|
isCreatingWallet: socialAuth.phase === "creating",
|
|
@@ -7352,6 +7654,10 @@ function useSocialWallet() {
|
|
|
7352
7654
|
socialHasRecoveryShare,
|
|
7353
7655
|
socialHasPasskey,
|
|
7354
7656
|
socialAddPasskey,
|
|
7657
|
+
socialSetUnlockPassword,
|
|
7658
|
+
socialCanUnlockWithPassword,
|
|
7659
|
+
socialUnlockWithPassword,
|
|
7660
|
+
socialRemoveUnlockPassword,
|
|
7355
7661
|
socialShouldPromptRecovery,
|
|
7356
7662
|
socialDismissRecoveryPrompt,
|
|
7357
7663
|
openSocialModal,
|
|
@@ -7449,6 +7755,37 @@ function useSocialWallet() {
|
|
|
7449
7755
|
throw e;
|
|
7450
7756
|
}
|
|
7451
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]);
|
|
7452
7789
|
const hasRecoveryShare = (0, import_react6.useCallback)(
|
|
7453
7790
|
() => run(() => socialHasRecoveryShare()),
|
|
7454
7791
|
[socialHasRecoveryShare]
|
|
@@ -7505,6 +7842,13 @@ function useSocialWallet() {
|
|
|
7505
7842
|
// Passkey management for THIS device
|
|
7506
7843
|
hasPasskey,
|
|
7507
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,
|
|
7508
7852
|
// Recovery (2-of-3 Shamir share set)
|
|
7509
7853
|
rotateShares,
|
|
7510
7854
|
exportPortableBackup,
|
|
@@ -7814,9 +8158,11 @@ function useSignTypedData() {
|
|
|
7814
8158
|
InvalidApiKeyError,
|
|
7815
8159
|
InvalidPinError,
|
|
7816
8160
|
InvalidRecoveryFileError,
|
|
8161
|
+
MIN_PASSWORD_LENGTH,
|
|
7817
8162
|
NetworkError,
|
|
7818
8163
|
NewDeviceError,
|
|
7819
8164
|
OriginNotAllowedError,
|
|
8165
|
+
PromptPasswordNotConfiguredError,
|
|
7820
8166
|
PromptPinNotConfiguredError,
|
|
7821
8167
|
ProvisioningTimeoutError,
|
|
7822
8168
|
ReconnectRequiredError,
|
|
@@ -7827,10 +8173,13 @@ function useSignTypedData() {
|
|
|
7827
8173
|
SocialWalletModal,
|
|
7828
8174
|
StaleDeviceShareError,
|
|
7829
8175
|
UnlockRequiredError,
|
|
8176
|
+
UnlockTokenRejectedError,
|
|
7830
8177
|
UserCancelledError,
|
|
7831
8178
|
WalletSelector,
|
|
8179
|
+
WeakPasswordError,
|
|
7832
8180
|
WebAuthnNotSupportedError,
|
|
7833
8181
|
WrongPasswordError,
|
|
8182
|
+
WrongUnlockPasswordError,
|
|
7834
8183
|
createDecane,
|
|
7835
8184
|
createDecaneConnect,
|
|
7836
8185
|
createEthereumProvider,
|
|
@@ -7846,6 +8195,7 @@ function useSignTypedData() {
|
|
|
7846
8195
|
useSocialBalance,
|
|
7847
8196
|
useSocialWallet,
|
|
7848
8197
|
useWalletSelector,
|
|
8198
|
+
validateUnlockPassword,
|
|
7849
8199
|
verifyEmailCode
|
|
7850
8200
|
});
|
|
7851
8201
|
//# sourceMappingURL=index.cjs.map
|