diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 0005109..8ba428a 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -823,6 +823,9 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str const incomingPasswordHistory = readCipherProp(cipherData, ['passwordHistory', 'PasswordHistory']); const incomingRevisionDate = readCipherRevisionDate(cipherData); const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData); + const preserveRevisionDate = + shouldPreserveRepairableCipherUris(request) + && (body.preserveRevisionDate === true || cipherData.preserveRevisionDate === true); if (incomingKey.present && !shouldAcceptCipherKey(incomingKey.value)) { 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. // Unknown/future fields from the client are preserved; server-controlled fields are protected. + const { preserveRevisionDate: _preserveRevisionDate, PreserveRevisionDate: _pascalPreserveRevisionDate, ...cipherDataWithoutFlags } = cipherData; const cipher: Cipher = { ...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) id: existingCipher.id, userId: existingCipher.userId, @@ -850,7 +854,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str favorite: cipherData.favorite ?? existingCipher.favorite, reprompt: cipherData.reprompt ?? existingCipher.reprompt, createdAt: existingCipher.createdAt, - updatedAt: new Date().toISOString(), + updatedAt: preserveRevisionDate ? existingCipher.updatedAt : new Date().toISOString(), archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null), deletedAt: existingCipher.deletedAt, }; diff --git a/webapp/src/lib/api/vault.ts b/webapp/src/lib/api/vault.ts index 2c3638f..a531b13 100644 --- a/webapp/src/lib/api/vault.ts +++ b/webapp/src/lib/api/vault.ts @@ -927,6 +927,7 @@ export async function repairCipherUriChecksums( ? cipher.fields.map(({ decName: _decName, decValue: _decValue, ...field }) => field) : null, lastKnownRevisionDate: cipher.revisionDate ?? null, + preserveRevisionDate: true, }; if (keys.key) payload.key = keys.key; @@ -1091,7 +1092,9 @@ export async function repairCipherKeyMismatches( if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue; if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue; if (hasUnresolvedEncryptedFields(cipher)) continue; - await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher)); + await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher), { + preserveRevisionDate: true, + }); repaired += 1; } @@ -1225,9 +1228,13 @@ export async function updateCipher( authedFetch: AuthedFetch, session: SessionState, cipher: Cipher, - draft: VaultDraft + draft: VaultDraft, + extraPayload?: Record ): Promise { const payload = await buildCipherPayload(session, draft, cipher); + if (extraPayload) { + Object.assign(payload, extraPayload); + } const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, { method: 'PUT',