From 58847e9788bf572803c87d4e602563478af1ba5d Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Sat, 27 Dec 2025 03:22:56 +0000 Subject: [PATCH] fix: add support for updating attachment keys and filenames during key rotation --- src/handlers/accounts.rs | 20 ++++++++++++++++++++ src/models/cipher.rs | 13 +++++++++++++ src/models/user.rs | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/handlers/accounts.rs b/src/handlers/accounts.rs index d20578a..d607a32 100644 --- a/src/handlers/accounts.rs +++ b/src/handlers/accounts.rs @@ -754,6 +754,7 @@ pub async fn post_rotatekey( // Only update personal ciphers (organization_id is None) let mut cipher_statements: Vec = Vec::with_capacity(personal_ciphers.len()); + let mut attachment_statements: Vec = Vec::new(); for cipher in personal_ciphers { // id is guaranteed to exist (validated above) let cipher_id = cipher.id.as_ref().unwrap(); @@ -778,8 +779,27 @@ pub async fn post_rotatekey( ) .map_err(|_| AppError::Database)?; cipher_statements.push(stmt); + + // Update attachments key and encrypted filename when rotating. + // The Bitwarden clients send `attachments2` only during key rotation. + if let Some(attachments2) = &cipher.attachments2 { + for (attachment_id, attachment) in attachments2 { + let stmt = query!( + &db, + "UPDATE attachments SET file_name = ?1, akey = ?2, updated_at = ?3 WHERE id = ?4 AND cipher_id = ?5", + attachment.file_name, + attachment.key, + now, + attachment_id, + cipher_id + ) + .map_err(|_| AppError::Database)?; + attachment_statements.push(stmt); + } + } } db::execute_in_batches(&db, cipher_statements, batch_size).await?; + db::execute_in_batches(&db, attachment_statements, batch_size).await?; // Generate new salt and hash the new password let new_salt = generate_salt()?; diff --git a/src/models/cipher.rs b/src/models/cipher.rs index 00a19c4..4348306 100644 --- a/src/models/cipher.rs +++ b/src/models/cipher.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{json, Map, Value}; @@ -317,12 +319,23 @@ pub struct CipherRequestData { pub favorite: Option, #[serde(flatten)] pub type_fields: CipherTypeFields, + /// Used during key rotation to update attachment keys and encrypted filenames. + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments2: Option>, // The revision datetime (in ISO 8601 format) of the client's local copy // Used to prevent updating a cipher when client doesn't have the latest version #[serde(skip_serializing_if = "Option::is_none")] pub last_known_revision_date: Option, } +/// Attachment metadata sent by clients during key rotation. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Attachments2Data { + pub file_name: String, + pub key: String, +} + /// Represents the full request payload for creating a cipher with collections. /// Supports both camelCase and PascalCase for compatibility with different clients. #[derive(Debug, Deserialize)] diff --git a/src/models/user.rs b/src/models/user.rs index ab2987c..c619b52 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -14,7 +14,7 @@ pub struct User { pub master_password_hash: String, pub master_password_hint: Option, pub password_salt: Option, // Salt for server-side PBKDF2 (NULL for legacy users) - pub password_iterations: i32, // Server-side PBKDF2 iterations used for master_password_hash + pub password_iterations: i32, // Server-side PBKDF2 iterations used for master_password_hash pub key: String, pub private_key: String, pub public_key: String,