mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: enhance URI handling and TOTP field extraction in import functions
This commit is contained in:
@@ -563,9 +563,13 @@ async function encryptUris(
|
|||||||
mac: Uint8Array
|
mac: Uint8Array
|
||||||
): Promise<Array<Record<string, unknown>>> {
|
): Promise<Array<Record<string, unknown>>> {
|
||||||
const out: Array<Record<string, unknown>> = [];
|
const out: Array<Record<string, unknown>> = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
for (const entry of uris || []) {
|
for (const entry of uris || []) {
|
||||||
const trimmed = String(entry?.uri || '').trim();
|
const trimmed = String(entry?.uri || '').trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
|
const key = trimmed.toLowerCase();
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
const preservedExtra =
|
const preservedExtra =
|
||||||
entry?.extra && typeof entry.extra === 'object'
|
entry?.extra && typeof entry.extra === 'object'
|
||||||
? { ...entry.extra }
|
? { ...entry.extra }
|
||||||
|
|||||||
@@ -31,6 +31,25 @@ export function asText(value: unknown): string {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImportTotpFieldName(value: unknown): boolean {
|
||||||
|
const name = asText(value).trim().toLowerCase().replace(/[\s_-]+/g, '');
|
||||||
|
return [
|
||||||
|
'totp',
|
||||||
|
'totpuri',
|
||||||
|
'otp',
|
||||||
|
'otpuri',
|
||||||
|
'otpurl',
|
||||||
|
'otpauth',
|
||||||
|
'onetimepassword',
|
||||||
|
'onetimepasscode',
|
||||||
|
'2fa',
|
||||||
|
'twofactor',
|
||||||
|
'twofactorauthentication',
|
||||||
|
'authenticator',
|
||||||
|
'verificationcode',
|
||||||
|
].includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
export function readInviteCodeFromUrl(): string {
|
export function readInviteCodeFromUrl(): string {
|
||||||
if (typeof window === 'undefined') return '';
|
if (typeof window === 'undefined') return '';
|
||||||
|
|
||||||
@@ -162,6 +181,7 @@ export function importCipherToDraft(cipher: Record<string, unknown>, folderId: s
|
|||||||
draft.loginPassword = asText(login.password);
|
draft.loginPassword = asText(login.password);
|
||||||
draft.loginTotp = asText(login.totp);
|
draft.loginTotp = asText(login.totp);
|
||||||
const urisRaw = Array.isArray(login.uris) ? login.uris : [];
|
const urisRaw = Array.isArray(login.uris) ? login.uris : [];
|
||||||
|
const seenUris = new Set<string>();
|
||||||
const uris = urisRaw
|
const uris = urisRaw
|
||||||
.map((u) => {
|
.map((u) => {
|
||||||
const row = (u || {}) as Record<string, unknown>;
|
const row = (u || {}) as Record<string, unknown>;
|
||||||
@@ -176,8 +196,21 @@ export function importCipherToDraft(cipher: Record<string, unknown>, folderId: s
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((u) => !!u.uri);
|
.filter((u) => {
|
||||||
|
if (!u.uri) return false;
|
||||||
|
const key = u.uri.toLowerCase();
|
||||||
|
if (seenUris.has(key)) return false;
|
||||||
|
seenUris.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
draft.loginUris = uris.length ? uris : [{ uri: '', match: null, originalUri: '', extra: {} }];
|
draft.loginUris = uris.length ? uris : [{ uri: '', match: null, originalUri: '', extra: {} }];
|
||||||
|
if (!draft.loginTotp) {
|
||||||
|
const totpFieldIndex = draft.customFields.findIndex((field) => isImportTotpFieldName(field.label));
|
||||||
|
if (totpFieldIndex >= 0) {
|
||||||
|
draft.loginTotp = asText(draft.customFields[totpFieldIndex].value);
|
||||||
|
draft.customFields = draft.customFields.filter((_, index) => index !== totpFieldIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
draft.loginFido2Credentials = Array.isArray(login.fido2Credentials)
|
draft.loginFido2Credentials = Array.isArray(login.fido2Credentials)
|
||||||
? login.fido2Credentials.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object')
|
? login.fido2Credentials.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object')
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -19,6 +19,71 @@ export function normalizeUri(raw: string): string | null {
|
|||||||
return s.slice(0, 1000);
|
return s.slice(0, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeUriList(rawUris: string[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const raw of rawUris) {
|
||||||
|
const uri = normalizeUri(raw);
|
||||||
|
if (!uri) continue;
|
||||||
|
const key = uri.toLowerCase();
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
out.push(uri);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLoginUris(login: Record<string, unknown>, rawUris: string[]): void {
|
||||||
|
const uris = normalizeUriList(rawUris);
|
||||||
|
login.uris = uris.length ? uris.map((uri) => ({ uri, match: null })) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLoginUri(login: Record<string, unknown>, rawUri: string): void {
|
||||||
|
const existing = Array.isArray(login.uris)
|
||||||
|
? login.uris.map((entry) => txt((entry as Record<string, unknown>)?.uri)).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
setLoginUris(login, [...existing, rawUri]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTotpFieldName(raw: unknown): boolean {
|
||||||
|
const name = txt(raw).toLowerCase().replace(/[\s_-]+/g, '');
|
||||||
|
if (!name) return false;
|
||||||
|
return [
|
||||||
|
'totp',
|
||||||
|
'totpuri',
|
||||||
|
'otp',
|
||||||
|
'otpuri',
|
||||||
|
'otpurl',
|
||||||
|
'otpauth',
|
||||||
|
'onetimepassword',
|
||||||
|
'onetimepasscode',
|
||||||
|
'2fa',
|
||||||
|
'twofactor',
|
||||||
|
'twofactorauthentication',
|
||||||
|
'authenticator',
|
||||||
|
'verificationcode',
|
||||||
|
].includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTotpValue(raw: unknown): string {
|
||||||
|
if (raw === null || raw === undefined) return '';
|
||||||
|
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') return txt(raw);
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
for (const item of raw) {
|
||||||
|
const value = extractTotpValue(item);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof raw !== 'object') return '';
|
||||||
|
const obj = raw as Record<string, unknown>;
|
||||||
|
for (const key of ['totpUri', 'otpAuth', 'otpauth', 'uri', 'url', 'secret', 'totp', 'otp', 'value', 'code']) {
|
||||||
|
const value = extractTotpValue(obj[key]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export function parseSerializedUris(raw: string): string[] {
|
export function parseSerializedUris(raw: string): string[] {
|
||||||
const source = txt(raw);
|
const source = txt(raw);
|
||||||
if (!source) return [];
|
if (!source) return [];
|
||||||
@@ -38,15 +103,7 @@ export function parseSerializedUris(raw: string): string[] {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [source];
|
: [source];
|
||||||
|
|
||||||
const seen = new Set<string>();
|
return normalizeUriList(parts);
|
||||||
const uris: string[] = [];
|
|
||||||
for (const part of parts) {
|
|
||||||
const normalized = normalizeUri(part);
|
|
||||||
if (!normalized || seen.has(normalized)) continue;
|
|
||||||
seen.add(normalized);
|
|
||||||
uris.push(normalized);
|
|
||||||
}
|
|
||||||
return uris;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nameFromUrl(raw: string): string | null {
|
export function nameFromUrl(raw: string): string | null {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import type { CiphersImportPayload } from '@/lib/api/vault';
|
import type { CiphersImportPayload } from '@/lib/api/vault';
|
||||||
import {
|
import {
|
||||||
addFolder,
|
addFolder,
|
||||||
|
addLoginUri,
|
||||||
cardBrand,
|
cardBrand,
|
||||||
convertToNoteIfNeeded,
|
convertToNoteIfNeeded,
|
||||||
|
extractTotpValue,
|
||||||
|
isTotpFieldName,
|
||||||
makeLoginCipher,
|
makeLoginCipher,
|
||||||
normalizeUri,
|
normalizeUri,
|
||||||
parseCardExpiry,
|
parseCardExpiry,
|
||||||
parseCsv,
|
parseCsv,
|
||||||
parseEpochMaybe,
|
parseEpochMaybe,
|
||||||
processKvp,
|
processKvp,
|
||||||
|
setLoginUris,
|
||||||
txt,
|
txt,
|
||||||
val,
|
val,
|
||||||
} from '@/lib/import-format-shared';
|
} from '@/lib/import-format-shared';
|
||||||
@@ -91,9 +95,12 @@ export function parseOnePasswordCsv(textRaw: string, isMac: boolean): CiphersImp
|
|||||||
login.password = rawVal;
|
login.password = rawVal;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((!Array.isArray(login.uris) || !login.uris.length) && (lower === 'url' || lower === 'website')) {
|
if (lower === 'url' || lower === 'website') {
|
||||||
const uri = normalizeUri(rawVal);
|
addLoginUri(login, rawVal);
|
||||||
login.uris = uri ? [{ uri, match: null }] : null;
|
continue;
|
||||||
|
}
|
||||||
|
if (!txt(login.totp) && isTotpFieldName(lower)) {
|
||||||
|
login.totp = rawVal;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (Number(cipher.type) === 3 && cipher.card) {
|
} else if (Number(cipher.type) === 3 && cipher.card) {
|
||||||
@@ -149,7 +156,7 @@ export function parseOnePasswordCsv(textRaw: string, isMac: boolean): CiphersImp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignored.has(lower) && !lower.startsWith('section:') && !lower.startsWith('section ')) {
|
if (!ignored.has(lower) && !isTotpFieldName(lower) && !lower.startsWith('section:') && !lower.startsWith('section ')) {
|
||||||
if (!altUsername && lower === 'email') altUsername = rawVal;
|
if (!altUsername && lower === 'email') altUsername = rawVal;
|
||||||
if (lower === 'created date' || lower === 'modified date') {
|
if (lower === 'created date' || lower === 'modified date') {
|
||||||
const readable = parseEpochMaybe(rawVal);
|
const readable = parseEpochMaybe(rawVal);
|
||||||
@@ -197,8 +204,8 @@ function parseOnePasswordFieldsIntoCipher(
|
|||||||
login.password = value;
|
login.password = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!txt(login.totp) && designation.startsWith('totp_')) {
|
if (!txt(login.totp) && (designation.startsWith('totp_') || isTotpFieldName(designation) || isTotpFieldName(fieldName))) {
|
||||||
login.totp = value;
|
login.totp = extractTotpValue(raw) || value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (Number(cipher.type) === 3 && cipher.card) {
|
} else if (Number(cipher.type) === 3 && cipher.card) {
|
||||||
@@ -327,7 +334,7 @@ export function parseOnePassword1Pif(textRaw: string): CiphersImportPayload {
|
|||||||
if (uri) uris.push(uri);
|
if (uri) uris.push(uri);
|
||||||
}
|
}
|
||||||
if (Number(cipher.type) === 1) {
|
if (Number(cipher.type) === 1) {
|
||||||
(cipher.login as Record<string, unknown>).uris = uris.length ? uris.map((uri) => ({ uri, match: null })) : null;
|
setLoginUris(cipher.login as Record<string, unknown>, uris);
|
||||||
(cipher.login as Record<string, unknown>).password = val(details?.password);
|
(cipher.login as Record<string, unknown>).password = val(details?.password);
|
||||||
}
|
}
|
||||||
cipher.notes = val(details?.notesPlain);
|
cipher.notes = val(details?.notesPlain);
|
||||||
@@ -394,7 +401,7 @@ export function parseOnePassword1PuxJson(textRaw: string): CiphersImportPayload
|
|||||||
}
|
}
|
||||||
const fallbackUrl = normalizeUri(item?.overview?.url || '');
|
const fallbackUrl = normalizeUri(item?.overview?.url || '');
|
||||||
if (fallbackUrl) urls.push(fallbackUrl);
|
if (fallbackUrl) urls.push(fallbackUrl);
|
||||||
(cipher.login as Record<string, unknown>).uris = urls.length ? urls.map((uri) => ({ uri, match: null })) : null;
|
setLoginUris(cipher.login as Record<string, unknown>, urls);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const loginField of item?.details?.loginFields || []) {
|
for (const loginField of item?.details?.loginFields || []) {
|
||||||
@@ -413,7 +420,7 @@ export function parseOnePassword1PuxJson(textRaw: string): CiphersImportPayload
|
|||||||
login.password = lv;
|
login.password = lv;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (designation.includes('totp') || fieldName.toLowerCase().includes('totp')) {
|
if (designation.includes('totp') || isTotpFieldName(fieldName) || isTotpFieldName(fieldType)) {
|
||||||
login.totp = lv;
|
login.totp = lv;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -510,12 +517,7 @@ export function parseOnePassword1PuxJson(textRaw: string): CiphersImportPayload
|
|||||||
} else if (Number(cipher.type) === 1) {
|
} else if (Number(cipher.type) === 1) {
|
||||||
const login = cipher.login as Record<string, unknown>;
|
const login = cipher.login as Record<string, unknown>;
|
||||||
if (fieldId === 'url') {
|
if (fieldId === 'url') {
|
||||||
const uri = normalizeUri(fieldValue);
|
addLoginUri(login, fieldValue);
|
||||||
if (uri) {
|
|
||||||
const uris = Array.isArray(login.uris) ? login.uris : [];
|
|
||||||
uris.push({ uri, match: null });
|
|
||||||
login.uris = uris;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (fieldId === 'username' && !txt(login.username)) {
|
if (fieldId === 'username' && !txt(login.username)) {
|
||||||
@@ -526,8 +528,11 @@ export function parseOnePassword1PuxJson(textRaw: string): CiphersImportPayload
|
|||||||
login.password = fieldValue;
|
login.password = fieldValue;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((fieldId === 'oneTimePassword' || fieldId === 'totp') && !txt(login.totp)) {
|
if (
|
||||||
login.totp = fieldValue;
|
(fieldId === 'oneTimePassword' || fieldId === 'totp' || fieldType === 'otp' || isTotpFieldName(fieldTitleLocal)) &&
|
||||||
|
!txt(login.totp)
|
||||||
|
) {
|
||||||
|
login.totp = extractTotpValue(fieldValueObj) || fieldValue;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user