mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
fix: enhance cipher login URI handling and import format support
This commit is contained in:
@@ -191,7 +191,6 @@ export function normalizeCipherLoginForCompatibility(
|
|||||||
const next = sanitizeEncryptedObject(normalized, ['username', 'password', 'totp', 'uri']);
|
const next = sanitizeEncryptedObject(normalized, ['username', 'password', 'totp', 'uri']);
|
||||||
if (!next) return null;
|
if (!next) return null;
|
||||||
next.uris = normalizeCipherLoginUrisForCompatibility(next.uris, {
|
next.uris = normalizeCipherLoginUrisForCompatibility(next.uris, {
|
||||||
hasLegacyLoginUri: isValidEncString(next.uri),
|
|
||||||
requiresUriChecksum,
|
requiresUriChecksum,
|
||||||
preserveRepairableUris,
|
preserveRepairableUris,
|
||||||
});
|
});
|
||||||
@@ -201,7 +200,7 @@ export function normalizeCipherLoginForCompatibility(
|
|||||||
|
|
||||||
function normalizeCipherLoginUrisForCompatibility(
|
function normalizeCipherLoginUrisForCompatibility(
|
||||||
uris: any,
|
uris: any,
|
||||||
options: { hasLegacyLoginUri?: boolean; requiresUriChecksum?: boolean; preserveRepairableUris?: boolean } = {}
|
options: { requiresUriChecksum?: boolean; preserveRepairableUris?: boolean } = {}
|
||||||
): any[] | null {
|
): any[] | null {
|
||||||
if (!Array.isArray(uris) || uris.length === 0) return null;
|
if (!Array.isArray(uris) || uris.length === 0) return null;
|
||||||
const out: any[] = [];
|
const out: any[] = [];
|
||||||
@@ -231,7 +230,7 @@ function normalizeCipherLoginUrisForCompatibility(
|
|||||||
// Bitwarden browser clients using the SDK drop item-key encrypted URIs
|
// Bitwarden browser clients using the SDK drop item-key encrypted URIs
|
||||||
// whose checksum is missing/invalid. User-key encrypted legacy/import
|
// whose checksum is missing/invalid. User-key encrypted legacy/import
|
||||||
// entries bypass this validation and can safely keep the URI.
|
// 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 });
|
out.push({ ...next, uriChecksum: null });
|
||||||
continue;
|
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);
|
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;
|
const nextType = Number(cipherData.type) || existingCipher.type;
|
||||||
|
|
||||||
// Opaque passthrough: merge existing stored data with ALL incoming client fields.
|
// Opaque passthrough: merge existing stored data with ALL incoming client fields.
|
||||||
|
|||||||
+24
-10
@@ -819,13 +819,15 @@ async function repairCipherLoginUris(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clearUri = String(entry.decUri || '').trim();
|
let clearUri = '';
|
||||||
if (!clearUri || looksLikeCipherString(clearUri)) {
|
let rawUriUsesCurrentKey = false;
|
||||||
try {
|
try {
|
||||||
clearUri = (await decryptStr(rawUri, enc, mac)).trim();
|
clearUri = (await decryptStr(rawUri, enc, mac)).trim();
|
||||||
|
rawUriUsesCurrentKey = !!clearUri;
|
||||||
} catch {
|
} catch {
|
||||||
uris.push({ ...encryptedEntry });
|
const fallbackUri = String(entry.decUri || '').trim();
|
||||||
continue;
|
if (fallbackUri && !looksLikeCipherString(fallbackUri)) {
|
||||||
|
clearUri = fallbackUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,15 +847,20 @@ async function repairCipherLoginUris(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentChecksumOk) {
|
if (currentChecksumOk && rawUriUsesCurrentKey) {
|
||||||
uris.push({ ...encryptedEntry });
|
uris.push({ ...encryptedEntry });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const repairedUri = rawUriUsesCurrentKey ? rawUri : await encryptTextValue(clearUri, enc, mac);
|
||||||
|
const repairedChecksum = currentChecksumOk
|
||||||
|
? rawChecksum
|
||||||
|
: await encryptTextValue(expectedChecksum, enc, mac);
|
||||||
|
|
||||||
uris.push({
|
uris.push({
|
||||||
...encryptedEntry,
|
...encryptedEntry,
|
||||||
uri: rawUri,
|
uri: repairedUri || rawUri,
|
||||||
uriChecksum: await encryptTextValue(expectedChecksum, enc, mac),
|
uriChecksum: repairedChecksum,
|
||||||
match: typeof entry.match === 'number' && Number.isFinite(entry.match) ? entry.match : null,
|
match: typeof entry.match === 'number' && Number.isFinite(entry.match) ? entry.match : null,
|
||||||
});
|
});
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -889,7 +896,13 @@ export async function repairCipherUriChecksums(
|
|||||||
let repaired = 0;
|
let repaired = 0;
|
||||||
|
|
||||||
for (const cipher of ciphers) {
|
for (const cipher of ciphers) {
|
||||||
if (!cipher?.id || cipher.type !== 1 || !looksLikeCipherString(cipher.key) || !cipher.login || !Array.isArray(cipher.login.uris)) 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;
|
let itemKey: Uint8Array;
|
||||||
try {
|
try {
|
||||||
itemKey = await decryptBw(String(cipher.key).trim(), userEnc, userMac);
|
itemKey = await decryptBw(String(cipher.key).trim(), userEnc, userMac);
|
||||||
@@ -897,7 +910,8 @@ export async function repairCipherUriChecksums(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (itemKey.length < 64) continue;
|
if (itemKey.length < 64) continue;
|
||||||
const keys = { enc: itemKey.slice(0, 32), mac: itemKey.slice(32, 64), key: String(cipher.key).trim() };
|
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);
|
const repair = await repairCipherLoginUris(cipher, keys.enc, keys.mac);
|
||||||
if (!repair.changed) continue;
|
if (!repair.changed) continue;
|
||||||
|
|
||||||
@@ -912,9 +926,9 @@ export async function repairCipherUriChecksums(
|
|||||||
fields: Array.isArray(cipher.fields)
|
fields: Array.isArray(cipher.fields)
|
||||||
? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field)
|
? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field)
|
||||||
: null,
|
: null,
|
||||||
key: keys.key,
|
|
||||||
lastKnownRevisionDate: cipher.revisionDate ?? null,
|
lastKnownRevisionDate: cipher.revisionDate ?? null,
|
||||||
};
|
};
|
||||||
|
if (keys.key) payload.key = keys.key;
|
||||||
|
|
||||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -189,12 +189,15 @@ function mapCipherEncrypted(cipher: Cipher): Record<string, unknown> {
|
|||||||
const login = cipher.login;
|
const login = cipher.login;
|
||||||
out.login = login
|
out.login = login
|
||||||
? {
|
? {
|
||||||
|
...cloneValue(login),
|
||||||
username: login.username ?? null,
|
username: login.username ?? null,
|
||||||
password: login.password ?? null,
|
password: login.password ?? null,
|
||||||
totp: login.totp ?? null,
|
totp: login.totp ?? null,
|
||||||
uris: Array.isArray(login.uris)
|
uris: Array.isArray(login.uris)
|
||||||
? login.uris.map((uri) => ({
|
? login.uris.map((uri) => ({
|
||||||
|
...cloneValue(uri),
|
||||||
uri: uri?.uri ?? null,
|
uri: uri?.uri ?? null,
|
||||||
|
uriChecksum: uri?.uriChecksum ?? null,
|
||||||
match: (uri as { match?: unknown })?.match ?? null,
|
match: (uri as { match?: unknown })?.match ?? null,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface BitwardenFolderInput {
|
|||||||
|
|
||||||
export interface BitwardenUriInput {
|
export interface BitwardenUriInput {
|
||||||
uri?: string | null;
|
uri?: string | null;
|
||||||
|
uriChecksum?: string | null;
|
||||||
match?: number | null;
|
match?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user