@kingdomchain/wallet-client 0.6.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/dist/index.d.ts +259 -11
- package/dist/index.js +614 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1940,6 +1940,7 @@ var EMBEDDED_WALLET_VAULT_VERSION = 1;
|
|
|
1940
1940
|
var EMBEDDED_WALLET_KDF_PRF = "webauthn-prf";
|
|
1941
1941
|
var EMBEDDED_WALLET_KDF_PBKDF2 = "pbkdf2-passkey";
|
|
1942
1942
|
var EMBEDDED_WALLET_KDF_PIN = "pbkdf2-pin";
|
|
1943
|
+
var EMBEDDED_WALLET_KDF_DEVICE_PIN = "device-pin";
|
|
1943
1944
|
var EMBEDDED_WALLET_KEY_HD_DERIVED = "hd-derived";
|
|
1944
1945
|
var EMBEDDED_WALLET_KEY_DIRECT_ENTROPY = "direct-entropy";
|
|
1945
1946
|
var EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING = {
|
|
@@ -1994,6 +1995,23 @@ var EmbeddedWalletWeakPasscodeError = class extends Error {
|
|
|
1994
1995
|
this.reason = reason;
|
|
1995
1996
|
}
|
|
1996
1997
|
};
|
|
1998
|
+
var EmbeddedWalletPasskeyUnsupportedError = class extends EmbeddedWalletVaultError {
|
|
1999
|
+
code = "passkey_prf_unsupported";
|
|
2000
|
+
constructor(message = "This device cannot protect the wallet with a passkey; keep using the passcode") {
|
|
2001
|
+
super(message);
|
|
2002
|
+
this.name = "EmbeddedWalletPasskeyUnsupportedError";
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
var EmbeddedWalletQuickUnlockError = class extends Error {
|
|
2006
|
+
code;
|
|
2007
|
+
attemptsLeft;
|
|
2008
|
+
constructor(code, message, attemptsLeft) {
|
|
2009
|
+
super(message);
|
|
2010
|
+
this.name = "EmbeddedWalletQuickUnlockError";
|
|
2011
|
+
this.code = code;
|
|
2012
|
+
this.attemptsLeft = attemptsLeft;
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
1997
2015
|
var DEFAULT_HD_PATH = "m/44'/118'/0'/0/0";
|
|
1998
2016
|
async function embeddedPlaintextToSigner(plaintext, prefix = BECH32_PREFIX) {
|
|
1999
2017
|
if (plaintext.privateKey.length !== 32) {
|
|
@@ -2321,6 +2339,30 @@ function assertVaultPermitsTransaction(vault) {
|
|
|
2321
2339
|
throw new EmbeddedWalletBackupRequiredError();
|
|
2322
2340
|
}
|
|
2323
2341
|
}
|
|
2342
|
+
async function encryptPlaintextWithKek(payload, kek) {
|
|
2343
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
2344
|
+
const aesKey = await importAesKey(kek);
|
|
2345
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
2346
|
+
{ name: "AES-GCM", iv },
|
|
2347
|
+
aesKey,
|
|
2348
|
+
new Uint8Array(serializePlaintext(payload))
|
|
2349
|
+
);
|
|
2350
|
+
return { iv: toBase64(iv), ciphertext: toBase64(new Uint8Array(ciphertext)) };
|
|
2351
|
+
}
|
|
2352
|
+
async function decryptPlaintextWithKek(iv, ciphertext, kek) {
|
|
2353
|
+
const aesKey = await importAesKey(kek);
|
|
2354
|
+
let plaintextBytes;
|
|
2355
|
+
try {
|
|
2356
|
+
plaintextBytes = await crypto.subtle.decrypt(
|
|
2357
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(iv)) },
|
|
2358
|
+
aesKey,
|
|
2359
|
+
new Uint8Array(fromBase64(ciphertext))
|
|
2360
|
+
);
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
throw new EmbeddedWalletDecryptError(err);
|
|
2363
|
+
}
|
|
2364
|
+
return deserializePlaintext(new Uint8Array(plaintextBytes));
|
|
2365
|
+
}
|
|
2324
2366
|
|
|
2325
2367
|
// src/embeddedWallet/prf.ts
|
|
2326
2368
|
function detectWebAuthnPrfCapabilities() {
|
|
@@ -2331,15 +2373,6 @@ function detectWebAuthnPrfCapabilities() {
|
|
|
2331
2373
|
prfFallbackReason: "WebAuthn is not available in this environment"
|
|
2332
2374
|
};
|
|
2333
2375
|
}
|
|
2334
|
-
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
2335
|
-
const isSafari = /Safari/i.test(ua) && !/Chrome|Chromium|CriOS|FxiOS/i.test(ua);
|
|
2336
|
-
if (isSafari) {
|
|
2337
|
-
return {
|
|
2338
|
-
webAuthnAvailable: true,
|
|
2339
|
-
prfLikelySupported: false,
|
|
2340
|
-
prfFallbackReason: "Safari does not support WebAuthn PRF for cross-device QR flows; use PBKDF2 fallback"
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
2376
|
const getCaps = PublicKeyCredential.getClientCapabilities;
|
|
2344
2377
|
if (typeof getCaps !== "function") {
|
|
2345
2378
|
return {
|
|
@@ -2370,6 +2403,11 @@ function selectEmbeddedWalletKdf(caps, prfOutput) {
|
|
|
2370
2403
|
|
|
2371
2404
|
// src/embeddedWallet/backup.ts
|
|
2372
2405
|
function exportEncryptedBackup(vault) {
|
|
2406
|
+
if (vault.kdf === "device-pin") {
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
"A PIN wallet is bound to this device and has no backup file; its backup is the 24-word recovery phrase"
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2373
2411
|
return {
|
|
2374
2412
|
format: "kingdom-os-embedded-wallet-backup",
|
|
2375
2413
|
version: EMBEDDED_WALLET_VAULT_VERSION,
|
|
@@ -2519,10 +2557,11 @@ function decodeCredentialId(encoded) {
|
|
|
2519
2557
|
|
|
2520
2558
|
// src/embeddedWallet/storage.ts
|
|
2521
2559
|
var DB_NAME = "kingdom-os-wallet";
|
|
2522
|
-
var DB_VERSION =
|
|
2560
|
+
var DB_VERSION = 4;
|
|
2523
2561
|
var VAULT_STORE_NAME = "vault";
|
|
2524
2562
|
var LOCKOUT_STORE_NAME = "passcode-lockout";
|
|
2525
2563
|
var SESSION_STORE_NAME = "session-cache";
|
|
2564
|
+
var QUICK_UNLOCK_STORE_NAME = "quick-unlock";
|
|
2526
2565
|
var VAULT_LEGACY_KEY = "primary";
|
|
2527
2566
|
function resolveVaultStorageKey(scopeId) {
|
|
2528
2567
|
const trimmed = scopeId?.trim();
|
|
@@ -2543,6 +2582,9 @@ function openDb() {
|
|
|
2543
2582
|
if (!db.objectStoreNames.contains(SESSION_STORE_NAME)) {
|
|
2544
2583
|
db.createObjectStore(SESSION_STORE_NAME);
|
|
2545
2584
|
}
|
|
2585
|
+
if (!db.objectStoreNames.contains(QUICK_UNLOCK_STORE_NAME)) {
|
|
2586
|
+
db.createObjectStore(QUICK_UNLOCK_STORE_NAME);
|
|
2587
|
+
}
|
|
2546
2588
|
};
|
|
2547
2589
|
request.onsuccess = () => resolve(request.result);
|
|
2548
2590
|
});
|
|
@@ -2651,6 +2693,40 @@ async function clearSessionCacheRecord(options = {}) {
|
|
|
2651
2693
|
});
|
|
2652
2694
|
db.close();
|
|
2653
2695
|
}
|
|
2696
|
+
async function saveQuickUnlockRecord(record, options = {}) {
|
|
2697
|
+
const key = resolveVaultStorageKey(options.scopeId);
|
|
2698
|
+
const db = await openDb();
|
|
2699
|
+
await new Promise((resolve, reject) => {
|
|
2700
|
+
const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readwrite");
|
|
2701
|
+
tx.objectStore(QUICK_UNLOCK_STORE_NAME).put(record, key);
|
|
2702
|
+
tx.oncomplete = () => resolve();
|
|
2703
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB write failed"));
|
|
2704
|
+
});
|
|
2705
|
+
db.close();
|
|
2706
|
+
}
|
|
2707
|
+
async function loadQuickUnlockRecord(options = {}) {
|
|
2708
|
+
const key = resolveVaultStorageKey(options.scopeId);
|
|
2709
|
+
const db = await openDb();
|
|
2710
|
+
const record = await new Promise((resolve, reject) => {
|
|
2711
|
+
const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readonly");
|
|
2712
|
+
const request = tx.objectStore(QUICK_UNLOCK_STORE_NAME).get(key);
|
|
2713
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
2714
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB read failed"));
|
|
2715
|
+
});
|
|
2716
|
+
db.close();
|
|
2717
|
+
return record;
|
|
2718
|
+
}
|
|
2719
|
+
async function clearQuickUnlockRecord(options = {}) {
|
|
2720
|
+
const key = resolveVaultStorageKey(options.scopeId);
|
|
2721
|
+
const db = await openDb();
|
|
2722
|
+
await new Promise((resolve, reject) => {
|
|
2723
|
+
const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readwrite");
|
|
2724
|
+
tx.objectStore(QUICK_UNLOCK_STORE_NAME).delete(key);
|
|
2725
|
+
tx.oncomplete = () => resolve();
|
|
2726
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB delete failed"));
|
|
2727
|
+
});
|
|
2728
|
+
db.close();
|
|
2729
|
+
}
|
|
2654
2730
|
var EMBEDDED_WALLET_SESSION_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
2655
2731
|
var AES_GCM_IV_BYTES2 = 12;
|
|
2656
2732
|
function serializeSessionPlaintext(plaintext) {
|
|
@@ -2890,10 +2966,264 @@ function assertPasscodeAllowedForCreation(passcode) {
|
|
|
2890
2966
|
throw new EmbeddedWalletWeakPasscodeError(strength.blockReason ?? "too_weak");
|
|
2891
2967
|
}
|
|
2892
2968
|
}
|
|
2969
|
+
var QUICK_UNLOCK_PIN_LENGTH = 6;
|
|
2970
|
+
var QUICK_UNLOCK_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2971
|
+
var QUICK_UNLOCK_MAX_ATTEMPTS = 5;
|
|
2972
|
+
var QUICK_UNLOCK_PBKDF2_ITERATIONS = 6e5;
|
|
2973
|
+
var AES_GCM_IV_BYTES3 = 12;
|
|
2974
|
+
function assertQuickUnlockPinAllowed(pin) {
|
|
2975
|
+
if (typeof pin !== "string" || !new RegExp(`^\\d{${QUICK_UNLOCK_PIN_LENGTH}}$`).test(pin)) {
|
|
2976
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
2977
|
+
"invalid_pin",
|
|
2978
|
+
`PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
if (isTrivialPin(pin)) {
|
|
2982
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
2983
|
+
"invalid_pin",
|
|
2984
|
+
"PIN is too easy to guess; avoid repeats and sequences like 123456"
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
function isTrivialPin(pin) {
|
|
2989
|
+
const digits = pin.split("").map(Number);
|
|
2990
|
+
const steps = digits.slice(1).map((d, i) => d - digits[i]);
|
|
2991
|
+
if (steps.every((s) => s === 0)) return true;
|
|
2992
|
+
if (steps.every((s) => s === 1) || steps.every((s) => s === -1)) return true;
|
|
2993
|
+
for (const period of [2, 3]) {
|
|
2994
|
+
if (pin === pin.slice(0, period).repeat(pin.length / period)) return true;
|
|
2995
|
+
}
|
|
2996
|
+
return false;
|
|
2997
|
+
}
|
|
2998
|
+
async function derivePinKey(pin, salt, iterations) {
|
|
2999
|
+
const material = await crypto.subtle.importKey(
|
|
3000
|
+
"raw",
|
|
3001
|
+
new TextEncoder().encode(pin),
|
|
3002
|
+
"PBKDF2",
|
|
3003
|
+
false,
|
|
3004
|
+
["deriveKey"]
|
|
3005
|
+
);
|
|
3006
|
+
return crypto.subtle.deriveKey(
|
|
3007
|
+
{ name: "PBKDF2", salt: new Uint8Array(salt), iterations, hash: "SHA-256" },
|
|
3008
|
+
material,
|
|
3009
|
+
{ name: "AES-GCM", length: 256 },
|
|
3010
|
+
false,
|
|
3011
|
+
["encrypt", "decrypt"]
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
async function createQuickUnlockRecord(options) {
|
|
3015
|
+
const { plaintext, publicAddress, pin, ttlMs, iterations, ...storageOptions } = options;
|
|
3016
|
+
assertQuickUnlockPinAllowed(pin);
|
|
3017
|
+
const rounds = iterations ?? QUICK_UNLOCK_PBKDF2_ITERATIONS;
|
|
3018
|
+
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
3019
|
+
const pinKey = await derivePinKey(pin, salt, rounds);
|
|
3020
|
+
const deviceKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
|
|
3021
|
+
"encrypt",
|
|
3022
|
+
"decrypt"
|
|
3023
|
+
]);
|
|
3024
|
+
const innerIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES3));
|
|
3025
|
+
const inner = await crypto.subtle.encrypt(
|
|
3026
|
+
{ name: "AES-GCM", iv: innerIv },
|
|
3027
|
+
pinKey,
|
|
3028
|
+
new Uint8Array(serializePlaintext(plaintext))
|
|
3029
|
+
);
|
|
3030
|
+
const outerIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES3));
|
|
3031
|
+
const outer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: outerIv }, deviceKey, inner);
|
|
3032
|
+
const record = {
|
|
3033
|
+
deviceKey,
|
|
3034
|
+
publicAddress,
|
|
3035
|
+
salt: toBase64(salt),
|
|
3036
|
+
iterations: rounds,
|
|
3037
|
+
innerIv: toBase64(innerIv),
|
|
3038
|
+
outerIv: toBase64(outerIv),
|
|
3039
|
+
ciphertext: toBase64(new Uint8Array(outer)),
|
|
3040
|
+
expiresAt: Date.now() + (ttlMs ?? QUICK_UNLOCK_TTL_MS),
|
|
3041
|
+
failedAttempts: 0
|
|
3042
|
+
};
|
|
3043
|
+
await saveQuickUnlockRecord(record, storageOptions);
|
|
3044
|
+
return record;
|
|
3045
|
+
}
|
|
3046
|
+
async function openQuickUnlockRecord(options) {
|
|
3047
|
+
const { pin, ...storageOptions } = options;
|
|
3048
|
+
if (typeof pin !== "string" || !/^\d+$/.test(pin) || pin.length !== QUICK_UNLOCK_PIN_LENGTH) {
|
|
3049
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3050
|
+
"invalid_pin",
|
|
3051
|
+
`PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
const record = await loadQuickUnlockRecord(storageOptions);
|
|
3055
|
+
if (!record) {
|
|
3056
|
+
throw new EmbeddedWalletQuickUnlockError("not_set", "No PIN is set up on this device");
|
|
3057
|
+
}
|
|
3058
|
+
if (typeof record.expiresAt !== "number" || Date.now() >= record.expiresAt) {
|
|
3059
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3060
|
+
"expired",
|
|
3061
|
+
"PIN expired; unlock with Face ID or your passcode"
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
const attempt = (record.failedAttempts ?? 0) + 1;
|
|
3065
|
+
await saveQuickUnlockRecord({ ...record, failedAttempts: attempt }, storageOptions);
|
|
3066
|
+
let inner;
|
|
3067
|
+
try {
|
|
3068
|
+
inner = await crypto.subtle.decrypt(
|
|
3069
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(record.outerIv)) },
|
|
3070
|
+
record.deviceKey,
|
|
3071
|
+
new Uint8Array(fromBase64(record.ciphertext))
|
|
3072
|
+
);
|
|
3073
|
+
} catch {
|
|
3074
|
+
await clearQuickUnlockRecord(storageOptions).catch(() => void 0);
|
|
3075
|
+
throw new EmbeddedWalletQuickUnlockError("not_set", "PIN record is unusable on this device");
|
|
3076
|
+
}
|
|
3077
|
+
const pinKey = await derivePinKey(pin, fromBase64(record.salt), record.iterations);
|
|
3078
|
+
let plaintextBytes;
|
|
3079
|
+
try {
|
|
3080
|
+
plaintextBytes = await crypto.subtle.decrypt(
|
|
3081
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(record.innerIv)) },
|
|
3082
|
+
pinKey,
|
|
3083
|
+
inner
|
|
3084
|
+
);
|
|
3085
|
+
} catch {
|
|
3086
|
+
if (attempt >= QUICK_UNLOCK_MAX_ATTEMPTS) {
|
|
3087
|
+
await clearQuickUnlockRecord(storageOptions).catch(() => void 0);
|
|
3088
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3089
|
+
"wiped",
|
|
3090
|
+
"Too many wrong PINs; unlock with Face ID or your passcode"
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3094
|
+
"wrong_pin",
|
|
3095
|
+
"Wrong PIN",
|
|
3096
|
+
QUICK_UNLOCK_MAX_ATTEMPTS - attempt
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
await saveQuickUnlockRecord({ ...record, failedAttempts: 0 }, storageOptions);
|
|
3100
|
+
return {
|
|
3101
|
+
plaintext: deserializePlaintext(new Uint8Array(plaintextBytes)),
|
|
3102
|
+
publicAddress: record.publicAddress
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
async function getQuickUnlockStatus(options = {}) {
|
|
3106
|
+
const record = await loadQuickUnlockRecord(options);
|
|
3107
|
+
if (!record) {
|
|
3108
|
+
return {
|
|
3109
|
+
enabled: false,
|
|
3110
|
+
expired: false,
|
|
3111
|
+
expiresAt: null,
|
|
3112
|
+
attemptsLeft: QUICK_UNLOCK_MAX_ATTEMPTS,
|
|
3113
|
+
publicAddress: null
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
return {
|
|
3117
|
+
enabled: true,
|
|
3118
|
+
expired: typeof record.expiresAt !== "number" || Date.now() >= record.expiresAt,
|
|
3119
|
+
expiresAt: record.expiresAt,
|
|
3120
|
+
attemptsLeft: Math.max(0, QUICK_UNLOCK_MAX_ATTEMPTS - (record.failedAttempts ?? 0)),
|
|
3121
|
+
publicAddress: record.publicAddress
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
async function refreshQuickUnlockExpiry(options) {
|
|
3125
|
+
const { publicAddress, ttlMs, ...storageOptions } = options;
|
|
3126
|
+
const record = await loadQuickUnlockRecord(storageOptions);
|
|
3127
|
+
if (!record) return;
|
|
3128
|
+
if (record.publicAddress !== publicAddress) {
|
|
3129
|
+
await clearQuickUnlockRecord(storageOptions);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
await saveQuickUnlockRecord(
|
|
3133
|
+
{ ...record, expiresAt: Date.now() + (ttlMs ?? QUICK_UNLOCK_TTL_MS), failedAttempts: 0 },
|
|
3134
|
+
storageOptions
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
async function clearQuickUnlock(options = {}) {
|
|
3138
|
+
await clearQuickUnlockRecord(options);
|
|
3139
|
+
}
|
|
3140
|
+
var DEVICE_PIN_PBKDF2_ITERATIONS = 6e5;
|
|
3141
|
+
var AES_GCM_IV_BYTES4 = 12;
|
|
3142
|
+
function vaultUsesPinUnlock(vault) {
|
|
3143
|
+
return vault.kdf === EMBEDDED_WALLET_KDF_DEVICE_PIN;
|
|
3144
|
+
}
|
|
3145
|
+
async function encryptPlaintextWithDevicePin(plaintext, pin, options) {
|
|
3146
|
+
assertQuickUnlockPinAllowed(pin);
|
|
3147
|
+
const iterations = options.iterations ?? DEVICE_PIN_PBKDF2_ITERATIONS;
|
|
3148
|
+
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
3149
|
+
const pinKey = await derivePinKey(pin, salt, iterations);
|
|
3150
|
+
const deviceKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
|
|
3151
|
+
"encrypt",
|
|
3152
|
+
"decrypt"
|
|
3153
|
+
]);
|
|
3154
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES4));
|
|
3155
|
+
const inner = await crypto.subtle.encrypt(
|
|
3156
|
+
{ name: "AES-GCM", iv },
|
|
3157
|
+
pinKey,
|
|
3158
|
+
new Uint8Array(serializePlaintext(plaintext))
|
|
3159
|
+
);
|
|
3160
|
+
const deviceIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES4));
|
|
3161
|
+
const outer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: deviceIv }, deviceKey, inner);
|
|
3162
|
+
const acknowledged = options.backupAcknowledged ?? false;
|
|
3163
|
+
return {
|
|
3164
|
+
version: EMBEDDED_WALLET_VAULT_VERSION,
|
|
3165
|
+
kdf: EMBEDDED_WALLET_KDF_DEVICE_PIN,
|
|
3166
|
+
credentialId: "",
|
|
3167
|
+
publicAddress: options.publicAddress,
|
|
3168
|
+
iv: toBase64(iv),
|
|
3169
|
+
ciphertext: toBase64(new Uint8Array(outer)),
|
|
3170
|
+
kdfSalt: toBase64(salt),
|
|
3171
|
+
kdfIterations: iterations,
|
|
3172
|
+
prfSalt: "",
|
|
3173
|
+
backupAcknowledged: acknowledged,
|
|
3174
|
+
backupAcknowledgedAt: acknowledged ? (/* @__PURE__ */ new Date()).toISOString() : void 0,
|
|
3175
|
+
createdAt: plaintext.createdAt,
|
|
3176
|
+
deviceKey,
|
|
3177
|
+
deviceIv: toBase64(deviceIv)
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
async function decryptDevicePinVault(vault, pin) {
|
|
3181
|
+
if (!vaultUsesPinUnlock(vault)) {
|
|
3182
|
+
throw new EmbeddedWalletVaultError("Vault does not use PIN unlock");
|
|
3183
|
+
}
|
|
3184
|
+
if (typeof pin !== "string" || !new RegExp(`^\\d{${QUICK_UNLOCK_PIN_LENGTH}}$`).test(pin)) {
|
|
3185
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3186
|
+
"invalid_pin",
|
|
3187
|
+
`PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
if (!vault.deviceKey || !vault.deviceIv) {
|
|
3191
|
+
throw new EmbeddedWalletVaultError(
|
|
3192
|
+
"This wallet's device key is missing; recover it with your 24 words"
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
let inner;
|
|
3196
|
+
try {
|
|
3197
|
+
inner = await crypto.subtle.decrypt(
|
|
3198
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(vault.deviceIv)) },
|
|
3199
|
+
vault.deviceKey,
|
|
3200
|
+
new Uint8Array(fromBase64(vault.ciphertext))
|
|
3201
|
+
);
|
|
3202
|
+
} catch (err) {
|
|
3203
|
+
throw new EmbeddedWalletVaultError(
|
|
3204
|
+
"This wallet's device layer is damaged; recover it with your 24 words",
|
|
3205
|
+
err
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
const pinKey = await derivePinKey(pin, fromBase64(vault.kdfSalt), vault.kdfIterations);
|
|
3209
|
+
let plaintext;
|
|
3210
|
+
try {
|
|
3211
|
+
plaintext = await crypto.subtle.decrypt(
|
|
3212
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(vault.iv)) },
|
|
3213
|
+
pinKey,
|
|
3214
|
+
inner
|
|
3215
|
+
);
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
throw new EmbeddedWalletDecryptError(err);
|
|
3218
|
+
}
|
|
3219
|
+
return deserializePlaintext(new Uint8Array(plaintext));
|
|
3220
|
+
}
|
|
2893
3221
|
|
|
2894
3222
|
// src/embeddedWallet/wallet.ts
|
|
2895
3223
|
var activeSession = null;
|
|
2896
3224
|
var activeScopeId;
|
|
3225
|
+
var activePlaintext = null;
|
|
3226
|
+
var activeUnlockWasStrong = false;
|
|
2897
3227
|
function resolveScopeId(scopeId) {
|
|
2898
3228
|
const trimmed = scopeId?.trim();
|
|
2899
3229
|
return trimmed || void 0;
|
|
@@ -2910,6 +3240,12 @@ function vaultUsesPasskeyUnlock(vault) {
|
|
|
2910
3240
|
function vaultUsesPasscodeUnlock(vault) {
|
|
2911
3241
|
return vault.kdf === EMBEDDED_WALLET_KDF_PIN;
|
|
2912
3242
|
}
|
|
3243
|
+
function vaultHasPasskeyUnlock(vault) {
|
|
3244
|
+
return Boolean(vault.passkeyWrap) || vaultUsesPasskeyUnlock(vault);
|
|
3245
|
+
}
|
|
3246
|
+
function vaultPersistsSessionCache(vault) {
|
|
3247
|
+
return !(vaultUsesPinUnlock(vault) || Boolean(vault.passkeyWrap));
|
|
3248
|
+
}
|
|
2913
3249
|
function isPasscodeDecryptFailure(err) {
|
|
2914
3250
|
return err instanceof EmbeddedWalletDecryptError;
|
|
2915
3251
|
}
|
|
@@ -2932,6 +3268,28 @@ async function decryptVaultWithPasscodeProtected(vault, passcode) {
|
|
|
2932
3268
|
throw err;
|
|
2933
3269
|
}
|
|
2934
3270
|
}
|
|
3271
|
+
async function decryptVaultWithPinProtected(vault, pin) {
|
|
3272
|
+
if (!pin) {
|
|
3273
|
+
throw new EmbeddedWalletVaultError("PIN is required to unlock this wallet");
|
|
3274
|
+
}
|
|
3275
|
+
await assertPasscodeUnlockAllowed(vault.publicAddress);
|
|
3276
|
+
try {
|
|
3277
|
+
const plaintext = await decryptDevicePinVault(vault, pin);
|
|
3278
|
+
await recordPasscodeUnlockSuccess(vault.publicAddress);
|
|
3279
|
+
return plaintext;
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
if (isPasscodeDecryptFailure(err)) {
|
|
3282
|
+
await recordPasscodeUnlockFailure(vault.publicAddress);
|
|
3283
|
+
}
|
|
3284
|
+
throw err;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
async function decryptVaultWithSecret(vault, secret) {
|
|
3288
|
+
if (vaultUsesPinUnlock(vault)) {
|
|
3289
|
+
return decryptVaultWithPinProtected(vault, secret.pin);
|
|
3290
|
+
}
|
|
3291
|
+
return decryptVaultWithPasscodeProtected(vault, secret.passcode);
|
|
3292
|
+
}
|
|
2935
3293
|
async function deriveKekFromPasskeyMaterial(vault, prfOutput, authenticatorData, clientDataJSON) {
|
|
2936
3294
|
const caps = detectWebAuthnPrfCapabilities();
|
|
2937
3295
|
const kdf = selectEmbeddedWalletKdf(caps, prfOutput);
|
|
@@ -2987,40 +3345,27 @@ async function encryptPlaintextWithPasscode(plaintext, passcode, backupAcknowled
|
|
|
2987
3345
|
});
|
|
2988
3346
|
return { address: account.address, vault };
|
|
2989
3347
|
}
|
|
2990
|
-
async function
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
kek = await deriveKekFromPrf(registration.prfOutput);
|
|
3000
|
-
prfSalt = registration.prfSalt;
|
|
3001
|
-
} else {
|
|
3002
|
-
kdfSalt = createPbkdf2Salt();
|
|
3003
|
-
const material = createPasskeyMaterialFromAssertion(
|
|
3004
|
-
registration.authenticatorData,
|
|
3005
|
-
registration.clientDataJSON
|
|
3006
|
-
);
|
|
3007
|
-
kek = await deriveKekFromPbkdf2(material, kdfSalt);
|
|
3348
|
+
async function encryptPlaintextWithSecret(plaintext, secret, backupAcknowledged = false) {
|
|
3349
|
+
if (secret.pin != null) {
|
|
3350
|
+
const signer = await embeddedPlaintextToSigner(plaintext);
|
|
3351
|
+
const [account] = await signer.getAccounts();
|
|
3352
|
+
const vault = await encryptPlaintextWithDevicePin(plaintext, secret.pin, {
|
|
3353
|
+
publicAddress: account.address,
|
|
3354
|
+
backupAcknowledged
|
|
3355
|
+
});
|
|
3356
|
+
return { address: account.address, vault };
|
|
3008
3357
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
kek,
|
|
3014
|
-
kdfSalt,
|
|
3015
|
-
prfSalt,
|
|
3016
|
-
backupAcknowledged
|
|
3017
|
-
});
|
|
3018
|
-
return { address: account.address, vault };
|
|
3358
|
+
if (secret.passcode == null) {
|
|
3359
|
+
throw new EmbeddedWalletVaultError("A PIN (or legacy passcode) is required");
|
|
3360
|
+
}
|
|
3361
|
+
return encryptPlaintextWithPasscode(plaintext, secret.passcode, backupAcknowledged);
|
|
3019
3362
|
}
|
|
3020
3363
|
async function unlockVaultToSession(vault, options = {}) {
|
|
3021
3364
|
const scopeId = scopeFromOptions(options);
|
|
3022
3365
|
let plaintext;
|
|
3023
|
-
if (
|
|
3366
|
+
if (vaultUsesPinUnlock(vault)) {
|
|
3367
|
+
plaintext = await decryptVaultWithPinProtected(vault, options.pin);
|
|
3368
|
+
} else if (vaultUsesPasscodeUnlock(vault)) {
|
|
3024
3369
|
plaintext = await decryptVaultWithPasscodeProtected(vault, options.passcode);
|
|
3025
3370
|
} else {
|
|
3026
3371
|
const credentialId = decodeCredentialId(vault.credentialId);
|
|
@@ -3028,14 +3373,32 @@ async function unlockVaultToSession(vault, options = {}) {
|
|
|
3028
3373
|
const assertion = await assertPasskey({ credentialId, rpId: void 0, prfSalt });
|
|
3029
3374
|
plaintext = await decryptVaultWithUnlockMaterial(vault, void 0, assertion);
|
|
3030
3375
|
}
|
|
3376
|
+
return establishSession(vault, plaintext, scopeId, { strong: true });
|
|
3377
|
+
}
|
|
3378
|
+
async function establishSession(vault, plaintext, scopeId, options) {
|
|
3031
3379
|
const signer = await embeddedPlaintextToSigner(plaintext);
|
|
3032
3380
|
const [account] = await signer.getAccounts();
|
|
3033
3381
|
const session = { address: account.address, vault, signer };
|
|
3034
3382
|
activeSession = session;
|
|
3035
3383
|
activeScopeId = scopeId;
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3384
|
+
activePlaintext = plaintext;
|
|
3385
|
+
activeUnlockWasStrong = options.strong;
|
|
3386
|
+
if (vaultPersistsSessionCache(vault)) {
|
|
3387
|
+
try {
|
|
3388
|
+
await cacheUnlockedSession({ scopeId, plaintext });
|
|
3389
|
+
} catch {
|
|
3390
|
+
}
|
|
3391
|
+
} else {
|
|
3392
|
+
try {
|
|
3393
|
+
await clearUnlockedSession({ scopeId });
|
|
3394
|
+
} catch {
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (options.strong) {
|
|
3398
|
+
try {
|
|
3399
|
+
await refreshQuickUnlockExpiry({ scopeId, publicAddress: account.address });
|
|
3400
|
+
} catch {
|
|
3401
|
+
}
|
|
3039
3402
|
}
|
|
3040
3403
|
return session;
|
|
3041
3404
|
}
|
|
@@ -3076,7 +3439,7 @@ async function createEmbeddedWallet(options) {
|
|
|
3076
3439
|
}
|
|
3077
3440
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3078
3441
|
const plaintext = await createHdDerivedPlaintext(createdAt);
|
|
3079
|
-
const { address, vault } = await
|
|
3442
|
+
const { address, vault } = await encryptPlaintextWithSecret(plaintext, options);
|
|
3080
3443
|
await saveVaultToIndexedDb(vault, { scopeId });
|
|
3081
3444
|
lockEmbeddedWallet();
|
|
3082
3445
|
activeScopeId = scopeId;
|
|
@@ -3089,7 +3452,11 @@ async function revealEmbeddedRecoveryPhrase(options = {}) {
|
|
|
3089
3452
|
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3090
3453
|
}
|
|
3091
3454
|
let plaintext;
|
|
3092
|
-
if (vaultUsesPasscodeUnlock(vault)) {
|
|
3455
|
+
if ((vaultUsesPasscodeUnlock(vault) || vaultUsesPinUnlock(vault)) && !options.passcode && !options.pin && vault.passkeyWrap) {
|
|
3456
|
+
plaintext = await decryptPasskeyWrap(vault);
|
|
3457
|
+
} else if (vaultUsesPinUnlock(vault)) {
|
|
3458
|
+
plaintext = await decryptVaultWithPinProtected(vault, options.pin);
|
|
3459
|
+
} else if (vaultUsesPasscodeUnlock(vault)) {
|
|
3093
3460
|
plaintext = await decryptVaultWithPasscodeProtected(vault, options.passcode);
|
|
3094
3461
|
} else {
|
|
3095
3462
|
const credentialId = decodeCredentialId(vault.credentialId);
|
|
@@ -3145,7 +3512,7 @@ async function recoverEmbeddedWalletFromMnemonic(options) {
|
|
|
3145
3512
|
"Recovered wallet address does not match expectedAddress"
|
|
3146
3513
|
);
|
|
3147
3514
|
}
|
|
3148
|
-
const result = await
|
|
3515
|
+
const result = await encryptPlaintextWithSecret(plaintext, options);
|
|
3149
3516
|
if (result.address !== address) {
|
|
3150
3517
|
throw new EmbeddedWalletVaultError("Internal error: recovered address mismatch");
|
|
3151
3518
|
}
|
|
@@ -3154,41 +3521,220 @@ async function recoverEmbeddedWalletFromMnemonic(options) {
|
|
|
3154
3521
|
activeScopeId = scopeId;
|
|
3155
3522
|
return { address: result.address, vault: result.vault, keyType };
|
|
3156
3523
|
}
|
|
3524
|
+
function activePlaintextFor(scopeId, vault) {
|
|
3525
|
+
if (!activePlaintext || !activeSession) return null;
|
|
3526
|
+
if (activeScopeId !== scopeId) return null;
|
|
3527
|
+
if (activeSession.address !== vault.publicAddress) return null;
|
|
3528
|
+
return activePlaintext;
|
|
3529
|
+
}
|
|
3157
3530
|
async function enableEmbeddedPasskeyUnlock(options) {
|
|
3158
3531
|
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3159
3532
|
const vault = await loadVaultForScope(scopeId);
|
|
3160
3533
|
if (!vault) {
|
|
3161
3534
|
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3162
3535
|
}
|
|
3163
|
-
if (
|
|
3536
|
+
if (vaultHasPasskeyUnlock(vault)) {
|
|
3164
3537
|
throw new EmbeddedWalletVaultError("Passkey unlock is already enabled for this wallet");
|
|
3165
3538
|
}
|
|
3166
|
-
if (!vaultUsesPasscodeUnlock(vault)) {
|
|
3539
|
+
if (!vaultUsesPasscodeUnlock(vault) && !vaultUsesPinUnlock(vault)) {
|
|
3167
3540
|
throw new EmbeddedWalletVaultError("Unsupported vault unlock method for passkey upgrade");
|
|
3168
3541
|
}
|
|
3169
|
-
const
|
|
3170
|
-
|
|
3171
|
-
|
|
3542
|
+
const plaintext = options.passcode != null || options.pin != null ? await decryptVaultWithSecret(vault, options) : activePlaintextFor(scopeId, vault);
|
|
3543
|
+
if (!plaintext) {
|
|
3544
|
+
throw new EmbeddedWalletVaultError("Unlock the wallet (or pass the passcode) before adding Face ID");
|
|
3545
|
+
}
|
|
3546
|
+
const prfSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
3172
3547
|
const registration = await registerPasskey({
|
|
3173
3548
|
userId: options.userId,
|
|
3174
3549
|
userName: options.userName,
|
|
3175
3550
|
displayName: options.displayName,
|
|
3176
|
-
rpId: options.rpId
|
|
3551
|
+
rpId: options.rpId,
|
|
3552
|
+
prfSalt
|
|
3177
3553
|
});
|
|
3178
|
-
|
|
3554
|
+
let prfOutput = registration.prfOutput;
|
|
3555
|
+
if (!prfOutput) {
|
|
3556
|
+
try {
|
|
3557
|
+
const assertion = await assertPasskey({
|
|
3558
|
+
credentialId: registration.credentialId,
|
|
3559
|
+
rpId: options.rpId,
|
|
3560
|
+
prfSalt
|
|
3561
|
+
});
|
|
3562
|
+
prfOutput = assertion.prfOutput;
|
|
3563
|
+
} catch {
|
|
3564
|
+
prfOutput = null;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (!prfOutput) {
|
|
3568
|
+
throw new EmbeddedWalletPasskeyUnsupportedError();
|
|
3569
|
+
}
|
|
3570
|
+
const previewSigner = await embeddedPlaintextToSigner(plaintext);
|
|
3571
|
+
const [account] = await previewSigner.getAccounts();
|
|
3572
|
+
if (account.address !== vault.publicAddress) {
|
|
3573
|
+
throw new EmbeddedWalletVaultError("Passkey upgrade must not change wallet address");
|
|
3574
|
+
}
|
|
3575
|
+
const kek = await deriveKekFromPrf(prfOutput);
|
|
3576
|
+
const wrapped = await encryptPlaintextWithKek(plaintext, kek);
|
|
3577
|
+
const updated = {
|
|
3578
|
+
...vault,
|
|
3579
|
+
passkeyWrap: {
|
|
3580
|
+
credentialId: encodeCredentialId(registration.credentialId),
|
|
3581
|
+
prfSalt: encodeCredentialId(prfSalt),
|
|
3582
|
+
rpId: options.rpId,
|
|
3583
|
+
iv: wrapped.iv,
|
|
3584
|
+
ciphertext: wrapped.ciphertext,
|
|
3585
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3588
|
+
await saveVaultToIndexedDb(updated, { scopeId });
|
|
3589
|
+
if (activeSession && activeSession.address === updated.publicAddress) {
|
|
3590
|
+
activeSession = { ...activeSession, vault: updated };
|
|
3591
|
+
}
|
|
3592
|
+
return { address: updated.publicAddress, vault: updated };
|
|
3593
|
+
}
|
|
3594
|
+
async function setEmbeddedWalletPin(options) {
|
|
3595
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3596
|
+
const vault = await loadVaultForScope(scopeId);
|
|
3597
|
+
if (!vault) {
|
|
3598
|
+
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3599
|
+
}
|
|
3600
|
+
if (!vaultUsesPasscodeUnlock(vault) && !vaultUsesPinUnlock(vault)) {
|
|
3601
|
+
throw new EmbeddedWalletVaultError("This wallet type cannot switch to a PIN");
|
|
3602
|
+
}
|
|
3603
|
+
let plaintext = null;
|
|
3604
|
+
if (options.currentPin != null || options.passcode != null) {
|
|
3605
|
+
plaintext = await decryptVaultWithSecret(vault, {
|
|
3606
|
+
pin: options.currentPin,
|
|
3607
|
+
passcode: options.passcode
|
|
3608
|
+
});
|
|
3609
|
+
} else if (activeUnlockWasStrong) {
|
|
3610
|
+
plaintext = activePlaintextFor(scopeId, vault);
|
|
3611
|
+
}
|
|
3612
|
+
if (!plaintext) {
|
|
3613
|
+
throw new EmbeddedWalletVaultError("Unlock the wallet before changing the PIN");
|
|
3614
|
+
}
|
|
3615
|
+
const result = await encryptPlaintextWithSecret(
|
|
3179
3616
|
plaintext,
|
|
3180
|
-
|
|
3181
|
-
backupAcknowledged
|
|
3617
|
+
{ pin: options.pin },
|
|
3618
|
+
vault.backupAcknowledged
|
|
3182
3619
|
);
|
|
3183
|
-
if (result.address !==
|
|
3184
|
-
throw new EmbeddedWalletVaultError("
|
|
3620
|
+
if (result.address !== vault.publicAddress) {
|
|
3621
|
+
throw new EmbeddedWalletVaultError("Changing the PIN must not change the wallet address");
|
|
3622
|
+
}
|
|
3623
|
+
const updated = {
|
|
3624
|
+
...result.vault,
|
|
3625
|
+
backupAcknowledgedAt: vault.backupAcknowledgedAt,
|
|
3626
|
+
createdAt: vault.createdAt,
|
|
3627
|
+
passkeyWrap: vault.passkeyWrap
|
|
3628
|
+
};
|
|
3629
|
+
if (!updated.passkeyWrap) delete updated.passkeyWrap;
|
|
3630
|
+
await saveVaultToIndexedDb(updated, { scopeId });
|
|
3631
|
+
await clearQuickUnlock({ scopeId }).catch(() => void 0);
|
|
3632
|
+
if (activeSession && activeSession.address === updated.publicAddress) {
|
|
3633
|
+
activeSession = { ...activeSession, vault: updated };
|
|
3185
3634
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3635
|
+
return { address: updated.publicAddress, vault: updated };
|
|
3636
|
+
}
|
|
3637
|
+
async function disableEmbeddedPasskeyUnlock(options = {}) {
|
|
3638
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3639
|
+
const vault = await loadVaultForScope(scopeId);
|
|
3640
|
+
if (!vault) {
|
|
3641
|
+
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3189
3642
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3643
|
+
if (!vault.passkeyWrap) {
|
|
3644
|
+
return vault;
|
|
3645
|
+
}
|
|
3646
|
+
const updated = { ...vault };
|
|
3647
|
+
delete updated.passkeyWrap;
|
|
3648
|
+
await saveVaultToIndexedDb(updated, { scopeId });
|
|
3649
|
+
if (activeSession && activeSession.address === updated.publicAddress) {
|
|
3650
|
+
activeSession = { ...activeSession, vault: updated };
|
|
3651
|
+
}
|
|
3652
|
+
return updated;
|
|
3653
|
+
}
|
|
3654
|
+
async function decryptPasskeyWrap(vault) {
|
|
3655
|
+
const wrap = vault.passkeyWrap;
|
|
3656
|
+
if (!wrap) {
|
|
3657
|
+
throw new EmbeddedWalletVaultError("Face ID is not set up for this wallet");
|
|
3658
|
+
}
|
|
3659
|
+
const prfSalt = decodeCredentialId(wrap.prfSalt);
|
|
3660
|
+
const assertion = await assertPasskey({
|
|
3661
|
+
credentialId: decodeCredentialId(wrap.credentialId),
|
|
3662
|
+
rpId: wrap.rpId,
|
|
3663
|
+
prfSalt
|
|
3664
|
+
});
|
|
3665
|
+
if (!assertion.prfOutput) {
|
|
3666
|
+
throw new EmbeddedWalletPasskeyUnsupportedError(
|
|
3667
|
+
"This browser did not return the passkey secret; unlock with your passcode"
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
const kek = await deriveKekFromPrf(assertion.prfOutput);
|
|
3671
|
+
const plaintext = await decryptPlaintextWithKek(wrap.iv, wrap.ciphertext, kek);
|
|
3672
|
+
const signer = await embeddedPlaintextToSigner(plaintext);
|
|
3673
|
+
const [account] = await signer.getAccounts();
|
|
3674
|
+
if (account.address !== vault.publicAddress) {
|
|
3675
|
+
throw new EmbeddedWalletVaultError("Passkey opened a key for a different address");
|
|
3676
|
+
}
|
|
3677
|
+
return plaintext;
|
|
3678
|
+
}
|
|
3679
|
+
async function unlockEmbeddedWalletWithPasskey(options = {}) {
|
|
3680
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3681
|
+
const vault = await loadVaultForScope(scopeId);
|
|
3682
|
+
if (!vault) {
|
|
3683
|
+
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3684
|
+
}
|
|
3685
|
+
if (vaultUsesPasskeyUnlock(vault)) {
|
|
3686
|
+
return unlockVaultToSession(vault, { scopeId });
|
|
3687
|
+
}
|
|
3688
|
+
const plaintext = await decryptPasskeyWrap(vault);
|
|
3689
|
+
return establishSession(vault, plaintext, scopeId, { strong: true });
|
|
3690
|
+
}
|
|
3691
|
+
async function setEmbeddedQuickUnlockPin(options) {
|
|
3692
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3693
|
+
const vault = await loadVaultForScope(scopeId);
|
|
3694
|
+
if (!vault) {
|
|
3695
|
+
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3696
|
+
}
|
|
3697
|
+
const plaintext = activePlaintextFor(scopeId, vault);
|
|
3698
|
+
if (!plaintext || !activeUnlockWasStrong) {
|
|
3699
|
+
throw new EmbeddedWalletQuickUnlockError(
|
|
3700
|
+
"no_session",
|
|
3701
|
+
"Unlock with Face ID or your passcode before choosing a PIN"
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
await createQuickUnlockRecord({
|
|
3705
|
+
scopeId,
|
|
3706
|
+
plaintext,
|
|
3707
|
+
publicAddress: vault.publicAddress,
|
|
3708
|
+
pin: options.pin
|
|
3709
|
+
});
|
|
3710
|
+
return getQuickUnlockStatus({ scopeId });
|
|
3711
|
+
}
|
|
3712
|
+
async function unlockEmbeddedWalletWithPin(options) {
|
|
3713
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3714
|
+
const vault = await loadVaultForScope(scopeId);
|
|
3715
|
+
if (!vault) {
|
|
3716
|
+
throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
|
|
3717
|
+
}
|
|
3718
|
+
const opened = await openQuickUnlockRecord({ scopeId, pin: options.pin });
|
|
3719
|
+
if (opened.publicAddress !== vault.publicAddress) {
|
|
3720
|
+
await clearQuickUnlock({ scopeId }).catch(() => void 0);
|
|
3721
|
+
throw new EmbeddedWalletQuickUnlockError("not_set", "PIN belongs to a different wallet");
|
|
3722
|
+
}
|
|
3723
|
+
const signer = await embeddedPlaintextToSigner(opened.plaintext);
|
|
3724
|
+
const [account] = await signer.getAccounts();
|
|
3725
|
+
if (account.address !== vault.publicAddress) {
|
|
3726
|
+
await clearQuickUnlock({ scopeId }).catch(() => void 0);
|
|
3727
|
+
throw new EmbeddedWalletQuickUnlockError("not_set", "PIN record does not match this wallet");
|
|
3728
|
+
}
|
|
3729
|
+
return establishSession(vault, opened.plaintext, scopeId, { strong: false });
|
|
3730
|
+
}
|
|
3731
|
+
async function getEmbeddedQuickUnlockStatus(options = {}) {
|
|
3732
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3733
|
+
return getQuickUnlockStatus({ scopeId });
|
|
3734
|
+
}
|
|
3735
|
+
async function clearEmbeddedQuickUnlockPin(options = {}) {
|
|
3736
|
+
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3737
|
+
await clearQuickUnlock({ scopeId });
|
|
3192
3738
|
}
|
|
3193
3739
|
async function unlockEmbeddedWallet(options = {}) {
|
|
3194
3740
|
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
@@ -3218,12 +3764,16 @@ async function tryRestoreUnlockedSession(options = {}) {
|
|
|
3218
3764
|
const session = { address: account.address, vault, signer };
|
|
3219
3765
|
activeSession = session;
|
|
3220
3766
|
activeScopeId = scopeId;
|
|
3767
|
+
activePlaintext = plaintext;
|
|
3768
|
+
activeUnlockWasStrong = false;
|
|
3221
3769
|
return session;
|
|
3222
3770
|
}
|
|
3223
3771
|
function lockEmbeddedWallet() {
|
|
3224
3772
|
const scopeId = activeScopeId;
|
|
3225
3773
|
activeSession = null;
|
|
3226
3774
|
activeScopeId = void 0;
|
|
3775
|
+
activePlaintext = null;
|
|
3776
|
+
activeUnlockWasStrong = false;
|
|
3227
3777
|
void clearUnlockedSession({ scopeId }).catch(() => void 0);
|
|
3228
3778
|
}
|
|
3229
3779
|
function getEmbeddedWalletSession() {
|
|
@@ -3251,7 +3801,7 @@ async function acknowledgeEmbeddedBackup(options = {}) {
|
|
|
3251
3801
|
if (activeSession) {
|
|
3252
3802
|
activeSession = { ...activeSession, vault: updated };
|
|
3253
3803
|
}
|
|
3254
|
-
return exportEncryptedBackup(updated);
|
|
3804
|
+
return vaultUsesPinUnlock(updated) ? null : exportEncryptedBackup(updated);
|
|
3255
3805
|
}
|
|
3256
3806
|
function downloadEmbeddedBackupFile(backup, filename) {
|
|
3257
3807
|
const blob = new Blob([serializeBackupFile(backup)], { type: "application/json" });
|
|
@@ -3280,6 +3830,7 @@ async function clearEmbeddedWallet(options = {}) {
|
|
|
3280
3830
|
const scopeId = scopeFromOptions(options) ?? activeScopeId;
|
|
3281
3831
|
lockEmbeddedWallet();
|
|
3282
3832
|
await clearUnlockedSession({ scopeId });
|
|
3833
|
+
await clearQuickUnlock({ scopeId }).catch(() => void 0);
|
|
3283
3834
|
await clearVaultFromIndexedDb({ scopeId });
|
|
3284
3835
|
}
|
|
3285
3836
|
|
|
@@ -3562,6 +4113,6 @@ function sleep(ms) {
|
|
|
3562
4113
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3563
4114
|
}
|
|
3564
4115
|
|
|
3565
|
-
export { ATTESTATION_TYPE_CITIZENSHIP_APPLICATION, ATTESTATION_TYPE_RECOVERY, ApplicationStatus, BECH32_PREFIX, CHAIN_COIN_TYPE, CitizenTxError, DEFAULT_CHAIN_ID, DEFAULT_REST_URL, DEFAULT_RPC_URL, EMBEDDED_WALLET_KDF_PBKDF2, EMBEDDED_WALLET_KDF_PIN, EMBEDDED_WALLET_KDF_PRF, EMBEDDED_WALLET_KEY_DIRECT_ENTROPY, EMBEDDED_WALLET_KEY_HD_DERIVED, EMBEDDED_WALLET_PASSCODE_MAX_LENGTH, EMBEDDED_WALLET_PASSCODE_MIN_LENGTH, EMBEDDED_WALLET_PASSKEY_PBKDF2_ITERATIONS, EMBEDDED_WALLET_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_RECOVERY_PHRASE_DIRECT_ENTROPY_WARNING, EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING, EMBEDDED_WALLET_SESSION_TTL_MS, EMBEDDED_WALLET_VAULT_VERSION, EmbeddedWalletBackupRequiredError, EmbeddedWalletDecryptError, EmbeddedWalletLockoutError, EmbeddedWalletVaultError, EmbeddedWalletWeakPasscodeError, InvalidChainConfigError, MsgClaimReward, MsgSubmitApplication, MsgSubmitRecoveryAttestation, PASSCODE_LOCKOUT_FAILURE_THRESHOLD, PASSCODE_LOCKOUT_INITIAL_MS, PASSCODE_LOCKOUT_MAX_MS, RecoveryAttestationStatus, RestQueryError, RpcConnectionError, UKDR_DECIMALS, UKDR_DISPLAY_DENOM, UKDR_MINIMAL_DENOM, VAULT_LEGACY_KEY, WALLETCONNECT_COSMOS_METHODS, WALLETCONNECT_COSMOS_NAMESPACE, WEBAUTHN_RP_NAME, WalletAccountError, WalletConnectNotInitializedError, WalletConnectSessionError, WalletConnectSigningError, WalletConnectionError, WalletConnectionRejectedError, WalletNotInstalledError, WebAuthnUnavailableError, WebAuthnUserCancelledError, acknowledgeEmbeddedBackup, addressFromHdMnemonic, addressFromPrivateKey, assertPasscodeAllowedForCreation, assertPasscodeUnlockAllowed, assertPasskey, assertValidPasscode, assertValidRecoveryMnemonic, assertVaultPermitsTransaction, attestationSignPayload, broadcastAndConfirmTx, buildClaimRewardMsg, buildKingdomChainInfo, buildRequiredCosmosNamespaces, buildSendUkdrMsg, buildSubmitApplicationMsg, cacheUnlockedSession, citizenTxErrorMessage, clearEmbeddedWallet, clearPasscodeLockoutRecord, clearSessionCacheRecord, clearUnlockedSession, clearVaultFromIndexedDb, computePasscodeLockoutDurationMs, connectCosmostation, connectKeplr, connectLeap, connectWalletConnect, createBrowserSigningClient, createEmbeddedWallet, createHdDerivedPlaintext, createKingdomChainRegistry, createPasskeyMaterialFromAssertion, createPbkdf2Salt, createWalletConnectOfflineSigner, decodeCredentialId, decodeDirectEntropyMnemonic, decryptWalletPayload, defaultEmbeddedHdPath, defaultUkdrTxFee, deriveKekFromPasscode, deriveKekFromPbkdf2, deriveKekFromPrf, derivePrivateKeyFromHdMnemonic, detectRecoveryMnemonicKeyType, detectWebAuthnPrfCapabilities, disconnectWalletConnect, downloadEmbeddedBackupFile, embeddedPlaintextToSigner, enableEmbeddedPasskeyUnlock, encodeCredentialId, encodeDirectEntropyMnemonic, encryptWalletPayload, evaluatePasscodeStrength, exportEncryptedBackup, fetchFeeGrantGranter, fetchUkdrBalance, fetchUnclaimedFinalizedRewards, formatUkdrAsKdr, getCosmostationKeplrApi, getEmbeddedOfflineSigner, getEmbeddedWalletSession, getPasscodeLockoutState, hasEmbeddedWallet, hasLegacyEmbeddedWallet, hasMixedCharacterClasses, importEmbeddedBackup, initWalletConnectClient, isCosmostationAvailable, isKeplrAvailable, isLeapAvailable, loadPasscodeLockoutRecord, loadSessionCacheRecord, loadVaultFromIndexedDb, lockEmbeddedWallet, markVaultBackupAcknowledged, migrateLegacyEmbeddedWallet, normalizePasscode, normalizeRecoveryMnemonic, parseEncryptedBackup, parseKdrToUkdr, parseWalletConnectAccount, plaintextFromRecoveryMnemonic, providerSeedFromHex, publicKeyFromPrivateSeed, readPrfOutput, recordPasscodeUnlockFailure, recordPasscodeUnlockSuccess, recoverEmbeddedWalletFromMnemonic, recoveryMnemonicAddress, recoveryMnemonicFromPlaintext, registerPasskey, resolvePlaintextKeyType, resolveVaultStorageKey, restoreCachedSessionPlaintext, restoreWalletConnectSession, revealEmbeddedRecoveryPhrase, savePasscodeLockoutRecord, saveSessionCacheRecord, saveVaultToIndexedDb, selectEmbeddedWalletKdf, serializeBackupFile, signAttestationPayload, signCitizenshipAttestation, suggestAndConnect, toWalletConnectCosmosChainId, tryRestoreUnlockedSession, ukdrCoin, unlockEmbeddedWallet, validateChainConfig, vaultUsesPasscodeUnlock, vaultUsesPasskeyUnlock };
|
|
4116
|
+
export { ATTESTATION_TYPE_CITIZENSHIP_APPLICATION, ATTESTATION_TYPE_RECOVERY, ApplicationStatus, BECH32_PREFIX, CHAIN_COIN_TYPE, CitizenTxError, DEFAULT_CHAIN_ID, DEFAULT_REST_URL, DEFAULT_RPC_URL, DEVICE_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_KDF_DEVICE_PIN, EMBEDDED_WALLET_KDF_PBKDF2, EMBEDDED_WALLET_KDF_PIN, EMBEDDED_WALLET_KDF_PRF, EMBEDDED_WALLET_KEY_DIRECT_ENTROPY, EMBEDDED_WALLET_KEY_HD_DERIVED, EMBEDDED_WALLET_PASSCODE_MAX_LENGTH, EMBEDDED_WALLET_PASSCODE_MIN_LENGTH, EMBEDDED_WALLET_PASSKEY_PBKDF2_ITERATIONS, EMBEDDED_WALLET_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_RECOVERY_PHRASE_DIRECT_ENTROPY_WARNING, EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING, EMBEDDED_WALLET_SESSION_TTL_MS, EMBEDDED_WALLET_VAULT_VERSION, EmbeddedWalletBackupRequiredError, EmbeddedWalletDecryptError, EmbeddedWalletLockoutError, EmbeddedWalletPasskeyUnsupportedError, EmbeddedWalletQuickUnlockError, EmbeddedWalletVaultError, EmbeddedWalletWeakPasscodeError, InvalidChainConfigError, MsgClaimReward, MsgSubmitApplication, MsgSubmitRecoveryAttestation, PASSCODE_LOCKOUT_FAILURE_THRESHOLD, PASSCODE_LOCKOUT_INITIAL_MS, PASSCODE_LOCKOUT_MAX_MS, QUICK_UNLOCK_MAX_ATTEMPTS, QUICK_UNLOCK_PBKDF2_ITERATIONS, QUICK_UNLOCK_PIN_LENGTH, QUICK_UNLOCK_TTL_MS, RecoveryAttestationStatus, RestQueryError, RpcConnectionError, UKDR_DECIMALS, UKDR_DISPLAY_DENOM, UKDR_MINIMAL_DENOM, VAULT_LEGACY_KEY, WALLETCONNECT_COSMOS_METHODS, WALLETCONNECT_COSMOS_NAMESPACE, WEBAUTHN_RP_NAME, WalletAccountError, WalletConnectNotInitializedError, WalletConnectSessionError, WalletConnectSigningError, WalletConnectionError, WalletConnectionRejectedError, WalletNotInstalledError, WebAuthnUnavailableError, WebAuthnUserCancelledError, acknowledgeEmbeddedBackup, addressFromHdMnemonic, addressFromPrivateKey, assertPasscodeAllowedForCreation, assertPasscodeUnlockAllowed, assertPasskey, assertQuickUnlockPinAllowed, assertValidPasscode, assertValidRecoveryMnemonic, assertVaultPermitsTransaction, attestationSignPayload, broadcastAndConfirmTx, buildClaimRewardMsg, buildKingdomChainInfo, buildRequiredCosmosNamespaces, buildSendUkdrMsg, buildSubmitApplicationMsg, cacheUnlockedSession, citizenTxErrorMessage, clearEmbeddedQuickUnlockPin, clearEmbeddedWallet, clearPasscodeLockoutRecord, clearQuickUnlock, clearQuickUnlockRecord, clearSessionCacheRecord, clearUnlockedSession, clearVaultFromIndexedDb, computePasscodeLockoutDurationMs, connectCosmostation, connectKeplr, connectLeap, connectWalletConnect, createBrowserSigningClient, createEmbeddedWallet, createHdDerivedPlaintext, createKingdomChainRegistry, createPasskeyMaterialFromAssertion, createPbkdf2Salt, createQuickUnlockRecord, createWalletConnectOfflineSigner, decodeCredentialId, decodeDirectEntropyMnemonic, decryptDevicePinVault, decryptPlaintextWithKek, decryptWalletPayload, defaultEmbeddedHdPath, defaultUkdrTxFee, deriveKekFromPasscode, deriveKekFromPbkdf2, deriveKekFromPrf, derivePinKey, derivePrivateKeyFromHdMnemonic, deserializePlaintext, detectRecoveryMnemonicKeyType, detectWebAuthnPrfCapabilities, disableEmbeddedPasskeyUnlock, disconnectWalletConnect, downloadEmbeddedBackupFile, embeddedPlaintextToSigner, enableEmbeddedPasskeyUnlock, encodeCredentialId, encodeDirectEntropyMnemonic, encryptPlaintextWithDevicePin, encryptPlaintextWithKek, encryptWalletPayload, evaluatePasscodeStrength, exportEncryptedBackup, fetchFeeGrantGranter, fetchUkdrBalance, fetchUnclaimedFinalizedRewards, formatUkdrAsKdr, getCosmostationKeplrApi, getEmbeddedOfflineSigner, getEmbeddedQuickUnlockStatus, getEmbeddedWalletSession, getPasscodeLockoutState, getQuickUnlockStatus, hasEmbeddedWallet, hasLegacyEmbeddedWallet, hasMixedCharacterClasses, importEmbeddedBackup, initWalletConnectClient, isCosmostationAvailable, isKeplrAvailable, isLeapAvailable, isTrivialPin, loadPasscodeLockoutRecord, loadQuickUnlockRecord, loadSessionCacheRecord, loadVaultFromIndexedDb, lockEmbeddedWallet, markVaultBackupAcknowledged, migrateLegacyEmbeddedWallet, normalizePasscode, normalizeRecoveryMnemonic, openQuickUnlockRecord, parseEncryptedBackup, parseKdrToUkdr, parseWalletConnectAccount, plaintextFromRecoveryMnemonic, providerSeedFromHex, publicKeyFromPrivateSeed, readPrfOutput, recordPasscodeUnlockFailure, recordPasscodeUnlockSuccess, recoverEmbeddedWalletFromMnemonic, recoveryMnemonicAddress, recoveryMnemonicFromPlaintext, refreshQuickUnlockExpiry, registerPasskey, resolvePlaintextKeyType, resolveVaultStorageKey, restoreCachedSessionPlaintext, restoreWalletConnectSession, revealEmbeddedRecoveryPhrase, savePasscodeLockoutRecord, saveQuickUnlockRecord, saveSessionCacheRecord, saveVaultToIndexedDb, selectEmbeddedWalletKdf, serializeBackupFile, serializePlaintext, setEmbeddedQuickUnlockPin, setEmbeddedWalletPin, signAttestationPayload, signCitizenshipAttestation, suggestAndConnect, toWalletConnectCosmosChainId, tryRestoreUnlockedSession, ukdrCoin, unlockEmbeddedWallet, unlockEmbeddedWalletWithPasskey, unlockEmbeddedWalletWithPin, validateChainConfig, vaultHasPasskeyUnlock, vaultUsesPasscodeUnlock, vaultUsesPasskeyUnlock, vaultUsesPinUnlock };
|
|
3566
4117
|
//# sourceMappingURL=index.js.map
|
|
3567
4118
|
//# sourceMappingURL=index.js.map
|