From cda654e1c3451f5c35e175aca9d37a2ba17ac82b Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sat, 6 Jun 2026 22:43:16 +0800 Subject: [PATCH] fix: enhance cipher login URI handling and import format support --- src/handlers/ciphers.ts | 9 ++-- webapp/src/lib/api/vault.ts | 52 ++++++++++++++-------- webapp/src/lib/export-formats.ts | 3 ++ webapp/src/lib/import-formats-bitwarden.ts | 1 + 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index a885cf5..0005109 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -191,7 +191,6 @@ export function normalizeCipherLoginForCompatibility( const next = sanitizeEncryptedObject(normalized, ['username', 'password', 'totp', 'uri']); if (!next) return null; next.uris = normalizeCipherLoginUrisForCompatibility(next.uris, { - hasLegacyLoginUri: isValidEncString(next.uri), requiresUriChecksum, preserveRepairableUris, }); @@ -201,7 +200,7 @@ export function normalizeCipherLoginForCompatibility( function normalizeCipherLoginUrisForCompatibility( uris: any, - options: { hasLegacyLoginUri?: boolean; requiresUriChecksum?: boolean; preserveRepairableUris?: boolean } = {} + options: { requiresUriChecksum?: boolean; preserveRepairableUris?: boolean } = {} ): any[] | null { if (!Array.isArray(uris) || uris.length === 0) return null; const out: any[] = []; @@ -231,7 +230,7 @@ function normalizeCipherLoginUrisForCompatibility( // Bitwarden browser clients using the SDK drop item-key encrypted URIs // whose checksum is missing/invalid. User-key encrypted legacy/import // entries bypass this validation and can safely keep the URI. - if (options.requiresUriChecksum || options.hasLegacyLoginUri) continue; + if (options.requiresUriChecksum) continue; out.push({ ...next, uriChecksum: null }); continue; } @@ -833,6 +832,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400); } + if (!shouldPreserveRepairableCipherUris(request) && incomingLogin.present && hasMissingLoginUriChecksum(existingCipher)) { + return errorResponse('This item has login URIs that must be repaired in NodeWarden Web before updating from this client. Open NodeWarden Web once, then resync.', 400); + } + const nextType = Number(cipherData.type) || existingCipher.type; // Opaque passthrough: merge existing stored data with ALL incoming client fields. diff --git a/webapp/src/lib/api/vault.ts b/webapp/src/lib/api/vault.ts index f4bb24e..2c3638f 100644 --- a/webapp/src/lib/api/vault.ts +++ b/webapp/src/lib/api/vault.ts @@ -819,13 +819,15 @@ async function repairCipherLoginUris( continue; } - let clearUri = String(entry.decUri || '').trim(); - if (!clearUri || looksLikeCipherString(clearUri)) { - try { - clearUri = (await decryptStr(rawUri, enc, mac)).trim(); - } catch { - uris.push({ ...encryptedEntry }); - continue; + let clearUri = ''; + let rawUriUsesCurrentKey = false; + try { + clearUri = (await decryptStr(rawUri, enc, mac)).trim(); + rawUriUsesCurrentKey = !!clearUri; + } catch { + const fallbackUri = String(entry.decUri || '').trim(); + if (fallbackUri && !looksLikeCipherString(fallbackUri)) { + clearUri = fallbackUri; } } @@ -845,15 +847,20 @@ async function repairCipherLoginUris( } } - if (currentChecksumOk) { + if (currentChecksumOk && rawUriUsesCurrentKey) { uris.push({ ...encryptedEntry }); continue; } + const repairedUri = rawUriUsesCurrentKey ? rawUri : await encryptTextValue(clearUri, enc, mac); + const repairedChecksum = currentChecksumOk + ? rawChecksum + : await encryptTextValue(expectedChecksum, enc, mac); + uris.push({ ...encryptedEntry, - uri: rawUri, - uriChecksum: await encryptTextValue(expectedChecksum, enc, mac), + uri: repairedUri || rawUri, + uriChecksum: repairedChecksum, match: typeof entry.match === 'number' && Number.isFinite(entry.match) ? entry.match : null, }); changed = true; @@ -889,15 +896,22 @@ export async function repairCipherUriChecksums( let repaired = 0; for (const cipher of ciphers) { - if (!cipher?.id || cipher.type !== 1 || !looksLikeCipherString(cipher.key) || !cipher.login || !Array.isArray(cipher.login.uris)) continue; - let itemKey: Uint8Array; - try { - itemKey = await decryptBw(String(cipher.key).trim(), userEnc, userMac); - } catch { - continue; + if (!cipher?.id || cipher.type !== 1 || !cipher.login || !Array.isArray(cipher.login.uris)) continue; + let keys: { enc: Uint8Array; mac: Uint8Array; key: string | null } = { + enc: userEnc, + mac: userMac, + key: null, + }; + if (looksLikeCipherString(cipher.key)) { + let itemKey: Uint8Array; + try { + itemKey = await decryptBw(String(cipher.key).trim(), userEnc, userMac); + } catch { + continue; + } + if (itemKey.length < 64) continue; + keys = { enc: itemKey.slice(0, 32), mac: itemKey.slice(32, 64), key: String(cipher.key).trim() }; } - if (itemKey.length < 64) continue; - const keys = { enc: itemKey.slice(0, 32), mac: itemKey.slice(32, 64), key: String(cipher.key).trim() }; const repair = await repairCipherLoginUris(cipher, keys.enc, keys.mac); if (!repair.changed) continue; @@ -912,9 +926,9 @@ export async function repairCipherUriChecksums( fields: Array.isArray(cipher.fields) ? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field) : null, - key: keys.key, lastKnownRevisionDate: cipher.revisionDate ?? null, }; + if (keys.key) payload.key = keys.key; const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, { method: 'PUT', diff --git a/webapp/src/lib/export-formats.ts b/webapp/src/lib/export-formats.ts index 2b1456e..7c6f956 100644 --- a/webapp/src/lib/export-formats.ts +++ b/webapp/src/lib/export-formats.ts @@ -189,12 +189,15 @@ function mapCipherEncrypted(cipher: Cipher): Record { const login = cipher.login; out.login = login ? { + ...cloneValue(login), username: login.username ?? null, password: login.password ?? null, totp: login.totp ?? null, uris: Array.isArray(login.uris) ? login.uris.map((uri) => ({ + ...cloneValue(uri), uri: uri?.uri ?? null, + uriChecksum: uri?.uriChecksum ?? null, match: (uri as { match?: unknown })?.match ?? null, })) : [], diff --git a/webapp/src/lib/import-formats-bitwarden.ts b/webapp/src/lib/import-formats-bitwarden.ts index ad14a35..9e677b7 100644 --- a/webapp/src/lib/import-formats-bitwarden.ts +++ b/webapp/src/lib/import-formats-bitwarden.ts @@ -7,6 +7,7 @@ export interface BitwardenFolderInput { export interface BitwardenUriInput { uri?: string | null; + uriChecksum?: string | null; match?: number | null; }