@vaultkeepr/core 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +146 -3
- package/dist/index.d.ts +146 -3
- package/dist/index.js +407 -35
- package/dist/index.mjs +384 -35
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -33,6 +33,78 @@ function getEntryGroupLabel(folder) {
|
|
|
33
33
|
if (!folder) return ENTRY_GROUP_LABELS.identifiants;
|
|
34
34
|
return ENTRY_GROUP_LABELS[folder] ?? folder;
|
|
35
35
|
}
|
|
36
|
+
var JUNK_SUBDOMAIN_PREFIXES = /* @__PURE__ */ new Set([
|
|
37
|
+
"www",
|
|
38
|
+
"app",
|
|
39
|
+
"mobile",
|
|
40
|
+
"accounts",
|
|
41
|
+
"dashboard",
|
|
42
|
+
"kyc",
|
|
43
|
+
"mail",
|
|
44
|
+
"web",
|
|
45
|
+
"my",
|
|
46
|
+
"login",
|
|
47
|
+
"portal",
|
|
48
|
+
"id",
|
|
49
|
+
"news"
|
|
50
|
+
]);
|
|
51
|
+
function brandFrom(url) {
|
|
52
|
+
if (!url?.trim()) return null;
|
|
53
|
+
try {
|
|
54
|
+
const raw = url.includes("://") ? url : `https://${url}`;
|
|
55
|
+
const host = new URL(raw).hostname.replace(/^www\./i, "");
|
|
56
|
+
if (/^\d+(\.\d+){3}$/.test(host)) return null;
|
|
57
|
+
const parts = host.split(".").filter(Boolean);
|
|
58
|
+
if (parts.length < 2) return null;
|
|
59
|
+
let label = parts[0];
|
|
60
|
+
if (JUNK_SUBDOMAIN_PREFIXES.has(label.toLowerCase()) && parts.length > 2) label = parts[1];
|
|
61
|
+
if (!label) return null;
|
|
62
|
+
return { brand: label.charAt(0).toUpperCase() + label.slice(1).toLowerCase(), host: host.toLowerCase() };
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function cleanEntryTitle(rawTitle, url) {
|
|
68
|
+
const raw = rawTitle.trim();
|
|
69
|
+
let candidate = raw;
|
|
70
|
+
{
|
|
71
|
+
const lower = raw.toLowerCase();
|
|
72
|
+
let pos = lower.indexOf("passkey");
|
|
73
|
+
while (pos > 0) {
|
|
74
|
+
let i = pos;
|
|
75
|
+
while (i > 0 && /\s/.test(raw.charAt(i - 1))) i--;
|
|
76
|
+
const prefix = raw.slice(0, i);
|
|
77
|
+
if (i < pos && !prefix.includes("\n")) {
|
|
78
|
+
let j = pos + 7;
|
|
79
|
+
while (j < raw.length && /\s/.test(raw.charAt(j))) j++;
|
|
80
|
+
const tail = raw.slice(j);
|
|
81
|
+
if (j > pos + 7 && tail.length > 0 && !tail.includes("\n")) {
|
|
82
|
+
if (prefix.trim() !== "" && /\d/.test(tail)) candidate = prefix.trim();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
pos = lower.indexOf("passkey", pos + 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const brandInfo = brandFrom(url);
|
|
90
|
+
if (brandInfo) {
|
|
91
|
+
if (!candidate) return brandInfo.brand;
|
|
92
|
+
if (/^[\d\s()+.-]+$/.test(candidate)) return brandInfo.brand;
|
|
93
|
+
if (candidate.toLowerCase() === brandInfo.brand.toLowerCase()) return brandInfo.brand;
|
|
94
|
+
const schemeStripped = candidate.replace(/^https?:\/\//i, "");
|
|
95
|
+
const nl = schemeStripped.lastIndexOf("\n");
|
|
96
|
+
const slash = schemeStripped.indexOf("/", nl + 1);
|
|
97
|
+
const hostCandidate = slash === -1 ? schemeStripped : schemeStripped.slice(0, slash);
|
|
98
|
+
if (hostCandidate.toLowerCase() === brandInfo.host) {
|
|
99
|
+
return brandInfo.brand;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return candidate || brandInfo?.brand || raw;
|
|
103
|
+
}
|
|
104
|
+
function getEntryDisplayName(entry, primaryUrl) {
|
|
105
|
+
const url = primaryUrl ?? entry.urls?.[0]?.uri ?? entry.url ?? void 0;
|
|
106
|
+
return cleanEntryTitle(entry.title ?? entry.username ?? "", url);
|
|
107
|
+
}
|
|
36
108
|
function resolveFolderName(folderId, folderTree) {
|
|
37
109
|
if (!folderId) return ENTRY_GROUP_LABELS.identifiants;
|
|
38
110
|
if (folderId in ENTRY_GROUP_LABELS) return ENTRY_GROUP_LABELS[folderId];
|
|
@@ -531,9 +603,12 @@ function normalizeUrl(raw) {
|
|
|
531
603
|
let u = (raw ?? "").trim().toLowerCase();
|
|
532
604
|
u = u.replace(/^https?:\/\//, "");
|
|
533
605
|
u = u.replace(/^www\./, "");
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
606
|
+
const nl = u.lastIndexOf("\n");
|
|
607
|
+
const hash = u.indexOf("#", nl + 1);
|
|
608
|
+
if (hash !== -1) u = u.slice(0, hash);
|
|
609
|
+
let end = u.length;
|
|
610
|
+
while (end > 0 && u.charAt(end - 1) === "/") end--;
|
|
611
|
+
return u.slice(0, end);
|
|
537
612
|
}
|
|
538
613
|
function entryFingerprint(entry) {
|
|
539
614
|
const url = normalizeUrl(entry.url ?? "");
|
|
@@ -702,14 +777,14 @@ function isEncryptedExport(jsonStr) {
|
|
|
702
777
|
return false;
|
|
703
778
|
}
|
|
704
779
|
}
|
|
705
|
-
async function importEncryptedVault(encryptedJson, password, argon2Opts) {
|
|
780
|
+
async function importEncryptedVault(encryptedJson, password, argon2Opts, precomputed) {
|
|
706
781
|
const payload = JSON.parse(encryptedJson.trim());
|
|
707
782
|
if (payload?.format !== ENCRYPTED_EXPORT_FORMAT || !payload.salt || !payload.nonce || !payload.ciphertext) {
|
|
708
783
|
throw new Error("Format d'export chiffr\xE9 invalide.");
|
|
709
784
|
}
|
|
710
785
|
const salt = hexToBytes3(payload.salt);
|
|
711
786
|
const effectiveOpts = payload.kdf ? { t: payload.kdf.t, m: payload.kdf.m, p: payload.kdf.p, dkLen: payload.kdf.dkLen } : argon2Opts ?? {};
|
|
712
|
-
const key = await deriveKeyFromPasswordArgon2(password, salt, effectiveOpts);
|
|
787
|
+
const key = precomputed?.key ?? await deriveKeyFromPasswordArgon2(password, precomputed?.salt ?? salt, effectiveOpts);
|
|
713
788
|
const plaintext = decryptVault(
|
|
714
789
|
{
|
|
715
790
|
ciphertext: payload.ciphertext,
|
|
@@ -1421,7 +1496,6 @@ function isEnpassCsv(text) {
|
|
|
1421
1496
|
const firstLine = text.split(/\r?\n/)[0];
|
|
1422
1497
|
if (!firstLine) return false;
|
|
1423
1498
|
const rawHeaders = firstLine.split(",");
|
|
1424
|
-
const headers = rawHeaders.map((h) => h.replace(/^["']|["']$/g, "").trim());
|
|
1425
1499
|
return rawHeaders.some((h) => {
|
|
1426
1500
|
const clean = h.replace(/^["']|["']$/g, "").trim();
|
|
1427
1501
|
return clean === "Title";
|
|
@@ -1959,22 +2033,28 @@ function parseCsvNoteExtras(rawNote) {
|
|
|
1959
2033
|
let totpSecret;
|
|
1960
2034
|
const customFields = [];
|
|
1961
2035
|
for (const line of lines) {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2036
|
+
if (line.startsWith("TOTP:")) {
|
|
2037
|
+
let j = 5;
|
|
2038
|
+
while (j < line.length && /\s/.test(line.charAt(j))) j++;
|
|
2039
|
+
if (j > 5 && j < line.length) {
|
|
2040
|
+
totpSecret = line.slice(j).trim();
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
1966
2043
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2044
|
+
if (line.startsWith("Fields:")) {
|
|
2045
|
+
let j = 7;
|
|
2046
|
+
while (j < line.length && /\s/.test(line.charAt(j))) j++;
|
|
2047
|
+
if (j > 7 && j < line.length) {
|
|
2048
|
+
const entries = line.slice(j).split("|");
|
|
2049
|
+
for (const entry of entries) {
|
|
2050
|
+
const colonIdx = entry.indexOf(":");
|
|
2051
|
+
if (colonIdx > 0) {
|
|
2052
|
+
customFields.push({
|
|
2053
|
+
name: entry.slice(0, colonIdx).trim(),
|
|
2054
|
+
value: entry.slice(colonIdx + 1).trim(),
|
|
2055
|
+
type: "text"
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1978
2058
|
}
|
|
1979
2059
|
}
|
|
1980
2060
|
}
|
|
@@ -2475,7 +2555,6 @@ function exportCsv(vault, opts = {}) {
|
|
|
2475
2555
|
continue;
|
|
2476
2556
|
}
|
|
2477
2557
|
if (e.folder === "cartes") {
|
|
2478
|
-
const m = parseCardMeta(e.notes);
|
|
2479
2558
|
rows.push(
|
|
2480
2559
|
[
|
|
2481
2560
|
csvEscape("cartes"),
|
|
@@ -2831,7 +2910,10 @@ import { sha512 } from "@noble/hashes/sha2.js";
|
|
|
2831
2910
|
var TOTP_STEP_SEC = 30;
|
|
2832
2911
|
function base32Decode(input) {
|
|
2833
2912
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
2834
|
-
const
|
|
2913
|
+
const upper = input.replace(/\s/g, "").toUpperCase();
|
|
2914
|
+
let end = upper.length;
|
|
2915
|
+
while (end > 0 && upper.charAt(end - 1) === "=") end--;
|
|
2916
|
+
const clean = upper.slice(0, end);
|
|
2835
2917
|
const bits = [];
|
|
2836
2918
|
for (let i = 0; i < clean.length; i++) {
|
|
2837
2919
|
const idx = alphabet.indexOf(clean[i]);
|
|
@@ -2902,9 +2984,9 @@ function parseTOTPUri(uri) {
|
|
|
2902
2984
|
import { sha1 as sha12 } from "@noble/hashes/legacy.js";
|
|
2903
2985
|
import { utf8ToBytes as utf8ToBytes4, bytesToHex as bytesToHex5 } from "@noble/hashes/utils.js";
|
|
2904
2986
|
var RANGE_BASE = "https://api.pwnedpasswords.com/range/";
|
|
2905
|
-
async function
|
|
2906
|
-
|
|
2907
|
-
|
|
2987
|
+
async function getPwnedCountForHash(sha1Hex, options) {
|
|
2988
|
+
const hash = sha1Hex.trim().toUpperCase();
|
|
2989
|
+
if (hash.length !== 40) throw new Error(`Invalid SHA-1 length: ${hash.length}`);
|
|
2908
2990
|
const prefix = hash.slice(0, 5);
|
|
2909
2991
|
const suffix = hash.slice(5);
|
|
2910
2992
|
const fetchFn = options?.fetchFn ?? fetch;
|
|
@@ -2920,7 +3002,6 @@ async function getPwnedPasswordCount(password, options) {
|
|
|
2920
3002
|
throw new Error(`PwnedPasswords API: ${res.status}`);
|
|
2921
3003
|
}
|
|
2922
3004
|
const text = await res.text();
|
|
2923
|
-
const suffixUpper = suffix.toUpperCase();
|
|
2924
3005
|
for (const line of text.split("\n")) {
|
|
2925
3006
|
const trimmed = line.trim();
|
|
2926
3007
|
if (!trimmed) continue;
|
|
@@ -2928,13 +3009,18 @@ async function getPwnedPasswordCount(password, options) {
|
|
|
2928
3009
|
if (colon < 0) continue;
|
|
2929
3010
|
const h = trimmed.slice(0, colon).toUpperCase();
|
|
2930
3011
|
const countStr = trimmed.slice(colon + 1);
|
|
2931
|
-
if (h ===
|
|
3012
|
+
if (h === suffix) {
|
|
2932
3013
|
const n = parseInt(countStr, 10);
|
|
2933
3014
|
return Number.isFinite(n) ? n : 0;
|
|
2934
3015
|
}
|
|
2935
3016
|
}
|
|
2936
3017
|
return 0;
|
|
2937
3018
|
}
|
|
3019
|
+
async function getPwnedPasswordCount(password, options) {
|
|
3020
|
+
if (!password.length) return 0;
|
|
3021
|
+
const hash = bytesToHex5(sha12(utf8ToBytes4(password))).toUpperCase();
|
|
3022
|
+
return getPwnedCountForHash(hash, options);
|
|
3023
|
+
}
|
|
2938
3024
|
|
|
2939
3025
|
// src/wordlist.ts
|
|
2940
3026
|
var EFF_WORDLIST = [
|
|
@@ -11512,7 +11598,10 @@ function toBase64Url2(bytes) {
|
|
|
11512
11598
|
for (let i = 0; i < bytes.length; i++) {
|
|
11513
11599
|
binary += String.fromCharCode(bytes[i]);
|
|
11514
11600
|
}
|
|
11515
|
-
|
|
11601
|
+
const b64 = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_");
|
|
11602
|
+
let end = b64.length;
|
|
11603
|
+
while (end > 0 && b64.charAt(end - 1) === "=") end -= 1;
|
|
11604
|
+
return b64.slice(0, end);
|
|
11516
11605
|
}
|
|
11517
11606
|
function fromBase64Url2(str) {
|
|
11518
11607
|
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -11575,6 +11664,26 @@ async function getHiddenWalletFromPassword(masterPassword, secretKey) {
|
|
|
11575
11664
|
_hwCache.set(cacheKey, account);
|
|
11576
11665
|
return account;
|
|
11577
11666
|
}
|
|
11667
|
+
async function getHiddenWalletFromSecretKey(secretKey) {
|
|
11668
|
+
const cacheKey = keccak256(toHex(`${secretKey}:sk-only:v1`));
|
|
11669
|
+
const cached = _hwCache.get(cacheKey);
|
|
11670
|
+
if (cached) return cached;
|
|
11671
|
+
const privateKey = await getHiddenWalletPrivateKeyFromSecretKey(secretKey);
|
|
11672
|
+
const account = privateKeyToAccount(privateKey);
|
|
11673
|
+
_hwCache.set(cacheKey, account);
|
|
11674
|
+
return account;
|
|
11675
|
+
}
|
|
11676
|
+
async function getHiddenWalletPrivateKeyFromSecretKey(secretKey) {
|
|
11677
|
+
const salt = deriveHwSalt(secretKey);
|
|
11678
|
+
const inputBytes = new TextEncoder().encode(secretKey);
|
|
11679
|
+
let derived;
|
|
11680
|
+
if (_argon2Provider) {
|
|
11681
|
+
derived = await _argon2Provider(inputBytes, salt, HW_ARGON2_OPTS);
|
|
11682
|
+
} else {
|
|
11683
|
+
derived = argon2id2(inputBytes, salt, HW_ARGON2_OPTS);
|
|
11684
|
+
}
|
|
11685
|
+
return bytesToHex8(derived);
|
|
11686
|
+
}
|
|
11578
11687
|
function clearHiddenWalletCache() {
|
|
11579
11688
|
_hwCache.clear();
|
|
11580
11689
|
}
|
|
@@ -11737,7 +11846,9 @@ function calculateMrzChecksum(str) {
|
|
|
11737
11846
|
return sum % 10;
|
|
11738
11847
|
}
|
|
11739
11848
|
function cleanMrzField(str) {
|
|
11740
|
-
|
|
11849
|
+
let end = str.length;
|
|
11850
|
+
while (end > 0 && str.charAt(end - 1) === "<") end--;
|
|
11851
|
+
return str.slice(0, end).replace(/</g, " ").trim();
|
|
11741
11852
|
}
|
|
11742
11853
|
function parseMrzDate(str) {
|
|
11743
11854
|
if (str.length !== 6) return "";
|
|
@@ -12006,6 +12117,7 @@ import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1.js";
|
|
|
12006
12117
|
import { xchacha20poly1305 as xchacha20poly13057 } from "@noble/ciphers/chacha.js";
|
|
12007
12118
|
import { randomBytes as randomBytes8 } from "@noble/ciphers/utils.js";
|
|
12008
12119
|
import { hkdf as hkdf5 } from "@noble/hashes/hkdf.js";
|
|
12120
|
+
import { hmac as hmac4 } from "@noble/hashes/hmac.js";
|
|
12009
12121
|
import { sha256 as sha2568 } from "@noble/hashes/sha2.js";
|
|
12010
12122
|
import { bytesToHex as bytesToHex10, hexToBytes as hexToBytes8, utf8ToBytes as utf8ToBytes8 } from "@noble/hashes/utils.js";
|
|
12011
12123
|
var NONCE_LENGTH5 = 24;
|
|
@@ -12020,8 +12132,10 @@ function generateSharePin() {
|
|
|
12020
12132
|
const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
|
|
12021
12133
|
return String(num % 1e6).padStart(PIN_LENGTH, "0");
|
|
12022
12134
|
}
|
|
12023
|
-
|
|
12024
|
-
|
|
12135
|
+
var PIN_PROOF_INFO = utf8ToBytes8("vaultkeepr-share-pin-proof-v1");
|
|
12136
|
+
function computeApiPinHash(pin, keyHex) {
|
|
12137
|
+
const proofKey = hmac4(sha2568, hexToBytes8(keyHex), PIN_PROOF_INFO);
|
|
12138
|
+
return bytesToHex10(hmac4(sha2568, proofKey, utf8ToBytes8(pin)));
|
|
12025
12139
|
}
|
|
12026
12140
|
function generateShareId() {
|
|
12027
12141
|
const ts = Date.now().toString(36);
|
|
@@ -12039,7 +12153,6 @@ function deriveShareKeyV1(sharedSecret, pin) {
|
|
|
12039
12153
|
function createSecureShare(payload, pin) {
|
|
12040
12154
|
const sharePin = pin ?? generateSharePin();
|
|
12041
12155
|
const shareId = generateShareId();
|
|
12042
|
-
const apiPinHash = computeApiPinHash(sharePin);
|
|
12043
12156
|
const fullPayload = {
|
|
12044
12157
|
...payload,
|
|
12045
12158
|
version: SHARE_VERSION,
|
|
@@ -12052,6 +12165,7 @@ function createSecureShare(payload, pin) {
|
|
|
12052
12165
|
}
|
|
12053
12166
|
const ephemeralPrivate = secp256k12.utils.randomSecretKey();
|
|
12054
12167
|
const ephemeralPublic = secp256k12.getPublicKey(ephemeralPrivate, false);
|
|
12168
|
+
const apiPinHash = computeApiPinHash(sharePin, bytesToHex10(ephemeralPrivate));
|
|
12055
12169
|
const shareKey = deriveShareKeyV2(ephemeralPrivate, sharePin);
|
|
12056
12170
|
const nonce = randomBytes8(NONCE_LENGTH5);
|
|
12057
12171
|
const chacha = xchacha20poly13057(shareKey, nonce);
|
|
@@ -12187,7 +12301,7 @@ function calculatePasswordStrength(password) {
|
|
|
12187
12301
|
} else {
|
|
12188
12302
|
feedback.push("passwordFeedback.addSpecialChars");
|
|
12189
12303
|
}
|
|
12190
|
-
if (!/(.)
|
|
12304
|
+
if (!/(.)\1{2,}/.test(password)) {
|
|
12191
12305
|
score += 10;
|
|
12192
12306
|
} else {
|
|
12193
12307
|
feedback.push("passwordFeedback.avoidRepeated");
|
|
@@ -12394,6 +12508,59 @@ function mergePrfCredentials(local, remote) {
|
|
|
12394
12508
|
for (const cred of local) byId.set(cred.credentialId, cred);
|
|
12395
12509
|
return Array.from(byId.values());
|
|
12396
12510
|
}
|
|
12511
|
+
function legacyContactKey(c) {
|
|
12512
|
+
return JSON.stringify([c.label, c.email, c.telegram, c.address, c.publicKey]);
|
|
12513
|
+
}
|
|
12514
|
+
function mergeLegacyContacts(base, local, remote) {
|
|
12515
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
12516
|
+
for (const c of [...local ?? [], ...remote ?? []]) {
|
|
12517
|
+
const key = legacyContactKey(c);
|
|
12518
|
+
const existing = byKey.get(key);
|
|
12519
|
+
if (!existing) {
|
|
12520
|
+
byKey.set(key, c);
|
|
12521
|
+
continue;
|
|
12522
|
+
}
|
|
12523
|
+
if (c.addedAt !== existing.addedAt) {
|
|
12524
|
+
byKey.set(key, c.addedAt > existing.addedAt ? c : existing);
|
|
12525
|
+
} else {
|
|
12526
|
+
byKey.set(key, deterministicPick(existing, c));
|
|
12527
|
+
}
|
|
12528
|
+
}
|
|
12529
|
+
if (byKey.size === 0) return void 0;
|
|
12530
|
+
const ordered = [];
|
|
12531
|
+
const placed = /* @__PURE__ */ new Set();
|
|
12532
|
+
for (const c of base ?? []) {
|
|
12533
|
+
const key = legacyContactKey(c);
|
|
12534
|
+
const kept = byKey.get(key);
|
|
12535
|
+
if (kept && !placed.has(key)) {
|
|
12536
|
+
placed.add(key);
|
|
12537
|
+
ordered.push(kept);
|
|
12538
|
+
}
|
|
12539
|
+
}
|
|
12540
|
+
const additions = Array.from(byKey.entries()).filter(([key]) => !placed.has(key));
|
|
12541
|
+
additions.sort((a, b) => {
|
|
12542
|
+
if (a[1].addedAt !== b[1].addedAt) return a[1].addedAt < b[1].addedAt ? -1 : 1;
|
|
12543
|
+
return JSON.stringify(a[1]) <= JSON.stringify(b[1]) ? -1 : 1;
|
|
12544
|
+
});
|
|
12545
|
+
for (const [, c] of additions) ordered.push(c);
|
|
12546
|
+
return ordered;
|
|
12547
|
+
}
|
|
12548
|
+
function mergeMonitoredEmails(base, local, remote) {
|
|
12549
|
+
const present = /* @__PURE__ */ new Set();
|
|
12550
|
+
for (const v of [...local ?? [], ...remote ?? []]) present.add(v);
|
|
12551
|
+
if (present.size === 0) return void 0;
|
|
12552
|
+
const ordered = [];
|
|
12553
|
+
for (const v of base ?? []) {
|
|
12554
|
+
if (present.has(v) && !ordered.includes(v)) ordered.push(v);
|
|
12555
|
+
}
|
|
12556
|
+
const additions = [];
|
|
12557
|
+
for (const v of present) {
|
|
12558
|
+
if (!ordered.includes(v)) additions.push(v);
|
|
12559
|
+
}
|
|
12560
|
+
additions.sort((a, b) => a.localeCompare(b));
|
|
12561
|
+
for (const v of additions) ordered.push(v);
|
|
12562
|
+
return ordered;
|
|
12563
|
+
}
|
|
12397
12564
|
function threeWayMerge(base, local, remote) {
|
|
12398
12565
|
const effectiveBase = base ?? {
|
|
12399
12566
|
version: local.version,
|
|
@@ -12457,6 +12624,16 @@ function threeWayMerge(base, local, remote) {
|
|
|
12457
12624
|
(sum, f) => sum + (f.originalSize || 0),
|
|
12458
12625
|
0
|
|
12459
12626
|
);
|
|
12627
|
+
const mergedLegacyContacts = mergeLegacyContacts(
|
|
12628
|
+
base ? base.legacyContacts : void 0,
|
|
12629
|
+
local.legacyContacts,
|
|
12630
|
+
remote.legacyContacts
|
|
12631
|
+
);
|
|
12632
|
+
const mergedMonitoredEmails = mergeMonitoredEmails(
|
|
12633
|
+
base ? base.monitoredEmails : void 0,
|
|
12634
|
+
local.monitoredEmails,
|
|
12635
|
+
remote.monitoredEmails
|
|
12636
|
+
);
|
|
12460
12637
|
const out = {
|
|
12461
12638
|
version: Math.max(local.version, remote.version),
|
|
12462
12639
|
createdAt: local.createdAt,
|
|
@@ -12481,6 +12658,15 @@ function threeWayMerge(base, local, remote) {
|
|
|
12481
12658
|
if (Object.keys(mergedEntryTombstones).length > 0) {
|
|
12482
12659
|
out.entryTombstones = mergedEntryTombstones;
|
|
12483
12660
|
}
|
|
12661
|
+
if (mergedLegacyContacts) {
|
|
12662
|
+
out.legacyContacts = mergedLegacyContacts;
|
|
12663
|
+
}
|
|
12664
|
+
if (mergedMonitoredEmails) {
|
|
12665
|
+
out.monitoredEmails = mergedMonitoredEmails;
|
|
12666
|
+
}
|
|
12667
|
+
if (local.secretKey !== void 0 || remote.secretKey !== void 0) {
|
|
12668
|
+
out.secretKey = local.secretKey !== void 0 && remote.secretKey !== void 0 ? deterministicPick(local.secretKey, remote.secretKey) : local.secretKey ?? remote.secretKey;
|
|
12669
|
+
}
|
|
12484
12670
|
return out;
|
|
12485
12671
|
}
|
|
12486
12672
|
function mergeFolders(base, local, remote, baseTombstones, localTombstones, remoteTombstones, now) {
|
|
@@ -14722,7 +14908,7 @@ var BIP39_WORDLIST = [
|
|
|
14722
14908
|
"zoo"
|
|
14723
14909
|
];
|
|
14724
14910
|
function suggestBip39Words(prefix, limit = 5) {
|
|
14725
|
-
const p = prefix.toLowerCase().trim();
|
|
14911
|
+
const p = (prefix ?? "").toLowerCase().trim();
|
|
14726
14912
|
if (!p) return [];
|
|
14727
14913
|
const results = [];
|
|
14728
14914
|
for (const word of BIP39_WORDLIST) {
|
|
@@ -15000,6 +15186,146 @@ var VaultKeepR_SLM = class {
|
|
|
15000
15186
|
return { severity, ...data };
|
|
15001
15187
|
}
|
|
15002
15188
|
};
|
|
15189
|
+
|
|
15190
|
+
// src/account-state.ts
|
|
15191
|
+
function accountKeyState(input) {
|
|
15192
|
+
if (input.hasSecretKey) return { kind: "ready" };
|
|
15193
|
+
if (input.remote?.moved) return { kind: "read_only_pair" };
|
|
15194
|
+
if (input.remote?.exists || input.hasLocalVault) return { kind: "passage" };
|
|
15195
|
+
return { kind: "choose" };
|
|
15196
|
+
}
|
|
15197
|
+
function pointerWalletFor(input) {
|
|
15198
|
+
return input.hasSecretKey && input.remoteMoved ? input.smartAccount : input.eoa;
|
|
15199
|
+
}
|
|
15200
|
+
function passageNext(step, event) {
|
|
15201
|
+
switch (`${step}:${event}`) {
|
|
15202
|
+
case "idle:key_generated":
|
|
15203
|
+
return { step: "key_pending", discardPendingKey: false };
|
|
15204
|
+
case "key_pending:copy_ok":
|
|
15205
|
+
return { step: "copied", discardPendingKey: false };
|
|
15206
|
+
case "key_pending:copy_failed":
|
|
15207
|
+
return { step: "idle", discardPendingKey: true };
|
|
15208
|
+
case "copied:move_ok":
|
|
15209
|
+
return { step: "moved", discardPendingKey: false };
|
|
15210
|
+
case "copied:move_failed":
|
|
15211
|
+
return { step: "copied", discardPendingKey: false };
|
|
15212
|
+
case "copied:move_conflict":
|
|
15213
|
+
return { step: "idle", discardPendingKey: true };
|
|
15214
|
+
case "moved:promote_ok":
|
|
15215
|
+
return { step: "done", discardPendingKey: false };
|
|
15216
|
+
default:
|
|
15217
|
+
return { error: `invalid passage transition: ${step} + ${event}` };
|
|
15218
|
+
}
|
|
15219
|
+
}
|
|
15220
|
+
function passageIdentity(step) {
|
|
15221
|
+
return step === "done" ? "smart_account" : "legacy";
|
|
15222
|
+
}
|
|
15223
|
+
|
|
15224
|
+
// src/recovery-code.ts
|
|
15225
|
+
import { argon2id as argon2id3 } from "@noble/hashes/argon2.js";
|
|
15226
|
+
import { bytesToHex as bytesToHex12, hexToBytes as hexToBytes10 } from "@noble/hashes/utils.js";
|
|
15227
|
+
import { randomBytes as randomBytes10 } from "@noble/ciphers/utils.js";
|
|
15228
|
+
var RECOVERY_CODE_LENGTH = 20;
|
|
15229
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
15230
|
+
var ID_SALT = new TextEncoder().encode("vaultkeepr-recovery-id-v1");
|
|
15231
|
+
var SALT_LENGTH2 = 16;
|
|
15232
|
+
var ARGON2_OPTS2 = { t: 3, m: 65536, p: 4, dkLen: 32 };
|
|
15233
|
+
var SECRET_KEY_HEX = /^[0-9a-fA-F]{64}$/;
|
|
15234
|
+
function isAccountKeyText(value) {
|
|
15235
|
+
if (SECRET_KEY_HEX.test(value)) return true;
|
|
15236
|
+
if (value.length < 32 || value.length > 4096 || value.includes("BEGIN PGP")) return false;
|
|
15237
|
+
return !isEnvelopeText(value);
|
|
15238
|
+
}
|
|
15239
|
+
function isEnvelopeText(value) {
|
|
15240
|
+
if (!value.startsWith("{")) return false;
|
|
15241
|
+
try {
|
|
15242
|
+
const o = JSON.parse(value);
|
|
15243
|
+
return !!o && typeof o === "object" && typeof o.v === "number" && typeof o.iv === "string" && typeof o.ct === "string";
|
|
15244
|
+
} catch {
|
|
15245
|
+
return false;
|
|
15246
|
+
}
|
|
15247
|
+
}
|
|
15248
|
+
var argon2RecoveryKdf = (codeBytes2, salt) => argon2id3(codeBytes2, salt, ARGON2_OPTS2);
|
|
15249
|
+
function generateRecoveryCode() {
|
|
15250
|
+
const bytes = randomBytes10(RECOVERY_CODE_LENGTH);
|
|
15251
|
+
let code = "";
|
|
15252
|
+
for (const b of bytes) code += ALPHABET[b % 32];
|
|
15253
|
+
return code;
|
|
15254
|
+
}
|
|
15255
|
+
function normalizeRecoveryCode(input) {
|
|
15256
|
+
return input.toUpperCase().replace(/[\s-]/g, "").replace(/O/g, "0").replace(/[IL]/g, "1");
|
|
15257
|
+
}
|
|
15258
|
+
function isValidRecoveryCode(input) {
|
|
15259
|
+
const code = normalizeRecoveryCode(input);
|
|
15260
|
+
return code.length === RECOVERY_CODE_LENGTH && [...code].every((c) => ALPHABET.includes(c));
|
|
15261
|
+
}
|
|
15262
|
+
function formatRecoveryCode(code) {
|
|
15263
|
+
const canonical = normalizeRecoveryCode(code);
|
|
15264
|
+
return canonical.match(/.{1,5}/g)?.join("-") ?? canonical;
|
|
15265
|
+
}
|
|
15266
|
+
function codeBytes(code) {
|
|
15267
|
+
const canonical = normalizeRecoveryCode(code);
|
|
15268
|
+
if (!isValidRecoveryCode(canonical)) throw new Error("Invalid recovery code");
|
|
15269
|
+
return new TextEncoder().encode(canonical);
|
|
15270
|
+
}
|
|
15271
|
+
async function deriveRecoveryId(code, kdf = argon2RecoveryKdf) {
|
|
15272
|
+
return bytesToHex12(await kdf(codeBytes(code), ID_SALT));
|
|
15273
|
+
}
|
|
15274
|
+
async function createRecoveryBlob(secretKeyHex, code, kdf = argon2RecoveryKdf) {
|
|
15275
|
+
if (!isAccountKeyText(secretKeyHex)) throw new Error("Invalid secret key");
|
|
15276
|
+
const salt = randomBytes10(SALT_LENGTH2);
|
|
15277
|
+
const key = await kdf(codeBytes(code), salt);
|
|
15278
|
+
const encrypted = encryptVault(secretKeyHex, key);
|
|
15279
|
+
return { version: 1, salt: bytesToHex12(salt), encrypted };
|
|
15280
|
+
}
|
|
15281
|
+
async function openRecoveryBlob(blob, code, kdf = argon2RecoveryKdf) {
|
|
15282
|
+
if (blob.version !== 1) throw new Error("Unsupported recovery blob version");
|
|
15283
|
+
let plain;
|
|
15284
|
+
try {
|
|
15285
|
+
const key = await kdf(codeBytes(code), hexToBytes10(blob.salt));
|
|
15286
|
+
plain = decryptVault(blob.encrypted, key);
|
|
15287
|
+
} catch {
|
|
15288
|
+
throw new Error("Recovery failed");
|
|
15289
|
+
}
|
|
15290
|
+
const secretKey = typeof plain === "string" ? plain : new TextDecoder().decode(plain);
|
|
15291
|
+
if (!isAccountKeyText(secretKey)) throw new Error("Recovery failed");
|
|
15292
|
+
return secretKey;
|
|
15293
|
+
}
|
|
15294
|
+
|
|
15295
|
+
// src/key-join.ts
|
|
15296
|
+
var KeyJoinError = class extends Error {
|
|
15297
|
+
constructor(code) {
|
|
15298
|
+
super(code);
|
|
15299
|
+
this.code = code;
|
|
15300
|
+
this.name = "KeyJoinError";
|
|
15301
|
+
}
|
|
15302
|
+
code;
|
|
15303
|
+
};
|
|
15304
|
+
async function prepareKeyJoin(secretKeyHex, post, kdf) {
|
|
15305
|
+
const code = generateRecoveryCode();
|
|
15306
|
+
const blob = await createRecoveryBlob(secretKeyHex, code, kdf);
|
|
15307
|
+
const id = await deriveRecoveryId(code, kdf);
|
|
15308
|
+
await post(id, blob);
|
|
15309
|
+
return { code: formatRecoveryCode(code) };
|
|
15310
|
+
}
|
|
15311
|
+
async function joinWithKeyCode(input, fetchBlob, kdf) {
|
|
15312
|
+
const code = normalizeRecoveryCode(input);
|
|
15313
|
+
if (!isValidRecoveryCode(code)) throw new KeyJoinError("invalid_code");
|
|
15314
|
+
const id = await deriveRecoveryId(code, kdf);
|
|
15315
|
+
const blob = await fetchBlob(id);
|
|
15316
|
+
if (!blob) throw new KeyJoinError("not_found");
|
|
15317
|
+
return openRecoveryBlob(blob, code, kdf);
|
|
15318
|
+
}
|
|
15319
|
+
|
|
15320
|
+
// src/import-key-policy.ts
|
|
15321
|
+
function decideEncryptedImport(input) {
|
|
15322
|
+
const device = input.deviceKey ? input.deviceKey.toLowerCase() : null;
|
|
15323
|
+
const file = input.fileKey ? input.fileKey.toLowerCase() : null;
|
|
15324
|
+
if (!device && !file) return { action: "import_keyless" };
|
|
15325
|
+
if (!device && file) return { action: "join_with_file_key" };
|
|
15326
|
+
if (device && !file) return { action: "refuse", reason: "keyless_file_on_keyed_device" };
|
|
15327
|
+
return device === file ? { action: "keep_device_key" } : { action: "refuse", reason: "other_account" };
|
|
15328
|
+
}
|
|
15003
15329
|
export {
|
|
15004
15330
|
BIP39_WORDLIST,
|
|
15005
15331
|
BUILTIN_FOLDER_IDS,
|
|
@@ -15014,6 +15340,7 @@ export {
|
|
|
15014
15340
|
EXTENSION_MIN_PAYLOAD_VERSION,
|
|
15015
15341
|
FRAGMENTED_DEFAULT_THRESHOLD,
|
|
15016
15342
|
FRAGMENTED_DEFAULT_TOTAL,
|
|
15343
|
+
KeyJoinError,
|
|
15017
15344
|
MOBILE_MAX_PAYLOAD_VERSION,
|
|
15018
15345
|
MOBILE_MIN_PAYLOAD_VERSION,
|
|
15019
15346
|
P256_SPKI_HEADER,
|
|
@@ -15024,11 +15351,14 @@ export {
|
|
|
15024
15351
|
PAYLOAD_VERSION_V2,
|
|
15025
15352
|
PAYLOAD_VERSION_V3,
|
|
15026
15353
|
PAYLOAD_VERSION_V4,
|
|
15354
|
+
RECOVERY_CODE_LENGTH,
|
|
15027
15355
|
TAG_COLORS,
|
|
15028
15356
|
VALID_TAGS,
|
|
15029
15357
|
VaultKeepR_SLM,
|
|
15358
|
+
accountKeyState,
|
|
15030
15359
|
addEntry,
|
|
15031
15360
|
analyzeVaultHealth,
|
|
15361
|
+
argon2RecoveryKdf,
|
|
15032
15362
|
buildAttestationObject,
|
|
15033
15363
|
buildAuthDataForCreate,
|
|
15034
15364
|
buildAuthDataForGet,
|
|
@@ -15043,6 +15373,7 @@ export {
|
|
|
15043
15373
|
captureError,
|
|
15044
15374
|
checkEmailBreaches,
|
|
15045
15375
|
checkVaultBreaches,
|
|
15376
|
+
cleanEntryTitle,
|
|
15046
15377
|
cleanMrzField,
|
|
15047
15378
|
clearErrorBuffer,
|
|
15048
15379
|
clearHiddenWalletCache,
|
|
@@ -15053,8 +15384,10 @@ export {
|
|
|
15053
15384
|
createExportPayload,
|
|
15054
15385
|
createFolder,
|
|
15055
15386
|
createKeyEnvelope,
|
|
15387
|
+
createRecoveryBlob,
|
|
15056
15388
|
createSecureShare,
|
|
15057
15389
|
createVaultPayload,
|
|
15390
|
+
decideEncryptedImport,
|
|
15058
15391
|
decompressVault,
|
|
15059
15392
|
decryptFromPair,
|
|
15060
15393
|
decryptKeyEnvelope,
|
|
@@ -15076,6 +15409,7 @@ export {
|
|
|
15076
15409
|
deriveMrzKseed,
|
|
15077
15410
|
deriveNfcKey,
|
|
15078
15411
|
derivePairSecret,
|
|
15412
|
+
deriveRecoveryId,
|
|
15079
15413
|
digitsOnly,
|
|
15080
15414
|
encryptAndFragmentDocument,
|
|
15081
15415
|
encryptForPair,
|
|
@@ -15093,6 +15427,7 @@ export {
|
|
|
15093
15427
|
formatCardDisplayMasked,
|
|
15094
15428
|
formatCardNumber,
|
|
15095
15429
|
formatErrorReport,
|
|
15430
|
+
formatRecoveryCode,
|
|
15096
15431
|
fromBase64Url,
|
|
15097
15432
|
generateBlurredThumbnailCanvas,
|
|
15098
15433
|
generateCredentialId,
|
|
@@ -15104,17 +15439,22 @@ export {
|
|
|
15104
15439
|
generatePassphrase,
|
|
15105
15440
|
generatePassword,
|
|
15106
15441
|
generatePrfSalt,
|
|
15442
|
+
generateRecoveryCode,
|
|
15107
15443
|
generateSaltArgon2,
|
|
15108
15444
|
generateSharePin,
|
|
15109
15445
|
getCardBrand,
|
|
15110
15446
|
getChecksumAddress,
|
|
15447
|
+
getEntryDisplayName,
|
|
15111
15448
|
getEntryGroupLabel,
|
|
15112
15449
|
getErrorBuffer,
|
|
15113
15450
|
getHiddenWalletFromPassword,
|
|
15451
|
+
getHiddenWalletFromSecretKey,
|
|
15114
15452
|
getHiddenWalletLegacy,
|
|
15115
15453
|
getHiddenWalletPrivateKey,
|
|
15454
|
+
getHiddenWalletPrivateKeyFromSecretKey,
|
|
15116
15455
|
getLast4,
|
|
15117
15456
|
getMostUsedEntries,
|
|
15457
|
+
getPwnedCountForHash,
|
|
15118
15458
|
getPwnedPasswordCount,
|
|
15119
15459
|
getTOTPCode,
|
|
15120
15460
|
getTOTPRemainingSeconds,
|
|
@@ -15128,6 +15468,7 @@ export {
|
|
|
15128
15468
|
importProtonPassJson,
|
|
15129
15469
|
importVaultText,
|
|
15130
15470
|
isAcceptablePayloadVersion,
|
|
15471
|
+
isAccountKeyText,
|
|
15131
15472
|
isCurrentPayloadVersion,
|
|
15132
15473
|
isEncryptedExport,
|
|
15133
15474
|
isNfcPayloadV1,
|
|
@@ -15139,15 +15480,19 @@ export {
|
|
|
15139
15480
|
isPasskeyPrivateKeyEncrypted,
|
|
15140
15481
|
isPrfSupported,
|
|
15141
15482
|
isProtonPassExport,
|
|
15483
|
+
isValidRecoveryCode,
|
|
15142
15484
|
isWebAuthnSupported,
|
|
15485
|
+
joinWithKeyCode,
|
|
15143
15486
|
maskCardNumber,
|
|
15144
15487
|
mergeFolderTrees,
|
|
15145
15488
|
mergeFolders,
|
|
15146
15489
|
mergeFragments,
|
|
15147
15490
|
mergeImportedEntries,
|
|
15148
15491
|
moveFolder,
|
|
15492
|
+
normalizeRecoveryCode,
|
|
15149
15493
|
normalizeSignatureForKdf,
|
|
15150
15494
|
normalizeUrl,
|
|
15495
|
+
openRecoveryBlob,
|
|
15151
15496
|
parseMRZ,
|
|
15152
15497
|
parseMrzDate,
|
|
15153
15498
|
parsePairPayload,
|
|
@@ -15157,6 +15502,10 @@ export {
|
|
|
15157
15502
|
parseTOTPUri,
|
|
15158
15503
|
parseVault,
|
|
15159
15504
|
parseVaultPayload,
|
|
15505
|
+
passageIdentity,
|
|
15506
|
+
passageNext,
|
|
15507
|
+
pointerWalletFor,
|
|
15508
|
+
prepareKeyJoin,
|
|
15160
15509
|
publicKeyToCose,
|
|
15161
15510
|
publicKeyToSpki,
|
|
15162
15511
|
reassembleAndDecryptDocument,
|