fix: enable cipher key encryption feature for 2026.4.x clients and streamline key handling

This commit is contained in:
shuaiplus
2026-05-31 01:03:32 +08:00
parent 192071e4a7
commit fd9707c396
6 changed files with 76 additions and 24 deletions
+5 -4
View File
@@ -149,9 +149,10 @@
// Single source of truth for /config.version and /api/version.
// /config.version 与 /api/version 的统一版本号来源。
bitwardenServerVersion: '2026.4.1',
// Advertise official per-cipher item-key encryption support only after
// NodeWarden can guarantee key/field consistency across all write paths.
// 在所有写入路径都能保证 cipher.key 与字段密文一致之前,不向官方客户端声明支持逐项密钥加密
cipherKeyEncryptionFeatureEnabled: false,
// Official 2026.4.x clients need this flag to receive and use cipher.key.
// Hiding existing item keys makes item-key encrypted vault data unreadable.
// 官方 2026.4.x 客户端需要该开关来接收并使用 cipher.key
// 隐藏已有逐项密钥会导致逐项密钥加密的密码库数据无法解密。
cipherKeyEncryptionFeatureEnabled: true,
},
} as const;
+8 -8
View File
@@ -10,7 +10,6 @@ import {
Attachment,
PasswordHistory,
} from '../types';
import { LIMITS } from '../config/limits';
import { StorageService } from '../services/storage';
import { notifyUserVaultSync } from '../durable/notifications-hub';
import { jsonResponse, errorResponse } from '../utils/response';
@@ -131,12 +130,10 @@ function optionalEncString(value: unknown): string | null {
}
function shouldAcceptCipherKey(value: unknown): boolean {
if (LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled) return true;
return optionalEncString(value) === null;
return value == null || value === '' || isValidEncString(value);
}
function normalizeCipherKeyForStorage(value: unknown): string | null {
if (!LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled) return null;
return optionalEncString(value);
}
@@ -562,9 +559,7 @@ export function cipherToResponse(
): CipherResponse {
// Strip internal-only fields that must not appear in the API response
const { userId, createdAt, updatedAt, archivedAt, deletedAt, ...passthrough } = cipher;
const responseCipherKey = LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled
? optionalEncString(cipher.key)
: null;
const responseCipherKey = optionalEncString(cipher.key);
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null, !!responseCipherKey);
const normalizedCard = sanitizeEncryptedObject((passthrough as any).card ?? null, ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code']);
const normalizedIdentity = sanitizeEncryptedObject((passthrough as any).identity ?? null, [
@@ -827,7 +822,12 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
if (incomingFolderId.present) {
cipher.folderId = normalizeOptionalId(incomingFolderId.value);
}
cipher.key = normalizeCipherKeyForStorage(incomingKey.present ? incomingKey.value : existingCipher.key);
if (incomingKey.present) {
const normalizedIncomingKey = normalizeCipherKeyForStorage(incomingKey.value);
cipher.key = normalizedIncomingKey || normalizeCipherKeyForStorage(existingCipher.key);
} else {
cipher.key = normalizeCipherKeyForStorage(existingCipher.key);
}
cipher.login = nextType === 1 ? (incomingLogin.present ? (incomingLogin.value ?? null) : (existingCipher.login ?? null)) : null;
cipher.secureNote = nextType === 2 ? (incomingSecureNote.present ? (incomingSecureNote.value ?? null) : (existingCipher.secureNote ?? null)) : null;
cipher.card = nextType === 3 ? (incomingCard.present ? (incomingCard.value ?? null) : (existingCipher.card ?? null)) : null;