fix: preserve cipher edit time during auto repair

This commit is contained in:
rootphantomer
2026-06-09 12:14:11 +08:00
committed by shuaiplus
parent d4749d3f82
commit 1a10df4a18
2 changed files with 15 additions and 4 deletions
+6 -2
View File
@@ -823,6 +823,9 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
const incomingPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']); const incomingPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
const incomingRevisionDate = readCipherRevisionDate(cipherData); const incomingRevisionDate = readCipherRevisionDate(cipherData);
const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData); const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData);
const preserveRevisionDate =
shouldPreserveRepairableCipherUris(request)
&& (body.preserveRevisionDate === true || cipherData.preserveRevisionDate === true);
if (incomingKey.present && !shouldAcceptCipherKey(incomingKey.value)) { if (incomingKey.present && !shouldAcceptCipherKey(incomingKey.value)) {
return errorResponse('Cipher key encryption is not supported by this server. Resync the client and try again.', 400); return errorResponse('Cipher key encryption is not supported by this server. Resync the client and try again.', 400);
@@ -840,9 +843,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
// Opaque passthrough: merge existing stored data with ALL incoming client fields. // Opaque passthrough: merge existing stored data with ALL incoming client fields.
// Unknown/future fields from the client are preserved; server-controlled fields are protected. // Unknown/future fields from the client are preserved; server-controlled fields are protected.
const { preserveRevisionDate: _preserveRevisionDate, PreserveRevisionDate: _pascalPreserveRevisionDate, ...cipherDataWithoutFlags } = cipherData;
const cipher: Cipher = { const cipher: Cipher = {
...existingCipher, // start with all existing stored data (including unknowns) ...existingCipher, // start with all existing stored data (including unknowns)
...cipherData, // overlay all client data (including new/unknown fields) ...cipherDataWithoutFlags, // overlay all client data (including new/unknown fields)
// Server-controlled fields (never from client) // Server-controlled fields (never from client)
id: existingCipher.id, id: existingCipher.id,
userId: existingCipher.userId, userId: existingCipher.userId,
@@ -850,7 +854,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
favorite: cipherData.favorite ?? existingCipher.favorite, favorite: cipherData.favorite ?? existingCipher.favorite,
reprompt: cipherData.reprompt ?? existingCipher.reprompt, reprompt: cipherData.reprompt ?? existingCipher.reprompt,
createdAt: existingCipher.createdAt, createdAt: existingCipher.createdAt,
updatedAt: new Date().toISOString(), updatedAt: preserveRevisionDate ? existingCipher.updatedAt : new Date().toISOString(),
archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null), archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null),
deletedAt: existingCipher.deletedAt, deletedAt: existingCipher.deletedAt,
}; };
+9 -2
View File
@@ -927,6 +927,7 @@ export async function repairCipherUriChecksums(
? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field) ? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field)
: null, : null,
lastKnownRevisionDate: cipher.revisionDate ?? null, lastKnownRevisionDate: cipher.revisionDate ?? null,
preserveRevisionDate: true,
}; };
if (keys.key) payload.key = keys.key; if (keys.key) payload.key = keys.key;
@@ -1091,7 +1092,9 @@ export async function repairCipherKeyMismatches(
if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue; if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue;
if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue; if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue;
if (hasUnresolvedEncryptedFields(cipher)) continue; if (hasUnresolvedEncryptedFields(cipher)) continue;
await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher)); await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher), {
preserveRevisionDate: true,
});
repaired += 1; repaired += 1;
} }
@@ -1225,9 +1228,13 @@ export async function updateCipher(
authedFetch: AuthedFetch, authedFetch: AuthedFetch,
session: SessionState, session: SessionState,
cipher: Cipher, cipher: Cipher,
draft: VaultDraft draft: VaultDraft,
extraPayload?: Record<string, unknown>
): Promise<Cipher> { ): Promise<Cipher> {
const payload = await buildCipherPayload(session, draft, cipher); const payload = await buildCipherPayload(session, draft, cipher);
if (extraPayload) {
Object.assign(payload, extraPayload);
}
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, { const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
method: 'PUT', method: 'PUT',