fix: enhance compatibility for cipher login normalization and uri handling

This commit is contained in:
shuaiplus
2026-05-30 02:26:36 +08:00
parent a75955ca6d
commit ed9251c014
+49 -8
View File
@@ -10,6 +10,7 @@ import {
Attachment, Attachment,
PasswordHistory, PasswordHistory,
} from '../types'; } from '../types';
import { LIMITS } from '../config/limits';
import { StorageService } from '../services/storage'; import { StorageService } from '../services/storage';
import { notifyUserVaultSync } from '../durable/notifications-hub'; import { notifyUserVaultSync } from '../durable/notifications-hub';
import { jsonResponse, errorResponse } from '../utils/response'; import { jsonResponse, errorResponse } from '../utils/response';
@@ -161,20 +162,57 @@ export function normalizeCipherLoginForStorage(login: any): any {
}; };
} }
export function normalizeCipherLoginForCompatibility(login: any): any { export function normalizeCipherLoginForCompatibility(login: any, requiresUriChecksum: boolean = false): any {
const normalized = normalizeCipherLoginForStorage(login); const normalized = normalizeCipherLoginForStorage(login);
if (!normalized || typeof normalized !== 'object') return normalized ?? null; if (!normalized || typeof normalized !== 'object') return normalized ?? null;
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 = Array.isArray(next.uris) next.uris = normalizeCipherLoginUrisForCompatibility(next.uris, {
? next.uris hasLegacyLoginUri: isValidEncString(next.uri),
.map((uri: any) => sanitizeEncryptedObject(uri, ['uri', 'uriChecksum'])) requiresUriChecksum,
.filter((uri: any) => !!uri && (uri.uri || uri.uriChecksum || uri.match != null)) });
: null;
next.fido2Credentials = normalizeFido2CredentialsForCompatibility(next.fido2Credentials); next.fido2Credentials = normalizeFido2CredentialsForCompatibility(next.fido2Credentials);
return next; return next;
} }
function normalizeCipherLoginUrisForCompatibility(
uris: any,
options: { hasLegacyLoginUri?: boolean; requiresUriChecksum?: boolean } = {}
): any[] | null {
if (!Array.isArray(uris) || uris.length === 0) return null;
const out: any[] = [];
for (const uri of uris) {
if (!uri || typeof uri !== 'object') continue;
const next = sanitizeEncryptedObject(uri, ['uri', 'uriChecksum']);
if (!next) continue;
const hasUri = isValidEncString(next.uri);
const hasChecksum = isValidEncString(next.uriChecksum);
const hasMatch = next.match != null;
if (hasUri && hasChecksum) {
out.push(next);
continue;
}
if (hasUri && !hasChecksum) {
// Bitwarden browser clients using the SDK can fail the whole vault load
// when an item-key encrypted URI has no encrypted checksum. The server
// cannot derive the checksum, so expose the item without the bad URI.
if (options.requiresUriChecksum || options.hasLegacyLoginUri) continue;
out.push({ ...next, uri: null, uriChecksum: null });
continue;
}
if (hasChecksum || hasMatch) {
out.push(next);
}
}
return out.length ? out : null;
}
function hasMissingLoginUriChecksum(cipher: Cipher): boolean { function hasMissingLoginUriChecksum(cipher: Cipher): boolean {
if (!cipher.key || !cipher.login || typeof cipher.login !== 'object') return false; if (!cipher.key || !cipher.login || typeof cipher.login !== 'object') return false;
const uris = (cipher.login as any).uris; const uris = (cipher.login as any).uris;
@@ -506,7 +544,10 @@ export function cipherToResponse(
): CipherResponse { ): CipherResponse {
// Strip internal-only fields that must not appear in the API response // Strip internal-only fields that must not appear in the API response
const { userId, createdAt, updatedAt, archivedAt, deletedAt, ...passthrough } = cipher; const { userId, createdAt, updatedAt, archivedAt, deletedAt, ...passthrough } = cipher;
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null); const responseCipherKey = LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled
? optionalEncString(cipher.key)
: null;
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null, !!responseCipherKey);
const normalizedCard = sanitizeEncryptedObject((passthrough as any).card ?? null, ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code']); const normalizedCard = sanitizeEncryptedObject((passthrough as any).card ?? null, ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code']);
const normalizedIdentity = sanitizeEncryptedObject((passthrough as any).identity ?? null, [ const normalizedIdentity = sanitizeEncryptedObject((passthrough as any).identity ?? null, [
'title', 'title',
@@ -560,7 +601,7 @@ export function cipherToResponse(
fields: normalizeCipherFieldsForCompatibility((passthrough as any).fields), fields: normalizeCipherFieldsForCompatibility((passthrough as any).fields),
passwordHistory: normalizePasswordHistoryForCompatibility((passthrough as any).passwordHistory), passwordHistory: normalizePasswordHistoryForCompatibility((passthrough as any).passwordHistory),
sshKey: normalizedSshKey, sshKey: normalizedSshKey,
key: optionalEncString(cipher.key), key: responseCipherKey,
encryptedFor: (passthrough as any).encryptedFor ?? null, encryptedFor: (passthrough as any).encryptedFor ?? null,
}; };
} }