mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 02:08:20 +02:00 
			
		
		
		
	Fix sync with new native clients (#4932)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							66baa5e7d8
						
					
				
				
					commit
					dca14285fd
				
			| @@ -490,7 +490,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, | |||||||
|     // Bitwarden does not process the import if there is one item invalid. |     // Bitwarden does not process the import if there is one item invalid. | ||||||
|     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. |     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. | ||||||
|     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. |     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. | ||||||
|     Cipher::validate_notes(&data.ciphers)?; |     Cipher::validate_cipher_data(&data.ciphers)?; | ||||||
|  |  | ||||||
|     let user_uuid = &headers.user.uuid; |     let user_uuid = &headers.user.uuid; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -233,7 +233,7 @@ pub struct CipherData { | |||||||
|     favorite: Option<bool>, |     favorite: Option<bool>, | ||||||
|     reprompt: Option<i32>, |     reprompt: Option<i32>, | ||||||
|  |  | ||||||
|     password_history: Option<Value>, |     pub password_history: Option<Value>, | ||||||
|  |  | ||||||
|     // These are used during key rotation |     // These are used during key rotation | ||||||
|     // 'Attachments' is unused, contains map of {id: filename} |     // 'Attachments' is unused, contains map of {id: filename} | ||||||
| @@ -563,7 +563,7 @@ async fn post_ciphers_import( | |||||||
|     // Bitwarden does not process the import if there is one item invalid. |     // Bitwarden does not process the import if there is one item invalid. | ||||||
|     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. |     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. | ||||||
|     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. |     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. | ||||||
|     Cipher::validate_notes(&data.ciphers)?; |     Cipher::validate_cipher_data(&data.ciphers)?; | ||||||
|  |  | ||||||
|     // Read and create the folders |     // Read and create the folders | ||||||
|     let existing_folders: Vec<String> = |     let existing_folders: Vec<String> = | ||||||
|   | |||||||
| @@ -1596,7 +1596,7 @@ async fn post_org_import( | |||||||
|     // Bitwarden does not process the import if there is one item invalid. |     // Bitwarden does not process the import if there is one item invalid. | ||||||
|     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. |     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. | ||||||
|     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. |     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. | ||||||
|     Cipher::validate_notes(&data.ciphers)?; |     Cipher::validate_cipher_data(&data.ciphers)?; | ||||||
|  |  | ||||||
|     let mut collections = Vec::new(); |     let mut collections = Vec::new(); | ||||||
|     for coll in data.collections { |     for coll in data.collections { | ||||||
|   | |||||||
| @@ -79,19 +79,39 @@ impl Cipher { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult { |     pub fn validate_cipher_data(cipher_data: &[CipherData]) -> EmptyResult { | ||||||
|         let mut validation_errors = serde_json::Map::new(); |         let mut validation_errors = serde_json::Map::new(); | ||||||
|         let max_note_size = CONFIG._max_note_size(); |         let max_note_size = CONFIG._max_note_size(); | ||||||
|         let max_note_size_msg = |         let max_note_size_msg = | ||||||
|             format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size); |             format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size); | ||||||
|         for (index, cipher) in cipher_data.iter().enumerate() { |         for (index, cipher) in cipher_data.iter().enumerate() { | ||||||
|  |             // Validate the note size and if it is exceeded return a warning | ||||||
|             if let Some(note) = &cipher.notes { |             if let Some(note) = &cipher.notes { | ||||||
|                 if note.len() > max_note_size { |                 if note.len() > max_note_size { | ||||||
|                     validation_errors |                     validation_errors | ||||||
|                         .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); |                         .insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap()); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // Validate the password history if it contains `null` values and if so, return a warning | ||||||
|  |             if let Some(Value::Array(password_history)) = &cipher.password_history { | ||||||
|  |                 for pwh in password_history { | ||||||
|  |                     if let Value::Object(pwo) = pwh { | ||||||
|  |                         if pwo.get("password").is_some_and(|p| !p.is_string()) { | ||||||
|  |                             validation_errors.insert( | ||||||
|  |                                 format!("Ciphers[{index}].Notes"), | ||||||
|  |                                 serde_json::to_value([ | ||||||
|  |                                     "The password history contains a `null` value. Only strings are allowed.", | ||||||
|  |                                 ]) | ||||||
|  |                                 .unwrap(), | ||||||
|  |                             ); | ||||||
|  |                             break; | ||||||
|                         } |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if !validation_errors.is_empty() { |         if !validation_errors.is_empty() { | ||||||
|             let err_json = json!({ |             let err_json = json!({ | ||||||
|                 "message": "The model state is invalid.", |                 "message": "The model state is invalid.", | ||||||
| @@ -153,27 +173,39 @@ impl Cipher { | |||||||
|             .as_ref() |             .as_ref() | ||||||
|             .and_then(|s| { |             .and_then(|s| { | ||||||
|                 serde_json::from_str::<Vec<LowerCase<Value>>>(s) |                 serde_json::from_str::<Vec<LowerCase<Value>>>(s) | ||||||
|                     .inspect_err(|e| warn!("Error parsing fields {:?}", e)) |                     .inspect_err(|e| warn!("Error parsing fields {e:?} for {}", self.uuid)) | ||||||
|                     .ok() |  | ||||||
|             }) |  | ||||||
|             .map(|d| d.into_iter().map(|d| d.data).collect()) |  | ||||||
|             .unwrap_or_default(); |  | ||||||
|         let password_history_json: Vec<_> = self |  | ||||||
|             .password_history |  | ||||||
|             .as_ref() |  | ||||||
|             .and_then(|s| { |  | ||||||
|                 serde_json::from_str::<Vec<LowerCase<Value>>>(s) |  | ||||||
|                     .inspect_err(|e| warn!("Error parsing password history {:?}", e)) |  | ||||||
|                     .ok() |                     .ok() | ||||||
|             }) |             }) | ||||||
|             .map(|d| d.into_iter().map(|d| d.data).collect()) |             .map(|d| d.into_iter().map(|d| d.data).collect()) | ||||||
|             .unwrap_or_default(); |             .unwrap_or_default(); | ||||||
|  |  | ||||||
|  |         let password_history_json: Vec<_> = self | ||||||
|  |             .password_history | ||||||
|  |             .as_ref() | ||||||
|  |             .and_then(|s| { | ||||||
|  |                 serde_json::from_str::<Vec<LowerCase<Value>>>(s) | ||||||
|  |                     .inspect_err(|e| warn!("Error parsing password history {e:?} for {}", self.uuid)) | ||||||
|  |                     .ok() | ||||||
|  |             }) | ||||||
|  |             .map(|d| { | ||||||
|  |                 // Check every password history item if they are valid and return it. | ||||||
|  |                 // If a password field has the type `null` skip it, it breaks newer Bitwarden clients | ||||||
|  |                 d.into_iter() | ||||||
|  |                     .filter_map(|d| match d.data.get("password") { | ||||||
|  |                         Some(p) if p.is_string() => Some(d.data), | ||||||
|  |                         _ => None, | ||||||
|  |                     }) | ||||||
|  |                     .collect() | ||||||
|  |             }) | ||||||
|  |             .unwrap_or_default(); | ||||||
|  |  | ||||||
|         // Get the type_data or a default to an empty json object '{}'. |         // Get the type_data or a default to an empty json object '{}'. | ||||||
|         // If not passing an empty object, mobile clients will crash. |         // If not passing an empty object, mobile clients will crash. | ||||||
|         let mut type_data_json = serde_json::from_str::<LowerCase<Value>>(&self.data) |         let mut type_data_json = | ||||||
|             .map(|d| d.data) |             serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_else(|_| { | ||||||
|             .unwrap_or_else(|_| Value::Object(serde_json::Map::new())); |                 warn!("Error parsing data field for {}", self.uuid); | ||||||
|  |                 Value::Object(serde_json::Map::new()) | ||||||
|  |             }); | ||||||
|  |  | ||||||
|         // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream |         // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream | ||||||
|         // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. |         // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. | ||||||
| @@ -189,11 +221,14 @@ impl Cipher { | |||||||
|  |  | ||||||
|         // Fix secure note issues when data is invalid |         // Fix secure note issues when data is invalid | ||||||
|         // This breaks at least the native mobile clients |         // This breaks at least the native mobile clients | ||||||
|         if self.atype == 2 |         if self.atype == 2 { | ||||||
|             && (self.data.is_empty() || self.data.eq("{}") || self.data.to_ascii_lowercase().eq("{\"type\":null}")) |             match type_data_json { | ||||||
|         { |                 Value::Object(ref t) if t.get("type").is_some_and(|t| t.is_number()) => {} | ||||||
|  |                 _ => { | ||||||
|                     type_data_json = json!({"type": 0}); |                     type_data_json = json!({"type": 0}); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // Clone the type_data and add some default value. |         // Clone the type_data and add some default value. | ||||||
|         let mut data_json = type_data_json.clone(); |         let mut data_json = type_data_json.clone(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user