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.js
CHANGED
|
@@ -21,6 +21,7 @@ __export(errors_exports, {
|
|
|
21
21
|
NetworkError: () => NetworkError,
|
|
22
22
|
NewDeviceError: () => NewDeviceError,
|
|
23
23
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
24
|
+
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
24
25
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
25
26
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
26
27
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
@@ -30,11 +31,13 @@ __export(errors_exports, {
|
|
|
30
31
|
ShareCorruptedError: () => ShareCorruptedError,
|
|
31
32
|
StaleDeviceShareError: () => StaleDeviceShareError,
|
|
32
33
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
34
|
+
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
33
35
|
UserCancelledError: () => UserCancelledError,
|
|
34
36
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
35
|
-
WrongPasswordError: () => WrongPasswordError
|
|
37
|
+
WrongPasswordError: () => WrongPasswordError,
|
|
38
|
+
WrongUnlockPasswordError: () => WrongUnlockPasswordError
|
|
36
39
|
});
|
|
37
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
40
|
+
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;
|
|
38
41
|
var init_errors = __esm({
|
|
39
42
|
"src/social/errors.ts"() {
|
|
40
43
|
"use strict";
|
|
@@ -129,6 +132,21 @@ var init_errors = __esm({
|
|
|
129
132
|
super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
|
|
130
133
|
}
|
|
131
134
|
};
|
|
135
|
+
PromptPasswordNotConfiguredError = class extends DecaneConnectError {
|
|
136
|
+
constructor() {
|
|
137
|
+
super("PROMPT_PASSWORD_NOT_CONFIGURED", "This device unlocks with a password and no promptUnlockPassword callback was provided in config");
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
WrongUnlockPasswordError = class extends DecaneConnectError {
|
|
141
|
+
constructor() {
|
|
142
|
+
super("WRONG_UNLOCK_PASSWORD", "That password did not unlock this device");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
UnlockTokenRejectedError = class extends DecaneConnectError {
|
|
146
|
+
constructor() {
|
|
147
|
+
super("UNLOCK_TOKEN_REJECTED", "This device's unlock token is no longer valid \u2014 sign in normally to re-enable password unlock");
|
|
148
|
+
}
|
|
149
|
+
};
|
|
132
150
|
AttestationError = class extends DecaneConnectError {
|
|
133
151
|
constructor(msg) {
|
|
134
152
|
super("ATTESTATION_FAILED", msg);
|
|
@@ -2108,6 +2126,32 @@ var ApiClient = class {
|
|
|
2108
2126
|
body: JSON.stringify({ userId, response, origin })
|
|
2109
2127
|
});
|
|
2110
2128
|
}
|
|
2129
|
+
// ── Device unlock token (the "unlock with your password" credential) ──────
|
|
2130
|
+
/**
|
|
2131
|
+
* Issue this device an unlock token. Returned in the clear once and never
|
|
2132
|
+
* again — the backend stores only its hash — so the caller must encrypt and
|
|
2133
|
+
* persist it before the value is dropped.
|
|
2134
|
+
*/
|
|
2135
|
+
async issueUnlockToken(jwt) {
|
|
2136
|
+
return this._request("/auth/unlock-token", {
|
|
2137
|
+
method: "POST",
|
|
2138
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
/** Redeem a decrypted unlock token for a session. */
|
|
2142
|
+
async exchangeUnlockToken(token) {
|
|
2143
|
+
return this._request("/auth/unlock-token/exchange", {
|
|
2144
|
+
method: "POST",
|
|
2145
|
+
body: JSON.stringify({ token })
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
/** Turn the password path off account-wide, so a lost device stops working. */
|
|
2149
|
+
async revokeUnlockTokens(jwt) {
|
|
2150
|
+
await this._request("/auth/unlock-token", {
|
|
2151
|
+
method: "DELETE",
|
|
2152
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2111
2155
|
async revoke(jwt) {
|
|
2112
2156
|
await this._request("/auth/revoke", {
|
|
2113
2157
|
method: "POST",
|
|
@@ -2756,16 +2800,86 @@ async function aesGcmDecrypt(key, ciphertext, iv) {
|
|
|
2756
2800
|
return new Uint8Array(pt);
|
|
2757
2801
|
}
|
|
2758
2802
|
|
|
2759
|
-
// src/social/crypto/
|
|
2803
|
+
// src/social/crypto/password.ts
|
|
2760
2804
|
var PBKDF2_ITERATIONS = 6e5;
|
|
2761
2805
|
function buf2(u) {
|
|
2762
2806
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2763
2807
|
}
|
|
2808
|
+
var MIN_PASSWORD_LENGTH = 12;
|
|
2809
|
+
var TOO_PREDICTABLE = [
|
|
2810
|
+
/^(.)\1+$/,
|
|
2811
|
+
// one repeated character
|
|
2812
|
+
/^0?123456789?0?1?2?$/,
|
|
2813
|
+
// ascending runs
|
|
2814
|
+
/^(?:password|passw0rd)/i,
|
|
2815
|
+
/^(?:qwerty|azerty|asdfgh)/i,
|
|
2816
|
+
/^(?:letmein|welcome|iloveyou|admin)/i
|
|
2817
|
+
];
|
|
2818
|
+
var WeakPasswordError = class extends Error {
|
|
2819
|
+
constructor(message) {
|
|
2820
|
+
super(message);
|
|
2821
|
+
this.name = "WeakPasswordError";
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
function validateUnlockPassword(password) {
|
|
2825
|
+
if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
|
|
2826
|
+
throw new WeakPasswordError(
|
|
2827
|
+
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
if (/^\d+$/.test(password)) {
|
|
2831
|
+
throw new WeakPasswordError("Password cannot be only digits \u2014 add letters or symbols");
|
|
2832
|
+
}
|
|
2833
|
+
if (/^\s|\s$/.test(password)) {
|
|
2834
|
+
throw new WeakPasswordError("Password cannot start or end with a space");
|
|
2835
|
+
}
|
|
2836
|
+
if (TOO_PREDICTABLE.some((re) => re.test(password))) {
|
|
2837
|
+
throw new WeakPasswordError("Password is too easy to guess \u2014 choose something less common");
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
async function derivePasswordKeys(password, salt) {
|
|
2841
|
+
const base = await crypto.subtle.importKey(
|
|
2842
|
+
"raw",
|
|
2843
|
+
buf2(new TextEncoder().encode(password)),
|
|
2844
|
+
"PBKDF2",
|
|
2845
|
+
false,
|
|
2846
|
+
["deriveBits"]
|
|
2847
|
+
);
|
|
2848
|
+
const master = await crypto.subtle.deriveBits(
|
|
2849
|
+
{ name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
2850
|
+
base,
|
|
2851
|
+
256
|
|
2852
|
+
);
|
|
2853
|
+
const hkdfKey = await crypto.subtle.importKey("raw", master, "HKDF", false, ["deriveKey"]);
|
|
2854
|
+
const expand = (label) => crypto.subtle.deriveKey(
|
|
2855
|
+
{
|
|
2856
|
+
name: "HKDF",
|
|
2857
|
+
hash: "SHA-256",
|
|
2858
|
+
salt: buf2(salt),
|
|
2859
|
+
info: buf2(new TextEncoder().encode(label))
|
|
2860
|
+
},
|
|
2861
|
+
hkdfKey,
|
|
2862
|
+
{ name: "AES-GCM", length: 256 },
|
|
2863
|
+
false,
|
|
2864
|
+
["encrypt", "decrypt"]
|
|
2865
|
+
);
|
|
2866
|
+
const [shareKey, tokenKey] = await Promise.all([
|
|
2867
|
+
expand("decane:unlock:share"),
|
|
2868
|
+
expand("decane:unlock:token")
|
|
2869
|
+
]);
|
|
2870
|
+
return { shareKey, tokenKey };
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/social/crypto/pin.ts
|
|
2874
|
+
var PBKDF2_ITERATIONS2 = 6e5;
|
|
2875
|
+
function buf3(u) {
|
|
2876
|
+
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2877
|
+
}
|
|
2764
2878
|
async function deriveKeyFromPin(pin, salt) {
|
|
2765
2879
|
const pinBytes = new TextEncoder().encode(pin);
|
|
2766
|
-
const base = await crypto.subtle.importKey("raw",
|
|
2880
|
+
const base = await crypto.subtle.importKey("raw", buf3(pinBytes), "PBKDF2", false, ["deriveKey"]);
|
|
2767
2881
|
return crypto.subtle.deriveKey(
|
|
2768
|
-
{ name: "PBKDF2", salt:
|
|
2882
|
+
{ name: "PBKDF2", salt: buf3(salt), iterations: PBKDF2_ITERATIONS2, hash: "SHA-256" },
|
|
2769
2883
|
base,
|
|
2770
2884
|
{ name: "AES-GCM", length: 256 },
|
|
2771
2885
|
false,
|
|
@@ -2782,7 +2896,7 @@ import { startRegistration, startAuthentication as startAuthentication2 } from "
|
|
|
2782
2896
|
function fromB64u(s) {
|
|
2783
2897
|
return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
2784
2898
|
}
|
|
2785
|
-
function
|
|
2899
|
+
function buf4(u) {
|
|
2786
2900
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2787
2901
|
}
|
|
2788
2902
|
function isPasskeySupported() {
|
|
@@ -2813,7 +2927,7 @@ async function authenticateWithPasskey(opts) {
|
|
|
2813
2927
|
...opts.optionsJSON,
|
|
2814
2928
|
extensions: {
|
|
2815
2929
|
...opts.optionsJSON.extensions,
|
|
2816
|
-
prf: { eval: { first:
|
|
2930
|
+
prf: { eval: { first: buf4(opts.prfSalt) } }
|
|
2817
2931
|
}
|
|
2818
2932
|
};
|
|
2819
2933
|
let response;
|
|
@@ -2837,11 +2951,11 @@ async function getPrfOutput(opts) {
|
|
|
2837
2951
|
try {
|
|
2838
2952
|
cred = await navigator.credentials.get({
|
|
2839
2953
|
publicKey: {
|
|
2840
|
-
challenge:
|
|
2954
|
+
challenge: buf4(challenge),
|
|
2841
2955
|
rpId: opts.rpId,
|
|
2842
|
-
allowCredentials: [{ id:
|
|
2956
|
+
allowCredentials: [{ id: buf4(opts.credentialId), type: "public-key" }],
|
|
2843
2957
|
userVerification: "required",
|
|
2844
|
-
extensions: { prf: { eval: { first:
|
|
2958
|
+
extensions: { prf: { eval: { first: buf4(opts.prfSalt) } } }
|
|
2845
2959
|
}
|
|
2846
2960
|
});
|
|
2847
2961
|
} catch (e) {
|
|
@@ -3208,7 +3322,7 @@ async function fetchWrappedShareBackup(api, jwt) {
|
|
|
3208
3322
|
|
|
3209
3323
|
// src/social/recovery/recoveryShareFile.ts
|
|
3210
3324
|
init_errors();
|
|
3211
|
-
var
|
|
3325
|
+
var PBKDF2_ITERATIONS3 = 6e5;
|
|
3212
3326
|
var FILE_VERSION = 2;
|
|
3213
3327
|
function b642(u) {
|
|
3214
3328
|
let s = "";
|
|
@@ -3218,7 +3332,7 @@ function b642(u) {
|
|
|
3218
3332
|
function unb642(s) {
|
|
3219
3333
|
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
3220
3334
|
}
|
|
3221
|
-
function
|
|
3335
|
+
function buf5(u) {
|
|
3222
3336
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
3223
3337
|
}
|
|
3224
3338
|
async function deriveFileKey(password, salt, iterations) {
|
|
@@ -3230,7 +3344,7 @@ async function deriveFileKey(password, salt, iterations) {
|
|
|
3230
3344
|
["deriveKey"]
|
|
3231
3345
|
);
|
|
3232
3346
|
return crypto.subtle.deriveKey(
|
|
3233
|
-
{ name: "PBKDF2", salt:
|
|
3347
|
+
{ name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
|
|
3234
3348
|
base,
|
|
3235
3349
|
{ name: "AES-GCM", length: 256 },
|
|
3236
3350
|
false,
|
|
@@ -3245,16 +3359,16 @@ async function buildRecoveryShareFile(share, password, addresses, shareSetVersio
|
|
|
3245
3359
|
}
|
|
3246
3360
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
3247
3361
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3248
|
-
const key = await deriveFileKey(password, salt,
|
|
3362
|
+
const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS3);
|
|
3249
3363
|
const ct = new Uint8Array(
|
|
3250
|
-
await crypto.subtle.encrypt({ name: "AES-GCM", iv:
|
|
3364
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(share))
|
|
3251
3365
|
);
|
|
3252
3366
|
return {
|
|
3253
3367
|
version: FILE_VERSION,
|
|
3254
3368
|
type: "decane-recovery-share",
|
|
3255
3369
|
shareSetVersion,
|
|
3256
3370
|
kdf: "pbkdf2-sha256",
|
|
3257
|
-
kdfParams: { iterations:
|
|
3371
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b642(salt) },
|
|
3258
3372
|
cipher: "aes-256-gcm",
|
|
3259
3373
|
iv: b642(iv),
|
|
3260
3374
|
ciphertext: b642(ct),
|
|
@@ -3285,7 +3399,7 @@ async function decryptRecoveryShareFile(file, password) {
|
|
|
3285
3399
|
let share;
|
|
3286
3400
|
try {
|
|
3287
3401
|
share = new Uint8Array(
|
|
3288
|
-
await crypto.subtle.decrypt({ name: "AES-GCM", iv:
|
|
3402
|
+
await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(ct))
|
|
3289
3403
|
);
|
|
3290
3404
|
} catch {
|
|
3291
3405
|
throw new WrongPasswordError();
|
|
@@ -3360,14 +3474,14 @@ async function recoverDeviceShare(input) {
|
|
|
3360
3474
|
|
|
3361
3475
|
// src/social/recovery/portableBackup.ts
|
|
3362
3476
|
init_errors();
|
|
3363
|
-
var
|
|
3477
|
+
var PBKDF2_ITERATIONS4 = 6e5;
|
|
3364
3478
|
var FILE_VERSION2 = 1;
|
|
3365
3479
|
function b643(u) {
|
|
3366
3480
|
let s = "";
|
|
3367
3481
|
for (const b of u) s += String.fromCharCode(b);
|
|
3368
3482
|
return btoa(s);
|
|
3369
3483
|
}
|
|
3370
|
-
function
|
|
3484
|
+
function buf6(u) {
|
|
3371
3485
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
3372
3486
|
}
|
|
3373
3487
|
async function deriveFileKey2(password, salt, iterations) {
|
|
@@ -3379,7 +3493,7 @@ async function deriveFileKey2(password, salt, iterations) {
|
|
|
3379
3493
|
["deriveKey"]
|
|
3380
3494
|
);
|
|
3381
3495
|
return crypto.subtle.deriveKey(
|
|
3382
|
-
{ name: "PBKDF2", salt:
|
|
3496
|
+
{ name: "PBKDF2", salt: buf6(salt), iterations, hash: "SHA-256" },
|
|
3383
3497
|
base,
|
|
3384
3498
|
{ name: "AES-GCM", length: 256 },
|
|
3385
3499
|
false,
|
|
@@ -3399,16 +3513,16 @@ async function buildPortableBackup(deviceShare, recoveryShare, password, address
|
|
|
3399
3513
|
);
|
|
3400
3514
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
3401
3515
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3402
|
-
const key = await deriveFileKey2(password, salt,
|
|
3516
|
+
const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS4);
|
|
3403
3517
|
const ct = new Uint8Array(
|
|
3404
|
-
await crypto.subtle.encrypt({ name: "AES-GCM", iv:
|
|
3518
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf6(iv) }, key, buf6(plaintext))
|
|
3405
3519
|
);
|
|
3406
3520
|
return {
|
|
3407
3521
|
version: FILE_VERSION2,
|
|
3408
3522
|
type: "decane-portable-backup",
|
|
3409
3523
|
shareSetVersion,
|
|
3410
3524
|
kdf: "pbkdf2-sha256",
|
|
3411
|
-
kdfParams: { iterations:
|
|
3525
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS4, salt: b643(salt) },
|
|
3412
3526
|
cipher: "aes-256-gcm",
|
|
3413
3527
|
iv: b643(iv),
|
|
3414
3528
|
ciphertext: b643(ct),
|
|
@@ -3615,7 +3729,24 @@ async function createDecaneConnect(config) {
|
|
|
3615
3729
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3616
3730
|
await storage.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3617
3731
|
}
|
|
3732
|
+
function hasPasswordUnlock(stored) {
|
|
3733
|
+
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3734
|
+
}
|
|
3735
|
+
async function passwordDeviceCandidate() {
|
|
3736
|
+
if (persisted?.userId) {
|
|
3737
|
+
const stored = await storage.getShare(persisted.userId);
|
|
3738
|
+
return hasPasswordUnlock(stored) ? stored : null;
|
|
3739
|
+
}
|
|
3740
|
+
const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
|
|
3741
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
3742
|
+
}
|
|
3618
3743
|
async function getWrappingKey(stored) {
|
|
3744
|
+
if (stored.unlockMethod === "password") {
|
|
3745
|
+
if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
|
|
3746
|
+
const password = await config.promptUnlockPassword({ kind: "unlock" });
|
|
3747
|
+
const { shareKey } = await derivePasswordKeys(password, stored.passwordSalt);
|
|
3748
|
+
return shareKey;
|
|
3749
|
+
}
|
|
3619
3750
|
if (stored.unlockMethod === "passkey") {
|
|
3620
3751
|
const prfOutput = await getPrfOutput({
|
|
3621
3752
|
rpId,
|
|
@@ -4146,6 +4277,127 @@ async function createDecaneConnect(config) {
|
|
|
4146
4277
|
if (!candidate) throw new NewDeviceError();
|
|
4147
4278
|
return passkeyLogin(candidate);
|
|
4148
4279
|
},
|
|
4280
|
+
/**
|
|
4281
|
+
* Turn on "unlock with your password" for this device.
|
|
4282
|
+
*
|
|
4283
|
+
* Re-wraps the device share under the password and stores an unlock token
|
|
4284
|
+
* encrypted beside it, replacing whatever wrapping was there. Requires a
|
|
4285
|
+
* live session, because issuing the token needs a JWT.
|
|
4286
|
+
*
|
|
4287
|
+
* The share is only overwritten once the new ciphertext is in hand, so a
|
|
4288
|
+
* failure part-way leaves the device exactly as it was rather than holding
|
|
4289
|
+
* a share nothing can open.
|
|
4290
|
+
*/
|
|
4291
|
+
async setUnlockPassword(password) {
|
|
4292
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4293
|
+
validateUnlockPassword(password);
|
|
4294
|
+
const stored = await storage.getShare(auth.userId);
|
|
4295
|
+
if (!stored) throw new NewDeviceError();
|
|
4296
|
+
const deviceShare = await aesGcmDecrypt(
|
|
4297
|
+
await getWrappingKey(stored),
|
|
4298
|
+
stored.wrappedDeviceShare,
|
|
4299
|
+
stored.iv
|
|
4300
|
+
);
|
|
4301
|
+
try {
|
|
4302
|
+
const passwordSalt = generateRandomBytes(16);
|
|
4303
|
+
const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
|
|
4304
|
+
const { token } = await api.issueUnlockToken(auth.jwt);
|
|
4305
|
+
const unlockTokenIv = generateRandomBytes(12);
|
|
4306
|
+
const wrappedUnlockToken = await aesGcmEncrypt(
|
|
4307
|
+
tokenKey,
|
|
4308
|
+
new TextEncoder().encode(token),
|
|
4309
|
+
unlockTokenIv
|
|
4310
|
+
);
|
|
4311
|
+
const iv = generateRandomBytes(12);
|
|
4312
|
+
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4313
|
+
await storage.putShare({
|
|
4314
|
+
...stored,
|
|
4315
|
+
unlockMethod: "password",
|
|
4316
|
+
wrappedDeviceShare,
|
|
4317
|
+
iv,
|
|
4318
|
+
passwordSalt,
|
|
4319
|
+
passwordIterations: PBKDF2_ITERATIONS,
|
|
4320
|
+
wrappedUnlockToken,
|
|
4321
|
+
unlockTokenIv,
|
|
4322
|
+
// The old wrapping's metadata would otherwise sit there describing a
|
|
4323
|
+
// key that no longer opens anything.
|
|
4324
|
+
credentialId: void 0,
|
|
4325
|
+
prfSalt: void 0,
|
|
4326
|
+
pinSalt: void 0,
|
|
4327
|
+
pinIterations: void 0
|
|
4328
|
+
});
|
|
4329
|
+
} finally {
|
|
4330
|
+
zeroize(deviceShare);
|
|
4331
|
+
}
|
|
4332
|
+
},
|
|
4333
|
+
/** True when this device can open a session from a password alone. */
|
|
4334
|
+
async canUnlockWithPassword() {
|
|
4335
|
+
return await passwordDeviceCandidate() !== null;
|
|
4336
|
+
},
|
|
4337
|
+
/**
|
|
4338
|
+
* Open a session from the password alone — no identity provider, no passkey.
|
|
4339
|
+
*
|
|
4340
|
+
* The password decrypts the unlock token, the token buys a JWT, and the
|
|
4341
|
+
* same password unwraps the device share. Both halves come from one
|
|
4342
|
+
* derivation, so one prompt does the whole job.
|
|
4343
|
+
*/
|
|
4344
|
+
async unlockWithPassword(password) {
|
|
4345
|
+
const stored = await passwordDeviceCandidate();
|
|
4346
|
+
if (!stored) throw new NewDeviceError();
|
|
4347
|
+
events.emit("wallet-unlocking");
|
|
4348
|
+
const { shareKey, tokenKey } = await derivePasswordKeys(password, stored.passwordSalt);
|
|
4349
|
+
let token;
|
|
4350
|
+
try {
|
|
4351
|
+
token = new TextDecoder().decode(
|
|
4352
|
+
await aesGcmDecrypt(tokenKey, stored.wrappedUnlockToken, stored.unlockTokenIv)
|
|
4353
|
+
);
|
|
4354
|
+
} catch {
|
|
4355
|
+
throw new WrongUnlockPasswordError();
|
|
4356
|
+
}
|
|
4357
|
+
let jwt;
|
|
4358
|
+
try {
|
|
4359
|
+
({ jwt } = await api.exchangeUnlockToken(token));
|
|
4360
|
+
} catch (e) {
|
|
4361
|
+
if (e instanceof DecaneConnectError && e.code === "NETWORK") throw e;
|
|
4362
|
+
throw new UnlockTokenRejectedError();
|
|
4363
|
+
}
|
|
4364
|
+
const deviceShare = await aesGcmDecrypt(shareKey, stored.wrappedDeviceShare, stored.iv);
|
|
4365
|
+
const rawAddresses = await api.getAddresses(jwt);
|
|
4366
|
+
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? {
|
|
4367
|
+
evm: rawAddresses.evm,
|
|
4368
|
+
solana: rawAddresses.solana,
|
|
4369
|
+
tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm)
|
|
4370
|
+
} : null;
|
|
4371
|
+
const addresses = await session.start(deviceShare, jwt, recordedAddresses);
|
|
4372
|
+
zeroize(deviceShare);
|
|
4373
|
+
auth = { jwt, userId: stored.userId, addresses };
|
|
4374
|
+
persisted = { userId: stored.userId, addresses };
|
|
4375
|
+
savePersisted(PERSIST_KEY, persisted);
|
|
4376
|
+
events.emit("connected");
|
|
4377
|
+
persistLiveSession();
|
|
4378
|
+
return {
|
|
4379
|
+
addresses,
|
|
4380
|
+
isNewUser: false,
|
|
4381
|
+
...stored.profile ? { profile: stored.profile } : {}
|
|
4382
|
+
};
|
|
4383
|
+
},
|
|
4384
|
+
/**
|
|
4385
|
+
* Turn the password path off.
|
|
4386
|
+
*
|
|
4387
|
+
* Revokes the token account-wide first — that is what makes a lost device
|
|
4388
|
+
* safe, and it must happen even if this device's local cleanup fails.
|
|
4389
|
+
*/
|
|
4390
|
+
async removeUnlockPassword() {
|
|
4391
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4392
|
+
await api.revokeUnlockTokens(auth.jwt);
|
|
4393
|
+
const stored = await storage.getShare(auth.userId);
|
|
4394
|
+
if (!stored || stored.unlockMethod !== "password") return;
|
|
4395
|
+
await storage.putShare({
|
|
4396
|
+
...stored,
|
|
4397
|
+
wrappedUnlockToken: void 0,
|
|
4398
|
+
unlockTokenIv: void 0
|
|
4399
|
+
});
|
|
4400
|
+
},
|
|
4149
4401
|
async hasPasskey() {
|
|
4150
4402
|
if (!auth) return false;
|
|
4151
4403
|
const stored = await storage.getShare(auth.userId);
|
|
@@ -4489,8 +4741,8 @@ async function createDecaneConnect(config) {
|
|
|
4489
4741
|
};
|
|
4490
4742
|
return client;
|
|
4491
4743
|
}
|
|
4492
|
-
function bufferToB64u(
|
|
4493
|
-
return btoa(String.fromCharCode(...
|
|
4744
|
+
function bufferToB64u(buf7) {
|
|
4745
|
+
return btoa(String.fromCharCode(...buf7)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
4494
4746
|
}
|
|
4495
4747
|
function b64uToBuffer(b64u) {
|
|
4496
4748
|
const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -6954,6 +7206,26 @@ function DecaneKit({ config, children }) {
|
|
|
6954
7206
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
6955
7207
|
return sdk.addPasskey();
|
|
6956
7208
|
}, []);
|
|
7209
|
+
const socialSetUnlockPassword = useCallback3(async (password) => {
|
|
7210
|
+
const sdk = socialRef.current;
|
|
7211
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7212
|
+
return sdk.setUnlockPassword(password);
|
|
7213
|
+
}, []);
|
|
7214
|
+
const socialCanUnlockWithPassword = useCallback3(async () => {
|
|
7215
|
+
const sdk = socialRef.current;
|
|
7216
|
+
if (!sdk) return false;
|
|
7217
|
+
return sdk.canUnlockWithPassword();
|
|
7218
|
+
}, []);
|
|
7219
|
+
const socialUnlockWithPassword = useCallback3(async (password) => {
|
|
7220
|
+
const sdk = socialRef.current;
|
|
7221
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7222
|
+
applyResult(await sdk.unlockWithPassword(password));
|
|
7223
|
+
}, []);
|
|
7224
|
+
const socialRemoveUnlockPassword = useCallback3(async () => {
|
|
7225
|
+
const sdk = socialRef.current;
|
|
7226
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7227
|
+
return sdk.removeUnlockPassword();
|
|
7228
|
+
}, []);
|
|
6957
7229
|
const socialHasRecoveryShare = useCallback3(async () => {
|
|
6958
7230
|
const sdk = socialRef.current;
|
|
6959
7231
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7017,6 +7289,10 @@ function DecaneKit({ config, children }) {
|
|
|
7017
7289
|
socialHasRecoveryShare,
|
|
7018
7290
|
socialHasPasskey,
|
|
7019
7291
|
socialAddPasskey,
|
|
7292
|
+
socialSetUnlockPassword,
|
|
7293
|
+
socialCanUnlockWithPassword,
|
|
7294
|
+
socialUnlockWithPassword,
|
|
7295
|
+
socialRemoveUnlockPassword,
|
|
7020
7296
|
socialShouldPromptRecovery,
|
|
7021
7297
|
socialDismissRecoveryPrompt,
|
|
7022
7298
|
socialModalOpen,
|
|
@@ -7303,6 +7579,10 @@ function useSocialWallet() {
|
|
|
7303
7579
|
socialHasRecoveryShare,
|
|
7304
7580
|
socialHasPasskey,
|
|
7305
7581
|
socialAddPasskey,
|
|
7582
|
+
socialSetUnlockPassword,
|
|
7583
|
+
socialCanUnlockWithPassword,
|
|
7584
|
+
socialUnlockWithPassword,
|
|
7585
|
+
socialRemoveUnlockPassword,
|
|
7306
7586
|
socialShouldPromptRecovery,
|
|
7307
7587
|
socialDismissRecoveryPrompt,
|
|
7308
7588
|
openSocialModal,
|
|
@@ -7400,6 +7680,37 @@ function useSocialWallet() {
|
|
|
7400
7680
|
throw e;
|
|
7401
7681
|
}
|
|
7402
7682
|
}, [socialAddPasskey]);
|
|
7683
|
+
const setUnlockPassword = useCallback4(async (password) => {
|
|
7684
|
+
setError(null);
|
|
7685
|
+
try {
|
|
7686
|
+
await socialSetUnlockPassword(password);
|
|
7687
|
+
} catch (e) {
|
|
7688
|
+
setError(e.message);
|
|
7689
|
+
throw e;
|
|
7690
|
+
}
|
|
7691
|
+
}, [socialSetUnlockPassword]);
|
|
7692
|
+
const canUnlockWithPassword = useCallback4(
|
|
7693
|
+
() => socialCanUnlockWithPassword(),
|
|
7694
|
+
[socialCanUnlockWithPassword]
|
|
7695
|
+
);
|
|
7696
|
+
const unlockWithPassword = useCallback4(async (password) => {
|
|
7697
|
+
setError(null);
|
|
7698
|
+
try {
|
|
7699
|
+
await socialUnlockWithPassword(password);
|
|
7700
|
+
} catch (e) {
|
|
7701
|
+
setError(e.message);
|
|
7702
|
+
throw e;
|
|
7703
|
+
}
|
|
7704
|
+
}, [socialUnlockWithPassword]);
|
|
7705
|
+
const removeUnlockPassword = useCallback4(async () => {
|
|
7706
|
+
setError(null);
|
|
7707
|
+
try {
|
|
7708
|
+
await socialRemoveUnlockPassword();
|
|
7709
|
+
} catch (e) {
|
|
7710
|
+
setError(e.message);
|
|
7711
|
+
throw e;
|
|
7712
|
+
}
|
|
7713
|
+
}, [socialRemoveUnlockPassword]);
|
|
7403
7714
|
const hasRecoveryShare = useCallback4(
|
|
7404
7715
|
() => run(() => socialHasRecoveryShare()),
|
|
7405
7716
|
[socialHasRecoveryShare]
|
|
@@ -7456,6 +7767,13 @@ function useSocialWallet() {
|
|
|
7456
7767
|
// Passkey management for THIS device
|
|
7457
7768
|
hasPasskey,
|
|
7458
7769
|
addPasskey,
|
|
7770
|
+
// "Unlock with your password": setUnlockPassword needs a live session;
|
|
7771
|
+
// canUnlockWithPassword and unlockWithPassword are the signed-out pair that
|
|
7772
|
+
// open one without an identity provider or a passkey.
|
|
7773
|
+
setUnlockPassword,
|
|
7774
|
+
canUnlockWithPassword,
|
|
7775
|
+
unlockWithPassword,
|
|
7776
|
+
removeUnlockPassword,
|
|
7459
7777
|
// Recovery (2-of-3 Shamir share set)
|
|
7460
7778
|
rotateShares,
|
|
7461
7779
|
exportPortableBackup,
|
|
@@ -7764,9 +8082,11 @@ export {
|
|
|
7764
8082
|
InvalidApiKeyError,
|
|
7765
8083
|
InvalidPinError,
|
|
7766
8084
|
InvalidRecoveryFileError,
|
|
8085
|
+
MIN_PASSWORD_LENGTH,
|
|
7767
8086
|
NetworkError,
|
|
7768
8087
|
NewDeviceError,
|
|
7769
8088
|
OriginNotAllowedError,
|
|
8089
|
+
PromptPasswordNotConfiguredError,
|
|
7770
8090
|
PromptPinNotConfiguredError,
|
|
7771
8091
|
ProvisioningTimeoutError,
|
|
7772
8092
|
ReconnectRequiredError,
|
|
@@ -7777,10 +8097,13 @@ export {
|
|
|
7777
8097
|
SocialWalletModal,
|
|
7778
8098
|
StaleDeviceShareError,
|
|
7779
8099
|
UnlockRequiredError,
|
|
8100
|
+
UnlockTokenRejectedError,
|
|
7780
8101
|
UserCancelledError,
|
|
7781
8102
|
WalletSelector,
|
|
8103
|
+
WeakPasswordError,
|
|
7782
8104
|
WebAuthnNotSupportedError,
|
|
7783
8105
|
WrongPasswordError,
|
|
8106
|
+
WrongUnlockPasswordError,
|
|
7784
8107
|
createDecane,
|
|
7785
8108
|
createDecaneConnect,
|
|
7786
8109
|
createEthereumProvider,
|
|
@@ -7796,6 +8119,7 @@ export {
|
|
|
7796
8119
|
useSocialBalance,
|
|
7797
8120
|
useSocialWallet,
|
|
7798
8121
|
useWalletSelector,
|
|
8122
|
+
validateUnlockPassword,
|
|
7799
8123
|
verifyEmailCode
|
|
7800
8124
|
};
|
|
7801
8125
|
//# sourceMappingURL=index.js.map
|