From d5fb7b28ea765b2e8e07cd8401881cd9428945a6 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 15 Sep 2025 09:39:50 -0400 Subject: [PATCH 01/60] Added conversions for ciphers --- crates/bitwarden-vault/src/cipher/card.rs | 13 +++ crates/bitwarden-vault/src/cipher/cipher.rs | 81 ++++++++++++++++++- crates/bitwarden-vault/src/cipher/field.rs | 22 +++++ crates/bitwarden-vault/src/cipher/identity.rs | 25 ++++++ crates/bitwarden-vault/src/cipher/login.rs | 68 +++++++++++++++- crates/bitwarden-vault/src/cipher/mod.rs | 3 + .../bitwarden-vault/src/cipher/secure_note.rs | 16 ++++ crates/bitwarden-vault/src/cipher/ssh_key.rs | 10 +++ .../bitwarden-vault/src/password_history.rs | 9 +++ 9 files changed, 244 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/card.rs b/crates/bitwarden-vault/src/cipher/card.rs index 4b622a80f..bb1260960 100644 --- a/crates/bitwarden-vault/src/cipher/card.rs +++ b/crates/bitwarden-vault/src/cipher/card.rs @@ -127,6 +127,19 @@ impl TryFrom for Card { } } +impl From for bitwarden_api_api::models::CipherCardModel { + fn from(card: Card) -> Self { + Self { + cardholder_name: card.cardholder_name.map(|n| n.to_string()), + brand: card.brand.map(|b| b.to_string()), + number: card.number.map(|n| n.to_string()), + exp_month: card.exp_month.map(|m| m.to_string()), + exp_year: card.exp_year.map(|y| y.to_string()), + code: card.code.map(|c| c.to_string()), + } + } +} + impl CipherKind for Card { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index a55a94911..248fb6632 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -1,4 +1,4 @@ -use bitwarden_api_api::models::CipherDetailsResponseModel; +use bitwarden_api_api::models::{CipherDetailsResponseModel, CipherResponseModel}; use bitwarden_collections::collection::CollectionId; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, @@ -688,6 +688,15 @@ impl Decryptable for Cipher { } } +#[cfg(feature = "wasm")] +impl wasm_bindgen::__rt::VectorIntoJsValue for CipherView { + fn vector_into_jsvalue( + vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>, + ) -> wasm_bindgen::JsValue { + wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector) + } +} + impl IdentifyKey for Cipher { fn key_identifier(&self) -> SymmetricKeyId { match self.organization_id { @@ -788,6 +797,76 @@ impl From for CipherRepromptType } } +impl From for bitwarden_api_api::models::CipherType { + fn from(t: CipherType) -> Self { + match t { + CipherType::Login => bitwarden_api_api::models::CipherType::Login, + CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote, + CipherType::Card => bitwarden_api_api::models::CipherType::Card, + CipherType::Identity => bitwarden_api_api::models::CipherType::Identity, + CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey, + } + } +} + +impl From for bitwarden_api_api::models::CipherRepromptType { + fn from(t: CipherRepromptType) -> Self { + match t { + CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None, + CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password, + } + } +} + +impl TryFrom for Cipher { + type Error = VaultParseError; + + fn try_from(cipher: CipherResponseModel) -> Result { + Ok(Self { + id: cipher.id.map(CipherId::new), + organization_id: cipher.organization_id.map(OrganizationId::new), + folder_id: cipher.folder_id.map(FolderId::new), + collection_ids: vec![], // CipherResponseModel doesn't include collection_ids + name: require!(EncString::try_from_optional(cipher.name)?), + notes: EncString::try_from_optional(cipher.notes)?, + r#type: require!(cipher.r#type).into(), + login: cipher.login.map(|l| (*l).try_into()).transpose()?, + identity: cipher.identity.map(|i| (*i).try_into()).transpose()?, + card: cipher.card.map(|c| (*c).try_into()).transpose()?, + secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?, + // TODO: add ssh_key + ssh_key: None, + favorite: cipher.favorite.unwrap_or(false), + reprompt: cipher + .reprompt + .map(|r| r.into()) + .unwrap_or(CipherRepromptType::None), + organization_use_totp: cipher.organization_use_totp.unwrap_or(true), + edit: cipher.edit.unwrap_or(true), + // TODO: add permissions + permissions: None, + view_password: cipher.view_password.unwrap_or(true), + local_data: None, // Not sent from server + attachments: cipher + .attachments + .map(|a| a.into_iter().map(|a| a.try_into()).collect()) + .transpose()?, + fields: cipher + .fields + .map(|f| f.into_iter().map(|f| f.try_into()).collect()) + .transpose()?, + password_history: cipher + .password_history + .map(|p| p.into_iter().map(|p| p.try_into()).collect()) + .transpose()?, + creation_date: require!(cipher.creation_date).parse()?, + deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, + revision_date: require!(cipher.revision_date).parse()?, + key: EncString::try_from_optional(cipher.key)?, + }) + } +} + #[cfg(test)] mod tests { diff --git a/crates/bitwarden-vault/src/cipher/field.rs b/crates/bitwarden-vault/src/cipher/field.rs index 9811e68b2..f1f826920 100644 --- a/crates/bitwarden-vault/src/cipher/field.rs +++ b/crates/bitwarden-vault/src/cipher/field.rs @@ -129,6 +129,28 @@ impl From for FieldType { } } +impl From for bitwarden_api_api::models::CipherFieldModel { + fn from(field: Field) -> Self { + Self { + name: field.name.map(|n| n.to_string()), + value: field.value.map(|v| v.to_string()), + r#type: Some(field.r#type.into()), + linked_id: field.linked_id.map(|id| u32::from(id) as i32), + } + } +} + +impl From for bitwarden_api_api::models::FieldType { + fn from(field_type: FieldType) -> Self { + match field_type { + FieldType::Text => bitwarden_api_api::models::FieldType::Text, + FieldType::Hidden => bitwarden_api_api::models::FieldType::Hidden, + FieldType::Boolean => bitwarden_api_api::models::FieldType::Boolean, + FieldType::Linked => bitwarden_api_api::models::FieldType::Linked, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bitwarden-vault/src/cipher/identity.rs b/crates/bitwarden-vault/src/cipher/identity.rs index 1484b3f44..6edd9cde8 100644 --- a/crates/bitwarden-vault/src/cipher/identity.rs +++ b/crates/bitwarden-vault/src/cipher/identity.rs @@ -147,6 +147,31 @@ impl TryFrom for Identity { } } +impl From for bitwarden_api_api::models::CipherIdentityModel { + fn from(identity: Identity) -> Self { + Self { + title: identity.title.map(|t| t.to_string()), + first_name: identity.first_name.map(|n| n.to_string()), + middle_name: identity.middle_name.map(|n| n.to_string()), + last_name: identity.last_name.map(|n| n.to_string()), + address1: identity.address1.map(|a| a.to_string()), + address2: identity.address2.map(|a| a.to_string()), + address3: identity.address3.map(|a| a.to_string()), + city: identity.city.map(|c| c.to_string()), + state: identity.state.map(|s| s.to_string()), + postal_code: identity.postal_code.map(|p| p.to_string()), + country: identity.country.map(|c| c.to_string()), + company: identity.company.map(|c| c.to_string()), + email: identity.email.map(|e| e.to_string()), + phone: identity.phone.map(|p| p.to_string()), + ssn: identity.ssn.map(|s| s.to_string()), + username: identity.username.map(|u| u.to_string()), + passport_number: identity.passport_number.map(|p| p.to_string()), + license_number: identity.license_number.map(|l| l.to_string()), + } + } +} + impl CipherKind for Identity { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index e3be991cb..7f92d02b0 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -84,7 +84,7 @@ impl LoginUriView { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -296,7 +296,7 @@ pub struct Login { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -562,6 +562,70 @@ impl TryFrom for Fido2Cre } } +impl From for bitwarden_api_api::models::CipherLoginUriModel { + fn from(uri: LoginUri) -> Self { + bitwarden_api_api::models::CipherLoginUriModel { + uri: uri.uri.map(|u| u.to_string()), + uri_checksum: uri.uri_checksum.map(|c| c.to_string()), + r#match: uri.r#match.map(|m| m.into()), + } + } +} + +impl From for bitwarden_api_api::models::UriMatchType { + fn from(match_type: UriMatchType) -> Self { + match match_type { + UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain, + UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host, + UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith, + UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact, + UriMatchType::RegularExpression => { + bitwarden_api_api::models::UriMatchType::RegularExpression + } + UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never, + } + } +} + +impl From for bitwarden_api_api::models::CipherFido2CredentialModel { + fn from(cred: Fido2Credential) -> Self { + bitwarden_api_api::models::CipherFido2CredentialModel { + credential_id: Some(cred.credential_id.to_string()), + key_type: Some(cred.key_type.to_string()), + key_algorithm: Some(cred.key_algorithm.to_string()), + key_curve: Some(cred.key_curve.to_string()), + key_value: Some(cred.key_value.to_string()), + rp_id: Some(cred.rp_id.to_string()), + user_handle: cred.user_handle.map(|h| h.to_string()), + user_name: cred.user_name.map(|n| n.to_string()), + counter: Some(cred.counter.to_string()), + rp_name: cred.rp_name.map(|n| n.to_string()), + user_display_name: cred.user_display_name.map(|n| n.to_string()), + discoverable: Some(cred.discoverable.to_string()), + creation_date: cred.creation_date.to_rfc3339(), + } + } +} + +impl From for bitwarden_api_api::models::CipherLoginModel { + fn from(login: Login) -> Self { + bitwarden_api_api::models::CipherLoginModel { + uri: None, + uris: login + .uris + .map(|u| u.into_iter().map(|u| u.into()).collect()), + username: login.username.map(|u| u.to_string()), + password: login.password.map(|p| p.to_string()), + password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()), + totp: login.totp.map(|t| t.to_string()), + autofill_on_page_load: login.autofill_on_page_load, + fido2_credentials: login + .fido2_credentials + .map(|c| c.into_iter().map(|c| c.into()).collect()), + } + } +} + impl CipherKind for Login { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 39fe85361..9afaf3bec 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -5,7 +5,10 @@ pub(crate) mod card; pub(crate) mod cipher; pub(crate) mod cipher_client; pub(crate) mod cipher_permissions; +pub(crate) mod create; +pub(crate) mod edit; pub(crate) mod field; +pub(crate) mod get_list; pub(crate) mod identity; pub(crate) mod linked_id; pub(crate) mod local_data; diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 821bded5c..4ee93d534 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -84,6 +84,22 @@ impl From for SecureNoteType { } } +impl From for bitwarden_api_api::models::SecureNoteType { + fn from(model: SecureNoteType) -> Self { + match model { + SecureNoteType::Generic => bitwarden_api_api::models::SecureNoteType::Generic, + } + } +} + +impl From for CipherSecureNoteModel { + fn from(model: SecureNote) -> Self { + Self { + r#type: Some(model.r#type.into()), + } + } +} + impl CipherKind for SecureNote { fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec { [cipher diff --git a/crates/bitwarden-vault/src/cipher/ssh_key.rs b/crates/bitwarden-vault/src/cipher/ssh_key.rs index e56da0033..2345953fc 100644 --- a/crates/bitwarden-vault/src/cipher/ssh_key.rs +++ b/crates/bitwarden-vault/src/cipher/ssh_key.rs @@ -95,6 +95,16 @@ impl TryFrom for SshKey { } } +impl From for CipherSshKeyModel { + fn from(ssh_key: SshKey) -> Self { + Self { + private_key: Some(ssh_key.private_key.to_string()), + public_key: Some(ssh_key.public_key.to_string()), + key_fingerprint: Some(ssh_key.fingerprint.to_string()), + } + } +} + #[cfg(test)] mod tests { use bitwarden_core::key_management::create_test_crypto_with_user_key; diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index 23df58daf..cdb407847 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -78,3 +78,12 @@ impl TryFrom for PasswordHistory { }) } } + +impl From for CipherPasswordHistoryModel { + fn from(history: PasswordHistory) -> Self { + Self { + password: history.password.to_string(), + last_used_date: history.last_used_date.to_rfc3339(), + } + } +} From 8144b9814eed26f13b8b091ffa5959290f73ddca Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 15 Sep 2025 09:49:46 -0400 Subject: [PATCH 02/60] Added create, edit and get --- crates/bitwarden-vault/src/cipher/create.rs | 304 ++++++++++++++++ crates/bitwarden-vault/src/cipher/edit.rs | 324 ++++++++++++++++++ crates/bitwarden-vault/src/cipher/get_list.rs | 41 +++ 3 files changed, 669 insertions(+) create mode 100644 crates/bitwarden-vault/src/cipher/create.rs create mode 100644 crates/bitwarden-vault/src/cipher/edit.rs create mode 100644 crates/bitwarden-vault/src/cipher/get_list.rs diff --git a/crates/bitwarden-vault/src/cipher/create.rs b/crates/bitwarden-vault/src/cipher/create.rs new file mode 100644 index 000000000..7dad15f40 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/create.rs @@ -0,0 +1,304 @@ +use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; +use bitwarden_core::{ + key_management::{KeyIds, SymmetricKeyId}, + require, ApiError, MissingFieldError, UserId, +}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify::Tsify; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{Cipher, CipherView, VaultParseError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateCipherError { + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +/// Request to add or edit a cipher. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct CipherAddEditRequest { + /// The cipher view data to be added or edited. + pub cipher: CipherView, + /// The user ID for whom this cipher is encrypted (internal use only). + pub encrypted_for: Option, +} + +impl CompositeEncryptable for CipherAddEditRequest { + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + let encrypted_cipher = self.cipher.encrypt_composite(ctx, key)?; + + let cipher_request = CipherRequestModel { + encrypted_for: self.encrypted_for.map(|id| id.into()), + r#type: Some(encrypted_cipher.r#type.into()), + organization_id: encrypted_cipher.organization_id.map(|id| id.to_string()), + folder_id: encrypted_cipher.folder_id.map(|id| id.to_string()), + favorite: Some(encrypted_cipher.favorite), + reprompt: Some(encrypted_cipher.reprompt.into()), + key: encrypted_cipher.key.map(|k| k.to_string()), + name: encrypted_cipher.name.to_string(), + notes: encrypted_cipher.notes.map(|n| n.to_string()), + fields: encrypted_cipher + .fields + .map(|f| f.into_iter().map(|f| f.into()).collect()), + password_history: encrypted_cipher + .password_history + .map(|ph| ph.into_iter().map(|ph| ph.into()).collect()), + attachments: None, + attachments2: encrypted_cipher.attachments.map(|a| { + a.into_iter() + .filter_map(|a| { + a.id.map(|id| { + ( + id, + bitwarden_api_api::models::CipherAttachmentModel { + file_name: a.file_name.map(|n| n.to_string()), + key: a.key.map(|k| k.to_string()), + }, + ) + }) + }) + .collect() + }), + login: encrypted_cipher.login.map(|l| Box::new(l.into())), + card: encrypted_cipher.card.map(|c| Box::new(c.into())), + identity: encrypted_cipher.identity.map(|i| Box::new(i.into())), + secure_note: encrypted_cipher + .secure_note + .map(|note| Box::new(note.into())), + ssh_key: encrypted_cipher.ssh_key.map(|key| Box::new(key.into())), + last_known_revision_date: Some(encrypted_cipher.revision_date.to_rfc3339()), + }; + + Ok(cipher_request) + } +} + +impl IdentifyKey for CipherAddEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + self.cipher.key_identifier() + } +} + +pub(super) async fn create_cipher + ?Sized>( + key_store: &KeyStore, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &R, + request: CipherAddEditRequest, +) -> Result { + let cipher_request = key_store.encrypt(request)?; + let resp = ciphers_api::ciphers_post(api_config, Some(cipher_request)) + .await + .map_err(ApiError::from)?; + + let cipher: Cipher = resp.try_into()?; + + repository + .set(require!(cipher.id).to_string(), cipher.clone()) + .await?; + + Ok(key_store.decrypt(&cipher)?) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::CipherResponseModel; + use bitwarden_crypto::SymmetricCryptoKey; + use bitwarden_test::{start_api_mock, MemoryRepository}; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; + + use crate::{CipherId, CipherRepromptType, CipherType, LoginView}; + + use super::*; + + const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; + const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; + + fn generate_test_cipher() -> CipherView { + CipherView { + id: None, + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: "Test Login".to_string(), + notes: Some("Test notes".to_string()), + r#type: CipherType::Login, + login: Some(LoginView { + username: Some("test@example.com".to_string()), + password: Some("password123".to_string()), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }), + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: true, + edit: true, + permissions: None, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deleted_date: None, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + } + + #[tokio::test] + async fn test_create_cipher() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); + let user_id: UserId = TEST_USER_ID.parse().unwrap(); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/ciphers")) + .respond_with(move |req: &Request| { + let body: CipherRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(CipherResponseModel { + object: Some("cipher".to_string()), + id: Some(cipher_id.into()), + name: Some(body.name.clone()), + r#type: body.r#type, + organization_id: body + .organization_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + folder_id: body + .folder_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + favorite: body.favorite, + reprompt: body.reprompt, + key: body.key.clone(), + notes: body.notes.clone(), + view_password: Some(true), + edit: Some(true), + organization_use_totp: Some(true), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + creation_date: Some("2025-01-01T00:00:00Z".to_string()), + deleted_date: None, + login: body.login, + card: body.card, + identity: body.identity, + secure_note: body.secure_note, + ssh_key: body.ssh_key, + fields: body.fields, + password_history: body.password_history, + attachments: None, + permissions: None, + data: None, + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); + let cipher_view = generate_test_cipher(); + + let request = CipherAddEditRequest { + cipher: cipher_view.clone(), + encrypted_for: Some(user_id), + }; + + let result = create_cipher(&store, &api_config, &repository, request) + .await + .unwrap(); + + assert_eq!(result.id, Some(cipher_id)); + assert_eq!(result.name, "Test Login"); + assert_eq!( + result.login, + Some(LoginView { + username: Some("test@example.com".to_string()), + password: Some("password123".to_string()), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }) + ); + + // Confirm the cipher was stored in the repository + let stored_cipher_view: CipherView = store + .decrypt( + &repository + .get(cipher_id.to_string()) + .await + .unwrap() + .unwrap(), + ) + .unwrap(); + + assert_eq!(stored_cipher_view.id, result.id); + assert_eq!(stored_cipher_view.name, result.name); + assert_eq!(stored_cipher_view.r#type, result.r#type); + assert!(stored_cipher_view.login.is_some()); + assert_eq!(stored_cipher_view.favorite, result.favorite); + } + + #[tokio::test] + async fn test_create_cipher_http_error() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let (_server, api_config) = start_api_mock(vec![ + Mock::given(matchers::path("/ciphers")).respond_with(ResponseTemplate::new(500)) + ]) + .await; + + let repository = MemoryRepository::::default(); + let cipher_view = generate_test_cipher(); + + let request = CipherAddEditRequest { + cipher: cipher_view, + encrypted_for: None, + }; + + let result = create_cipher(&store, &api_config, &repository, request).await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CreateCipherError::Api(_))); + } +} diff --git a/crates/bitwarden-vault/src/cipher/edit.rs b/crates/bitwarden-vault/src/cipher/edit.rs new file mode 100644 index 000000000..ec756d280 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/edit.rs @@ -0,0 +1,324 @@ +use bitwarden_api_api::apis::ciphers_api; +use bitwarden_core::{key_management::KeyIds, ApiError, MissingFieldError}; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; + +use crate::{create::CipherAddEditRequest, Cipher, CipherView, ItemNotFoundError, VaultParseError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum EditCipherError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), + #[error(transparent)] + Uuid(#[from] uuid::Error), +} + +pub(super) async fn edit_cipher + ?Sized>( + key_store: &KeyStore, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &R, + cipher_id: &str, + request: CipherAddEditRequest, +) -> Result { + repository + .get(cipher_id.to_owned()) + .await? + .ok_or(ItemNotFoundError)?; + + let cipher_request = key_store.encrypt(request)?; + + let parsed_cipher_id = uuid::Uuid::parse_str(cipher_id)?; + + let response = ciphers_api::ciphers_id_put(api_config, parsed_cipher_id, Some(cipher_request)) + .await + .map_err(ApiError::from)?; + + let cipher: Cipher = response.try_into()?; + + debug_assert!(cipher.id.unwrap_or_default().to_string() == cipher_id); + + repository + .set(cipher_id.to_string(), cipher.clone()) + .await?; + + Ok(key_store.decrypt(&cipher)?) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::{ + apis::configuration::Configuration, + models::{CipherRequestModel, CipherResponseModel}, + }; + use bitwarden_core::{key_management::SymmetricKeyId, UserId}; + use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; + use bitwarden_test::{start_api_mock, MemoryRepository}; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; + + use crate::{Cipher, CipherId, CipherRepromptType, CipherType, Login, LoginView}; + + use super::*; + + const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; + const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; + + fn generate_test_cipher() -> CipherView { + CipherView { + id: None, + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: "Test Login".to_string(), + notes: None, + r#type: CipherType::Login, + login: Some(LoginView { + username: Some("test@example.com".to_string()), + password: Some("password123".to_string()), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }), + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: true, + edit: true, + permissions: None, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deleted_date: None, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + } + + async fn repository_add_cipher( + repository: &MemoryRepository, + store: &KeyStore, + cipher_id: CipherId, + name: &str, + ) { + let mut ctx = store.context(); + + repository + .set( + cipher_id.to_string(), + Cipher { + id: Some(cipher_id), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: name.encrypt(&mut ctx, SymmetricKeyId::User).unwrap(), + notes: None, + r#type: CipherType::Login, + login: Some(Login { + username: Some("test@example.com") + .map(|u| u.encrypt(&mut ctx, SymmetricKeyId::User)) + .transpose() + .unwrap(), + password: Some("password123") + .map(|p| p.encrypt(&mut ctx, SymmetricKeyId::User)) + .transpose() + .unwrap(), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }), + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: true, + edit: true, + permissions: None, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-01T00:00:00Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_edit_cipher() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); + let user_id: UserId = TEST_USER_ID.parse().unwrap(); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/ciphers/{}", + cipher_id + ))) + .respond_with(move |req: &Request| { + let body: CipherRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(200).set_body_json(CipherResponseModel { + object: Some("cipher".to_string()), + id: Some(cipher_id.into()), + name: Some(body.name), + r#type: body.r#type, + organization_id: body + .organization_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + folder_id: body + .folder_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + favorite: body.favorite, + reprompt: body.reprompt, + key: body.key, + notes: body.notes, + view_password: Some(true), + edit: Some(true), + organization_use_totp: Some(true), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + creation_date: Some("2025-01-01T00:00:00Z".to_string()), + deleted_date: None, + login: body.login, + card: body.card, + identity: body.identity, + secure_note: body.secure_note, + ssh_key: body.ssh_key, + fields: body.fields, + password_history: body.password_history, + attachments: None, + permissions: None, + data: None, + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); + repository_add_cipher(&repository, &store, cipher_id, "old_name").await; + let cipher_view = generate_test_cipher(); + + let request = CipherAddEditRequest { + cipher: cipher_view.clone(), + encrypted_for: Some(user_id), + }; + + let result = edit_cipher( + &store, + &api_config, + &repository, + &cipher_id.to_string(), + request, + ) + .await + .unwrap(); + + assert_eq!(result.id, Some(cipher_id)); + assert_eq!(result.name, "Test Login"); + } + + #[tokio::test] + async fn test_edit_cipher_does_not_exist() { + let store: KeyStore = KeyStore::default(); + + let repository = MemoryRepository::::default(); + let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); + + let cipher_view = generate_test_cipher(); + + let request = CipherAddEditRequest { + cipher: cipher_view.clone(), + encrypted_for: None, + }; + + let result = edit_cipher( + &store, + &Configuration::default(), + &repository, + &cipher_id.to_string(), + request, + ) + .await; + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + EditCipherError::ItemNotFound(_) + )); + } + + #[tokio::test] + async fn test_edit_cipher_http_error() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let cipher_id: CipherId = "5faa9684-c793-4a2d-8a12-b33900187097".parse().unwrap(); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/ciphers/{}", + cipher_id + ))) + .respond_with(ResponseTemplate::new(500))]) + .await; + + let repository = MemoryRepository::::default(); + repository_add_cipher(&repository, &store, cipher_id, "old_name").await; + let cipher_view = generate_test_cipher(); + + let request = CipherAddEditRequest { + cipher: cipher_view.clone(), + encrypted_for: None, + }; + + let result = edit_cipher( + &store, + &api_config, + &repository, + &cipher_id.to_string(), + request, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), EditCipherError::Api(_))); + } +} diff --git a/crates/bitwarden-vault/src/cipher/get_list.rs b/crates/bitwarden-vault/src/cipher/get_list.rs new file mode 100644 index 000000000..fb5fb0178 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/get_list.rs @@ -0,0 +1,41 @@ +use bitwarden_core::key_management::KeyIds; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; + +use crate::{Cipher, CipherView, ItemNotFoundError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum GetCipherError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +pub(super) async fn get_cipher( + store: &KeyStore, + repository: &dyn Repository, + id: &str, +) -> Result { + let cipher = repository + .get(id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(store.decrypt(&cipher)?) +} + +pub(super) async fn list_ciphers( + store: &KeyStore, + repository: &dyn Repository, +) -> Result, GetCipherError> { + let ciphers = repository.list().await?; + let views = store.decrypt_list(&ciphers)?; + Ok(views) +} From 0734d40454dd2c9181dbbf635a2b263cad2bf425 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 15 Sep 2025 09:50:14 -0400 Subject: [PATCH 03/60] Exposed through cipher client --- .../src/cipher/cipher_client.rs | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index 335787e9a..7df7cb497 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -1,14 +1,21 @@ +use std::sync::Arc; + use bitwarden_core::{key_management::SymmetricKeyId, Client, OrganizationId}; use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey}; #[cfg(feature = "wasm")] use bitwarden_encoding::B64; +use bitwarden_state::repository::{Repository, RepositoryError}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use super::EncryptionContext; use crate::{ - cipher::cipher::DecryptCipherListResult, Cipher, CipherError, CipherListView, CipherView, - DecryptError, EncryptError, Fido2CredentialFullView, + cipher::cipher::DecryptCipherListResult, + create::{create_cipher, CipherAddEditRequest, CreateCipherError}, + edit::{edit_cipher, EditCipherError}, + get_list::{get_cipher, list_ciphers, GetCipherError}, + Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError, + Fido2CredentialFullView, }; #[allow(missing_docs)] @@ -174,6 +181,68 @@ impl CiphersClient { let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?; Ok(decrypted_key) } + + /// Get all ciphers from state and decrypt them to a list of [CipherView]. + pub async fn list(&self) -> Result, GetCipherError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_ciphers(key_store, repository.as_ref()).await + } + + /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. + pub async fn get(&self, cipher_id: &str) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_cipher(key_store, repository.as_ref(), cipher_id).await + } + + /// Create a new [Cipher] and save it to the server. + pub async fn create( + &self, + mut request: CipherAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + request.encrypted_for = self.client.internal.get_user_id(); + + create_cipher(key_store, &config.api, repository.as_ref(), request).await + } + + /// Edit an existing [Cipher] and save it to the server. + pub async fn edit( + &self, + cipher_id: &str, + mut request: CipherAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + request.encrypted_for = self.client.internal.get_user_id(); + + edit_cipher( + key_store, + &config.api, + repository.as_ref(), + cipher_id, + request, + ) + .await + } +} + +impl CiphersClient { + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self + .client + .platform() + .state() + .get_client_managed::()?) + } } #[cfg(test)] From 0987ba7d79760a749d6365badc95e863e8c9fac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 11 Sep 2025 23:01:00 +0200 Subject: [PATCH 04/60] Fix missing features (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## đŸŽŸī¸ Tracking ## 📔 Objective These crates had some missing features, which caused compilation errors when trying to run the tests from VSCode. The reason that CI kept building was that rust unifies all the workspace features, so as long as one crate has them enabled it's fine. On the other hand, when running a test from VSCode, it will only have that crates features in mind. Note that the uniffi feature of core requires internal because of these two types: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/uniffi_support.rs#L30-L42 ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## đŸĻŽ Reviewer guidelines - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or â„šī¸ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or âš ī¸ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or â™ģī¸ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --- crates/bitwarden-core/Cargo.toml | 2 ++ crates/bitwarden-vault/Cargo.toml | 1 + 2 files changed, 3 insertions(+) diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index a76a1a92e..2b498f26a 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -21,11 +21,13 @@ no-memory-hardening = [ ] # Disable memory hardening features secrets = [] # Secrets manager API uniffi = [ + "internal", "bitwarden-crypto/uniffi", "bitwarden-encoding/uniffi", "dep:uniffi" ] # Uniffi bindings wasm = [ + "bitwarden-encoding/wasm", "bitwarden-error/wasm", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index fd5e40540..e91d63a0e 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -16,6 +16,7 @@ keywords.workspace = true [features] uniffi = [ + "bitwarden-collections/uniffi", "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi" From 0535582e33b0e29d22554fbad922a2c9049b364b Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 12 Sep 2025 13:37:58 +0200 Subject: [PATCH 05/60] Scaffold workflow for generating api bindings (#441) Scaffolds a new workflow for updating API bindings. --- .../workflows/download-server-artifact.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/download-server-artifact.yml diff --git a/.github/workflows/download-server-artifact.yml b/.github/workflows/download-server-artifact.yml new file mode 100644 index 000000000..9052ce017 --- /dev/null +++ b/.github/workflows/download-server-artifact.yml @@ -0,0 +1,35 @@ +name: Generate api bindings + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + download: + name: Download internal.json from bitwarden/server + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Download internal.json artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + repo: bitwarden/server + branch: main + artifacts: (internal|identity).json + path: artifacts/ + name_is_regexp: true + + - name: List downloaded files + run: | + echo "Downloaded files:" + find artifacts/ -type f -name "*.json" | head -10 + if [ -f "artifacts/internal.json" ]; then + echo "internal.json file size: $(stat -c%s artifacts/internal.json) bytes" + fi From d0082604b395f7eb9a7a24fa1a87ecac9a5e4422 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Fri, 12 Sep 2025 13:51:59 -0400 Subject: [PATCH 06/60] [PM-23193] Add ArchivedDate (#422) Updated SDK to use an `archivedDate` in the ciphers. --- .../.openapi-generator/FILES | 2 + crates/bitwarden-api-api/README.md | 6 + .../bitwarden-api-api/src/apis/ciphers_api.rs | 224 +++++++++++++++++- .../apis/organization_billing_v_next_api.rs | 35 +++ crates/bitwarden-api-api/src/models/cipher.rs | 3 + .../cipher_bulk_archive_request_model.rs | 25 ++ .../cipher_bulk_unarchive_request_model.rs | 25 ++ .../models/cipher_details_response_model.rs | 3 + .../cipher_mini_details_response_model.rs | 3 + .../src/models/cipher_mini_response_model.rs | 3 + .../src/models/cipher_request_model.rs | 3 + .../src/models/cipher_response_model.rs | 3 + .../models/cipher_with_id_request_model.rs | 3 + .../src/models/event_type.rs | 4 + crates/bitwarden-api-api/src/models/mod.rs | 4 + ...ization_user_bulk_confirm_request_model.rs | 10 +- crates/bitwarden-exporters/src/lib.rs | 1 + crates/bitwarden-exporters/src/models.rs | 2 + .../bitwarden-vault/src/cipher/attachment.rs | 3 + crates/bitwarden-vault/src/cipher/cipher.rs | 10 + .../src/cipher/cipher_client.rs | 3 + .../bitwarden-vault/src/cipher/secure_note.rs | 1 + crates/bitwarden-vault/src/totp.rs | 1 + 23 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 crates/bitwarden-api-api/src/models/cipher_bulk_archive_request_model.rs create mode 100644 crates/bitwarden-api-api/src/models/cipher_bulk_unarchive_request_model.rs diff --git a/crates/bitwarden-api-api/.openapi-generator/FILES b/crates/bitwarden-api-api/.openapi-generator/FILES index d477e416e..b01ce9d70 100644 --- a/crates/bitwarden-api-api/.openapi-generator/FILES +++ b/crates/bitwarden-api-api/.openapi-generator/FILES @@ -120,10 +120,12 @@ src/models/bulk_deny_admin_auth_request_request_model.rs src/models/change_plan_frequency_request.rs src/models/cipher.rs src/models/cipher_attachment_model.rs +src/models/cipher_bulk_archive_request_model.rs src/models/cipher_bulk_delete_request_model.rs src/models/cipher_bulk_move_request_model.rs src/models/cipher_bulk_restore_request_model.rs src/models/cipher_bulk_share_request_model.rs +src/models/cipher_bulk_unarchive_request_model.rs src/models/cipher_bulk_update_collections_request_model.rs src/models/cipher_card_model.rs src/models/cipher_collections_request_model.rs diff --git a/crates/bitwarden-api-api/README.md b/crates/bitwarden-api-api/README.md index f79a76f09..f08240b7a 100644 --- a/crates/bitwarden-api-api/README.md +++ b/crates/bitwarden-api-api/README.md @@ -106,6 +106,7 @@ All URIs are relative to _http://localhost_ | _AuthRequestsApi_ | [**auth_requests_post**](docs/AuthRequestsApi.md#auth_requests_post) | **POST** /auth-requests | | _CiphersApi_ | [**ciphers_admin_delete**](docs/CiphersApi.md#ciphers_admin_delete) | **DELETE** /ciphers/admin | | _CiphersApi_ | [**ciphers_admin_post**](docs/CiphersApi.md#ciphers_admin_post) | **POST** /ciphers/admin | +| _CiphersApi_ | [**ciphers_archive_put**](docs/CiphersApi.md#ciphers_archive_put) | **PUT** /ciphers/archive | | _CiphersApi_ | [**ciphers_attachment_validate_azure_post**](docs/CiphersApi.md#ciphers_attachment_validate_azure_post) | **POST** /ciphers/attachment/validate/azure | | _CiphersApi_ | [**ciphers_bulk_collections_post**](docs/CiphersApi.md#ciphers_bulk_collections_post) | **POST** /ciphers/bulk-collections | | _CiphersApi_ | [**ciphers_create_post**](docs/CiphersApi.md#ciphers_create_post) | **POST** /ciphers/create | @@ -119,6 +120,7 @@ All URIs are relative to _http://localhost_ | _CiphersApi_ | [**ciphers_id_admin_get**](docs/CiphersApi.md#ciphers_id_admin_get) | **GET** /ciphers/{id}/admin | | _CiphersApi_ | [**ciphers_id_admin_post**](docs/CiphersApi.md#ciphers_id_admin_post) | **POST** /ciphers/{id}/admin | | _CiphersApi_ | [**ciphers_id_admin_put**](docs/CiphersApi.md#ciphers_id_admin_put) | **PUT** /ciphers/{id}/admin | +| _CiphersApi_ | [**ciphers_id_archive_put**](docs/CiphersApi.md#ciphers_id_archive_put) | **PUT** /ciphers/{id}/archive | | _CiphersApi_ | [**ciphers_id_attachment_admin_post**](docs/CiphersApi.md#ciphers_id_attachment_admin_post) | **POST** /ciphers/{id}/attachment-admin | | _CiphersApi_ | [**ciphers_id_attachment_attachment_id_admin_delete**](docs/CiphersApi.md#ciphers_id_attachment_attachment_id_admin_delete) | **DELETE** /ciphers/{id}/attachment/{attachmentId}/admin | | _CiphersApi_ | [**ciphers_id_attachment_attachment_id_admin_get**](docs/CiphersApi.md#ciphers_id_attachment_attachment_id_admin_get) | **GET** /ciphers/{id}/attachment/{attachmentId}/admin | @@ -153,6 +155,7 @@ All URIs are relative to _http://localhost_ | _CiphersApi_ | [**ciphers_id_restore_put**](docs/CiphersApi.md#ciphers_id_restore_put) | **PUT** /ciphers/{id}/restore | | _CiphersApi_ | [**ciphers_id_share_post**](docs/CiphersApi.md#ciphers_id_share_post) | **POST** /ciphers/{id}/share | | _CiphersApi_ | [**ciphers_id_share_put**](docs/CiphersApi.md#ciphers_id_share_put) | **PUT** /ciphers/{id}/share | +| _CiphersApi_ | [**ciphers_id_unarchive_put**](docs/CiphersApi.md#ciphers_id_unarchive_put) | **PUT** /ciphers/{id}/unarchive | | _CiphersApi_ | [**ciphers_move_post**](docs/CiphersApi.md#ciphers_move_post) | **POST** /ciphers/move | | _CiphersApi_ | [**ciphers_move_put**](docs/CiphersApi.md#ciphers_move_put) | **PUT** /ciphers/move | | _CiphersApi_ | [**ciphers_organization_details_assigned_get**](docs/CiphersApi.md#ciphers_organization_details_assigned_get) | **GET** /ciphers/organization-details/assigned | @@ -163,6 +166,7 @@ All URIs are relative to _http://localhost_ | _CiphersApi_ | [**ciphers_restore_put**](docs/CiphersApi.md#ciphers_restore_put) | **PUT** /ciphers/restore | | _CiphersApi_ | [**ciphers_share_post**](docs/CiphersApi.md#ciphers_share_post) | **POST** /ciphers/share | | _CiphersApi_ | [**ciphers_share_put**](docs/CiphersApi.md#ciphers_share_put) | **PUT** /ciphers/share | +| _CiphersApi_ | [**ciphers_unarchive_put**](docs/CiphersApi.md#ciphers_unarchive_put) | **PUT** /ciphers/unarchive | | _CollectionsApi_ | [**collections_get**](docs/CollectionsApi.md#collections_get) | **GET** /collections | | _CollectionsApi_ | [**organizations_org_id_collections_bulk_access_post**](docs/CollectionsApi.md#organizations_org_id_collections_bulk_access_post) | **POST** /organizations/{orgId}/collections/bulk-access | | _CollectionsApi_ | [**organizations_org_id_collections_delete**](docs/CollectionsApi.md#organizations_org_id_collections_delete) | **DELETE** /organizations/{orgId}/collections | @@ -625,10 +629,12 @@ All URIs are relative to _http://localhost_ - [ChangePlanFrequencyRequest](docs/ChangePlanFrequencyRequest.md) - [Cipher](docs/Cipher.md) - [CipherAttachmentModel](docs/CipherAttachmentModel.md) +- [CipherBulkArchiveRequestModel](docs/CipherBulkArchiveRequestModel.md) - [CipherBulkDeleteRequestModel](docs/CipherBulkDeleteRequestModel.md) - [CipherBulkMoveRequestModel](docs/CipherBulkMoveRequestModel.md) - [CipherBulkRestoreRequestModel](docs/CipherBulkRestoreRequestModel.md) - [CipherBulkShareRequestModel](docs/CipherBulkShareRequestModel.md) +- [CipherBulkUnarchiveRequestModel](docs/CipherBulkUnarchiveRequestModel.md) - [CipherBulkUpdateCollectionsRequestModel](docs/CipherBulkUpdateCollectionsRequestModel.md) - [CipherCardModel](docs/CipherCardModel.md) - [CipherCollectionsRequestModel](docs/CipherCollectionsRequestModel.md) diff --git a/crates/bitwarden-api-api/src/apis/ciphers_api.rs b/crates/bitwarden-api-api/src/apis/ciphers_api.rs index c5f0c2ce2..e1adfeea9 100644 --- a/crates/bitwarden-api-api/src/apis/ciphers_api.rs +++ b/crates/bitwarden-api-api/src/apis/ciphers_api.rs @@ -28,6 +28,13 @@ pub enum CiphersAdminPostError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`ciphers_archive_put`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CiphersArchivePutError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`ciphers_attachment_validate_azure_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -119,6 +126,13 @@ pub enum CiphersIdAdminPutError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`ciphers_id_archive_put`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CiphersIdArchivePutError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`ciphers_id_attachment_admin_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -357,6 +371,13 @@ pub enum CiphersIdSharePutError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`ciphers_id_unarchive_put`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CiphersIdUnarchivePutError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`ciphers_move_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -427,6 +448,13 @@ pub enum CiphersSharePutError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`ciphers_unarchive_put`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CiphersUnarchivePutError { + UnknownValue(serde_json::Value), +} + pub async fn ciphers_admin_delete( configuration: &configuration::Configuration, cipher_bulk_delete_request_model: Option, @@ -514,6 +542,53 @@ pub async fn ciphers_admin_post( } } +pub async fn ciphers_archive_put( + configuration: &configuration::Configuration, + cipher_bulk_archive_request_model: Option, +) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_cipher_bulk_archive_request_model = cipher_bulk_archive_request_model; + + let uri_str = format!("{}/ciphers/archive", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.oauth_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_cipher_bulk_archive_request_model); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CipherMiniResponseModelListResponseModel`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CipherMiniResponseModelListResponseModel`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} + pub async fn ciphers_attachment_validate_azure_post( configuration: &configuration::Configuration, ) -> Result<(), Error> { @@ -1068,6 +1143,56 @@ pub async fn ciphers_id_admin_put( } } +pub async fn ciphers_id_archive_put( + configuration: &configuration::Configuration, + id: uuid::Uuid, +) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_id = id; + + let uri_str = format!( + "{}/ciphers/{id}/archive", + configuration.base_path, + id = crate::apis::urlencode(p_id.to_string()) + ); + let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.oauth_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CipherMiniResponseModel`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CipherMiniResponseModel`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} + pub async fn ciphers_id_attachment_admin_post( configuration: &configuration::Configuration, id: &str, @@ -2821,6 +2946,56 @@ pub async fn ciphers_id_share_put( } } +pub async fn ciphers_id_unarchive_put( + configuration: &configuration::Configuration, + id: uuid::Uuid, +) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_id = id; + + let uri_str = format!( + "{}/ciphers/{id}/unarchive", + configuration.base_path, + id = crate::apis::urlencode(p_id.to_string()) + ); + let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.oauth_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CipherMiniResponseModel`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CipherMiniResponseModel`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} + pub async fn ciphers_move_post( configuration: &configuration::Configuration, cipher_bulk_move_request_model: Option, @@ -3055,7 +3230,7 @@ pub async fn ciphers_post( pub async fn ciphers_purge_post( configuration: &configuration::Configuration, - organization_id: Option<&str>, + organization_id: Option, secret_verification_request_model: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions @@ -3285,3 +3460,50 @@ pub async fn ciphers_share_put( })) } } + +pub async fn ciphers_unarchive_put( + configuration: &configuration::Configuration, + cipher_bulk_unarchive_request_model: Option, +) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_cipher_bulk_unarchive_request_model = cipher_bulk_unarchive_request_model; + + let uri_str = format!("{}/ciphers/unarchive", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.oauth_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_cipher_bulk_unarchive_request_model); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CipherMiniResponseModelListResponseModel`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CipherMiniResponseModelListResponseModel`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} diff --git a/crates/bitwarden-api-api/src/apis/organization_billing_v_next_api.rs b/crates/bitwarden-api-api/src/apis/organization_billing_v_next_api.rs index 4d8bc6dcf..99573dcba 100644 --- a/crates/bitwarden-api-api/src/apis/organization_billing_v_next_api.rs +++ b/crates/bitwarden-api-api/src/apis/organization_billing_v_next_api.rs @@ -128,6 +128,7 @@ pub async fn organizations_organization_id_billing_vnext_address_get( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions let p_organization_id = organization_id; @@ -189,6 +190,7 @@ pub async fn organizations_organization_id_billing_vnext_address_get( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let uri_str = format!( "{}/organizations/{organizationId}/billing/vnext/address", @@ -376,6 +378,9 @@ pub async fn organizations_organization_id_billing_vnext_address_get( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -463,6 +468,7 @@ pub async fn organizations_organization_id_billing_vnext_address_put( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, billing_address_request: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions @@ -525,6 +531,7 @@ pub async fn organizations_organization_id_billing_vnext_address_put( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let p_billing_address_request = billing_address_request; let uri_str = format!( @@ -713,6 +720,9 @@ pub async fn organizations_organization_id_billing_vnext_address_put( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -801,6 +811,7 @@ pub async fn organizations_organization_id_billing_vnext_credit_bitpay_post( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, bit_pay_credit_request: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions @@ -863,6 +874,7 @@ pub async fn organizations_organization_id_billing_vnext_credit_bitpay_post( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let p_bit_pay_credit_request = bit_pay_credit_request; let uri_str = format!( @@ -1053,6 +1065,9 @@ pub async fn organizations_organization_id_billing_vnext_credit_bitpay_post( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -1141,6 +1156,7 @@ pub async fn organizations_organization_id_billing_vnext_credit_get( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions let p_organization_id = organization_id; @@ -1202,6 +1218,7 @@ pub async fn organizations_organization_id_billing_vnext_credit_get( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let uri_str = format!( "{}/organizations/{organizationId}/billing/vnext/credit", @@ -1389,6 +1406,9 @@ pub async fn organizations_organization_id_billing_vnext_credit_get( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -1476,6 +1496,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_get( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions let p_organization_id = organization_id; @@ -1537,6 +1558,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_get( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let uri_str = format!( "{}/organizations/{organizationId}/billing/vnext/payment-method", @@ -1724,6 +1746,9 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_get( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -1811,6 +1836,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_put( use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, tokenized_payment_method_request: Option, ) -> Result<(), Error> { // add a prefix to parameters to efficiently prevent name collisions @@ -1873,6 +1899,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_put( let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let p_tokenized_payment_method_request = tokenized_payment_method_request; let uri_str = format!( @@ -2061,6 +2088,9 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_put( if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } @@ -2149,6 +2179,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_verify_b use_risk_insights: Option, use_organization_domains: Option, use_admin_sponsored_families: Option, + sync_seats: Option, verify_bank_account_request: Option, ) -> Result<(), Error> { @@ -2212,6 +2243,7 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_verify_b let p_use_risk_insights = use_risk_insights; let p_use_organization_domains = use_organization_domains; let p_use_admin_sponsored_families = use_admin_sponsored_families; + let p_sync_seats = sync_seats; let p_verify_bank_account_request = verify_bank_account_request; let uri_str = format!( @@ -2402,6 +2434,9 @@ pub async fn organizations_organization_id_billing_vnext_payment_method_verify_b if let Some(ref param_value) = p_use_admin_sponsored_families { req_builder = req_builder.query(&[("useAdminSponsoredFamilies", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_sync_seats { + req_builder = req_builder.query(&[("syncSeats", ¶m_value.to_string())]); + } if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } diff --git a/crates/bitwarden-api-api/src/models/cipher.rs b/crates/bitwarden-api-api/src/models/cipher.rs index 8bdcdb0bd..cea411cae 100644 --- a/crates/bitwarden-api-api/src/models/cipher.rs +++ b/crates/bitwarden-api-api/src/models/cipher.rs @@ -40,6 +40,8 @@ pub struct Cipher { pub reprompt: Option, #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, } impl Cipher { @@ -58,6 +60,7 @@ impl Cipher { deleted_date: None, reprompt: None, key: None, + archived_date: None, } } } diff --git a/crates/bitwarden-api-api/src/models/cipher_bulk_archive_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_bulk_archive_request_model.rs new file mode 100644 index 000000000..b32db1950 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/cipher_bulk_archive_request_model.rs @@ -0,0 +1,25 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CipherBulkArchiveRequestModel { + #[serde(rename = "ids")] + pub ids: Vec, +} + +impl CipherBulkArchiveRequestModel { + pub fn new(ids: Vec) -> CipherBulkArchiveRequestModel { + CipherBulkArchiveRequestModel { ids } + } +} diff --git a/crates/bitwarden-api-api/src/models/cipher_bulk_unarchive_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_bulk_unarchive_request_model.rs new file mode 100644 index 000000000..0bf375689 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/cipher_bulk_unarchive_request_model.rs @@ -0,0 +1,25 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CipherBulkUnarchiveRequestModel { + #[serde(rename = "ids")] + pub ids: Vec, +} + +impl CipherBulkUnarchiveRequestModel { + pub fn new(ids: Vec) -> CipherBulkUnarchiveRequestModel { + CipherBulkUnarchiveRequestModel { ids } + } +} diff --git a/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs index e0b1958d9..e71a0529d 100644 --- a/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs @@ -59,6 +59,8 @@ pub struct CipherDetailsResponseModel { pub reprompt: Option, #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, #[serde(rename = "folderId", skip_serializing_if = "Option::is_none")] pub folder_id: Option, #[serde(rename = "favorite", skip_serializing_if = "Option::is_none")] @@ -97,6 +99,7 @@ impl CipherDetailsResponseModel { deleted_date: None, reprompt: None, key: None, + archived_date: None, folder_id: None, favorite: None, edit: None, diff --git a/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs index 37d33da67..86a1d62d9 100644 --- a/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs @@ -59,6 +59,8 @@ pub struct CipherMiniDetailsResponseModel { pub reprompt: Option, #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, #[serde(rename = "collectionIds", skip_serializing_if = "Option::is_none")] pub collection_ids: Option>, } @@ -87,6 +89,7 @@ impl CipherMiniDetailsResponseModel { deleted_date: None, reprompt: None, key: None, + archived_date: None, collection_ids: None, } } diff --git a/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs index f4596cf22..9e660a791 100644 --- a/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs @@ -59,6 +59,8 @@ pub struct CipherMiniResponseModel { pub reprompt: Option, #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, } impl CipherMiniResponseModel { @@ -85,6 +87,7 @@ impl CipherMiniResponseModel { deleted_date: None, reprompt: None, key: None, + archived_date: None, } } } diff --git a/crates/bitwarden-api-api/src/models/cipher_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_request_model.rs index 0da128035..d639a320b 100644 --- a/crates/bitwarden-api-api/src/models/cipher_request_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_request_model.rs @@ -56,6 +56,8 @@ pub struct CipherRequestModel { skip_serializing_if = "Option::is_none" )] pub last_known_revision_date: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, } impl CipherRequestModel { @@ -80,6 +82,7 @@ impl CipherRequestModel { secure_note: None, ssh_key: None, last_known_revision_date: None, + archived_date: None, } } } diff --git a/crates/bitwarden-api-api/src/models/cipher_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_response_model.rs index 4470969c8..8a529831b 100644 --- a/crates/bitwarden-api-api/src/models/cipher_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_response_model.rs @@ -59,6 +59,8 @@ pub struct CipherResponseModel { pub reprompt: Option, #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, #[serde(rename = "folderId", skip_serializing_if = "Option::is_none")] pub folder_id: Option, #[serde(rename = "favorite", skip_serializing_if = "Option::is_none")] @@ -95,6 +97,7 @@ impl CipherResponseModel { deleted_date: None, reprompt: None, key: None, + archived_date: None, folder_id: None, favorite: None, edit: None, diff --git a/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs index a7f207c55..86f19c6a0 100644 --- a/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs @@ -56,6 +56,8 @@ pub struct CipherWithIdRequestModel { skip_serializing_if = "Option::is_none" )] pub last_known_revision_date: Option, + #[serde(rename = "archivedDate", skip_serializing_if = "Option::is_none")] + pub archived_date: Option, #[serde(rename = "id")] pub id: uuid::Uuid, } @@ -82,6 +84,7 @@ impl CipherWithIdRequestModel { secure_note: None, ssh_key: None, last_known_revision_date: None, + archived_date: None, id, } } diff --git a/crates/bitwarden-api-api/src/models/event_type.rs b/crates/bitwarden-api-api/src/models/event_type.rs index 29cce16a4..40cb4efc4 100644 --- a/crates/bitwarden-api-api/src/models/event_type.rs +++ b/crates/bitwarden-api-api/src/models/event_type.rs @@ -48,6 +48,8 @@ pub enum EventType { Cipher_SoftDeleted = 1115, Cipher_Restored = 1116, Cipher_ClientToggledCardNumberVisible = 1117, + Cipher_Archived = 1118, + Cipher_Unarchived = 1119, Collection_Created = 1300, Collection_Updated = 1301, Collection_Deleted = 1302, @@ -136,6 +138,8 @@ impl std::fmt::Display for EventType { Self::Cipher_SoftDeleted => "1115", Self::Cipher_Restored => "1116", Self::Cipher_ClientToggledCardNumberVisible => "1117", + Self::Cipher_Archived => "1118", + Self::Cipher_Unarchived => "1119", Self::Collection_Created => "1300", Self::Collection_Updated => "1301", Self::Collection_Deleted => "1302", diff --git a/crates/bitwarden-api-api/src/models/mod.rs b/crates/bitwarden-api-api/src/models/mod.rs index 0529112f4..78a50cff6 100644 --- a/crates/bitwarden-api-api/src/models/mod.rs +++ b/crates/bitwarden-api-api/src/models/mod.rs @@ -102,6 +102,8 @@ pub mod cipher; pub use self::cipher::Cipher; pub mod cipher_attachment_model; pub use self::cipher_attachment_model::CipherAttachmentModel; +pub mod cipher_bulk_archive_request_model; +pub use self::cipher_bulk_archive_request_model::CipherBulkArchiveRequestModel; pub mod cipher_bulk_delete_request_model; pub use self::cipher_bulk_delete_request_model::CipherBulkDeleteRequestModel; pub mod cipher_bulk_move_request_model; @@ -110,6 +112,8 @@ pub mod cipher_bulk_restore_request_model; pub use self::cipher_bulk_restore_request_model::CipherBulkRestoreRequestModel; pub mod cipher_bulk_share_request_model; pub use self::cipher_bulk_share_request_model::CipherBulkShareRequestModel; +pub mod cipher_bulk_unarchive_request_model; +pub use self::cipher_bulk_unarchive_request_model::CipherBulkUnarchiveRequestModel; pub mod cipher_bulk_update_collections_request_model; pub use self::cipher_bulk_update_collections_request_model::CipherBulkUpdateCollectionsRequestModel; pub mod cipher_card_model; diff --git a/crates/bitwarden-api-api/src/models/organization_user_bulk_confirm_request_model.rs b/crates/bitwarden-api-api/src/models/organization_user_bulk_confirm_request_model.rs index 7b0383cb8..c39a877f7 100644 --- a/crates/bitwarden-api-api/src/models/organization_user_bulk_confirm_request_model.rs +++ b/crates/bitwarden-api-api/src/models/organization_user_bulk_confirm_request_model.rs @@ -16,12 +16,20 @@ use crate::models; pub struct OrganizationUserBulkConfirmRequestModel { #[serde(rename = "keys")] pub keys: Vec, + #[serde( + rename = "defaultUserCollectionName", + skip_serializing_if = "Option::is_none" + )] + pub default_user_collection_name: Option, } impl OrganizationUserBulkConfirmRequestModel { pub fn new( keys: Vec, ) -> OrganizationUserBulkConfirmRequestModel { - OrganizationUserBulkConfirmRequestModel { keys } + OrganizationUserBulkConfirmRequestModel { + keys, + default_user_collection_name: None, + } } } diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index b92f86b2f..5c30c9644 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -255,6 +255,7 @@ impl From for CipherView { creation_date: value.creation_date, deleted_date: None, revision_date: value.revision_date, + archived_date: None, } } } diff --git a/crates/bitwarden-exporters/src/models.rs b/crates/bitwarden-exporters/src/models.rs index 89d85efdc..d66f12d1a 100644 --- a/crates/bitwarden-exporters/src/models.rs +++ b/crates/bitwarden-exporters/src/models.rs @@ -280,6 +280,7 @@ mod tests { creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + archived_date: None, }; let login = from_login(&view, &key_store).unwrap(); @@ -331,6 +332,7 @@ mod tests { creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + archived_date: None, }; let encrypted = key_store.encrypt(cipher_view).unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index af63efc5e..1536dc7d1 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -286,6 +286,7 @@ mod tests { creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(), deleted_date: None, revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(), + archived_date: None, }, attachment, contents: contents.as_slice(), @@ -340,6 +341,7 @@ mod tests { creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(), deleted_date: None, revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(), + archived_date: None, }; let enc_file = STANDARD.decode(b"Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap(); @@ -398,6 +400,7 @@ mod tests { creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(), deleted_date: None, revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(), + archived_date: None, }; let enc_file = STANDARD.decode(b"AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 248fb6632..36ecec937 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -139,6 +139,7 @@ pub struct Cipher { pub creation_date: DateTime, pub deleted_date: Option>, pub revision_date: DateTime, + pub archived_date: Option>, } bitwarden_state::register_repository_item!(Cipher, "Cipher"); @@ -182,6 +183,7 @@ pub struct CipherView { pub creation_date: DateTime, pub deleted_date: Option>, pub revision_date: DateTime, + pub archived_date: Option>, } #[allow(missing_docs)] @@ -250,6 +252,7 @@ pub struct CipherListView { pub creation_date: DateTime, pub deleted_date: Option>, pub revision_date: DateTime, + pub archived_date: Option>, /// Hints for the presentation layer for which fields can be copied. pub copyable_fields: Vec, @@ -336,6 +339,7 @@ impl CompositeEncryptable for CipherView { deleted_date: cipher_view.deleted_date, revision_date: cipher_view.revision_date, permissions: cipher_view.permissions, + archived_date: cipher_view.archived_date, }) } } @@ -379,6 +383,7 @@ impl Decryptable for Cipher { creation_date: self.creation_date, deleted_date: self.deleted_date, revision_date: self.revision_date, + archived_date: self.archived_date, }; // For compatibility we only remove URLs with invalid checksums if the cipher has a key @@ -684,6 +689,7 @@ impl Decryptable for Cipher { revision_date: self.revision_date, copyable_fields: self.get_copyable_fields(), local_data: self.local_data.decrypt(ctx, ciphers_key)?, + archived_date: self.archived_date, }) } } @@ -772,6 +778,7 @@ impl TryFrom for Cipher { deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, revision_date: require!(cipher.revision_date).parse()?, key: EncString::try_from_optional(cipher.key)?, + archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?, }) } } @@ -916,6 +923,7 @@ mod tests { creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + archived_date: None, } } @@ -980,6 +988,7 @@ mod tests { creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + archived_date: None, }; let view: CipherListView = key_store.decrypt(&cipher).unwrap(); @@ -1025,6 +1034,7 @@ mod tests { CopyableCipherFields::LoginTotp ], local_data: None, + archived_date: cipher.archived_date, } ) } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index 7df7cb497..ff6e59847 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -290,6 +290,7 @@ mod tests { creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(), deleted_date: None, revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(), + archived_date: None, } } @@ -330,6 +331,7 @@ mod tests { creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + archived_date: None, } } @@ -392,6 +394,7 @@ mod tests { creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(), deleted_date: None, revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(), + archived_date: None, }]) .unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 4ee93d534..cc14bafcf 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -155,6 +155,7 @@ mod tests { creation_date: "2024-01-01T00:00:00.000Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-01T00:00:00.000Z".parse().unwrap(), + archived_date: None, } } diff --git a/crates/bitwarden-vault/src/totp.rs b/crates/bitwarden-vault/src/totp.rs index 196182102..446379631 100644 --- a/crates/bitwarden-vault/src/totp.rs +++ b/crates/bitwarden-vault/src/totp.rs @@ -753,6 +753,7 @@ mod tests { revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), copyable_fields: vec![CopyableCipherFields::LoginTotp], local_data: None, + archived_date: None, }; let key = SymmetricCryptoKey::try_from("w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string()).unwrap(); From 47ea335365ee26e6a6a3bf84450ead1ee54c4a8d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:32:49 +0200 Subject: [PATCH 07/60] [deps]: Update @openapitools/openapi-generator-cli to v2.23.1 (#387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Confidence | |---|---|---|---| | [@openapitools/openapi-generator-cli](https://redirect.github.com/OpenAPITools/openapi-generator-cli) | [`2.20.2` -> `2.23.1`](https://renovatebot.com/diffs/npm/@openapitools%2fopenapi-generator-cli/2.20.2/2.23.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@openapitools%2fopenapi-generator-cli/2.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@openapitools%2fopenapi-generator-cli/2.20.2/2.23.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
OpenAPITools/openapi-generator-cli (@​openapitools/openapi-generator-cli) ### [`v2.23.1`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.23.1) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.23.0...v2.23.1) ##### Bug Fixes - **deps:** update dependency concurrently to v9.2.1 ([#​978](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/978)) ([4847fe0](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/4847fe0117995db49017c5943e18a6771a88929a)) ### [`v2.23.0`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.23.0) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.22.0...v2.23.0) ##### Features - **release:** v7.15.0 release ([#​973](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/973)) ([24443a5](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/24443a5ff0b1d201b1ecef2299d82b96ffefd10c)) ### [`v2.22.0`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.22.0) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.5...v2.22.0) ##### Features - **release:** trigger a release ([#​963](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/963)) ([7ce2ed9](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/7ce2ed95eb0bc3fb03bbe7c6f7bdcecd0091794b)) ### [`v2.21.5`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.5) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.4...v2.21.5) ##### Bug Fixes - **deps:** update dependency fs-extra to v11.3.1 ([#​962](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/962)) ([e0ce66f](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/e0ce66f36f3dc54539425ff58ddb0d8fd730dc98)) ### [`v2.21.4`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.4) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.3...v2.21.4) ##### Bug Fixes - **deps:** update dependency axios to v1.11.0 \[security] ([#​956](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/956)) ([e517c31](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/e517c31ce8697225329e630c03697c147952b660)) ### [`v2.21.3`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.3) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.2...v2.21.3) ##### Bug Fixes - **deps:** update nest monorepo to v11.1.5 ([#​950](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/950)) ([28c5f0d](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/28c5f0d8b937bee531a16efb4d6c51017d0ac16c)) ### [`v2.21.2`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.2) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.1...v2.21.2) ##### Bug Fixes - **deps:** update dependency [@​nestjs/axios](https://redirect.github.com/nestjs/axios) to v4.0.1 ([#​947](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/947)) ([9f16faf](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/9f16fafa0f757650c423567b2057683b284a88ec)) ### [`v2.21.1`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.1) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.21.0...v2.21.1) ##### Bug Fixes - **deps:** update dependency concurrently to v9 ([#​848](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/848)) ([5a52eaf](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/5a52eaf77db3403b249a1cf30c0eb1bbc1f10671)) ### [`v2.21.0`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.21.0) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.20.6...v2.21.0) ##### Features - **release:** v7.14.0 release ([#​942](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/942)) ([cd3c9a4](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/cd3c9a4b86de43000ff0f915eb5abb8fc5e86915)) ### [`v2.20.6`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.20.6) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.20.5...v2.20.6) ##### Bug Fixes - **deps:** update dependency axios to v1.10.0 ([#​939](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/939)) ([0f623cc](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/0f623ccd9de6751b9ae8a7fcdd0bd491807baeac)) ### [`v2.20.5`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.20.5) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.20.4...v2.20.5) ##### Bug Fixes - **deps:** update nest monorepo to v11.1.3 ([#​935](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/935)) ([2575f55](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/2575f55752012d8442095e76fbea0cfe35a258e8)) ### [`v2.20.4`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.20.4) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.20.3...v2.20.4) ##### Bug Fixes - **deps:** update dependency glob to v11 ([#​904](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/904)) ([7b8ac55](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/7b8ac55ace0f2d803f80ee125ff301735a0dcb7f)) ### [`v2.20.3`](https://redirect.github.com/OpenAPITools/openapi-generator-cli/releases/tag/v2.20.3) [Compare Source](https://redirect.github.com/OpenAPITools/openapi-generator-cli/compare/v2.20.2...v2.20.3) ##### Bug Fixes - **deps:** update dependency glob to v10 ([#​933](https://redirect.github.com/OpenAPITools/openapi-generator-cli/issues/933)) ([a6b3d6c](https://redirect.github.com/OpenAPITools/openapi-generator-cli/commit/a6b3d6c93be95eb3fedfaa5789b5556a64fb2c5a))
--- ### Configuration 📅 **Schedule**: Branch creation - "every 2nd week starting on the 2 week of the year before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). đŸšĻ **Automerge**: Disabled by config. Please merge this manually once you are satisfied. â™ģ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/bitwarden/sdk-internal). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- package-lock.json | 706 +++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 474 insertions(+), 234 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07ed7f038..e3b067d98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,169 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@openapitools/openapi-generator-cli": "2.20.2", + "@openapitools/openapi-generator-cli": "2.23.1", "husky": "9.1.7", "lint-staged": "16.1.5", "prettier": "3.5.3" } }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@lukeed/csprng": { @@ -36,9 +185,9 @@ } }, "node_modules/@nestjs/axios": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.0.tgz", - "integrity": "sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", "dev": true, "license": "MIT", "peerDependencies": { @@ -48,13 +197,13 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz", - "integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "dev": true, "license": "MIT", "dependencies": { - "file-type": "20.5.0", + "file-type": "21.0.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -80,9 +229,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz", - "integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", + "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -165,27 +314,26 @@ "license": "MIT" }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.20.2", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.20.2.tgz", - "integrity": "sha512-dNFwQcQu6+rmEWSJj4KUx468+p6Co7nfpVgi5QEfVhzKj7wBytz9GEhCN2qmVgtg3ZX8H6nxbXI8cjh7hAxAqg==", + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.23.1.tgz", + "integrity": "sha512-Kd5EZqzbcIXf6KRlpUrheHMzQNRHsJWzAGrm4ncWCNhnQl+Mh6TsFcqq+hIetgiFCknWBH6cZ2f37SxPxaon4w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@nestjs/axios": "4.0.0", - "@nestjs/common": "11.1.1", - "@nestjs/core": "11.1.1", + "@nestjs/axios": "4.0.1", + "@nestjs/common": "11.1.6", + "@nestjs/core": "11.1.6", "@nuxtjs/opencollective": "0.3.2", - "axios": "1.9.0", + "axios": "1.11.0", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", - "concurrently": "6.5.1", + "concurrently": "9.2.1", "console.table": "0.10.0", - "fs-extra": "11.3.0", - "glob": "9.3.5", - "inquirer": "8.2.6", - "lodash": "4.17.21", + "fs-extra": "11.3.1", + "glob": "11.0.3", + "inquirer": "8.2.7", "proxy-agent": "6.5.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", @@ -308,24 +456,17 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -369,16 +510,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -449,9 +580,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true, "license": "MIT" }, @@ -563,15 +694,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/cliui/node_modules/wrap-ansi": { @@ -660,39 +794,28 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", - "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "date-fns": "^2.16.1", - "lodash": "^4.17.21", - "rxjs": "^6.6.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^16.2.0" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { - "concurrently": "bin/concurrently.js" + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/concurrently/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" + "node": ">=18" }, - "engines": { - "npm": ">=2.0.0" + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, "node_modules/concurrently/node_modules/supports-color": { @@ -711,13 +834,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/concurrently/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -741,31 +857,29 @@ "node": "> 0.10" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">= 14" + "node": ">= 8" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "node": ">= 14" } }, "node_modules/debug": { @@ -839,6 +953,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/easy-table": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", @@ -1001,21 +1122,6 @@ "dev": true, "license": "MIT" }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1047,19 +1153,19 @@ } }, "node_modules/file-type": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", - "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "dev": true, "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -1099,10 +1205,40 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -1117,9 +1253,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1131,13 +1267,6 @@ "node": ">=14.14" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1226,19 +1355,24 @@ } }, "node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1361,13 +1495,13 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -1402,17 +1536,17 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "dev": true, "license": "MIT", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -1485,6 +1619,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -1495,6 +1636,22 @@ "node": ">=6" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -1943,11 +2100,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -2020,29 +2180,29 @@ } }, "node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "license": "ISC", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -2143,16 +2303,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -2187,33 +2337,40 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -2404,6 +2561,42 @@ "dev": true, "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2506,12 +2699,6 @@ "node": ">=0.10.0" } }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -2554,6 +2741,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2567,10 +2770,24 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strtok3": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", - "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -2604,19 +2821,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2631,12 +2835,13 @@ } }, "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", "dev": true, "license": "MIT", "dependencies": { + "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, @@ -2699,9 +2904,9 @@ } }, "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", "dev": true, "license": "MIT", "engines": { @@ -2756,6 +2961,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -2771,6 +2992,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -2795,32 +3035,32 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } } } diff --git a/package.json b/package.json index 072764431..f51730cbd 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "prepare": "husky" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "2.20.2", + "@openapitools/openapi-generator-cli": "2.23.1", "husky": "9.1.7", "lint-staged": "16.1.5", "prettier": "3.5.3" From d204e424c14b692c152454307d1284cce4d098e4 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 15 Sep 2025 15:03:54 +0200 Subject: [PATCH 08/60] Add dylint lints for errors (#430) This adds two custom dylint lint. - `error_suffix`: Requires all types that implements `Error` ends with an `Error` suffix. - `error_enum`: Forbids ending enum error variants with `Error`. --- Cargo.toml | 4 + support/lints/.gitignore | 1 + support/lints/Cargo.lock | 1746 +++++++++++++++++ support/lints/Cargo.toml | 27 + support/lints/README.md | 9 + support/lints/error_enum/.cargo/config.toml | 6 + support/lints/error_enum/.gitignore | 1 + support/lints/error_enum/Cargo.toml | 23 + support/lints/error_enum/src/lib.rs | 64 + support/lints/error_enum/ui/main.rs | 25 + support/lints/error_enum/ui/main.stderr | 46 + support/lints/error_suffix/.cargo/config.toml | 6 + support/lints/error_suffix/Cargo.toml | 28 + support/lints/error_suffix/src/lib.rs | 60 + support/lints/error_suffix/ui/main.rs | 31 + support/lints/error_suffix/ui/main.stderr | 16 + support/lints/rust-toolchain.toml | 3 + support/lints/src/lib.rs | 14 + 18 files changed, 2110 insertions(+) create mode 100644 support/lints/.gitignore create mode 100644 support/lints/Cargo.lock create mode 100644 support/lints/Cargo.toml create mode 100644 support/lints/README.md create mode 100644 support/lints/error_enum/.cargo/config.toml create mode 100644 support/lints/error_enum/.gitignore create mode 100644 support/lints/error_enum/Cargo.toml create mode 100644 support/lints/error_enum/src/lib.rs create mode 100644 support/lints/error_enum/ui/main.rs create mode 100644 support/lints/error_enum/ui/main.stderr create mode 100644 support/lints/error_suffix/.cargo/config.toml create mode 100644 support/lints/error_suffix/Cargo.toml create mode 100644 support/lints/error_suffix/src/lib.rs create mode 100644 support/lints/error_suffix/ui/main.rs create mode 100644 support/lints/error_suffix/ui/main.stderr create mode 100644 support/lints/rust-toolchain.toml create mode 100644 support/lints/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 827edd519..03b5e4646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = ["bitwarden_license/*", "crates/*"] +exclude = ["support/lints"] # Global settings for all crates should be defined here [workspace.package] @@ -97,6 +98,9 @@ string_slice = "warn" [workspace.lints.rust] missing_docs = "warn" +[workspace.metadata.dylint] +libraries = [{ path = "support/lints" }] + # Compile all dependencies with some optimizations when building this crate on debug # This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster # As clean builds won't occur very often, this won't slow down the development process diff --git a/support/lints/.gitignore b/support/lints/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/support/lints/.gitignore @@ -0,0 +1 @@ +/target diff --git a/support/lints/Cargo.lock b/support/lints/Cargo.lock new file mode 100644 index 000000000..7de05bb27 --- /dev/null +++ b/support/lints/Cargo.lock @@ -0,0 +1,1746 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bitwarden_error_enum" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting", + "dylint_testing", +] + +[[package]] +name = "bitwarden_error_suffix" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting", + "dylint_testing", + "thiserror 1.0.69", +] + +[[package]] +name = "bitwarden_lints" +version = "0.1.0" +dependencies = [ + "bitwarden_error_enum", + "bitwarden_error_suffix", + "dylint_linting", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "camino" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "cc" +version = "1.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clippy_utils" +version = "0.1.91" +source = "git+https://github.com/rust-lang/rust-clippy?rev=334fb906aef13d20050987b13448f37391bb97a2#334fb906aef13d20050987b13448f37391bb97a2" +dependencies = [ + "arrayvec", + "itertools", + "rustc_apfloat", + "serde", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compiletest_rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f150fe9105fcd2a57cad53f0c079a24de65195903ef670990f5909f695eac04c" +dependencies = [ + "diff", + "filetime", + "getopts", + "lazy_static", + "libc", + "log", + "miow", + "regex", + "rustfix", + "serde", + "serde_derive", + "serde_json", + "tester", + "windows-sys 0.59.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dylint" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288a5390c058d39da1ca1d952b712798904a1a48b10ebd0fa53c86ed90783965" +dependencies = [ + "ansi_term", + "anyhow", + "cargo_metadata", + "dylint_internal", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", + "walkdir", +] + +[[package]] +name = "dylint_internal" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258d36f53de789719a4c97ecde1df80c16abbcd029c5d25f19c86164caa54cff" +dependencies = [ + "ansi_term", + "anyhow", + "bitflags", + "cargo_metadata", + "git2", + "home", + "if_chain", + "log", + "regex", + "rust-embed", + "rustversion", + "serde", + "thiserror 2.0.16", + "toml", +] + +[[package]] +name = "dylint_linting" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3293a3baa87904faeea12506ec152b56655e40fc49709d905a9be6d277b56c8e" +dependencies = [ + "cargo_metadata", + "dylint_internal", + "paste", + "rustversion", + "serde", + "thiserror 2.0.16", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c6119c632fc3bdb0d38c2ad41b6d417e5dfc535339850237d461a8725e1651" +dependencies = [ + "anyhow", + "cargo_metadata", + "compiletest_rs", + "dylint", + "dylint_internal", + "env_logger", + "once_cell", + "regex", + "serde_json", + "tempfile", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "git2" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miow" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc_apfloat" +version = "0.2.3+llvm-462a31f5a5ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" +dependencies = [ + "bitflags", + "smallvec", +] + +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "tester" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8bf7e0eb2dd7b4228cc1b6821fc5114cd6841ae59f652a85488c016091e5f" +dependencies = [ + "cfg-if", + "getopts", + "libc", + "num_cpus", + "term", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/support/lints/Cargo.toml b/support/lints/Cargo.toml new file mode 100644 index 000000000..7e89a5d96 --- /dev/null +++ b/support/lints/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bitwarden_lints" +version = "0.1.0" +authors = ["Bitwarden Inc"] +description = "Bitwarden lints" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +bitwarden_error_enum = { path = "error_enum", features = ["rlib"] } +bitwarden_error_suffix = { path = "error_suffix", features = ["rlib"] } +dylint_linting = { workspace = true } + +[package.metadata.rust-analyzer] +rustc_private = true + +[workspace] +members = ["*"] +exclude = [".cargo", "src", "target"] + +[workspace.dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "334fb906aef13d20050987b13448f37391bb97a2" } +dylint_linting = "4.1.0" +dylint_testing = "4.1.0" diff --git a/support/lints/README.md b/support/lints/README.md new file mode 100644 index 000000000..2bb3e02ac --- /dev/null +++ b/support/lints/README.md @@ -0,0 +1,9 @@ +# Bitwarden SDK Lints + +This directory contains custom lints for the Bitwarden SDK, implemented using +[dylint](https://github.com/trailofbits/dylint). + +The following lints are currently available: + +- `error_enum`: Forbids enum variants from having the `Error` suffix. +- `error_suffix`: Ensures that types deriving `std::error::Error` have the `Error` suffix. diff --git a/support/lints/error_enum/.cargo/config.toml b/support/lints/error_enum/.cargo/config.toml new file mode 100644 index 000000000..226eca535 --- /dev/null +++ b/support/lints/error_enum/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/support/lints/error_enum/.gitignore b/support/lints/error_enum/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/support/lints/error_enum/.gitignore @@ -0,0 +1 @@ +/target diff --git a/support/lints/error_enum/Cargo.toml b/support/lints/error_enum/Cargo.toml new file mode 100644 index 000000000..e9e671a1c --- /dev/null +++ b/support/lints/error_enum/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bitwarden_error_enum" +version = "0.1.0" +authors = ["Bitwarden Inc"] +description = "Lint forbidding enum variants from having Error suffix" +edition = "2021" +publish = false + +[features] +rlib = ["dylint_linting/constituent"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +clippy_utils = { workspace = true } +dylint_linting = { workspace = true } + +[dev-dependencies] +dylint_testing = { workspace = true } + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/support/lints/error_enum/src/lib.rs b/support/lints/error_enum/src/lib.rs new file mode 100644 index 000000000..33ddc5807 --- /dev/null +++ b/support/lints/error_enum/src/lib.rs @@ -0,0 +1,64 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_hir; + +use clippy_utils::diagnostics::span_lint; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::LateLintPass; + +dylint_linting::declare_late_lint! { + /// ### What it does + /// + /// Warns when an enum variant name ends with "Error". + /// + /// ### Why is this bad? + /// + /// Enum variant names ending with "Error" can be redundant and verbose, + /// especially when the enum itself represents error types. This can lead + /// to awkward naming like `MyError::SomeError`. + /// + /// ### Example + /// + /// ```rust + /// enum ApiError { + /// NetworkError, // warns: variant ends with "Error" + /// TimeoutError, // warns: variant ends with "Error" + /// } + /// ``` + /// + /// Use instead: + /// + /// ```rust + /// enum ApiError { + /// Network, + /// Timeout, + /// } + /// ``` + pub ENUM_VARIANT_ENDS_WITH_ERROR, + Warn, + "enum variant names should not end with 'Error'" +} + +impl<'tcx> LateLintPass<'tcx> for EnumVariantEndsWithError { + fn check_item(&mut self, cx: &rustc_lint::LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if let ItemKind::Enum(_, _, enum_def) = &item.kind { + for variant in enum_def.variants { + let variant_name = variant.ident.name.as_str(); + if variant_name.ends_with("Error") { + span_lint( + cx, + ENUM_VARIANT_ENDS_WITH_ERROR, + variant.ident.span, + format!("enum variant `{}` ends with 'Error'", variant_name), + ); + } + } + } + } +} + +#[test] +fn ui() { + dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); +} diff --git a/support/lints/error_enum/ui/main.rs b/support/lints/error_enum/ui/main.rs new file mode 100644 index 000000000..d25381e92 --- /dev/null +++ b/support/lints/error_enum/ui/main.rs @@ -0,0 +1,25 @@ +enum ApiError { + NetworkError, + TimeoutError, + ParseError, +} + +enum DatabaseError { + ConnectionError, + QueryError, +} + +enum GoodEnum { + Network, + Timeout, + Parse, +} + +enum MixedEnum { + Success, + FailureError, + InvalidError, + Retry, +} + +fn main() {} diff --git a/support/lints/error_enum/ui/main.stderr b/support/lints/error_enum/ui/main.stderr new file mode 100644 index 000000000..1c26e2426 --- /dev/null +++ b/support/lints/error_enum/ui/main.stderr @@ -0,0 +1,46 @@ +warning: enum variant `NetworkError` ends with 'Error' + --> $DIR/main.rs:2:5 + | +LL | NetworkError, + | ^^^^^^^^^^^^ + | + = note: `#[warn(enum_variant_ends_with_error)]` on by default + +warning: enum variant `TimeoutError` ends with 'Error' + --> $DIR/main.rs:3:5 + | +LL | TimeoutError, + | ^^^^^^^^^^^^ + +warning: enum variant `ParseError` ends with 'Error' + --> $DIR/main.rs:4:5 + | +LL | ParseError, + | ^^^^^^^^^^ + +warning: enum variant `ConnectionError` ends with 'Error' + --> $DIR/main.rs:8:5 + | +LL | ConnectionError, + | ^^^^^^^^^^^^^^^ + +warning: enum variant `QueryError` ends with 'Error' + --> $DIR/main.rs:9:5 + | +LL | QueryError, + | ^^^^^^^^^^ + +warning: enum variant `FailureError` ends with 'Error' + --> $DIR/main.rs:20:5 + | +LL | FailureError, + | ^^^^^^^^^^^^ + +warning: enum variant `InvalidError` ends with 'Error' + --> $DIR/main.rs:21:5 + | +LL | InvalidError, + | ^^^^^^^^^^^^ + +warning: 7 warnings emitted + diff --git a/support/lints/error_suffix/.cargo/config.toml b/support/lints/error_suffix/.cargo/config.toml new file mode 100644 index 000000000..226eca535 --- /dev/null +++ b/support/lints/error_suffix/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/support/lints/error_suffix/Cargo.toml b/support/lints/error_suffix/Cargo.toml new file mode 100644 index 000000000..1e6c8e37b --- /dev/null +++ b/support/lints/error_suffix/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bitwarden_error_suffix" +version = "0.1.0" +authors = ["Bitwarden Inc"] +description = "Lints for Error suffix naming" +edition = "2021" +publish = false + +[features] +rlib = ["dylint_linting/constituent"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[[example]] +name = "ui" +path = "ui/main.rs" + +[dependencies] +clippy_utils = { workspace = true } +dylint_linting = { workspace = true } + +[dev-dependencies] +dylint_testing = { workspace = true } +thiserror = ">=1.0.40, <3" + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/support/lints/error_suffix/src/lib.rs b/support/lints/error_suffix/src/lib.rs new file mode 100644 index 000000000..df4bcb513 --- /dev/null +++ b/support/lints/error_suffix/src/lib.rs @@ -0,0 +1,60 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_hir; +extern crate rustc_span; +use clippy_utils::{diagnostics::span_lint, ty::implements_trait}; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::LateLintPass; +use rustc_span::symbol::sym; + +dylint_linting::declare_late_lint! { + pub ERROR_SUFFIX, + Warn, + "enums and structs implementing Error should end with 'Error'" +} + +impl<'tcx> LateLintPass<'tcx> for ErrorSuffix { + fn check_item(&mut self, cx: &rustc_lint::LateContext<'tcx>, item: &'tcx Item<'tcx>) { + let ident = match item.kind { + ItemKind::Enum(ident, ..) | ItemKind::Struct(ident, ..) => ident, + _ => return, + }; + + let item_name = ident.name.as_str(); + + match &item.kind { + ItemKind::Enum(..) | ItemKind::Struct(..) => { + let ty = cx.tcx.type_of(item.owner_id.def_id).instantiate_identity(); + let implements_error = cx + .tcx + .get_diagnostic_item(sym::Error) + .map_or(false, |id| implements_trait(cx, ty, id, &[])); + + if implements_error && !item_name.ends_with("Error") { + let item_type = match &item.kind { + ItemKind::Enum(..) => "enum", + ItemKind::Struct(..) => "struct", + _ => unreachable!(), + }; + + span_lint( + cx, + ERROR_SUFFIX, + ident.span, + format!( + "{} `{}` implements Error but doesn't end with 'Error'", + item_type, item_name + ), + ); + } + } + _ => {} + } + } +} + +#[test] +fn ui() { + dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "ui"); +} diff --git a/support/lints/error_suffix/ui/main.rs b/support/lints/error_suffix/ui/main.rs new file mode 100644 index 000000000..99ad922d1 --- /dev/null +++ b/support/lints/error_suffix/ui/main.rs @@ -0,0 +1,31 @@ +#[derive(Debug, thiserror::Error)] +#[error("Bad example error")] +struct BadExample {} + +#[derive(Debug, thiserror::Error)] +#[error("Good example error")] +struct GoodExampleError {} + +#[derive(Debug)] +struct BadManualExample {} + +impl std::fmt::Display for BadManualExample { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Bad manual example error") + } +} + +impl std::error::Error for BadManualExample {} + +#[derive(Debug)] +struct GoodManualError {} + +impl std::fmt::Display for GoodManualError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Good manual error") + } +} + +impl std::error::Error for GoodManualError {} + +fn main() {} diff --git a/support/lints/error_suffix/ui/main.stderr b/support/lints/error_suffix/ui/main.stderr new file mode 100644 index 000000000..a18d2c09a --- /dev/null +++ b/support/lints/error_suffix/ui/main.stderr @@ -0,0 +1,16 @@ +warning: struct `BadExample` implements Error but doesn't end with 'Error' + --> $DIR/main.rs:3:8 + | +LL | struct BadExample {} + | ^^^^^^^^^^ + | + = note: `#[warn(error_suffix)]` on by default + +warning: struct `BadManualExample` implements Error but doesn't end with 'Error' + --> $DIR/main.rs:10:8 + | +LL | struct BadManualExample {} + | ^^^^^^^^^^^^^^^^ + +warning: 2 warnings emitted + diff --git a/support/lints/rust-toolchain.toml b/support/lints/rust-toolchain.toml new file mode 100644 index 000000000..07038b722 --- /dev/null +++ b/support/lints/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2025-08-07" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/support/lints/src/lib.rs b/support/lints/src/lib.rs new file mode 100644 index 000000000..0d567a0c9 --- /dev/null +++ b/support/lints/src/lib.rs @@ -0,0 +1,14 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +dylint_linting::dylint_library!(); + +extern crate rustc_lint; +extern crate rustc_session; + +#[expect(clippy::no_mangle_with_rust_abi)] +#[unsafe(no_mangle)] +pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) { + bitwarden_error_enum::register_lints(sess, lint_store); + bitwarden_error_suffix::register_lints(sess, lint_store); +} From 9a8d48273837c9c70773911be251b13075698fc6 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 15 Sep 2025 17:33:15 +0200 Subject: [PATCH 09/60] Migrate more b64 (#411) Wraps up the b64 migration. --- Cargo.lock | 7 +- .../src/key_management/crypto.rs | 2 +- .../safe/password_protected_key_envelope.rs | 2 +- crates/bitwarden-encoding/src/b64.rs | 5 ++ crates/bitwarden-encoding/src/b64url.rs | 5 ++ crates/bitwarden-exporters/Cargo.toml | 2 +- crates/bitwarden-exporters/src/cxf/import.rs | 85 +++++-------------- crates/bitwarden-exporters/src/cxf/login.rs | 19 ++--- .../bitwarden-exporters/src/encrypted_json.rs | 6 +- crates/bitwarden-fido/Cargo.toml | 2 +- crates/bitwarden-fido/src/lib.rs | 31 +++---- crates/bitwarden-fido/src/types.rs | 12 +-- crates/bitwarden-send/Cargo.toml | 2 +- crates/bitwarden-send/src/send.rs | 16 ++-- crates/bitwarden-vault/Cargo.toml | 3 +- .../bitwarden-vault/src/cipher/attachment.rs | 18 ++-- crates/bitwarden-vault/src/cipher/login.rs | 8 +- 17 files changed, 93 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04fc2f0f3..7ff34e890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,10 +518,10 @@ dependencies = [ name = "bitwarden-exporters" version = "1.0.0" dependencies = [ - "base64", "bitwarden-collections", "bitwarden-core", "bitwarden-crypto", + "bitwarden-encoding", "bitwarden-error", "bitwarden-fido", "bitwarden-ssh", @@ -544,9 +544,9 @@ name = "bitwarden-fido" version = "1.0.0" dependencies = [ "async-trait", - "base64", "bitwarden-core", "bitwarden-crypto", + "bitwarden-encoding", "bitwarden-vault", "chrono", "coset", @@ -608,10 +608,10 @@ dependencies = [ name = "bitwarden-send" version = "1.0.0" dependencies = [ - "base64", "bitwarden-api-api", "bitwarden-core", "bitwarden-crypto", + "bitwarden-encoding", "chrono", "serde", "serde_repr", @@ -772,7 +772,6 @@ dependencies = [ name = "bitwarden-vault" version = "1.0.0" dependencies = [ - "base64", "bitwarden-api-api", "bitwarden-collections", "bitwarden-core", diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index f5253a9a7..4c61279e9 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -233,7 +233,7 @@ pub(super) async fn initialize_user_crypto( master_key, user_key, } => { - let mut bytes = master_key.as_bytes().to_vec(); + let mut bytes = master_key.into_bytes(); let master_key = MasterKey::try_from(bytes.as_mut_slice())?; client diff --git a/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs b/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs index f41fe07b2..233efeb51 100644 --- a/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs +++ b/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs @@ -272,7 +272,7 @@ impl FromStr for PasswordProtectedKeyEnvelope { "Invalid PasswordProtectedKeyEnvelope Base64 encoding".to_string(), ) })?; - Self::try_from(&data.as_bytes().to_vec()).map_err(|_| { + Self::try_from(&data.into_bytes()).map_err(|_| { PasswordProtectedKeyEnvelopeError::ParsingError( "Failed to parse PasswordProtectedKeyEnvelope".to_string(), ) diff --git a/crates/bitwarden-encoding/src/b64.rs b/crates/bitwarden-encoding/src/b64.rs index be73913a4..322f820e5 100644 --- a/crates/bitwarden-encoding/src/b64.rs +++ b/crates/bitwarden-encoding/src/b64.rs @@ -31,6 +31,11 @@ impl B64 { pub fn as_bytes(&self) -> &[u8] { &self.0 } + + /// Returns the inner byte vector. + pub fn into_bytes(self) -> Vec { + self.0 + } } // We manually implement this to handle both `String` and `&str` diff --git a/crates/bitwarden-encoding/src/b64url.rs b/crates/bitwarden-encoding/src/b64url.rs index b006b1fc5..e9981f417 100644 --- a/crates/bitwarden-encoding/src/b64url.rs +++ b/crates/bitwarden-encoding/src/b64url.rs @@ -16,6 +16,11 @@ impl B64Url { pub fn as_bytes(&self) -> &[u8] { &self.0 } + + /// Returns the inner byte vector. + pub fn into_bytes(self) -> Vec { + self.0 + } } impl From> for B64Url { diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml index 5381f7662..39d5ee207 100644 --- a/crates/bitwarden-exporters/Cargo.toml +++ b/crates/bitwarden-exporters/Cargo.toml @@ -25,10 +25,10 @@ wasm = [ ] # WebAssembly bindings [dependencies] -base64 = ">=0.22.1, <0.23" bitwarden-collections = { workspace = true } bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } +bitwarden-encoding = { workspace = true } bitwarden-error = { workspace = true } bitwarden-fido = { workspace = true } bitwarden-ssh = { workspace = true } diff --git a/crates/bitwarden-exporters/src/cxf/import.rs b/crates/bitwarden-exporters/src/cxf/import.rs index 96916e117..1c3c309d9 100644 --- a/crates/bitwarden-exporters/src/cxf/import.rs +++ b/crates/bitwarden-exporters/src/cxf/import.rs @@ -275,9 +275,8 @@ struct GroupedCredentials { #[cfg(test)] mod tests { - use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use chrono::{Duration, Month}; - use credential_exchange_format::{CreditCardCredential, EditableFieldYearMonth}; + use credential_exchange_format::{B64Url, CreditCardCredential, EditableFieldYearMonth}; use super::*; @@ -320,35 +319,22 @@ mod tests { #[test] fn test_parse_passkey() { let item = Item { - id: URL_SAFE_NO_PAD - .decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF") - .unwrap() - .as_slice() - .into(), + id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF") + .unwrap(), creation_at: Some(1732181986), modified_at: Some(1732182026), title: "example.com".to_string(), subtitle: None, favorite: None, credentials: vec![Credential::Passkey(Box::new(PasskeyCredential { - credential_id: URL_SAFE_NO_PAD - .decode("6NiHiekW4ZY8vYHa-ucbvA") - .unwrap() - .as_slice() - .into(), + credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA") + .unwrap(), rp_id: "example.com".to_string(), username: "pj-fry".to_string(), user_display_name: "Philip J. Fry".to_string(), - user_handle: URL_SAFE_NO_PAD - .decode("YWxleCBtdWxsZXI") - .unwrap() - .as_slice() - .into(), - key: URL_SAFE_NO_PAD - .decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") - .unwrap() - .as_slice() - .into(), + user_handle: B64Url::try_from("YWxleCBtdWxsZXI").unwrap(), + key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") + .unwrap(), fido2_extensions: None, }))], tags: None, @@ -407,11 +393,8 @@ mod tests { use credential_exchange_format::{BasicAuthCredential, CredentialScope}; let item = Item { - id: URL_SAFE_NO_PAD - .decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF") - .unwrap() - .as_slice() - .into(), + id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF") + .unwrap(), creation_at: Some(1732181986), modified_at: Some(1732182026), title: "Combined Login".to_string(), @@ -423,24 +406,15 @@ mod tests { password: Some("basic_password".to_string().into()), })), Credential::Passkey(Box::new(PasskeyCredential { - credential_id: URL_SAFE_NO_PAD - .decode("6NiHiekW4ZY8vYHa-ucbvA") - .unwrap() - .as_slice() - .into(), + credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA") + .unwrap(), rp_id: "passkey-domain.com".to_string(), username: "passkey_username".to_string(), user_display_name: "Passkey User".to_string(), - user_handle: URL_SAFE_NO_PAD - .decode("YWxleCBtdWxsZXI") - .unwrap() - .as_slice() - .into(), - key: URL_SAFE_NO_PAD - .decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") - .unwrap() - .as_slice() - .into(), + user_handle: B64Url::try_from("YWxleCBtdWxsZXI") + .unwrap(), + key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") + .unwrap(), fido2_extensions: None, })) ], @@ -479,35 +453,22 @@ mod tests { #[test] fn test_passkey_with_empty_username() { let item = Item { - id: URL_SAFE_NO_PAD - .decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF") - .unwrap() - .as_slice() - .into(), + id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF").unwrap(), creation_at: Some(1732181986), modified_at: Some(1732182026), title: "Empty Username Passkey".to_string(), subtitle: None, favorite: None, credentials: vec![Credential::Passkey(Box::new(PasskeyCredential { - credential_id: URL_SAFE_NO_PAD - .decode("6NiHiekW4ZY8vYHa-ucbvA") - .unwrap() - .as_slice() - .into(), + credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA") + .unwrap(), rp_id: "example.com".to_string(), username: "".to_string(), // Empty username user_display_name: "User Display".to_string(), - user_handle: URL_SAFE_NO_PAD - .decode("YWxleCBtdWxsZXI") - .unwrap() - .as_slice() - .into(), - key: URL_SAFE_NO_PAD - .decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") - .unwrap() - .as_slice() - .into(), + user_handle: B64Url::try_from("YWxleCBtdWxsZXI") + .unwrap(), + key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl") + .unwrap(), fido2_extensions: None, }))], tags: None, diff --git a/crates/bitwarden-exporters/src/cxf/login.rs b/crates/bitwarden-exporters/src/cxf/login.rs index ace35aabb..a722527b3 100644 --- a/crates/bitwarden-exporters/src/cxf/login.rs +++ b/crates/bitwarden-exporters/src/cxf/login.rs @@ -3,14 +3,13 @@ //! Handles conversion between internal [Login] and credential exchange [BasicAuthCredential] and //! [PasskeyCredential]. -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use bitwarden_core::MissingFieldError; use bitwarden_fido::{string_to_guid_bytes, InvalidGuid}; use bitwarden_vault::{FieldType, Totp, TotpAlgorithm}; use chrono::{DateTime, Utc}; use credential_exchange_format::{ - AndroidAppIdCredential, BasicAuthCredential, CredentialScope, OTPHashAlgorithm, - PasskeyCredential, TotpCredential, + AndroidAppIdCredential, B64Url, BasicAuthCredential, CredentialScope, NotB64UrlEncoded, + OTPHashAlgorithm, PasskeyCredential, TotpCredential, }; use thiserror::Error; @@ -82,7 +81,7 @@ pub(super) fn to_login( key_type: "public-key".to_string(), key_algorithm: "ECDSA".to_string(), key_curve: "P-256".to_string(), - key_value: URL_SAFE_NO_PAD.encode(&p.key), + key_value: p.key.to_string(), rp_id: p.rp_id.clone(), user_handle: Some(p.user_handle.to_string()), user_name: Some(p.username.clone()), @@ -191,8 +190,8 @@ pub enum PasskeyError { InvalidGuid(InvalidGuid), #[error(transparent)] MissingField(MissingFieldError), - #[error(transparent)] - InvalidBase64(#[from] base64::DecodeError), + #[error("Data isn't base64url encoded")] + InvalidBase64(NotB64UrlEncoded), } impl TryFrom for PasskeyCredential { @@ -212,11 +211,11 @@ impl TryFrom for PasskeyCredential { user_display_name: value.user_display_name.unwrap_or_default(), user_handle: value .user_handle - .map(|v| URL_SAFE_NO_PAD.decode(v)) - .transpose()? - .map(|v| v.into()) + .map(|v| B64Url::try_from(v.as_str())) + .transpose() + .map_err(PasskeyError::InvalidBase64)? .ok_or(PasskeyError::MissingField(MissingFieldError("user_handle")))?, - key: URL_SAFE_NO_PAD.decode(value.key_value)?.into(), + key: B64Url::try_from(value.key_value.as_str()).map_err(PasskeyError::InvalidBase64)?, fido2_extensions: None, }) } diff --git a/crates/bitwarden-exporters/src/encrypted_json.rs b/crates/bitwarden-exporters/src/encrypted_json.rs index 32f1c9307..d0a855738 100644 --- a/crates/bitwarden-exporters/src/encrypted_json.rs +++ b/crates/bitwarden-exporters/src/encrypted_json.rs @@ -1,5 +1,5 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{generate_random_bytes, Kdf, KeyEncryptable, PinKey}; +use bitwarden_encoding::B64; use serde::Serialize; use thiserror::Error; use uuid::Uuid; @@ -44,7 +44,7 @@ pub(crate) fn export_encrypted_json( }; let salt = generate_random_bytes::<[u8; 16]>(); - let salt = STANDARD.encode(salt); + let salt = B64::from(salt.as_slice()); let key = PinKey::derive(password.as_bytes(), salt.as_bytes(), &kdf)?; let enc_key_validation = Uuid::new_v4().to_string(); @@ -52,7 +52,7 @@ pub(crate) fn export_encrypted_json( let encrypted_export = EncryptedJsonExport { encrypted: true, password_protected: true, - salt, + salt: salt.to_string(), kdf_type, kdf_iterations, kdf_memory, diff --git a/crates/bitwarden-fido/Cargo.toml b/crates/bitwarden-fido/Cargo.toml index 0f880a945..9d8637b96 100644 --- a/crates/bitwarden-fido/Cargo.toml +++ b/crates/bitwarden-fido/Cargo.toml @@ -19,9 +19,9 @@ uniffi = ["dep:uniffi", "bitwarden-core/uniffi", "bitwarden-vault/uniffi"] [dependencies] async-trait = { workspace = true } -base64 = ">=0.22.1, <0.23" bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } +bitwarden-encoding = { workspace = true } bitwarden-vault = { workspace = true } chrono = { workspace = true } coset = ">=0.3.7, <0.4" diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index 79a431daa..52d11b4b2 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -1,8 +1,8 @@ #![doc = include_str!("../README.md")] -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use bitwarden_core::key_management::KeyIds; use bitwarden_crypto::KeyStoreContext; +use bitwarden_encoding::{B64Url, NotB64UrlEncoded}; use bitwarden_vault::{ CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView, }; @@ -78,7 +78,7 @@ impl CipherViewContainer { #[derive(Debug, Error)] pub enum Fido2Error { #[error(transparent)] - DecodeError(#[from] base64::DecodeError), + DecodeError(#[from] NotB64UrlEncoded), #[error(transparent)] UnknownEnum(#[from] UnknownEnum), @@ -115,19 +115,16 @@ fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result = value.credential_id.into(); let user_handle = value .user_handle - .map(|u| URL_SAFE_NO_PAD.encode(u.to_vec())); - let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?); + .map(|u| B64Url::from(u.to_vec()).to_string()); + let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string(); Ok(Fido2CredentialFullView { credential_id: guid_bytes_to_string(&cred_id)?, @@ -175,7 +172,7 @@ pub(crate) fn try_from_credential_new_view( rp: &passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity, ) -> Result { let cred_id: Vec = vec![0; 16]; - let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec()); + let user_handle = B64Url::from(user.id.to_vec()).to_string(); Ok(Fido2CredentialNewView { // TODO: Why do we have a credential id here? @@ -201,8 +198,8 @@ pub(crate) fn try_from_credential_full( options: passkey::types::ctap2::get_assertion::Options, ) -> Result { let cred_id: Vec = value.credential_id.into(); - let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?); - let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec()); + let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string(); + let user_handle = B64Url::from(user.id.to_vec()).to_string(); Ok(Fido2CredentialFullView { credential_id: guid_bytes_to_string(&cred_id)?, @@ -243,10 +240,8 @@ pub struct InvalidGuid; #[allow(missing_docs)] pub fn string_to_guid_bytes(source: &str) -> Result, InvalidGuid> { if source.starts_with("b64.") { - let bytes = URL_SAFE_NO_PAD - .decode(source.trim_start_matches("b64.")) - .map_err(|_| InvalidGuid)?; - Ok(bytes) + let bytes = B64Url::try_from(source.trim_start_matches("b64.")).map_err(|_| InvalidGuid)?; + Ok(bytes.as_bytes().to_vec()) } else { let Ok(uuid) = uuid::Uuid::try_parse(source) else { return Err(InvalidGuid); diff --git a/crates/bitwarden-fido/src/types.rs b/crates/bitwarden-fido/src/types.rs index 867f7d196..4f62bc167 100644 --- a/crates/bitwarden-fido/src/types.rs +++ b/crates/bitwarden-fido/src/types.rs @@ -1,8 +1,8 @@ use std::borrow::Cow; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use bitwarden_core::key_management::KeyIds; use bitwarden_crypto::{CryptoError, KeyStoreContext}; +use bitwarden_encoding::{B64Url, NotB64UrlEncoded}; use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView}; use passkey::types::webauthn::UserVerificationRequirement; use reqwest::Url; @@ -65,7 +65,7 @@ pub enum Fido2CredentialAutofillViewError { CryptoError(#[from] CryptoError), #[error(transparent)] - Base64DecodeError(#[from] base64::DecodeError), + Base64DecodeError(#[from] NotB64UrlEncoded), } impl Fido2CredentialAutofillView { @@ -81,7 +81,7 @@ impl Fido2CredentialAutofillView { .filter_map(|c| -> Option> { c.user_handle .as_ref() - .map(|u| URL_SAFE_NO_PAD.decode(u)) + .map(|u| B64Url::try_from(u.as_str())) .map(|user_handle| { Ok(Fido2CredentialAutofillView { credential_id: string_to_guid_bytes(&c.credential_id)?, @@ -90,7 +90,7 @@ impl Fido2CredentialAutofillView { .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)? .into(), rp_id: c.rp_id.clone(), - user_handle: user_handle?, + user_handle: user_handle?.into_bytes(), user_name_for_ui: c .user_name .none_whitespace() @@ -121,7 +121,7 @@ impl Fido2CredentialAutofillView { .filter_map(|c| -> Option> { c.user_handle .as_ref() - .map(|u| URL_SAFE_NO_PAD.decode(u)) + .map(|u| B64Url::try_from(u.as_str())) .map(|user_handle| { Ok(Fido2CredentialAutofillView { credential_id: string_to_guid_bytes(&c.credential_id)?, @@ -130,7 +130,7 @@ impl Fido2CredentialAutofillView { .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)? .into(), rp_id: c.rp_id.clone(), - user_handle: user_handle?, + user_handle: user_handle?.into_bytes(), user_name_for_ui: c .user_name .none_whitespace() diff --git a/crates/bitwarden-send/Cargo.toml b/crates/bitwarden-send/Cargo.toml index 161be4b93..ee6522f25 100644 --- a/crates/bitwarden-send/Cargo.toml +++ b/crates/bitwarden-send/Cargo.toml @@ -22,10 +22,10 @@ uniffi = [ ] # Uniffi bindings [dependencies] -base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } +bitwarden-encoding = { workspace = true } chrono = { workspace = true } serde = { workspace = true } serde_repr = { workspace = true } diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index f00632136..6c2bfa946 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -1,7 +1,3 @@ -use base64::{ - engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, - Engine, -}; use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, @@ -11,6 +7,7 @@ use bitwarden_crypto::{ generate_random_bytes, CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, }; +use bitwarden_encoding::{B64Url, B64}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -254,7 +251,7 @@ impl Decryptable for Send { name: self.name.decrypt(ctx, key).ok().unwrap_or_default(), notes: self.notes.decrypt(ctx, key).ok().flatten(), - key: Some(URL_SAFE_NO_PAD.encode(k)), + key: Some(B64Url::from(k).to_string()), new_password: None, has_password: self.password.is_some(), @@ -312,9 +309,10 @@ impl CompositeEncryptable for SendView { // the stretched key let k = match (&self.key, &self.id) { // Existing send, decrypt key - (Some(k), _) => URL_SAFE_NO_PAD - .decode(k) - .map_err(|_| CryptoError::InvalidKey)?, + (Some(k), _) => B64Url::try_from(k.as_str()) + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_vec(), // New send, generate random key (None, None) => { let key = generate_random_bytes::<[u8; 16]>(); @@ -334,7 +332,7 @@ impl CompositeEncryptable for SendView { key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?, password: self.new_password.as_ref().map(|password| { let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - STANDARD.encode(password) + B64::from(password.as_slice()).to_string() }), r#type: self.r#type, diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index e91d63a0e..4d6fbb2a9 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -31,12 +31,11 @@ wasm = [ ] # WASM support [dependencies] -base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-collections = { workspace = true } bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } -bitwarden-encoding.workspace = true +bitwarden-encoding = { workspace = true } bitwarden-error = { workspace = true } bitwarden-state = { workspace = true } bitwarden-uuid = { workspace = true } diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index 1536dc7d1..593378a63 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -220,9 +220,9 @@ impl TryFrom for Attachment #[cfg(test)] mod tests { - use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_core::key_management::create_test_crypto_with_user_key; use bitwarden_crypto::{EncString, SymmetricCryptoKey}; + use bitwarden_encoding::B64; use crate::{ cipher::cipher::{CipherRepromptType, CipherType}, @@ -344,18 +344,18 @@ mod tests { archived_date: None, }; - let enc_file = STANDARD.decode(b"Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap(); - let original = STANDARD.decode(b"rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); + let enc_file = B64::try_from("Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap(); + let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); let dec = key_store .decrypt(&AttachmentFile { cipher, attachment, - contents: EncString::from_buffer(&enc_file).unwrap(), + contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(), }) .unwrap(); - assert_eq!(dec, original); + assert_eq!(dec, original.as_bytes()); } #[test] @@ -403,17 +403,17 @@ mod tests { archived_date: None, }; - let enc_file = STANDARD.decode(b"AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap(); - let original = STANDARD.decode(b"rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); + let enc_file = B64::try_from("AsQLXOBHrJ8porroTUlPxeJOm9XID7LL9D2+KwYATXEpR1EFjLBpcCvMmnqcnYLXIEefe9TCeY4Us50ux43kRSpvdB7YkjxDKV0O1/y6tB7qC4vvv9J9+O/uDEnMx/9yXuEhAW/LA/TsU/WAgxkOM0uTvm8JdD9LUR1z9Ql7zOWycMVzkvGsk2KBNcqAdrotS5FlDftZOXyU8pWecNeyA/w=").unwrap(); + let original = B64::try_from("rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); let dec = key_store .decrypt(&AttachmentFile { cipher, attachment, - contents: EncString::from_buffer(&enc_file).unwrap(), + contents: EncString::from_buffer(enc_file.as_bytes()).unwrap(), }) .unwrap(); - assert_eq!(dec, original); + assert_eq!(dec, original.as_bytes()); } } diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 7f92d02b0..0f5a164d6 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -1,4 +1,3 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, @@ -8,6 +7,7 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext, PrimitiveEncryptable, }; +use bitwarden_encoding::B64; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -63,21 +63,21 @@ impl LoginUriView { let Some(cs) = &self.uri_checksum else { return false; }; - let Ok(cs) = STANDARD.decode(cs) else { + let Ok(cs) = B64::try_from(cs.as_str()) else { return false; }; use sha2::Digest; let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); - uri_hash.as_slice() == cs + uri_hash.as_slice() == cs.as_bytes() } pub(crate) fn generate_checksum(&mut self) { if let Some(uri) = &self.uri { use sha2::Digest; let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); - let uri_hash = STANDARD.encode(uri_hash.as_slice()); + let uri_hash = B64::from(uri_hash.as_slice()).to_string(); self.uri_checksum = Some(uri_hash); } } From 778c3ac1755920036c3b3ef0e418bc92895ddc4e Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 15 Sep 2025 17:45:06 +0200 Subject: [PATCH 10/60] Remove unused vault locked error (#440) We removed the ability to throw `VaultLocked` a while back. This removes the actual error struct and any place it's used in. --- bitwarden_license/bitwarden-sm/src/error.rs | 2 -- crates/bitwarden-core/src/auth/auth_client.rs | 2 -- .../bitwarden-core/src/auth/auth_request.rs | 4 +--- crates/bitwarden-core/src/auth/mod.rs | 4 +--- .../src/client/encryption_settings.rs | 5 +---- crates/bitwarden-core/src/error.rs | 5 ----- .../src/key_management/crypto.rs | 6 +----- crates/bitwarden-core/src/lib.rs | 3 +-- .../src/platform/generate_fingerprint.rs | 4 +--- crates/bitwarden-exporters/src/error.rs | 2 -- crates/bitwarden-fido/src/authenticator.rs | 20 ++++--------------- crates/bitwarden-fido/src/client_fido.rs | 2 -- crates/bitwarden-send/src/send_client.rs | 4 ---- crates/bitwarden-vault/src/cipher/cipher.rs | 4 +--- crates/bitwarden-vault/src/error.rs | 4 ---- crates/bitwarden-vault/src/totp.rs | 4 +--- 16 files changed, 12 insertions(+), 63 deletions(-) diff --git a/bitwarden_license/bitwarden-sm/src/error.rs b/bitwarden_license/bitwarden-sm/src/error.rs index 76203759b..51b45ffeb 100644 --- a/bitwarden_license/bitwarden-sm/src/error.rs +++ b/bitwarden_license/bitwarden-sm/src/error.rs @@ -8,8 +8,6 @@ pub enum SecretsManagerError { #[error(transparent)] ValidationError(ValidationError), #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), - #[error(transparent)] CryptoError(#[from] bitwarden_crypto::CryptoError), #[error(transparent)] Chrono(#[from] chrono::ParseError), diff --git a/crates/bitwarden-core/src/auth/auth_client.rs b/crates/bitwarden-core/src/auth/auth_client.rs index 9c9b2da8f..dd5f68ae3 100644 --- a/crates/bitwarden-core/src/auth/auth_client.rs +++ b/crates/bitwarden-core/src/auth/auth_client.rs @@ -207,8 +207,6 @@ impl AuthClient { #[cfg(feature = "internal")] #[derive(Debug, thiserror::Error)] pub enum TrustDeviceError { - #[error(transparent)] - VaultLocked(#[from] crate::VaultLockedError), #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), } diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index a83cc6da0..0a9eb8425 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -10,7 +10,7 @@ use thiserror::Error; #[cfg(feature = "internal")] use crate::client::encryption_settings::EncryptionSettingsError; -use crate::{key_management::SymmetricKeyId, Client, VaultLockedError}; +use crate::{key_management::SymmetricKeyId, Client}; /// Response for `new_auth_request`. #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -78,8 +78,6 @@ pub(crate) fn auth_request_decrypt_master_key( pub enum ApproveAuthRequestError { #[error(transparent)] Crypto(#[from] CryptoError), - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), } /// Approve an auth request. diff --git a/crates/bitwarden-core/src/auth/mod.rs b/crates/bitwarden-core/src/auth/mod.rs index 7d742b9a9..ab2d7619d 100644 --- a/crates/bitwarden-core/src/auth/mod.rs +++ b/crates/bitwarden-core/src/auth/mod.rs @@ -4,7 +4,7 @@ use thiserror::Error; -use crate::{NotAuthenticatedError, VaultLockedError, WrongPasswordError}; +use crate::{NotAuthenticatedError, WrongPasswordError}; mod access_token; // API is intentionally not visible outside of `auth` as these should be considered private. @@ -56,8 +56,6 @@ pub enum AuthValidateError { NotAuthenticated(#[from] NotAuthenticatedError), #[error(transparent)] WrongPassword(#[from] WrongPasswordError), - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), #[error("wrong user key")] WrongUserKey, #[error(transparent)] diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 2a8ff2c1c..533d4a079 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -19,7 +19,7 @@ use crate::key_management::{AsymmetricKeyId, SecurityState, SignedSecurityState, use crate::key_management::{KeyIds, SymmetricKeyId}; #[cfg(any(feature = "secrets", feature = "internal"))] use crate::OrganizationId; -use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; +use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -28,9 +28,6 @@ pub enum EncryptionSettingsError { #[error("Cryptography error, {0}")] Crypto(#[from] bitwarden_crypto::CryptoError), - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error("Invalid private key")] InvalidPrivateKey, diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index 996d7d43d..b2b2054f7 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -60,11 +60,6 @@ pub struct UserIdAlreadySetError; #[error("The response received was missing a required field: {0}")] pub struct MissingFieldError(pub &'static str); -/// Client vault is locked. -#[derive(Debug, Error)] -#[error("The client vault is locked and needs to be unlocked before use")] -pub struct VaultLockedError; - /// Wrong password. #[derive(Debug, thiserror::Error)] #[error("Wrong password")] diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 4c61279e9..bc53356c4 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -27,7 +27,7 @@ use crate::{ non_generic_wrappers::PasswordProtectedKeyEnvelope, AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, }, - Client, NotAuthenticatedError, OrganizationId, UserId, VaultLockedError, WrongPasswordError, + Client, NotAuthenticatedError, OrganizationId, UserId, WrongPasswordError, }; /// Catch all error for mobile crypto operations. @@ -38,8 +38,6 @@ pub enum CryptoClientError { #[error(transparent)] NotAuthenticated(#[from] NotAuthenticatedError), #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), #[error(transparent)] PasswordProtectedKeyEnvelope(#[from] PasswordProtectedKeyEnvelopeError), @@ -444,8 +442,6 @@ fn derive_pin_protected_user_key( #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum EnrollAdminPasswordResetError { - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), } diff --git a/crates/bitwarden-core/src/lib.rs b/crates/bitwarden-core/src/lib.rs index a43745dea..95541ec3f 100644 --- a/crates/bitwarden-core/src/lib.rs +++ b/crates/bitwarden-core/src/lib.rs @@ -12,8 +12,7 @@ pub mod client; mod error; pub mod key_management; pub use error::{ - ApiError, MissingFieldError, MissingPrivateKeyError, NotAuthenticatedError, VaultLockedError, - WrongPasswordError, + ApiError, MissingFieldError, MissingPrivateKeyError, NotAuthenticatedError, WrongPasswordError, }; #[cfg(feature = "internal")] pub mod mobile; diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 8de1702cd..74c968d55 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -7,7 +7,7 @@ use bitwarden_encoding::B64; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{key_management::AsymmetricKeyId, MissingPrivateKeyError, VaultLockedError}; +use crate::{key_management::AsymmetricKeyId, MissingPrivateKeyError}; /// Request to generate a fingerprint. #[derive(Serialize, Deserialize, Debug)] @@ -52,8 +52,6 @@ pub enum UserFingerprintError { #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] MissingPrivateKey(#[from] MissingPrivateKeyError), } diff --git a/crates/bitwarden-exporters/src/error.rs b/crates/bitwarden-exporters/src/error.rs index 062d7a9de..5f7f2c101 100644 --- a/crates/bitwarden-exporters/src/error.rs +++ b/crates/bitwarden-exporters/src/error.rs @@ -9,8 +9,6 @@ pub enum ExportError { MissingField(#[from] bitwarden_core::MissingFieldError), #[error(transparent)] NotAuthenticated(#[from] bitwarden_core::NotAuthenticatedError), - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), #[error("CSV error: {0}")] Csv(#[from] crate::csv::CsvError), diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index ba1972d09..d0496da3e 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use bitwarden_core::{Client, VaultLockedError}; +use bitwarden_core::Client; use bitwarden_crypto::CryptoError; use bitwarden_vault::{CipherError, CipherView, EncryptionContext}; use itertools::Itertools; @@ -30,8 +30,6 @@ pub enum GetSelectedCredentialError { #[error("No fido2 credentials found")] NoCredentialFound, - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), #[error(transparent)] CryptoError(#[from] CryptoError), } @@ -74,8 +72,6 @@ pub enum SilentlyDiscoverCredentialsError { #[error(transparent)] CipherError(#[from] CipherError), #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] InvalidGuid(#[from] InvalidGuid), #[error(transparent)] Fido2CallbackError(#[from] Fido2CallbackError), @@ -89,8 +85,6 @@ pub enum CredentialsForAutofillError { #[error(transparent)] CipherError(#[from] CipherError), #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] InvalidGuid(#[from] InvalidGuid), #[error(transparent)] Fido2CallbackError(#[from] Fido2CallbackError), @@ -359,13 +353,11 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { #[derive(Debug, Error)] enum InnerError { #[error(transparent)] - VaultLocked(#[from] VaultLockedError), + Cipher(#[from] CipherError), #[error(transparent)] - CipherError(#[from] CipherError), + Crypto(#[from] CryptoError), #[error(transparent)] - CryptoError(#[from] CryptoError), - #[error(transparent)] - Fido2CallbackError(#[from] Fido2CallbackError), + Fido2Callback(#[from] Fido2CallbackError), } // This is just a wrapper around the actual implementation to allow for ? error handling @@ -443,8 +435,6 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { #[error("Client User Id has not been set")] MissingUserId, #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] FillCredentialError(#[from] FillCredentialError), #[error(transparent)] CipherError(#[from] CipherError), @@ -521,8 +511,6 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { #[error("Client User Id has not been set")] MissingUserId, #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] InvalidGuid(#[from] InvalidGuid), #[error("Credential ID does not match selected credential")] CredentialIdMismatch, diff --git a/crates/bitwarden-fido/src/client_fido.rs b/crates/bitwarden-fido/src/client_fido.rs index 49af1d10b..5c935ae2e 100644 --- a/crates/bitwarden-fido/src/client_fido.rs +++ b/crates/bitwarden-fido/src/client_fido.rs @@ -16,8 +16,6 @@ pub struct ClientFido2 { #[allow(missing_docs)] #[derive(Debug, Error)] pub enum DecryptFido2AutofillCredentialsError { - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), #[error(transparent)] Fido2CredentialAutofillViewError(#[from] Fido2CredentialAutofillViewError), } diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index f703f47f4..1cd7edbe6 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -14,8 +14,6 @@ use crate::{Send, SendListView, SendView}; pub enum SendEncryptError { #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), } /// Generic error type for send decryption errors @@ -24,8 +22,6 @@ pub enum SendEncryptError { pub enum SendDecryptError { #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), } /// Generic error type for send encryption errors. diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 36ecec937..f2c1e5009 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -2,7 +2,7 @@ use bitwarden_api_api::models::{CipherDetailsResponseModel, CipherResponseModel} use bitwarden_collections::collection::CollectionId; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, - require, MissingFieldError, OrganizationId, UserId, VaultLockedError, + require, MissingFieldError, OrganizationId, UserId, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, @@ -42,8 +42,6 @@ pub enum CipherError { #[error(transparent)] MissingFieldError(#[from] MissingFieldError), #[error(transparent)] - VaultLocked(#[from] VaultLockedError), - #[error(transparent)] CryptoError(#[from] CryptoError), #[error(transparent)] EncryptError(#[from] EncryptError), diff --git a/crates/bitwarden-vault/src/error.rs b/crates/bitwarden-vault/src/error.rs index e014da04a..c610cf63a 100644 --- a/crates/bitwarden-vault/src/error.rs +++ b/crates/bitwarden-vault/src/error.rs @@ -8,8 +8,6 @@ use thiserror::Error; pub enum EncryptError { #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), #[error("Client User Id has not been set")] MissingUserId, } @@ -21,8 +19,6 @@ pub enum EncryptError { pub enum DecryptError { #[error(transparent)] Crypto(#[from] bitwarden_crypto::CryptoError), - #[error(transparent)] - VaultLocked(#[from] bitwarden_core::VaultLockedError), } #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/totp.rs b/crates/bitwarden-vault/src/totp.rs index 446379631..d71f7edc3 100644 --- a/crates/bitwarden-vault/src/totp.rs +++ b/crates/bitwarden-vault/src/totp.rs @@ -4,7 +4,7 @@ use std::{ str::FromStr, }; -use bitwarden_core::{key_management::KeyIds, VaultLockedError}; +use bitwarden_core::key_management::KeyIds; use bitwarden_crypto::{CryptoError, KeyStoreContext}; use bitwarden_error::bitwarden_error; use chrono::{DateTime, Utc}; @@ -41,8 +41,6 @@ pub enum TotpError { #[error(transparent)] CryptoError(#[from] CryptoError), - #[error(transparent)] - VaultLocked(#[from] VaultLockedError), } #[allow(missing_docs)] From 09af3cb23c8b2ba4d163c4f18ad15aa360ab6200 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 15 Sep 2025 16:49:32 -0400 Subject: [PATCH 11/60] Set default values --- crates/bitwarden-vault/src/cipher/cipher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index f2c1e5009..41e4395af 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -846,8 +846,8 @@ impl TryFrom for Cipher { .reprompt .map(|r| r.into()) .unwrap_or(CipherRepromptType::None), - organization_use_totp: cipher.organization_use_totp.unwrap_or(true), - edit: cipher.edit.unwrap_or(true), + organization_use_totp: cipher.organization_use_totp.unwrap_or(false), + edit: cipher.edit.unwrap_or(false), // TODO: add permissions permissions: None, view_password: cipher.view_password.unwrap_or(true), From 298b336ebf10a1ac2644be3a1ddb7340de328195 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 15 Sep 2025 16:57:45 -0400 Subject: [PATCH 12/60] Added todo's --- crates/bitwarden-vault/src/cipher/cipher.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 41e4395af..9cb5d7819 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -839,8 +839,7 @@ impl TryFrom for Cipher { identity: cipher.identity.map(|i| (*i).try_into()).transpose()?, card: cipher.card.map(|c| (*c).try_into()).transpose()?, secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?, - // TODO: add ssh_key - ssh_key: None, + ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?, favorite: cipher.favorite.unwrap_or(false), reprompt: cipher .reprompt @@ -848,8 +847,7 @@ impl TryFrom for Cipher { .unwrap_or(CipherRepromptType::None), organization_use_totp: cipher.organization_use_totp.unwrap_or(false), edit: cipher.edit.unwrap_or(false), - // TODO: add permissions - permissions: None, + permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?, view_password: cipher.view_password.unwrap_or(true), local_data: None, // Not sent from server attachments: cipher @@ -868,6 +866,7 @@ impl TryFrom for Cipher { deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, revision_date: require!(cipher.revision_date).parse()?, key: EncString::try_from_optional(cipher.key)?, + archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?, }) } } From 841f40ab2581e192f2197a60969ec6b759a84701 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 16 Sep 2025 12:23:37 -0400 Subject: [PATCH 13/60] Added archived date to request and response --- crates/bitwarden-vault/src/cipher/create.rs | 3 +++ crates/bitwarden-vault/src/cipher/edit.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/create.rs b/crates/bitwarden-vault/src/cipher/create.rs index 7dad15f40..1b3fce455 100644 --- a/crates/bitwarden-vault/src/cipher/create.rs +++ b/crates/bitwarden-vault/src/cipher/create.rs @@ -91,6 +91,7 @@ impl CompositeEncryptable for Cipher .map(|note| Box::new(note.into())), ssh_key: encrypted_cipher.ssh_key.map(|key| Box::new(key.into())), last_known_revision_date: Some(encrypted_cipher.revision_date.to_rfc3339()), + archived_date: None, }; Ok(cipher_request) @@ -173,6 +174,7 @@ mod tests { creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), deleted_date: None, revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + archived_date: None, } } @@ -224,6 +226,7 @@ mod tests { attachments: None, permissions: None, data: None, + archived_date: None, }) }) .expect(1)]) diff --git a/crates/bitwarden-vault/src/cipher/edit.rs b/crates/bitwarden-vault/src/cipher/edit.rs index ec756d280..9809e81a4 100644 --- a/crates/bitwarden-vault/src/cipher/edit.rs +++ b/crates/bitwarden-vault/src/cipher/edit.rs @@ -112,6 +112,7 @@ mod tests { creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), deleted_date: None, revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + archived_date: None, } } @@ -167,6 +168,7 @@ mod tests { creation_date: "2024-01-01T00:00:00Z".parse().unwrap(), deleted_date: None, revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), + archived_date: None, }, ) .await @@ -224,6 +226,7 @@ mod tests { attachments: None, permissions: None, data: None, + archived_date: None, }) }) .expect(1)]) From 9d27d7fd9368a7f91f9cb3afc5402571a5527db8 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 16 Sep 2025 15:06:41 -0400 Subject: [PATCH 14/60] Removed error --- crates/bitwarden-vault/src/cipher/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/create.rs b/crates/bitwarden-vault/src/cipher/create.rs index 1b3fce455..01b50555c 100644 --- a/crates/bitwarden-vault/src/cipher/create.rs +++ b/crates/bitwarden-vault/src/cipher/create.rs @@ -28,7 +28,7 @@ pub enum CreateCipherError { #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] - RepositoryError(#[from] RepositoryError), + Repository(#[from] RepositoryError), } /// Request to add or edit a cipher. From d219627ad7854acf7900c03be0386decf1ccbf45 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 16 Sep 2025 16:47:48 -0400 Subject: [PATCH 15/60] Added list with failures --- crates/bitwarden-vault/src/cipher/cipher_client.rs | 12 +++++++++++- crates/bitwarden-vault/src/cipher/get_list.rs | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index ff6e59847..f13267966 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -13,7 +13,7 @@ use crate::{ cipher::cipher::DecryptCipherListResult, create::{create_cipher, CipherAddEditRequest, CreateCipherError}, edit::{edit_cipher, EditCipherError}, - get_list::{get_cipher, list_ciphers, GetCipherError}, + get_list::{get_cipher, list_ciphers, list_ciphers_with_failures, GetCipherError}, Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError, Fido2CredentialFullView, }; @@ -190,6 +190,16 @@ impl CiphersClient { list_ciphers(key_store, repository.as_ref()).await } + /// Get all ciphers from state and decrypt them, returning both successes and failures. + /// This method will not fail when some ciphers fail to decrypt, allowing for graceful + /// handling of corrupted or problematic cipher data. + pub async fn list_with_failures(&self) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_ciphers_with_failures(key_store, repository.as_ref()).await + } + /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. pub async fn get(&self, cipher_id: &str) -> Result { let key_store = self.client.internal.get_key_store(); diff --git a/crates/bitwarden-vault/src/cipher/get_list.rs b/crates/bitwarden-vault/src/cipher/get_list.rs index fb5fb0178..432e79c2b 100644 --- a/crates/bitwarden-vault/src/cipher/get_list.rs +++ b/crates/bitwarden-vault/src/cipher/get_list.rs @@ -4,7 +4,7 @@ use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; -use crate::{Cipher, CipherView, ItemNotFoundError}; +use crate::{Cipher, CipherView, DecryptCipherListResult, ItemNotFoundError}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -39,3 +39,15 @@ pub(super) async fn list_ciphers( let views = store.decrypt_list(&ciphers)?; Ok(views) } + +pub(super) async fn list_ciphers_with_failures( + store: &KeyStore, + repository: &dyn Repository, +) -> Result { + let ciphers = repository.list().await?; + let (successes, failures) = store.decrypt_list_with_failures(&ciphers); + Ok(DecryptCipherListResult { + successes, + failures: failures.into_iter().cloned().collect(), + }) +} From c1fcadc6f73c49635d56a299d109da48758a1e20 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 16 Sep 2025 16:55:33 -0400 Subject: [PATCH 16/60] fixed lint issues --- crates/bitwarden-vault/src/cipher/create.rs | 3 +-- crates/bitwarden-vault/src/cipher/edit.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/create.rs b/crates/bitwarden-vault/src/cipher/create.rs index 01b50555c..e3d331a5f 100644 --- a/crates/bitwarden-vault/src/cipher/create.rs +++ b/crates/bitwarden-vault/src/cipher/create.rs @@ -131,9 +131,8 @@ mod tests { use bitwarden_test::{start_api_mock, MemoryRepository}; use wiremock::{matchers, Mock, Request, ResponseTemplate}; - use crate::{CipherId, CipherRepromptType, CipherType, LoginView}; - use super::*; + use crate::{CipherId, CipherRepromptType, CipherType, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; diff --git a/crates/bitwarden-vault/src/cipher/edit.rs b/crates/bitwarden-vault/src/cipher/edit.rs index 9809e81a4..9beed6d43 100644 --- a/crates/bitwarden-vault/src/cipher/edit.rs +++ b/crates/bitwarden-vault/src/cipher/edit.rs @@ -69,9 +69,8 @@ mod tests { use bitwarden_test::{start_api_mock, MemoryRepository}; use wiremock::{matchers, Mock, Request, ResponseTemplate}; - use crate::{Cipher, CipherId, CipherRepromptType, CipherType, Login, LoginView}; - use super::*; + use crate::{Cipher, CipherId, CipherRepromptType, CipherType, Login, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; From 2b8491ac10e69b8df46339e00627b4fa5dd2394d Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Tue, 16 Sep 2025 17:16:26 -0400 Subject: [PATCH 17/60] removed error --- crates/bitwarden-vault/src/cipher/edit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/edit.rs b/crates/bitwarden-vault/src/cipher/edit.rs index 9beed6d43..7b2ed6dc8 100644 --- a/crates/bitwarden-vault/src/cipher/edit.rs +++ b/crates/bitwarden-vault/src/cipher/edit.rs @@ -22,7 +22,7 @@ pub enum EditCipherError { #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] - RepositoryError(#[from] RepositoryError), + Repository(#[from] RepositoryError), #[error(transparent)] Uuid(#[from] uuid::Error), } From a0bca6b85e548b489788eac442cfa7f4dfd4d146 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 18 Sep 2025 14:11:01 -0400 Subject: [PATCH 18/60] restructured cipher client to sub folders --- .../src/cipher/{ => cipher_client}/create.rs | 19 ++++- .../src/cipher/{ => cipher_client}/edit.rs | 32 +++++++- .../src/cipher/cipher_client/get.rs | 82 +++++++++++++++++++ .../mod.rs} | 74 ++--------------- crates/bitwarden-vault/src/cipher/get_list.rs | 53 ------------ crates/bitwarden-vault/src/cipher/mod.rs | 3 - 6 files changed, 136 insertions(+), 127 deletions(-) rename crates/bitwarden-vault/src/cipher/{ => cipher_client}/create.rs (94%) rename crates/bitwarden-vault/src/cipher/{ => cipher_client}/edit.rs (92%) create mode 100644 crates/bitwarden-vault/src/cipher/cipher_client/get.rs rename crates/bitwarden-vault/src/cipher/{cipher_client.rs => cipher_client/mod.rs} (88%) delete mode 100644 crates/bitwarden-vault/src/cipher/get_list.rs diff --git a/crates/bitwarden-vault/src/cipher/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs similarity index 94% rename from crates/bitwarden-vault/src/cipher/create.rs rename to crates/bitwarden-vault/src/cipher/cipher_client/create.rs index e3d331a5f..55618c2e1 100644 --- a/crates/bitwarden-vault/src/cipher/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -13,6 +13,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; +use super::CiphersClient; use crate::{Cipher, CipherView, VaultParseError}; #[allow(missing_docs)] @@ -104,7 +105,7 @@ impl IdentifyKey for CipherAddEditRequest { } } -pub(super) async fn create_cipher + ?Sized>( +async fn create_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, @@ -124,6 +125,22 @@ pub(super) async fn create_cipher + ?Sized>( Ok(key_store.decrypt(&cipher)?) } +impl CiphersClient { + /// Create a new [Cipher] and save it to the server. + pub async fn create( + &self, + mut request: CipherAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + request.encrypted_for = self.client.internal.get_user_id(); + + create_cipher(key_store, &config.api, repository.as_ref(), request).await + } +} + #[cfg(test)] mod tests { use bitwarden_api_api::models::CipherResponseModel; diff --git a/crates/bitwarden-vault/src/cipher/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs similarity index 92% rename from crates/bitwarden-vault/src/cipher/edit.rs rename to crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 7b2ed6dc8..d9996eb25 100644 --- a/crates/bitwarden-vault/src/cipher/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -5,7 +5,11 @@ use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; -use crate::{create::CipherAddEditRequest, Cipher, CipherView, ItemNotFoundError, VaultParseError}; +use super::CiphersClient; +use crate::{ + cipher_client::create::CipherAddEditRequest, Cipher, CipherView, ItemNotFoundError, + VaultParseError, +}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -27,7 +31,7 @@ pub enum EditCipherError { Uuid(#[from] uuid::Error), } -pub(super) async fn edit_cipher + ?Sized>( +async fn edit_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, @@ -58,6 +62,30 @@ pub(super) async fn edit_cipher + ?Sized>( Ok(key_store.decrypt(&cipher)?) } +impl CiphersClient { + /// Edit an existing [Cipher] and save it to the server. + pub async fn edit( + &self, + cipher_id: &str, + mut request: CipherAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + request.encrypted_for = self.client.internal.get_user_id(); + + edit_cipher( + key_store, + &config.api, + repository.as_ref(), + cipher_id, + request, + ) + .await + } +} + #[cfg(test)] mod tests { use bitwarden_api_api::{ diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs new file mode 100644 index 000000000..73b09b625 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs @@ -0,0 +1,82 @@ +use bitwarden_core::key_management::KeyIds; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; + +use super::CiphersClient; +use crate::{cipher::cipher::DecryptCipherListResult, Cipher, CipherView, ItemNotFoundError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum GetCipherError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +async fn get_cipher( + store: &KeyStore, + repository: &dyn Repository, + id: &str, +) -> Result { + let cipher = repository + .get(id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(store.decrypt(&cipher)?) +} + +async fn list_ciphers( + store: &KeyStore, + repository: &dyn Repository, +) -> Result, GetCipherError> { + let ciphers = repository.list().await?; + let views = store.decrypt_list(&ciphers)?; + Ok(views) +} + +async fn list_ciphers_with_failures( + store: &KeyStore, + repository: &dyn Repository, +) -> Result { + let ciphers = repository.list().await?; + let (successes, failures) = store.decrypt_list_with_failures(&ciphers); + Ok(DecryptCipherListResult { + successes, + failures: failures.into_iter().cloned().collect(), + }) +} + +impl CiphersClient { + /// Get all ciphers from state and decrypt them to a list of [CipherView]. + pub async fn list(&self) -> Result, GetCipherError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_ciphers(key_store, repository.as_ref()).await + } + + /// Get all ciphers from state and decrypt them, returning both successes and failures. + /// This method will not fail when some ciphers fail to decrypt, allowing for graceful + /// handling of corrupted or problematic cipher data. + pub async fn list_with_failures(&self) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_ciphers_with_failures(key_store, repository.as_ref()).await + } + + /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. + pub async fn get(&self, cipher_id: &str) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_cipher(key_store, repository.as_ref(), cipher_id).await + } +} diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs similarity index 88% rename from crates/bitwarden-vault/src/cipher/cipher_client.rs rename to crates/bitwarden-vault/src/cipher/cipher_client/mod.rs index f13267966..8449e8536 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs @@ -10,14 +10,14 @@ use wasm_bindgen::prelude::*; use super::EncryptionContext; use crate::{ - cipher::cipher::DecryptCipherListResult, - create::{create_cipher, CipherAddEditRequest, CreateCipherError}, - edit::{edit_cipher, EditCipherError}, - get_list::{get_cipher, list_ciphers, list_ciphers_with_failures, GetCipherError}, - Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError, - Fido2CredentialFullView, + cipher::cipher::DecryptCipherListResult, Cipher, CipherError, CipherListView, CipherView, + DecryptError, EncryptError, Fido2CredentialFullView, }; +pub mod create; +pub mod edit; +pub mod get; + #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct CiphersClient { @@ -181,68 +181,6 @@ impl CiphersClient { let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?; Ok(decrypted_key) } - - /// Get all ciphers from state and decrypt them to a list of [CipherView]. - pub async fn list(&self) -> Result, GetCipherError> { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - list_ciphers(key_store, repository.as_ref()).await - } - - /// Get all ciphers from state and decrypt them, returning both successes and failures. - /// This method will not fail when some ciphers fail to decrypt, allowing for graceful - /// handling of corrupted or problematic cipher data. - pub async fn list_with_failures(&self) -> Result { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - list_ciphers_with_failures(key_store, repository.as_ref()).await - } - - /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. - pub async fn get(&self, cipher_id: &str) -> Result { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - get_cipher(key_store, repository.as_ref(), cipher_id).await - } - - /// Create a new [Cipher] and save it to the server. - pub async fn create( - &self, - mut request: CipherAddEditRequest, - ) -> Result { - let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations().await; - let repository = self.get_repository()?; - - request.encrypted_for = self.client.internal.get_user_id(); - - create_cipher(key_store, &config.api, repository.as_ref(), request).await - } - - /// Edit an existing [Cipher] and save it to the server. - pub async fn edit( - &self, - cipher_id: &str, - mut request: CipherAddEditRequest, - ) -> Result { - let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations().await; - let repository = self.get_repository()?; - - request.encrypted_for = self.client.internal.get_user_id(); - - edit_cipher( - key_store, - &config.api, - repository.as_ref(), - cipher_id, - request, - ) - .await - } } impl CiphersClient { diff --git a/crates/bitwarden-vault/src/cipher/get_list.rs b/crates/bitwarden-vault/src/cipher/get_list.rs deleted file mode 100644 index 432e79c2b..000000000 --- a/crates/bitwarden-vault/src/cipher/get_list.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bitwarden_core::key_management::KeyIds; -use bitwarden_crypto::{CryptoError, KeyStore}; -use bitwarden_error::bitwarden_error; -use bitwarden_state::repository::{Repository, RepositoryError}; -use thiserror::Error; - -use crate::{Cipher, CipherView, DecryptCipherListResult, ItemNotFoundError}; - -#[allow(missing_docs)] -#[bitwarden_error(flat)] -#[derive(Debug, Error)] -pub enum GetCipherError { - #[error(transparent)] - ItemNotFound(#[from] ItemNotFoundError), - #[error(transparent)] - Crypto(#[from] CryptoError), - #[error(transparent)] - RepositoryError(#[from] RepositoryError), -} - -pub(super) async fn get_cipher( - store: &KeyStore, - repository: &dyn Repository, - id: &str, -) -> Result { - let cipher = repository - .get(id.to_string()) - .await? - .ok_or(ItemNotFoundError)?; - - Ok(store.decrypt(&cipher)?) -} - -pub(super) async fn list_ciphers( - store: &KeyStore, - repository: &dyn Repository, -) -> Result, GetCipherError> { - let ciphers = repository.list().await?; - let views = store.decrypt_list(&ciphers)?; - Ok(views) -} - -pub(super) async fn list_ciphers_with_failures( - store: &KeyStore, - repository: &dyn Repository, -) -> Result { - let ciphers = repository.list().await?; - let (successes, failures) = store.decrypt_list_with_failures(&ciphers); - Ok(DecryptCipherListResult { - successes, - failures: failures.into_iter().cloned().collect(), - }) -} diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 9afaf3bec..39fe85361 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -5,10 +5,7 @@ pub(crate) mod card; pub(crate) mod cipher; pub(crate) mod cipher_client; pub(crate) mod cipher_permissions; -pub(crate) mod create; -pub(crate) mod edit; pub(crate) mod field; -pub(crate) mod get_list; pub(crate) mod identity; pub(crate) mod linked_id; pub(crate) mod local_data; From 0f35f92a05d53ec06ca9b6cca4824cee44c1d8b7 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 22 Sep 2025 09:29:15 -0400 Subject: [PATCH 19/60] Reafctored create to use cipher create request and rmoved cipher view usage Updated edit to handle password history --- .../src/cipher/cipher_client/create.rs | 179 ++++----- .../src/cipher/cipher_client/edit.rs | 339 ++++++++++++++++-- .../bitwarden-vault/src/password_history.rs | 36 +- 3 files changed, 429 insertions(+), 125 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 55618c2e1..4e0f847a0 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -1,9 +1,11 @@ use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, - require, ApiError, MissingFieldError, UserId, + require, ApiError, MissingFieldError, OrganizationId, UserId, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, }; -use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use serde::{Deserialize, Serialize}; @@ -14,7 +16,10 @@ use tsify::Tsify; use wasm_bindgen::prelude::*; use super::CiphersClient; -use crate::{Cipher, CipherView, VaultParseError}; +use crate::{ + CardView, Cipher, CipherRepromptType, CipherType, CipherView, FieldView, FolderId, + IdentityView, LoginView, SecureNoteView, SshKeyView, VaultParseError, +}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -37,61 +42,89 @@ pub enum CreateCipherError { #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct CipherAddEditRequest { - /// The cipher view data to be added or edited. - pub cipher: CipherView, - /// The user ID for whom this cipher is encrypted (internal use only). +pub struct CipherCreateRequest { pub encrypted_for: Option, + pub organization_id: Option, + pub folder_id: Option, + pub name: String, + pub notes: Option, + pub r#type: CipherType, + pub favorite: bool, + pub reprompt: CipherRepromptType, + pub login: Option, + pub identity: Option, + pub card: Option, + pub secure_note: Option, + pub ssh_key: Option, + pub fields: Option>, } -impl CompositeEncryptable for CipherAddEditRequest { +impl CompositeEncryptable for CipherCreateRequest { fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - let encrypted_cipher = self.cipher.encrypt_composite(ctx, key)?; - let cipher_request = CipherRequestModel { encrypted_for: self.encrypted_for.map(|id| id.into()), - r#type: Some(encrypted_cipher.r#type.into()), - organization_id: encrypted_cipher.organization_id.map(|id| id.to_string()), - folder_id: encrypted_cipher.folder_id.map(|id| id.to_string()), - favorite: Some(encrypted_cipher.favorite), - reprompt: Some(encrypted_cipher.reprompt.into()), - key: encrypted_cipher.key.map(|k| k.to_string()), - name: encrypted_cipher.name.to_string(), - notes: encrypted_cipher.notes.map(|n| n.to_string()), - fields: encrypted_cipher + r#type: Some(self.r#type.into()), + organization_id: self.organization_id.map(|id| id.to_string()), + folder_id: self.folder_id.map(|id| id.to_string()), + favorite: Some(self.favorite), + reprompt: Some(self.reprompt.into()), + key: None, + name: self.name.encrypt(ctx, key)?.to_string(), + notes: self + .notes + .as_ref() + .map(|n| n.encrypt(ctx, key)) + .transpose()? + .map(|n| n.to_string()), + login: self + .login + .as_ref() + .map(|l| l.encrypt_composite(ctx, key)) + .transpose()? + .map(|l| Box::new(l.into())), + card: self + .card + .as_ref() + .map(|c| c.encrypt_composite(ctx, key)) + .transpose()? + .map(|c| Box::new(c.into())), + identity: self + .identity + .as_ref() + .map(|i| i.encrypt_composite(ctx, key)) + .transpose()? + .map(|i| Box::new(i.into())), + secure_note: self + .secure_note + .as_ref() + .map(|s| s.encrypt_composite(ctx, key)) + .transpose()? + .map(|s| Box::new(s.into())), + ssh_key: self + .ssh_key + .as_ref() + .map(|s| s.encrypt_composite(ctx, key)) + .transpose()? + .map(|s| Box::new(s.into())), + fields: self .fields + .as_ref() + .map(|fields| { + fields + .iter() + .map(|f| f.encrypt_composite(ctx, key)) + .collect::, _>>() + }) + .transpose()? .map(|f| f.into_iter().map(|f| f.into()).collect()), - password_history: encrypted_cipher - .password_history - .map(|ph| ph.into_iter().map(|ph| ph.into()).collect()), + password_history: None, attachments: None, - attachments2: encrypted_cipher.attachments.map(|a| { - a.into_iter() - .filter_map(|a| { - a.id.map(|id| { - ( - id, - bitwarden_api_api::models::CipherAttachmentModel { - file_name: a.file_name.map(|n| n.to_string()), - key: a.key.map(|k| k.to_string()), - }, - ) - }) - }) - .collect() - }), - login: encrypted_cipher.login.map(|l| Box::new(l.into())), - card: encrypted_cipher.card.map(|c| Box::new(c.into())), - identity: encrypted_cipher.identity.map(|i| Box::new(i.into())), - secure_note: encrypted_cipher - .secure_note - .map(|note| Box::new(note.into())), - ssh_key: encrypted_cipher.ssh_key.map(|key| Box::new(key.into())), - last_known_revision_date: Some(encrypted_cipher.revision_date.to_rfc3339()), + attachments2: None, + last_known_revision_date: None, archived_date: None, }; @@ -99,9 +132,12 @@ impl CompositeEncryptable for Cipher } } -impl IdentifyKey for CipherAddEditRequest { +impl IdentifyKey for CipherCreateRequest { fn key_identifier(&self) -> SymmetricKeyId { - self.cipher.key_identifier() + match self.organization_id { + Some(organization_id) => SymmetricKeyId::Organization(organization_id), + None => SymmetricKeyId::User, + } } } @@ -109,19 +145,16 @@ async fn create_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, - request: CipherAddEditRequest, + request: CipherCreateRequest, ) -> Result { let cipher_request = key_store.encrypt(request)?; let resp = ciphers_api::ciphers_post(api_config, Some(cipher_request)) .await .map_err(ApiError::from)?; - let cipher: Cipher = resp.try_into()?; - repository .set(require!(cipher.id).to_string(), cipher.clone()) .await?; - Ok(key_store.decrypt(&cipher)?) } @@ -129,14 +162,13 @@ impl CiphersClient { /// Create a new [Cipher] and save it to the server. pub async fn create( &self, - mut request: CipherAddEditRequest, + mut request: CipherCreateRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; request.encrypted_for = self.client.internal.get_user_id(); - create_cipher(key_store, &config.api, repository.as_ref(), request).await } } @@ -152,18 +184,17 @@ mod tests { use crate::{CipherId, CipherRepromptType, CipherType, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; - const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; - fn generate_test_cipher() -> CipherView { - CipherView { - id: None, + fn generate_test_cipher_create_request() -> CipherCreateRequest { + CipherCreateRequest { + encrypted_for: None, organization_id: None, folder_id: None, - collection_ids: vec![], - key: None, name: "Test Login".to_string(), notes: Some("Test notes".to_string()), r#type: CipherType::Login, + favorite: false, + reprompt: CipherRepromptType::None, login: Some(LoginView { username: Some("test@example.com".to_string()), password: Some("password123".to_string()), @@ -177,20 +208,7 @@ mod tests { card: None, secure_note: None, ssh_key: None, - favorite: false, - reprompt: CipherRepromptType::None, - organization_use_totp: true, - edit: true, - permissions: None, - view_password: true, - local_data: None, - attachments: None, fields: None, - password_history: None, - creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), - deleted_date: None, - revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), - archived_date: None, } } @@ -204,7 +222,6 @@ mod tests { ); let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); - let user_id: UserId = TEST_USER_ID.parse().unwrap(); let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/ciphers")) .respond_with(move |req: &Request| { @@ -249,12 +266,7 @@ mod tests { .await; let repository = MemoryRepository::::default(); - let cipher_view = generate_test_cipher(); - - let request = CipherAddEditRequest { - cipher: cipher_view.clone(), - encrypted_for: Some(user_id), - }; + let request = generate_test_cipher_create_request(); let result = create_cipher(&store, &api_config, &repository, request) .await @@ -285,7 +297,6 @@ mod tests { .unwrap(), ) .unwrap(); - assert_eq!(stored_cipher_view.id, result.id); assert_eq!(stored_cipher_view.name, result.name); assert_eq!(stored_cipher_view.r#type, result.r#type); @@ -301,19 +312,13 @@ mod tests { SymmetricKeyId::User, SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); - let (_server, api_config) = start_api_mock(vec![ Mock::given(matchers::path("/ciphers")).respond_with(ResponseTemplate::new(500)) ]) .await; - let repository = MemoryRepository::::default(); - let cipher_view = generate_test_cipher(); - let request = CipherAddEditRequest { - cipher: cipher_view, - encrypted_for: None, - }; + let request = generate_test_cipher_create_request(); let result = create_cipher(&store, &api_config, &repository, request).await; diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 0beb2a196..5a3e58d5d 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -1,16 +1,27 @@ -use bitwarden_api_api::apis::ciphers_api; -use bitwarden_core::{key_management::KeyIds, ApiError, MissingFieldError}; -use bitwarden_crypto::{CryptoError, KeyStore}; +use std::collections::HashMap; + +use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; +use bitwarden_core::{ + key_management::{KeyIds, SymmetricKeyId}, + ApiError, MissingFieldError, UserId, +}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; use thiserror::Error; +use tsify::Tsify; use super::CiphersClient; use crate::{ - cipher_client::create::CipherAddEditRequest, Cipher, CipherView, ItemNotFoundError, - VaultParseError, + password_history::PasswordChange, Cipher, CipherId, CipherType, CipherView, FieldType, + FieldView, ItemNotFoundError, PasswordHistoryView, VaultParseError, }; +/// Maximum number of password history entries to retain +const MAX_PASSWORD_HISTORY_ENTRIES: usize = 5; + #[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, Error)] @@ -31,28 +42,200 @@ pub enum EditCipherError { Uuid(#[from] uuid::Error), } +/// Request to edit a cipher. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct CipherEditRequest { + /// The cipher view data to be added or edited. + pub cipher: CipherView, + /// The user ID for whom this cipher is encrypted (internal use only). + pub encrypted_for: Option, +} + +impl CipherEditRequest { + pub fn update_password_history(&mut self, original_cipher: &CipherView) { + let mut history = original_cipher.password_history.clone().unwrap_or_default(); + + let mut changes = Vec::new(); + + if let Some(login_changes) = self.detect_login_password_changes(original_cipher) { + changes.extend(login_changes); + } + + changes.extend(self.detect_hidden_field_changes(original_cipher)); + + for change in changes.into_iter().rev() { + history.insert(0, change.into_history_entry()); + } + + Self::limit_history_length(&mut history); + + self.cipher.password_history = (!history.is_empty()).then_some(history); + } + + fn detect_login_password_changes( + &mut self, + original_cipher: &CipherView, + ) -> Option> { + if self.cipher.r#type != CipherType::Login || original_cipher.r#type != CipherType::Login { + return None; + } + + let original_login = original_cipher.login.as_ref()?; + let current_login = self.cipher.login.as_mut()?; + + let original_password = original_login.password.as_deref().unwrap_or(""); + let current_password = current_login.password.as_deref().unwrap_or(""); + + if original_password.is_empty() { + // No original password - set revision date only if adding new password + if !current_password.is_empty() { + current_login.password_revision_date = Some(Utc::now()); + } + None + } else if original_password == current_password { + // Password unchanged - preserve original revision date + current_login.password_revision_date = original_login.password_revision_date; + None + } else { + // Password changed - update revision date and track change + current_login.password_revision_date = Some(Utc::now()); + Some(vec![PasswordChange::new_password(original_password)]) + } + } + + fn detect_hidden_field_changes(&self, original_cipher: &CipherView) -> Vec { + let original_fields = Self::extract_hidden_fields(&original_cipher.fields); + let current_fields = Self::extract_hidden_fields(&self.cipher.fields); + + original_fields + .into_iter() + .filter_map(|(field_name, original_value)| { + let current_value = current_fields.get(&field_name); + if current_value != Some(&original_value) { + Some(PasswordChange::new_field(&field_name, &original_value)) + } else { + None + } + }) + .collect() + } + + fn extract_hidden_fields(fields: &Option>) -> HashMap { + fields + .as_ref() + .map(|field_vec| { + field_vec + .iter() + .filter_map(|f| match (&f.r#type, &f.name, &f.value) { + (FieldType::Hidden, Some(name), Some(value)) + if !name.is_empty() && !value.is_empty() => + { + Some((name.clone(), value.clone())) + } + _ => None, + }) + .collect() + }) + .unwrap_or_default() + } + + fn limit_history_length(history: &mut Vec) { + if history.len() > MAX_PASSWORD_HISTORY_ENTRIES { + history.truncate(MAX_PASSWORD_HISTORY_ENTRIES); + } + } +} + +impl CompositeEncryptable for CipherEditRequest { + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + let encrypted_cipher = self.cipher.encrypt_composite(ctx, key)?; + + let cipher_request = CipherRequestModel { + encrypted_for: self.encrypted_for.map(|id| id.into()), + r#type: Some(encrypted_cipher.r#type.into()), + organization_id: encrypted_cipher.organization_id.map(|id| id.to_string()), + folder_id: encrypted_cipher.folder_id.map(|id| id.to_string()), + favorite: Some(encrypted_cipher.favorite), + reprompt: Some(encrypted_cipher.reprompt.into()), + key: encrypted_cipher.key.map(|k| k.to_string()), + name: encrypted_cipher.name.to_string(), + notes: encrypted_cipher.notes.map(|n| n.to_string()), + fields: encrypted_cipher + .fields + .map(|f| f.into_iter().map(|f| f.into()).collect()), + password_history: encrypted_cipher + .password_history + .map(|ph| ph.into_iter().map(|ph| ph.into()).collect()), + attachments: None, + attachments2: encrypted_cipher.attachments.map(|a| { + a.into_iter() + .filter_map(|a| { + a.id.map(|id| { + ( + id, + bitwarden_api_api::models::CipherAttachmentModel { + file_name: a.file_name.map(|n| n.to_string()), + key: a.key.map(|k| k.to_string()), + }, + ) + }) + }) + .collect() + }), + login: encrypted_cipher.login.map(|l| Box::new(l.into())), + card: encrypted_cipher.card.map(|c| Box::new(c.into())), + identity: encrypted_cipher.identity.map(|i| Box::new(i.into())), + secure_note: encrypted_cipher + .secure_note + .map(|note| Box::new(note.into())), + ssh_key: encrypted_cipher.ssh_key.map(|key| Box::new(key.into())), + last_known_revision_date: Some(encrypted_cipher.revision_date.to_rfc3339()), + archived_date: None, + }; + + Ok(cipher_request) + } +} + +impl IdentifyKey for CipherEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + self.cipher.key_identifier() + } +} + async fn edit_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, - cipher_id: &str, - request: CipherAddEditRequest, + cipher_id: CipherId, + mut request: CipherEditRequest, ) -> Result { - repository - .get(cipher_id.to_owned()) - .await? - .ok_or(ItemNotFoundError)?; + let id = cipher_id.to_string(); + repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + + let original_cipher = repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + let original_cipher_view: CipherView = key_store.decrypt(&original_cipher)?; + + // Update password history + request.update_password_history(&original_cipher_view); let cipher_request = key_store.encrypt(request)?; - let parsed_cipher_id = uuid::Uuid::parse_str(cipher_id)?; + let parsed_cipher_id = uuid::Uuid::parse_str(&id)?; let response = ciphers_api::ciphers_put(api_config, parsed_cipher_id, Some(cipher_request)) .await .map_err(ApiError::from)?; let cipher: Cipher = response.try_into()?; - debug_assert!(cipher.id.unwrap_or_default().to_string() == cipher_id); + debug_assert!(cipher.id.unwrap_or_default() == cipher_id); repository .set(cipher_id.to_string(), cipher.clone()) @@ -65,8 +248,8 @@ impl CiphersClient { /// Edit an existing [Cipher] and save it to the server. pub async fn edit( &self, - cipher_id: &str, - mut request: CipherAddEditRequest, + cipher_id: CipherId, + mut request: CipherEditRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; @@ -142,6 +325,14 @@ mod tests { } } + fn create_test_login_cipher(password: &str) -> CipherView { + let mut cipher_view = generate_test_cipher(); + if let Some(ref mut login) = cipher_view.login { + login.password = Some(password.to_string()); + } + cipher_view + } + async fn repository_add_cipher( repository: &MemoryRepository, store: &KeyStore, @@ -262,20 +453,14 @@ mod tests { repository_add_cipher(&repository, &store, cipher_id, "old_name").await; let cipher_view = generate_test_cipher(); - let request = CipherAddEditRequest { + let request = CipherEditRequest { cipher: cipher_view.clone(), encrypted_for: Some(user_id), }; - let result = edit_cipher( - &store, - &api_config, - &repository, - &cipher_id.to_string(), - request, - ) - .await - .unwrap(); + let result = edit_cipher(&store, &api_config, &repository, cipher_id, request) + .await + .unwrap(); assert_eq!(result.id, Some(cipher_id)); assert_eq!(result.name, "Test Login"); @@ -290,7 +475,7 @@ mod tests { let cipher_view = generate_test_cipher(); - let request = CipherAddEditRequest { + let request = CipherEditRequest { cipher: cipher_view.clone(), encrypted_for: None, }; @@ -299,7 +484,7 @@ mod tests { &store, &Configuration::default(), &repository, - &cipher_id.to_string(), + cipher_id, request, ) .await; @@ -333,21 +518,103 @@ mod tests { repository_add_cipher(&repository, &store, cipher_id, "old_name").await; let cipher_view = generate_test_cipher(); - let request = CipherAddEditRequest { + let request = CipherEditRequest { cipher: cipher_view.clone(), encrypted_for: None, }; - let result = edit_cipher( - &store, - &api_config, - &repository, - &cipher_id.to_string(), - request, - ) - .await; + let result = edit_cipher(&store, &api_config, &repository, cipher_id, request).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), EditCipherError::Api(_))); } + + #[test] + fn test_password_history_on_password_change() { + let original_cipher = create_test_login_cipher("old_password"); + let mut edit_request = CipherEditRequest { + cipher: create_test_login_cipher("new_password"), + encrypted_for: None, + }; + + edit_request.update_password_history(&original_cipher); + + let history = edit_request.cipher.password_history.unwrap(); + assert_eq!(history.len(), 1); + assert_eq!(history[0].password, "old_password"); + } + + #[test] + fn test_password_history_on_unchanged_password() { + let original_cipher = create_test_login_cipher("same_password"); + let mut edit_request = CipherEditRequest { + cipher: create_test_login_cipher("same_password"), + encrypted_for: None, + }; + + edit_request.update_password_history(&original_cipher); + + assert!(edit_request.cipher.password_history.is_none()); + } + + #[test] + fn test_password_history_with_hidden_fields() { + let mut original_cipher = create_test_login_cipher("password"); + original_cipher.fields = Some(vec![FieldView { + name: Some("Secret Key".to_string()), + value: Some("old_secret_value".to_string()), + r#type: FieldType::Hidden, + linked_id: None, + }]); + + let mut new_cipher = create_test_login_cipher("password"); + new_cipher.fields = Some(vec![FieldView { + name: Some("Secret Key".to_string()), + value: Some("new_secret_value".to_string()), + r#type: FieldType::Hidden, + linked_id: None, + }]); + + let mut edit_request = CipherEditRequest { + cipher: new_cipher, + encrypted_for: None, + }; + + edit_request.update_password_history(&original_cipher); + + let history = edit_request.cipher.password_history.unwrap(); + assert_eq!(history.len(), 1); + assert_eq!(history[0].password, "Secret Key: old_secret_value"); + } + + #[test] + fn test_password_history_length_limit() { + let mut original_cipher = create_test_login_cipher("password"); + original_cipher.password_history = Some( + (0..10) + .map(|i| PasswordHistoryView { + password: format!("old_password_{}", i), + last_used_date: Utc::now(), + }) + .collect(), + ); + + // Create edit request with new password (no existing history) + let mut edit_request = CipherEditRequest { + cipher: create_test_login_cipher("new_password"), + encrypted_for: None, + }; + + edit_request.update_password_history(&original_cipher); + + let history = edit_request.cipher.password_history.unwrap(); + assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES); + // Most recent change (original password) should be first + assert_eq!(history[0].password, "password"); + + assert_eq!(history[1].password, "old_password_0"); + assert_eq!(history[2].password, "old_password_1"); + assert_eq!(history[3].password, "old_password_2"); + assert_eq!(history[4].password, "old_password_3"); + } } diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index cdb407847..48a2c77a1 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -27,8 +27,8 @@ pub struct PasswordHistory { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct PasswordHistoryView { - password: String, - last_used_date: DateTime, + pub password: String, + pub last_used_date: DateTime, } impl IdentifyKey for PasswordHistory { @@ -87,3 +87,35 @@ impl From for CipherPasswordHistoryModel { } } } + +/// Tracks password and sensitive field changes for history purposes. +#[derive(Debug, Clone)] +pub struct PasswordChange { + /// Display name for the changed item + display_name: String, + /// When this change occurred + changed_at: DateTime, +} + +impl PasswordChange { + pub fn new_password(old_password: &str) -> Self { + Self { + display_name: old_password.to_string(), + changed_at: Utc::now(), + } + } + + pub fn new_field(field_name: &str, old_value: &str) -> Self { + Self { + display_name: format!("{field_name}: {old_value}"), + changed_at: Utc::now(), + } + } + + pub fn into_history_entry(self) -> PasswordHistoryView { + PasswordHistoryView { + password: self.display_name, + last_used_date: self.changed_at, + } + } +} From 5bffe2a24af55fb3eea7a7236c2f438a45b9acf0 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 22 Sep 2025 09:40:08 -0400 Subject: [PATCH 20/60] Added wasm flag --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 5a3e58d5d..87f1fc428 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -11,7 +11,10 @@ use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::Utc; use serde::{Deserialize, Serialize}; use thiserror::Error; +#[cfg(feature = "wasm")] use tsify::Tsify; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ From 04269c6fc00b3c92e97fc002a22af918526d48bd Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 24 Sep 2025 17:39:36 -0700 Subject: [PATCH 21/60] Address PR Feedback - simplify types & logic --- crates/bitwarden-vault/src/cipher/cipher.rs | 8 +- .../src/cipher/cipher_client/create.rs | 51 ++++++------ .../src/cipher/cipher_client/edit.rs | 82 +++++++++---------- .../src/cipher/cipher_client/mod.rs | 6 +- 4 files changed, 68 insertions(+), 79 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index da63a6d02..7d26411a4 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -63,11 +63,12 @@ pub(super) trait CipherKind { } #[allow(missing_docs)] -#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] +#[derive(Default, Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] #[repr(u8)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum CipherType { + #[default] Login = 1, SecureNote = 2, Card = 3, @@ -76,11 +77,12 @@ pub enum CipherType { } #[allow(missing_docs)] -#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] +#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)] #[repr(u8)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum CipherRepromptType { + #[default] None = 0, Password = 1, } @@ -832,7 +834,7 @@ impl TryFrom for Cipher { organization_id: cipher.organization_id.map(OrganizationId::new), folder_id: cipher.folder_id.map(FolderId::new), collection_ids: vec![], // CipherResponseModel doesn't include collection_ids - name: require!(EncString::try_from_optional(cipher.name)?), + name: require!(cipher.name).parse()?, notes: EncString::try_from_optional(cipher.notes)?, r#type: require!(cipher.r#type).into(), login: cipher.login.map(|l| (*l).try_into()).transpose()?, diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 4e0f847a0..69cf0ae2f 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -37,13 +37,15 @@ pub enum CreateCipherError { Repository(#[from] RepositoryError), } -/// Request to add or edit a cipher. -#[derive(Serialize, Deserialize, Debug)] +/// Request to add a cipher. +#[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct CipherCreateRequest { - pub encrypted_for: Option, + /// The ID of the user that is encrypting the cipher - this should always match the user + /// calling the API. + pub encrypted_for: UserId, pub organization_id: Option, pub folder_id: Option, pub name: String, @@ -56,7 +58,7 @@ pub struct CipherCreateRequest { pub card: Option, pub secure_note: Option, pub ssh_key: Option, - pub fields: Option>, + pub fields: Vec, } impl CompositeEncryptable for CipherCreateRequest { @@ -66,7 +68,7 @@ impl CompositeEncryptable for Cipher key: SymmetricKeyId, ) -> Result { let cipher_request = CipherRequestModel { - encrypted_for: self.encrypted_for.map(|id| id.into()), + encrypted_for: Some(self.encrypted_for.into()), r#type: Some(self.r#type.into()), organization_id: self.organization_id.map(|id| id.to_string()), folder_id: self.folder_id.map(|id| id.to_string()), @@ -110,17 +112,13 @@ impl CompositeEncryptable for Cipher .map(|s| s.encrypt_composite(ctx, key)) .transpose()? .map(|s| Box::new(s.into())), - fields: self - .fields - .as_ref() - .map(|fields| { - fields - .iter() - .map(|f| f.encrypt_composite(ctx, key)) - .collect::, _>>() - }) - .transpose()? - .map(|f| f.into_iter().map(|f| f.into()).collect()), + fields: Some( + self.fields + .iter() + .map(|f| f.encrypt_composite(ctx, key)) + .map(|f| f.map(|f| f.into())) + .collect::, _>>()?, + ), password_history: None, attachments: None, attachments2: None, @@ -168,7 +166,13 @@ impl CiphersClient { let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - request.encrypted_for = self.client.internal.get_user_id(); + request.encrypted_for = + self.client + .internal + .get_user_id() + .ok_or(RepositoryError::Internal( + "No user ID was found".to_string(), + ))?; create_cipher(key_store, &config.api, repository.as_ref(), request).await } } @@ -181,20 +185,15 @@ mod tests { use wiremock::{matchers, Mock, Request, ResponseTemplate}; use super::*; - use crate::{CipherId, CipherRepromptType, CipherType, LoginView}; + use crate::{CipherId, CipherType, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; fn generate_test_cipher_create_request() -> CipherCreateRequest { CipherCreateRequest { - encrypted_for: None, - organization_id: None, - folder_id: None, name: "Test Login".to_string(), notes: Some("Test notes".to_string()), r#type: CipherType::Login, - favorite: false, - reprompt: CipherRepromptType::None, login: Some(LoginView { username: Some("test@example.com".to_string()), password: Some("password123".to_string()), @@ -204,11 +203,7 @@ mod tests { autofill_on_page_load: None, fido2_credentials: None, }), - identity: None, - card: None, - secure_note: None, - ssh_key: None, - fields: None, + ..Default::default() } } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 87f1fc428..7843198b5 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -19,7 +19,7 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ password_history::PasswordChange, Cipher, CipherId, CipherType, CipherView, FieldType, - FieldView, ItemNotFoundError, PasswordHistoryView, VaultParseError, + FieldView, ItemNotFoundError, VaultParseError, }; /// Maximum number of password history entries to retain @@ -59,21 +59,16 @@ pub struct CipherEditRequest { impl CipherEditRequest { pub fn update_password_history(&mut self, original_cipher: &CipherView) { - let mut history = original_cipher.password_history.clone().unwrap_or_default(); - - let mut changes = Vec::new(); - - if let Some(login_changes) = self.detect_login_password_changes(original_cipher) { - changes.extend(login_changes); - } - - changes.extend(self.detect_hidden_field_changes(original_cipher)); - - for change in changes.into_iter().rev() { - history.insert(0, change.into_history_entry()); - } - - Self::limit_history_length(&mut history); + let changes = self + .detect_login_password_changes(original_cipher) + .into_iter() + .chain(self.detect_hidden_field_changes(original_cipher)); + let history: Vec<_> = changes + .rev() + .map(|change| change.into_history_entry()) + .chain(original_cipher.password_history.iter().flatten().cloned()) + .take(MAX_PASSWORD_HISTORY_ENTRIES) + .collect(); self.cipher.password_history = (!history.is_empty()).then_some(history); } @@ -81,13 +76,16 @@ impl CipherEditRequest { fn detect_login_password_changes( &mut self, original_cipher: &CipherView, - ) -> Option> { + ) -> Vec { if self.cipher.r#type != CipherType::Login || original_cipher.r#type != CipherType::Login { - return None; + return Default::default(); } - let original_login = original_cipher.login.as_ref()?; - let current_login = self.cipher.login.as_mut()?; + let (Some(original_login), Some(current_login)) = + (original_cipher.login.as_ref(), self.cipher.login.as_mut()) + else { + return Default::default(); + }; let original_password = original_login.password.as_deref().unwrap_or(""); let current_password = current_login.password.as_deref().unwrap_or(""); @@ -97,15 +95,15 @@ impl CipherEditRequest { if !current_password.is_empty() { current_login.password_revision_date = Some(Utc::now()); } - None + Default::default() } else if original_password == current_password { // Password unchanged - preserve original revision date current_login.password_revision_date = original_login.password_revision_date; - None + Default::default() } else { // Password changed - update revision date and track change current_login.password_revision_date = Some(Utc::now()); - Some(vec![PasswordChange::new_password(original_password)]) + vec![PasswordChange::new_password(original_password)] } } @@ -128,27 +126,17 @@ impl CipherEditRequest { fn extract_hidden_fields(fields: &Option>) -> HashMap { fields - .as_ref() - .map(|field_vec| { - field_vec - .iter() - .filter_map(|f| match (&f.r#type, &f.name, &f.value) { - (FieldType::Hidden, Some(name), Some(value)) - if !name.is_empty() && !value.is_empty() => - { - Some((name.clone(), value.clone())) - } - _ => None, - }) - .collect() + .iter() + .flatten() + .filter_map(|f| match (&f.r#type, &f.name, &f.value) { + (FieldType::Hidden, Some(name), Some(value)) + if !name.is_empty() && !value.is_empty() => + { + Some((name.clone(), value.clone())) + } + _ => None, }) - .unwrap_or_default() - } - - fn limit_history_length(history: &mut Vec) { - if history.len() > MAX_PASSWORD_HISTORY_ENTRIES { - history.truncate(MAX_PASSWORD_HISTORY_ENTRIES); - } + .collect() } } @@ -158,7 +146,9 @@ impl CompositeEncryptable for Cipher ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - let encrypted_cipher = self.cipher.encrypt_composite(ctx, key)?; + let mut cipher_view = self.cipher.clone(); + cipher_view.generate_checksums(); + let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; let cipher_request = CipherRequestModel { encrypted_for: self.encrypted_for.map(|id| id.into()), @@ -283,7 +273,9 @@ mod tests { use wiremock::{matchers, Mock, Request, ResponseTemplate}; use super::*; - use crate::{Cipher, CipherId, CipherRepromptType, CipherType, Login, LoginView}; + use crate::{ + Cipher, CipherId, CipherRepromptType, CipherType, Login, LoginView, PasswordHistoryView, + }; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs index 8449e8536..5dc2a4ea1 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs @@ -14,9 +14,9 @@ use crate::{ DecryptError, EncryptError, Fido2CredentialFullView, }; -pub mod create; -pub mod edit; -pub mod get; +mod create; +mod edit; +mod get; #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] From 4593cd3d7080a0bdd7a4667402301779faa92b28 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 25 Sep 2025 16:15:05 -0700 Subject: [PATCH 22/60] Refactor CipherEditRequest to exclude uneditable values. --- crates/bitwarden-vault/src/cipher/cipher.rs | 6 +- .../src/cipher/cipher_client/create.rs | 16 +- .../src/cipher/cipher_client/edit.rs | 306 ++++++++++++------ crates/bitwarden-vault/src/cipher/login.rs | 11 + 4 files changed, 222 insertions(+), 117 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 7d26411a4..3fb0049bf 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -471,11 +471,7 @@ impl CipherView { #[allow(missing_docs)] pub fn generate_checksums(&mut self) { - if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) { - for uri in uris { - uri.generate_checksum(); - } - } + self.login.as_mut().map(|l| l.generate_checksums()); } #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 69cf0ae2f..be2847e34 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -1,7 +1,7 @@ use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, - require, ApiError, MissingFieldError, OrganizationId, UserId, + require, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, @@ -34,6 +34,8 @@ pub enum CreateCipherError { #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] + NotAuthenticated(#[from] NotAuthenticatedError), + #[error(transparent)] Repository(#[from] RepositoryError), } @@ -166,13 +168,11 @@ impl CiphersClient { let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - request.encrypted_for = - self.client - .internal - .get_user_id() - .ok_or(RepositoryError::Internal( - "No user ID was found".to_string(), - ))?; + request.encrypted_for = self + .client + .internal + .get_user_id() + .ok_or(NotAuthenticatedError)?; create_cipher(key_store, &config.api, repository.as_ref(), request).await } } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 7843198b5..76df58bb7 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -3,12 +3,15 @@ use std::collections::HashMap; use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, - ApiError, MissingFieldError, UserId, + ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, EncString, IdentifyKey, KeyStore, KeyStoreContext, + PrimitiveEncryptable, }; -use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; -use chrono::Utc; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use thiserror::Error; #[cfg(feature = "wasm")] @@ -18,8 +21,9 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - password_history::PasswordChange, Cipher, CipherId, CipherType, CipherView, FieldType, - FieldView, ItemNotFoundError, VaultParseError, + password_history::PasswordChange, AttachmentView, CardView, Cipher, CipherId, + CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, IdentityView, + ItemNotFoundError, LoginView, PasswordHistoryView, SecureNoteView, SshKeyView, VaultParseError, }; /// Maximum number of password history entries to retain @@ -40,25 +44,73 @@ pub enum EditCipherError { #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] + NotAuthenticated(#[from] NotAuthenticatedError), + #[error(transparent)] Repository(#[from] RepositoryError), #[error(transparent)] Uuid(#[from] uuid::Error), } /// Request to edit a cipher. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct CipherEditRequest { - /// The cipher view data to be added or edited. - pub cipher: CipherView, - /// The user ID for whom this cipher is encrypted (internal use only). - pub encrypted_for: Option, + pub id: CipherId, + + pub r#type: CipherType, + pub organization_id: Option, + pub folder_id: Option, + pub favorite: bool, + pub reprompt: CipherRepromptType, + pub key: Option, + pub name: String, + pub notes: Option, + pub fields: Vec, + pub password_history: Option>, + pub attachments: Option>, + + pub login: Option, + pub identity: Option, + pub card: Option, + pub secure_note: Option, + pub ssh_key: Option, + pub revision_date: DateTime, + pub archived_date: Option>, +} + +impl TryFrom for CipherEditRequest { + type Error = MissingFieldError; + + fn try_from(value: CipherView) -> Result { + Ok(Self { + id: value.id.ok_or(MissingFieldError("id"))?, + r#type: value.r#type, + organization_id: value.organization_id, + folder_id: value.folder_id, + favorite: value.favorite, + reprompt: value.reprompt, + key: value.key, + name: value.name, + notes: value.notes, + fields: value.fields.unwrap_or_default(), + password_history: value.password_history, + attachments: value.attachments, + + login: value.login, + identity: value.identity, + card: value.card, + secure_note: value.secure_note, + ssh_key: value.ssh_key, + revision_date: value.revision_date, + archived_date: value.archived_date, + }) + } } impl CipherEditRequest { - pub fn update_password_history(&mut self, original_cipher: &CipherView) { + fn update_password_history(&mut self, original_cipher: &CipherView) { let changes = self .detect_login_password_changes(original_cipher) .into_iter() @@ -70,19 +122,19 @@ impl CipherEditRequest { .take(MAX_PASSWORD_HISTORY_ENTRIES) .collect(); - self.cipher.password_history = (!history.is_empty()).then_some(history); + self.password_history = (!history.is_empty()).then_some(history); } fn detect_login_password_changes( &mut self, original_cipher: &CipherView, ) -> Vec { - if self.cipher.r#type != CipherType::Login || original_cipher.r#type != CipherType::Login { + if self.r#type != CipherType::Login || original_cipher.r#type != CipherType::Login { return Default::default(); } let (Some(original_login), Some(current_login)) = - (original_cipher.login.as_ref(), self.cipher.login.as_mut()) + (original_cipher.login.as_ref(), self.login.as_mut()) else { return Default::default(); }; @@ -108,8 +160,9 @@ impl CipherEditRequest { } fn detect_hidden_field_changes(&self, original_cipher: &CipherView) -> Vec { - let original_fields = Self::extract_hidden_fields(&original_cipher.fields); - let current_fields = Self::extract_hidden_fields(&self.cipher.fields); + let original_fields = + Self::extract_hidden_fields(original_cipher.fields.as_ref().unwrap_or(&vec![])); + let current_fields = Self::extract_hidden_fields(&self.fields); original_fields .into_iter() @@ -124,10 +177,9 @@ impl CipherEditRequest { .collect() } - fn extract_hidden_fields(fields: &Option>) -> HashMap { + fn extract_hidden_fields(fields: &Vec) -> HashMap { fields .iter() - .flatten() .filter_map(|f| match (&f.r#type, &f.name, &f.value) { (FieldType::Hidden, Some(name), Some(value)) if !name.is_empty() && !value.is_empty() => @@ -138,6 +190,12 @@ impl CipherEditRequest { }) .collect() } + + fn generate_checksums(&mut self) { + if let Some(login) = &mut self.login { + login.generate_checksums(); + } + } } impl CompositeEncryptable for CipherEditRequest { @@ -146,29 +204,51 @@ impl CompositeEncryptable for Cipher ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - let mut cipher_view = self.cipher.clone(); - cipher_view.generate_checksums(); - let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; + let mut cipher_data = (*self).clone(); + cipher_data.generate_checksums(); + let key = key; // TODO: Swap key for cipher key if available + + // let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; let cipher_request = CipherRequestModel { - encrypted_for: self.encrypted_for.map(|id| id.into()), - r#type: Some(encrypted_cipher.r#type.into()), - organization_id: encrypted_cipher.organization_id.map(|id| id.to_string()), - folder_id: encrypted_cipher.folder_id.map(|id| id.to_string()), - favorite: Some(encrypted_cipher.favorite), - reprompt: Some(encrypted_cipher.reprompt.into()), - key: encrypted_cipher.key.map(|k| k.to_string()), - name: encrypted_cipher.name.to_string(), - notes: encrypted_cipher.notes.map(|n| n.to_string()), - fields: encrypted_cipher - .fields - .map(|f| f.into_iter().map(|f| f.into()).collect()), - password_history: encrypted_cipher - .password_history - .map(|ph| ph.into_iter().map(|ph| ph.into()).collect()), + encrypted_for: None, + r#type: Some(cipher_data.r#type.into()), + organization_id: cipher_data.organization_id.map(|id| id.to_string()), + folder_id: cipher_data.folder_id.map(|id| id.to_string()), + favorite: Some(cipher_data.favorite), + reprompt: Some(cipher_data.reprompt.into()), + key: cipher_data.key.map(|k| k.to_string()), + name: cipher_data.name.encrypt(ctx, key)?.to_string(), + notes: cipher_data + .notes + .as_ref() + .map(|n| n.encrypt(ctx, key)) + .transpose()? + .map(|n| n.to_string()), + fields: Some( + cipher_data + .fields + .encrypt_composite(ctx, key)? + .into_iter() + .map(|f| f.into()) + .collect(), + ), + password_history: Some( + cipher_data + .password_history + .encrypt_composite(ctx, key)? + .into_iter() + .flatten() + .map(|ph| ph.into()) + .collect(), + ), attachments: None, - attachments2: encrypted_cipher.attachments.map(|a| { - a.into_iter() + attachments2: Some( + cipher_data + .attachments + .encrypt_composite(ctx, key)? + .into_iter() + .flatten() .filter_map(|a| { a.id.map(|id| { ( @@ -180,17 +260,37 @@ impl CompositeEncryptable for Cipher ) }) }) - .collect() - }), - login: encrypted_cipher.login.map(|l| Box::new(l.into())), - card: encrypted_cipher.card.map(|c| Box::new(c.into())), - identity: encrypted_cipher.identity.map(|i| Box::new(i.into())), - secure_note: encrypted_cipher + .collect(), + ), + login: cipher_data + .login + .map(|l| l.encrypt_composite(ctx, key)) + .transpose()? + .map(|l| Box::new(l.into())), + card: cipher_data + .card + .map(|c| c.encrypt_composite(ctx, key)) + .transpose()? + .map(|c| Box::new(c.into())), + identity: cipher_data + .identity + .map(|i| i.encrypt_composite(ctx, key)) + .transpose()? + .map(|c| Box::new(c.into())), + + secure_note: cipher_data .secure_note - .map(|note| Box::new(note.into())), - ssh_key: encrypted_cipher.ssh_key.map(|key| Box::new(key.into())), - last_known_revision_date: Some(encrypted_cipher.revision_date.to_rfc3339()), - archived_date: None, + .map(|i| i.encrypt_composite(ctx, key)) + .transpose()? + .map(|c| Box::new(c.into())), + ssh_key: cipher_data + .ssh_key + .map(|i| i.encrypt_composite(ctx, key)) + .transpose()? + .map(|c| Box::new(c.into())), + + last_known_revision_date: Some(cipher_data.revision_date.to_rfc3339()), + archived_date: cipher_data.archived_date.map(|d| d.to_rfc3339()), }; Ok(cipher_request) @@ -199,7 +299,10 @@ impl CompositeEncryptable for Cipher impl IdentifyKey for CipherEditRequest { fn key_identifier(&self) -> SymmetricKeyId { - self.cipher.key_identifier() + match self.organization_id { + Some(organization_id) => SymmetricKeyId::Organization(organization_id), + None => SymmetricKeyId::User, + } } } @@ -207,22 +310,24 @@ async fn edit_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, - cipher_id: CipherId, + encrypted_for: UserId, mut request: CipherEditRequest, ) -> Result { - let id = cipher_id.to_string(); - repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + let cipher_id = request.id; - let original_cipher = repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + let original_cipher = repository + .get(cipher_id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; let original_cipher_view: CipherView = key_store.decrypt(&original_cipher)?; // Update password history request.update_password_history(&original_cipher_view); - let cipher_request = key_store.encrypt(request)?; + let mut cipher_request = key_store.encrypt(request)?; + cipher_request.encrypted_for = Some(encrypted_for.into()); - let parsed_cipher_id = uuid::Uuid::parse_str(&id)?; - let response = ciphers_api::ciphers_put(api_config, parsed_cipher_id, Some(cipher_request)) + let response = ciphers_api::ciphers_put(api_config, cipher_id.into(), Some(cipher_request)) .await .map_err(ApiError::from)?; @@ -239,22 +344,22 @@ async fn edit_cipher + ?Sized>( impl CiphersClient { /// Edit an existing [Cipher] and save it to the server. - pub async fn edit( - &self, - cipher_id: CipherId, - mut request: CipherEditRequest, - ) -> Result { + pub async fn edit(&self, request: CipherEditRequest) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - request.encrypted_for = self.client.internal.get_user_id(); + let user_id = self + .client + .internal + .get_user_id() + .ok_or(NotAuthenticatedError)?; edit_cipher( key_store, &config.api, repository.as_ref(), - cipher_id, + user_id, request, ) .await @@ -267,7 +372,7 @@ mod tests { apis::configuration::Configuration, models::{CipherRequestModel, CipherResponseModel}, }; - use bitwarden_core::{key_management::SymmetricKeyId, UserId}; + use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; use bitwarden_test::{start_api_mock, MemoryRepository}; use wiremock::{matchers, Mock, Request, ResponseTemplate}; @@ -282,7 +387,7 @@ mod tests { fn generate_test_cipher() -> CipherView { CipherView { - id: None, + id: Some(TEST_CIPHER_ID.parse().unwrap()), organization_id: None, folder_id: None, collection_ids: vec![], @@ -397,7 +502,6 @@ mod tests { ); let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); - let user_id: UserId = TEST_USER_ID.parse().unwrap(); let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( "/ciphers/{}", @@ -448,14 +552,17 @@ mod tests { repository_add_cipher(&repository, &store, cipher_id, "old_name").await; let cipher_view = generate_test_cipher(); - let request = CipherEditRequest { - cipher: cipher_view.clone(), - encrypted_for: Some(user_id), - }; + let request = cipher_view.try_into().unwrap(); - let result = edit_cipher(&store, &api_config, &repository, cipher_id, request) - .await - .unwrap(); + let result = edit_cipher( + &store, + &api_config, + &repository, + TEST_USER_ID.parse().unwrap(), + request, + ) + .await + .unwrap(); assert_eq!(result.id, Some(cipher_id)); assert_eq!(result.name, "Test Login"); @@ -466,20 +573,16 @@ mod tests { let store: KeyStore = KeyStore::default(); let repository = MemoryRepository::::default(); - let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); let cipher_view = generate_test_cipher(); - let request = CipherEditRequest { - cipher: cipher_view.clone(), - encrypted_for: None, - }; + let request = cipher_view.try_into().unwrap(); let result = edit_cipher( &store, &Configuration::default(), &repository, - cipher_id, + TEST_USER_ID.parse().unwrap(), request, ) .await; @@ -513,12 +616,16 @@ mod tests { repository_add_cipher(&repository, &store, cipher_id, "old_name").await; let cipher_view = generate_test_cipher(); - let request = CipherEditRequest { - cipher: cipher_view.clone(), - encrypted_for: None, - }; + let request = cipher_view.try_into().unwrap(); - let result = edit_cipher(&store, &api_config, &repository, cipher_id, request).await; + let result = edit_cipher( + &store, + &api_config, + &repository, + TEST_USER_ID.parse().unwrap(), + request, + ) + .await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), EditCipherError::Api(_))); @@ -527,14 +634,12 @@ mod tests { #[test] fn test_password_history_on_password_change() { let original_cipher = create_test_login_cipher("old_password"); - let mut edit_request = CipherEditRequest { - cipher: create_test_login_cipher("new_password"), - encrypted_for: None, - }; + let mut edit_request = + CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); edit_request.update_password_history(&original_cipher); - let history = edit_request.cipher.password_history.unwrap(); + let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].password, "old_password"); } @@ -542,14 +647,12 @@ mod tests { #[test] fn test_password_history_on_unchanged_password() { let original_cipher = create_test_login_cipher("same_password"); - let mut edit_request = CipherEditRequest { - cipher: create_test_login_cipher("same_password"), - encrypted_for: None, - }; + let mut edit_request = + CipherEditRequest::try_from(create_test_login_cipher("same_password")).unwrap(); edit_request.update_password_history(&original_cipher); - assert!(edit_request.cipher.password_history.is_none()); + assert!(edit_request.password_history.is_none()); } #[test] @@ -570,14 +673,11 @@ mod tests { linked_id: None, }]); - let mut edit_request = CipherEditRequest { - cipher: new_cipher, - encrypted_for: None, - }; + let mut edit_request = CipherEditRequest::try_from(new_cipher).unwrap(); edit_request.update_password_history(&original_cipher); - let history = edit_request.cipher.password_history.unwrap(); + let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].password, "Secret Key: old_secret_value"); } @@ -595,14 +695,12 @@ mod tests { ); // Create edit request with new password (no existing history) - let mut edit_request = CipherEditRequest { - cipher: create_test_login_cipher("new_password"), - encrypted_for: None, - }; + let mut edit_request = + CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); edit_request.update_password_history(&original_cipher); - let history = edit_request.cipher.password_history.unwrap(); + let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES); // Most recent change (original password) should be first assert_eq!(history[0].password, "password"); diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 0f5a164d6..d3a193ce9 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -313,6 +313,17 @@ pub struct LoginView { pub fido2_credentials: Option>, } +impl LoginView { + /// Generate checksums for all URIs in the login view + pub fn generate_checksums(&mut self) { + if let Some(uris) = &mut self.uris { + for uri in uris { + uri.generate_checksum(); + } + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] From 3f079569dbb6dd8e42695bb1757aa78fd4764f05 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 25 Sep 2025 16:52:49 -0700 Subject: [PATCH 23/60] Update CipherEditRequest to use CipherViewType enum instead of individual values --- .../src/cipher/cipher_client/edit.rs | 51 +++--- .../src/cipher/cipher_view_type.rs | 164 ++++++++++++++++++ crates/bitwarden-vault/src/cipher/mod.rs | 1 + 3 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 crates/bitwarden-vault/src/cipher/cipher_view_type.rs diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 76df58bb7..db4c240a0 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -21,9 +21,10 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - password_history::PasswordChange, AttachmentView, CardView, Cipher, CipherId, - CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, IdentityView, - ItemNotFoundError, LoginView, PasswordHistoryView, SecureNoteView, SshKeyView, VaultParseError, + cipher_view_type::{CipherViewType, CipherViewTypeExt}, + password_history::PasswordChange, + AttachmentView, Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, + FieldView, FolderId, ItemNotFoundError, PasswordHistoryView, VaultParseError, }; /// Maximum number of password history entries to retain @@ -71,11 +72,7 @@ pub struct CipherEditRequest { pub password_history: Option>, pub attachments: Option>, - pub login: Option, - pub identity: Option, - pub card: Option, - pub secure_note: Option, - pub ssh_key: Option, + pub type_data: Option, pub revision_date: DateTime, pub archived_date: Option>, } @@ -84,6 +81,13 @@ impl TryFrom for CipherEditRequest { type Error = MissingFieldError; fn try_from(value: CipherView) -> Result { + let type_data = match value.r#type { + CipherType::Login => value.login.map(CipherViewType::Login), + CipherType::SecureNote => value.secure_note.map(CipherViewType::SecureNote), + CipherType::Card => value.card.map(CipherViewType::Card), + CipherType::Identity => value.identity.map(CipherViewType::Identity), + CipherType::SshKey => value.ssh_key.map(CipherViewType::SshKey), + }; Ok(Self { id: value.id.ok_or(MissingFieldError("id"))?, r#type: value.r#type, @@ -97,12 +101,7 @@ impl TryFrom for CipherEditRequest { fields: value.fields.unwrap_or_default(), password_history: value.password_history, attachments: value.attachments, - - login: value.login, - identity: value.identity, - card: value.card, - secure_note: value.secure_note, - ssh_key: value.ssh_key, + type_data, revision_date: value.revision_date, archived_date: value.archived_date, }) @@ -133,9 +132,10 @@ impl CipherEditRequest { return Default::default(); } - let (Some(original_login), Some(current_login)) = - (original_cipher.login.as_ref(), self.login.as_mut()) - else { + let (Some(original_login), Some(current_login)) = ( + original_cipher.login.as_ref(), + self.type_data.as_login_view_mut(), + ) else { return Default::default(); }; @@ -192,7 +192,7 @@ impl CipherEditRequest { } fn generate_checksums(&mut self) { - if let Some(login) = &mut self.login { + if let Some(login) = &mut self.type_data.as_login_view_mut() { login.generate_checksums(); } } @@ -263,28 +263,33 @@ impl CompositeEncryptable for Cipher .collect(), ), login: cipher_data - .login + .type_data + .as_login_view() .map(|l| l.encrypt_composite(ctx, key)) .transpose()? .map(|l| Box::new(l.into())), card: cipher_data - .card + .type_data + .as_card_view() .map(|c| c.encrypt_composite(ctx, key)) .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data - .identity + .type_data + .as_identity_view() .map(|i| i.encrypt_composite(ctx, key)) .transpose()? .map(|c| Box::new(c.into())), secure_note: cipher_data - .secure_note + .type_data + .as_secure_note_view() .map(|i| i.encrypt_composite(ctx, key)) .transpose()? .map(|c| Box::new(c.into())), ssh_key: cipher_data - .ssh_key + .type_data + .as_ssh_key_view() .map(|i| i.encrypt_composite(ctx, key)) .transpose()? .map(|c| Box::new(c.into())), diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs new file mode 100644 index 000000000..6b47e5eea --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -0,0 +1,164 @@ +use serde::{Deserialize, Serialize}; +use tsify::Tsify; + +use crate::{CardView, IdentityView, LoginView, SecureNoteView, SshKeyView}; + +/// Represents the inner data of a cipher view. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase", untagged)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub enum CipherViewType { + Login(LoginView), + Card(CardView), + Identity(IdentityView), + SecureNote(SecureNoteView), + SshKey(SshKeyView), +} + +/// Extension trait to provide type-safe accessors for the different cipher view types. +#[allow(private_bounds)] +pub trait CipherViewTypeExt +where + Self: LockedTrait, +{ + fn as_login_view_mut(&mut self) -> Option<&mut LoginView>; + fn as_card_view_mut(&mut self) -> Option<&mut CardView>; + fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView>; + fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView>; + fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView>; + fn as_login_view(&self) -> Option<&LoginView>; + fn as_card_view(&self) -> Option<&CardView>; + fn as_identity_view(&self) -> Option<&IdentityView>; + fn as_secure_note_view(&self) -> Option<&SecureNoteView>; + fn as_ssh_key_view(&self) -> Option<&SshKeyView>; +} + +impl CipherViewTypeExt for Option { + fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { + self.as_mut() + .map(|inner| inner.as_login_view_mut()) + .flatten() + } + fn as_card_view_mut(&mut self) -> Option<&mut CardView> { + self.as_mut() + .map(|inner| inner.as_card_view_mut()) + .flatten() + } + + fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { + self.as_mut() + .map(|inner| inner.as_identity_view_mut()) + .flatten() + } + + fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { + self.as_mut() + .map(|inner| inner.as_secure_note_view_mut()) + .flatten() + } + + fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { + self.as_mut() + .map(|inner| inner.as_ssh_key_view_mut()) + .flatten() + } + + fn as_login_view(&self) -> Option<&LoginView> { + self.as_ref().map(|inner| inner.as_login_view()).flatten() + } + + fn as_card_view(&self) -> Option<&CardView> { + self.as_ref().map(|inner| inner.as_card_view()).flatten() + } + + fn as_identity_view(&self) -> Option<&IdentityView> { + self.as_ref() + .map(|inner| inner.as_identity_view()) + .flatten() + } + + fn as_secure_note_view(&self) -> Option<&SecureNoteView> { + self.as_ref() + .map(|inner| inner.as_secure_note_view()) + .flatten() + } + + fn as_ssh_key_view(&self) -> Option<&SshKeyView> { + self.as_ref().map(|inner| inner.as_ssh_key_view()).flatten() + } +} + +trait LockedTrait {} +impl LockedTrait for CipherViewType {} +impl LockedTrait for Option {} +impl CipherViewTypeExt for CipherViewType { + fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { + match self { + CipherViewType::Login(l) => Some(l), + _ => None, + } + } + + fn as_card_view_mut(&mut self) -> Option<&mut CardView> { + match self { + CipherViewType::Card(c) => Some(c), + _ => None, + } + } + + fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { + match self { + CipherViewType::Identity(i) => Some(i), + _ => None, + } + } + + fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { + match self { + CipherViewType::SecureNote(s) => Some(s), + _ => None, + } + } + + fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { + match self { + CipherViewType::SshKey(s) => Some(s), + _ => None, + } + } + fn as_login_view(&self) -> Option<&LoginView> { + match self { + CipherViewType::Login(l) => Some(l), + _ => None, + } + } + + fn as_card_view(&self) -> Option<&CardView> { + match self { + CipherViewType::Card(c) => Some(c), + _ => None, + } + } + + fn as_identity_view(&self) -> Option<&IdentityView> { + match self { + CipherViewType::Identity(i) => Some(i), + _ => None, + } + } + + fn as_secure_note_view(&self) -> Option<&SecureNoteView> { + match self { + CipherViewType::SecureNote(s) => Some(s), + _ => None, + } + } + + fn as_ssh_key_view(&self) -> Option<&SshKeyView> { + match self { + CipherViewType::SshKey(s) => Some(s), + _ => None, + } + } +} diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 39fe85361..2ba1c5bb9 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod card; pub(crate) mod cipher; pub(crate) mod cipher_client; pub(crate) mod cipher_permissions; +pub(crate) mod cipher_view_type; pub(crate) mod field; pub(crate) mod identity; pub(crate) mod linked_id; From 223cfe1756230a1e57f8952d24d3824e57900c39 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 25 Sep 2025 16:57:17 -0700 Subject: [PATCH 24/60] Update CipherCreateRequest to use CipherViewType enum --- .../src/cipher/cipher_client/create.rs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index be2847e34..150a0c392 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -17,8 +17,8 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - CardView, Cipher, CipherRepromptType, CipherType, CipherView, FieldView, FolderId, - IdentityView, LoginView, SecureNoteView, SshKeyView, VaultParseError, + cipher_view_type::{CipherViewType, CipherViewTypeExt}, + Cipher, CipherRepromptType, CipherType, CipherView, FieldView, FolderId, VaultParseError, }; #[allow(missing_docs)] @@ -55,11 +55,7 @@ pub struct CipherCreateRequest { pub r#type: CipherType, pub favorite: bool, pub reprompt: CipherRepromptType, - pub login: Option, - pub identity: Option, - pub card: Option, - pub secure_note: Option, - pub ssh_key: Option, + pub type_data: Option, pub fields: Vec, } @@ -85,31 +81,36 @@ impl CompositeEncryptable for Cipher .transpose()? .map(|n| n.to_string()), login: self - .login + .type_data + .as_login_view() .as_ref() .map(|l| l.encrypt_composite(ctx, key)) .transpose()? .map(|l| Box::new(l.into())), card: self - .card + .type_data + .as_card_view() .as_ref() .map(|c| c.encrypt_composite(ctx, key)) .transpose()? .map(|c| Box::new(c.into())), identity: self - .identity + .type_data + .as_identity_view() .as_ref() .map(|i| i.encrypt_composite(ctx, key)) .transpose()? .map(|i| Box::new(i.into())), secure_note: self - .secure_note + .type_data + .as_secure_note_view() .as_ref() .map(|s| s.encrypt_composite(ctx, key)) .transpose()? .map(|s| Box::new(s.into())), ssh_key: self - .ssh_key + .type_data + .as_ssh_key_view() .as_ref() .map(|s| s.encrypt_composite(ctx, key)) .transpose()? @@ -194,7 +195,7 @@ mod tests { name: "Test Login".to_string(), notes: Some("Test notes".to_string()), r#type: CipherType::Login, - login: Some(LoginView { + type_data: Some(CipherViewType::Login(LoginView { username: Some("test@example.com".to_string()), password: Some("password123".to_string()), password_revision_date: None, @@ -202,7 +203,7 @@ mod tests { totp: None, autofill_on_page_load: None, fido2_credentials: None, - }), + })), ..Default::default() } } From bde6ddeb32dd8a9ac5fd97e05273e087e0905822 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 25 Sep 2025 17:03:09 -0700 Subject: [PATCH 25/60] Remove encrypted_for from client-facing CreateCipherError --- .../src/cipher/cipher_client/create.rs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 150a0c392..5dcb7812f 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -47,7 +47,6 @@ pub enum CreateCipherError { pub struct CipherCreateRequest { /// The ID of the user that is encrypting the cipher - this should always match the user /// calling the API. - pub encrypted_for: UserId, pub organization_id: Option, pub folder_id: Option, pub name: String, @@ -66,7 +65,7 @@ impl CompositeEncryptable for Cipher key: SymmetricKeyId, ) -> Result { let cipher_request = CipherRequestModel { - encrypted_for: Some(self.encrypted_for.into()), + encrypted_for: None, r#type: Some(self.r#type.into()), organization_id: self.organization_id.map(|id| id.to_string()), folder_id: self.folder_id.map(|id| id.to_string()), @@ -146,9 +145,12 @@ async fn create_cipher + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &R, + encrypted_for: UserId, request: CipherCreateRequest, ) -> Result { - let cipher_request = key_store.encrypt(request)?; + let mut cipher_request = key_store.encrypt(request)?; + cipher_request.encrypted_for = Some(encrypted_for.into()); + let resp = ciphers_api::ciphers_post(api_config, Some(cipher_request)) .await .map_err(ApiError::from)?; @@ -163,18 +165,26 @@ impl CiphersClient { /// Create a new [Cipher] and save it to the server. pub async fn create( &self, - mut request: CipherCreateRequest, + request: CipherCreateRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - request.encrypted_for = self + let user_id = self .client .internal .get_user_id() .ok_or(NotAuthenticatedError)?; - create_cipher(key_store, &config.api, repository.as_ref(), request).await + + create_cipher( + key_store, + &config.api, + repository.as_ref(), + user_id, + request, + ) + .await } } @@ -189,6 +199,7 @@ mod tests { use crate::{CipherId, CipherType, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; + const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; fn generate_test_cipher_create_request() -> CipherCreateRequest { CipherCreateRequest { @@ -264,9 +275,15 @@ mod tests { let repository = MemoryRepository::::default(); let request = generate_test_cipher_create_request(); - let result = create_cipher(&store, &api_config, &repository, request) - .await - .unwrap(); + let result = create_cipher( + &store, + &api_config, + &repository, + TEST_USER_ID.parse().unwrap(), + request, + ) + .await + .unwrap(); assert_eq!(result.id, Some(cipher_id)); assert_eq!(result.name, "Test Login"); @@ -316,7 +333,14 @@ mod tests { let request = generate_test_cipher_create_request(); - let result = create_cipher(&store, &api_config, &repository, request).await; + let result = create_cipher( + &store, + &api_config, + &repository, + TEST_USER_ID.parse().unwrap(), + request, + ) + .await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), CreateCipherError::Api(_))); From f8dfeeaa5e968279e2616efc44ed0987b2d1c8cd Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 25 Sep 2025 17:51:45 -0700 Subject: [PATCH 26/60] Handle cipher-key encryption for Create/Edit requests --- crates/bitwarden-vault/src/cipher/cipher.rs | 6 +- .../src/cipher/cipher_client/create.rs | 59 +++++++++++++++---- .../src/cipher/cipher_client/edit.rs | 22 +++---- crates/bitwarden-vault/src/cipher/login.rs | 14 +++++ 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 3fb0049bf..4640942df 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -523,11 +523,7 @@ impl CipherView { new_key: SymmetricKeyId, ) -> Result<(), CryptoError> { if let Some(login) = self.login.as_mut() { - if let Some(fido2_credentials) = &mut login.fido2_credentials { - let dec_fido2_credentials: Vec = - fido2_credentials.decrypt(ctx, old_key)?; - *fido2_credentials = dec_fido2_credentials.encrypt_composite(ctx, new_key)?; - } + login.reencrypt_fido2_credentials(ctx, old_key, new_key)?; } Ok(()) } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 5dcb7812f..1c49d0300 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -4,7 +4,8 @@ use bitwarden_core::{ require, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, }; use bitwarden_crypto::{ - CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, + CompositeEncryptable, CryptoError, EncString, IdentifyKey, KeyStore, KeyStoreContext, + PrimitiveEncryptable, }; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; @@ -56,6 +57,30 @@ pub struct CipherCreateRequest { pub reprompt: CipherRepromptType, pub type_data: Option, pub fields: Vec, + pub key: Option, +} + +impl CipherCreateRequest { + /// Generate a new key for the cipher, re-encrypting internal data, if necessary, and stores the + /// encrypted key to the cipher data. + pub fn generate_cipher_key( + &mut self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result<(), CryptoError> { + let old_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + + const NEW_KEY_ID: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key"); + + let new_key = ctx.generate_symmetric_key(NEW_KEY_ID)?; + self.type_data + .as_login_view_mut() + .map(|l| l.reencrypt_fido2_credentials(ctx, old_key, new_key)) + .transpose()?; + + self.key = Some(ctx.wrap_symmetric_key(key, new_key)?); + Ok(()) + } } impl CompositeEncryptable for CipherCreateRequest { @@ -64,6 +89,8 @@ impl CompositeEncryptable for Cipher ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { + let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + let cipher_request = CipherRequestModel { encrypted_for: None, r#type: Some(self.r#type.into()), @@ -72,52 +99,52 @@ impl CompositeEncryptable for Cipher favorite: Some(self.favorite), reprompt: Some(self.reprompt.into()), key: None, - name: self.name.encrypt(ctx, key)?.to_string(), + name: self.name.encrypt(ctx, cipher_key)?.to_string(), notes: self .notes .as_ref() - .map(|n| n.encrypt(ctx, key)) + .map(|n| n.encrypt(ctx, cipher_key)) .transpose()? .map(|n| n.to_string()), login: self .type_data .as_login_view() .as_ref() - .map(|l| l.encrypt_composite(ctx, key)) + .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), card: self .type_data .as_card_view() .as_ref() - .map(|c| c.encrypt_composite(ctx, key)) + .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), identity: self .type_data .as_identity_view() .as_ref() - .map(|i| i.encrypt_composite(ctx, key)) + .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|i| Box::new(i.into())), secure_note: self .type_data .as_secure_note_view() .as_ref() - .map(|s| s.encrypt_composite(ctx, key)) + .map(|s| s.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|s| Box::new(s.into())), ssh_key: self .type_data .as_ssh_key_view() .as_ref() - .map(|s| s.encrypt_composite(ctx, key)) + .map(|s| s.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|s| Box::new(s.into())), fields: Some( self.fields .iter() - .map(|f| f.encrypt_composite(ctx, key)) + .map(|f| f.encrypt_composite(ctx, cipher_key)) .map(|f| f.map(|f| f.into())) .collect::, _>>()?, ), @@ -165,7 +192,7 @@ impl CiphersClient { /// Create a new [Cipher] and save it to the server. pub async fn create( &self, - request: CipherCreateRequest, + mut request: CipherCreateRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; @@ -177,6 +204,18 @@ impl CiphersClient { .get_user_id() .ok_or(NotAuthenticatedError)?; + // TODO: Once this flag is removed, the key generation logic should + // be moved closer to the actual encryption logic. + if self + .client + .internal + .get_flags() + .enable_cipher_key_encryption + { + let key = request.key_identifier(); + request.generate_cipher_key(&mut key_store.context(), key)?; + } + create_cipher( key_store, &config.api, diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index db4c240a0..e6118f8d7 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -207,7 +207,7 @@ impl CompositeEncryptable for Cipher let mut cipher_data = (*self).clone(); cipher_data.generate_checksums(); - let key = key; // TODO: Swap key for cipher key if available + let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; // let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; let cipher_request = CipherRequestModel { @@ -218,17 +218,17 @@ impl CompositeEncryptable for Cipher favorite: Some(cipher_data.favorite), reprompt: Some(cipher_data.reprompt.into()), key: cipher_data.key.map(|k| k.to_string()), - name: cipher_data.name.encrypt(ctx, key)?.to_string(), + name: cipher_data.name.encrypt(ctx, cipher_key)?.to_string(), notes: cipher_data .notes .as_ref() - .map(|n| n.encrypt(ctx, key)) + .map(|n| n.encrypt(ctx, cipher_key)) .transpose()? .map(|n| n.to_string()), fields: Some( cipher_data .fields - .encrypt_composite(ctx, key)? + .encrypt_composite(ctx, cipher_key)? .into_iter() .map(|f| f.into()) .collect(), @@ -236,7 +236,7 @@ impl CompositeEncryptable for Cipher password_history: Some( cipher_data .password_history - .encrypt_composite(ctx, key)? + .encrypt_composite(ctx, cipher_key)? .into_iter() .flatten() .map(|ph| ph.into()) @@ -246,7 +246,7 @@ impl CompositeEncryptable for Cipher attachments2: Some( cipher_data .attachments - .encrypt_composite(ctx, key)? + .encrypt_composite(ctx, cipher_key)? .into_iter() .flatten() .filter_map(|a| { @@ -265,32 +265,32 @@ impl CompositeEncryptable for Cipher login: cipher_data .type_data .as_login_view() - .map(|l| l.encrypt_composite(ctx, key)) + .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), card: cipher_data .type_data .as_card_view() - .map(|c| c.encrypt_composite(ctx, key)) + .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data .type_data .as_identity_view() - .map(|i| i.encrypt_composite(ctx, key)) + .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), secure_note: cipher_data .type_data .as_secure_note_view() - .map(|i| i.encrypt_composite(ctx, key)) + .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), ssh_key: cipher_data .type_data .as_ssh_key_view() - .map(|i| i.encrypt_composite(ctx, key)) + .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index d3a193ce9..834dc2cea 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -322,6 +322,20 @@ impl LoginView { } } } + + /// Re-encrypts the fido2 credentials with a new key, replacing the old encrypted values. + pub fn reencrypt_fido2_credentials( + &mut self, + ctx: &mut KeyStoreContext, + old_key: SymmetricKeyId, + new_key: SymmetricKeyId, + ) -> Result<(), CryptoError> { + if let Some(creds) = &mut self.fido2_credentials { + let decrypted_creds: Vec = creds.decrypt(ctx, old_key)?; + *creds = decrypted_creds.encrypt_composite(ctx, new_key)?; + } + Ok(()) + } } #[allow(missing_docs)] From b3bbda7d5e27b05bf24664c018b355d7d2a3ef43 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 13:06:59 -0700 Subject: [PATCH 27/60] Add missing field to cipher requests --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 1 + crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 1c49d0300..54408c3c6 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -153,6 +153,7 @@ impl CompositeEncryptable for Cipher attachments2: None, last_known_revision_date: None, archived_date: None, + data: None, }; Ok(cipher_request) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index e6118f8d7..a286bc4cd 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -296,6 +296,7 @@ impl CompositeEncryptable for Cipher last_known_revision_date: Some(cipher_data.revision_date.to_rfc3339()), archived_date: cipher_data.archived_date.map(|d| d.to_rfc3339()), + data: None, }; Ok(cipher_request) From 8c331aa1b55e4370a8797706c0002e718035923a Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 13:07:44 -0700 Subject: [PATCH 28/60] Export CipherViewType and fix TS export errors --- crates/bitwarden-vault/src/cipher/cipher_view_type.rs | 3 ++- crates/bitwarden-vault/src/cipher/mod.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index 6b47e5eea..a1f934947 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -5,9 +5,10 @@ use crate::{CardView, IdentityView, LoginView, SecureNoteView, SshKeyView}; /// Represents the inner data of a cipher view. #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase", untagged)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[allow(missing_docs)] pub enum CipherViewType { Login(LoginView), Card(CardView), diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 2ba1c5bb9..6342c744a 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -24,6 +24,7 @@ pub use cipher::{ CipherType, CipherView, DecryptCipherListResult, EncryptionContext, }; pub use cipher_client::CiphersClient; +pub use cipher_view_type::CipherViewType; pub use field::{FieldType, FieldView}; pub use identity::IdentityView; pub use login::{ From cc785568eba2a57448f39f834fe4909c5c6c9de4 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 15:06:41 -0700 Subject: [PATCH 29/60] Consume new ApiClient approach from main --- .../src/cipher/cipher_client/create.rs | 114 ++++++++-------- .../src/cipher/cipher_client/edit.rs | 125 +++++++++--------- .../src/cipher/cipher_view_type.rs | 1 + 3 files changed, 124 insertions(+), 116 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 54408c3c6..39b8c9c7f 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -1,4 +1,4 @@ -use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; +use bitwarden_api_api::models::CipherRequestModel; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, require, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, @@ -171,7 +171,7 @@ impl IdentifyKey for CipherCreateRequest { async fn create_cipher + ?Sized>( key_store: &KeyStore, - api_config: &bitwarden_api_api::apis::configuration::Configuration, + api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, encrypted_for: UserId, request: CipherCreateRequest, @@ -179,7 +179,9 @@ async fn create_cipher + ?Sized>( let mut cipher_request = key_store.encrypt(request)?; cipher_request.encrypted_for = Some(encrypted_for.into()); - let resp = ciphers_api::ciphers_post(api_config, Some(cipher_request)) + let resp = api_client + .ciphers_api() + .post(Some(cipher_request)) .await .map_err(ApiError::from)?; let cipher: Cipher = resp.try_into()?; @@ -219,7 +221,7 @@ impl CiphersClient { create_cipher( key_store, - &config.api, + &config.api_client, repository.as_ref(), user_id, request, @@ -230,10 +232,9 @@ impl CiphersClient { #[cfg(test)] mod tests { - use bitwarden_api_api::models::CipherResponseModel; + use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel}; use bitwarden_crypto::SymmetricCryptoKey; - use bitwarden_test::{start_api_mock, MemoryRepository}; - use wiremock::{matchers, Mock, Request, ResponseTemplate}; + use bitwarden_test::MemoryRepository; use super::*; use crate::{CipherId, CipherType, LoginView}; @@ -270,54 +271,56 @@ mod tests { let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); - let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/ciphers")) - .respond_with(move |req: &Request| { - let body: CipherRequestModel = req.body_json().unwrap(); - ResponseTemplate::new(201).set_body_json(CipherResponseModel { - object: Some("cipher".to_string()), - id: Some(cipher_id.into()), - name: Some(body.name.clone()), - r#type: body.r#type, - organization_id: body - .organization_id - .as_ref() - .and_then(|id| uuid::Uuid::parse_str(id).ok()), - folder_id: body - .folder_id - .as_ref() - .and_then(|id| uuid::Uuid::parse_str(id).ok()), - favorite: body.favorite, - reprompt: body.reprompt, - key: body.key.clone(), - notes: body.notes.clone(), - view_password: Some(true), - edit: Some(true), - organization_use_totp: Some(true), - revision_date: Some("2025-01-01T00:00:00Z".to_string()), - creation_date: Some("2025-01-01T00:00:00Z".to_string()), - deleted_date: None, - login: body.login, - card: body.card, - identity: body.identity, - secure_note: body.secure_note, - ssh_key: body.ssh_key, - fields: body.fields, - password_history: body.password_history, - attachments: None, - permissions: None, - data: None, - archived_date: None, + let api_client = ApiClient::new_mocked(move |mock| { + mock.ciphers_api + .expect_post() + .returning(move |body| { + let body = body.unwrap(); + Ok(CipherResponseModel { + object: Some("cipher".to_string()), + id: Some(cipher_id.into()), + name: Some(body.name.clone()), + r#type: body.r#type, + organization_id: body + .organization_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + folder_id: body + .folder_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + favorite: body.favorite, + reprompt: body.reprompt, + key: body.key.clone(), + notes: body.notes.clone(), + view_password: Some(true), + edit: Some(true), + organization_use_totp: Some(true), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + creation_date: Some("2025-01-01T00:00:00Z".to_string()), + deleted_date: None, + login: body.login, + card: body.card, + identity: body.identity, + secure_note: body.secure_note, + ssh_key: body.ssh_key, + fields: body.fields, + password_history: body.password_history, + attachments: None, + permissions: None, + data: None, + archived_date: None, + }) }) - }) - .expect(1)]) - .await; + .once(); + }); let repository = MemoryRepository::::default(); let request = generate_test_cipher_create_request(); let result = create_cipher( &store, - &api_config, + &api_client, &repository, TEST_USER_ID.parse().unwrap(), request, @@ -365,17 +368,22 @@ mod tests { SymmetricKeyId::User, SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); - let (_server, api_config) = start_api_mock(vec![ - Mock::given(matchers::path("/ciphers")).respond_with(ResponseTemplate::new(500)) - ]) - .await; + + let api_client = ApiClient::new_mocked(move |mock| { + mock.ciphers_api.expect_post().returning(move |_body| { + Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other( + "Simulated error", + ))) + }); + }); + let repository = MemoryRepository::::default(); let request = generate_test_cipher_create_request(); let result = create_cipher( &store, - &api_config, + &api_client, &repository, TEST_USER_ID.parse().unwrap(), request, diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index a286bc4cd..2564b9060 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use bitwarden_api_api::{apis::ciphers_api, models::CipherRequestModel}; +use bitwarden_api_api::models::CipherRequestModel; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, @@ -314,7 +314,7 @@ impl IdentifyKey for CipherEditRequest { async fn edit_cipher + ?Sized>( key_store: &KeyStore, - api_config: &bitwarden_api_api::apis::configuration::Configuration, + api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, encrypted_for: UserId, mut request: CipherEditRequest, @@ -333,7 +333,9 @@ async fn edit_cipher + ?Sized>( let mut cipher_request = key_store.encrypt(request)?; cipher_request.encrypted_for = Some(encrypted_for.into()); - let response = ciphers_api::ciphers_put(api_config, cipher_id.into(), Some(cipher_request)) + let response = api_client + .ciphers_api() + .put(cipher_id.into(), Some(cipher_request)) .await .map_err(ApiError::from)?; @@ -363,7 +365,7 @@ impl CiphersClient { edit_cipher( key_store, - &config.api, + &config.api_client, repository.as_ref(), user_id, request, @@ -374,14 +376,10 @@ impl CiphersClient { #[cfg(test)] mod tests { - use bitwarden_api_api::{ - apis::configuration::Configuration, - models::{CipherRequestModel, CipherResponseModel}, - }; + use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel}; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; - use bitwarden_test::{start_api_mock, MemoryRepository}; - use wiremock::{matchers, Mock, Request, ResponseTemplate}; + use bitwarden_test::MemoryRepository; use super::*; use crate::{ @@ -509,50 +507,49 @@ mod tests { let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap(); - let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( - "/ciphers/{}", - cipher_id - ))) - .respond_with(move |req: &Request| { - let body: CipherRequestModel = req.body_json().unwrap(); - ResponseTemplate::new(200).set_body_json(CipherResponseModel { - object: Some("cipher".to_string()), - id: Some(cipher_id.into()), - name: Some(body.name), - r#type: body.r#type, - organization_id: body - .organization_id - .as_ref() - .and_then(|id| uuid::Uuid::parse_str(id).ok()), - folder_id: body - .folder_id - .as_ref() - .and_then(|id| uuid::Uuid::parse_str(id).ok()), - favorite: body.favorite, - reprompt: body.reprompt, - key: body.key, - notes: body.notes, - view_password: Some(true), - edit: Some(true), - organization_use_totp: Some(true), - revision_date: Some("2025-01-01T00:00:00Z".to_string()), - creation_date: Some("2025-01-01T00:00:00Z".to_string()), - deleted_date: None, - login: body.login, - card: body.card, - identity: body.identity, - secure_note: body.secure_note, - ssh_key: body.ssh_key, - fields: body.fields, - password_history: body.password_history, - attachments: None, - permissions: None, - data: None, - archived_date: None, - }) - }) - .expect(1)]) - .await; + let api_client = ApiClient::new_mocked(move |mock| { + mock.ciphers_api + .expect_put() + .returning(move |_id, body| { + let body = body.unwrap(); + Ok(CipherResponseModel { + object: Some("cipher".to_string()), + id: Some(cipher_id.into()), + name: Some(body.name), + r#type: body.r#type, + organization_id: body + .organization_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + folder_id: body + .folder_id + .as_ref() + .and_then(|id| uuid::Uuid::parse_str(id).ok()), + favorite: body.favorite, + reprompt: body.reprompt, + key: body.key, + notes: body.notes, + view_password: Some(true), + edit: Some(true), + organization_use_totp: Some(true), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + creation_date: Some("2025-01-01T00:00:00Z".to_string()), + deleted_date: None, + login: body.login, + card: body.card, + identity: body.identity, + secure_note: body.secure_note, + ssh_key: body.ssh_key, + fields: body.fields, + password_history: body.password_history, + attachments: None, + permissions: None, + data: None, + archived_date: None, + }) + }) + .once(); + }); let repository = MemoryRepository::::default(); repository_add_cipher(&repository, &store, cipher_id, "old_name").await; @@ -562,7 +559,7 @@ mod tests { let result = edit_cipher( &store, - &api_config, + &api_client, &repository, TEST_USER_ID.parse().unwrap(), request, @@ -581,12 +578,13 @@ mod tests { let repository = MemoryRepository::::default(); let cipher_view = generate_test_cipher(); + let api_client = ApiClient::new_mocked(|_| {}); let request = cipher_view.try_into().unwrap(); let result = edit_cipher( &store, - &Configuration::default(), + &api_client, &repository, TEST_USER_ID.parse().unwrap(), request, @@ -611,12 +609,13 @@ mod tests { let cipher_id: CipherId = "5faa9684-c793-4a2d-8a12-b33900187097".parse().unwrap(); - let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( - "/ciphers/{}", - cipher_id - ))) - .respond_with(ResponseTemplate::new(500))]) - .await; + let api_client = ApiClient::new_mocked(move |mock| { + mock.ciphers_api.expect_put().returning(move |_id, _body| { + Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other( + "Simulated error", + ))) + }); + }); let repository = MemoryRepository::::default(); repository_add_cipher(&repository, &store, cipher_id, "old_name").await; @@ -626,7 +625,7 @@ mod tests { let result = edit_cipher( &store, - &api_config, + &api_client, &repository, TEST_USER_ID.parse().unwrap(), request, diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index a1f934947..e0402cf10 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] use tsify::Tsify; use crate::{CardView, IdentityView, LoginView, SecureNoteView, SshKeyView}; From 532a6ba0c6a62edbb8d1880c9b72baa1cdb17a57 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 15:27:17 -0700 Subject: [PATCH 30/60] Remove outdated comment from cipher_client/create.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 39b8c9c7f..6daa627aa 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -46,8 +46,6 @@ pub enum CreateCipherError { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct CipherCreateRequest { - /// The ID of the user that is encrypting the cipher - this should always match the user - /// calling the API. pub organization_id: Option, pub folder_id: Option, pub name: String, From 7f4033c4a0d79b594281e3ecd611e5a93aed1bb8 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 15:42:58 -0700 Subject: [PATCH 31/60] Remove `pub` from CipherCreateRequest::generate_cipher_key Co-authored-by: Oscar Hinton --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 6daa627aa..d73319381 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -61,7 +61,7 @@ pub struct CipherCreateRequest { impl CipherCreateRequest { /// Generate a new key for the cipher, re-encrypting internal data, if necessary, and stores the /// encrypted key to the cipher data. - pub fn generate_cipher_key( + fn generate_cipher_key( &mut self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, From 69bbc9973fe612f73e59937a3f7ed0c1ebf2328e Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:11:29 -0700 Subject: [PATCH 32/60] Address PR feedback --- .../src/cipher/cipher_client/create.rs | 10 ++++--- .../src/cipher/cipher_client/edit.rs | 27 +++++++++++-------- .../src/cipher/cipher_view_type.rs | 12 +++++++++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index d73319381..480f36434 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -50,7 +50,6 @@ pub struct CipherCreateRequest { pub folder_id: Option, pub name: String, pub notes: Option, - pub r#type: CipherType, pub favorite: bool, pub reprompt: CipherRepromptType, pub type_data: Option, @@ -61,7 +60,7 @@ pub struct CipherCreateRequest { impl CipherCreateRequest { /// Generate a new key for the cipher, re-encrypting internal data, if necessary, and stores the /// encrypted key to the cipher data. - fn generate_cipher_key( + fn generate_cipher_key( &mut self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, @@ -91,7 +90,11 @@ impl CompositeEncryptable for Cipher let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: Some(self.r#type.into()), + r#type: self + .type_data + .as_ref() + .map(CipherViewType::get_cipher_type) + .map(<_ as Into<_>>::into), organization_id: self.organization_id.map(|id| id.to_string()), folder_id: self.folder_id.map(|id| id.to_string()), favorite: Some(self.favorite), @@ -244,7 +247,6 @@ mod tests { CipherCreateRequest { name: "Test Login".to_string(), notes: Some("Test notes".to_string()), - r#type: CipherType::Login, type_data: Some(CipherViewType::Login(LoginView { username: Some("test@example.com".to_string()), password: Some("password123".to_string()), diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 2564b9060..6fa205210 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -60,12 +60,10 @@ pub enum EditCipherError { pub struct CipherEditRequest { pub id: CipherId, - pub r#type: CipherType, pub organization_id: Option, pub folder_id: Option, pub favorite: bool, pub reprompt: CipherRepromptType, - pub key: Option, pub name: String, pub notes: Option, pub fields: Vec, @@ -75,6 +73,8 @@ pub struct CipherEditRequest { pub type_data: Option, pub revision_date: DateTime, pub archived_date: Option>, + /// For internal use only. Do not set this from clients. + key: Option, } impl TryFrom for CipherEditRequest { @@ -90,7 +90,6 @@ impl TryFrom for CipherEditRequest { }; Ok(Self { id: value.id.ok_or(MissingFieldError("id"))?, - r#type: value.r#type, organization_id: value.organization_id, folder_id: value.folder_id, favorite: value.favorite, @@ -128,15 +127,17 @@ impl CipherEditRequest { &mut self, original_cipher: &CipherView, ) -> Vec { - if self.r#type != CipherType::Login || original_cipher.r#type != CipherType::Login { - return Default::default(); + if !matches!(self.type_data, Some(CipherViewType::Login(_))) + || original_cipher.r#type != CipherType::Login + { + return vec![]; } let (Some(original_login), Some(current_login)) = ( original_cipher.login.as_ref(), self.type_data.as_login_view_mut(), ) else { - return Default::default(); + return vec![]; }; let original_password = original_login.password.as_deref().unwrap_or(""); @@ -147,11 +148,11 @@ impl CipherEditRequest { if !current_password.is_empty() { current_login.password_revision_date = Some(Utc::now()); } - Default::default() + vec![] } else if original_password == current_password { // Password unchanged - preserve original revision date current_login.password_revision_date = original_login.password_revision_date; - Default::default() + vec![] } else { // Password changed - update revision date and track change current_login.password_revision_date = Some(Utc::now()); @@ -161,7 +162,7 @@ impl CipherEditRequest { fn detect_hidden_field_changes(&self, original_cipher: &CipherView) -> Vec { let original_fields = - Self::extract_hidden_fields(original_cipher.fields.as_ref().unwrap_or(&vec![])); + Self::extract_hidden_fields(original_cipher.fields.as_ref().unwrap_or_default()); let current_fields = Self::extract_hidden_fields(&self.fields); original_fields @@ -177,7 +178,7 @@ impl CipherEditRequest { .collect() } - fn extract_hidden_fields(fields: &Vec) -> HashMap { + fn extract_hidden_fields(fields: &[FieldView]) -> HashMap { fields .iter() .filter_map(|f| match (&f.r#type, &f.name, &f.value) { @@ -212,7 +213,11 @@ impl CompositeEncryptable for Cipher // let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: Some(cipher_data.r#type.into()), + r#type: cipher_data + .type_data + .as_ref() + .map(CipherViewType::get_cipher_type) + .map(<_ as Into<_>>::into), organization_id: cipher_data.organization_id.map(|id| id.to_string()), folder_id: cipher_data.folder_id.map(|id| id.to_string()), favorite: Some(cipher_data.favorite), diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index e0402cf10..543dc93f5 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -18,6 +18,18 @@ pub enum CipherViewType { SshKey(SshKeyView), } +impl CipherViewType { + pub fn get_cipher_type(&self) -> crate::CipherType { + match self { + CipherViewType::Login(_) => crate::CipherType::Login, + CipherViewType::Card(_) => crate::CipherType::Card, + CipherViewType::Identity(_) => crate::CipherType::Identity, + CipherViewType::SecureNote(_) => crate::CipherType::SecureNote, + CipherViewType::SshKey(_) => crate::CipherType::SshKey, + } + } +} + /// Extension trait to provide type-safe accessors for the different cipher view types. #[allow(private_bounds)] pub trait CipherViewTypeExt From d06a363c31398bbd1e1b77e0aeb38a0d4370b6db Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:19:10 -0700 Subject: [PATCH 33/60] Use slice instead of \&Vec<_> in detect_hidden_field_changes --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 6fa205210..f0d5232cf 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -162,7 +162,7 @@ impl CipherEditRequest { fn detect_hidden_field_changes(&self, original_cipher: &CipherView) -> Vec { let original_fields = - Self::extract_hidden_fields(original_cipher.fields.as_ref().unwrap_or_default()); + Self::extract_hidden_fields(original_cipher.fields.as_deref().unwrap_or_default()); let current_fields = Self::extract_hidden_fields(&self.fields); original_fields From a331fc6901c09b29d1206a4b06ce0be17fdb98e2 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:19:23 -0700 Subject: [PATCH 34/60] Remove Default from CipherType --- crates/bitwarden-vault/src/cipher/cipher.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 4640942df..d080d767c 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -63,12 +63,11 @@ pub(super) trait CipherKind { } #[allow(missing_docs)] -#[derive(Default, Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] +#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] #[repr(u8)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum CipherType { - #[default] Login = 1, SecureNote = 2, Card = 3, From f931e47e7e9662cb32868267aa8a2f240d26e3ff Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:36:17 -0700 Subject: [PATCH 35/60] Generate login checksums before encrypting --- .../src/cipher/cipher_client/create.rs | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 480f36434..ec02c36e2 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -19,7 +19,7 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ cipher_view_type::{CipherViewType, CipherViewTypeExt}, - Cipher, CipherRepromptType, CipherType, CipherView, FieldView, FolderId, VaultParseError, + Cipher, CipherRepromptType, CipherView, FieldView, FolderId, VaultParseError, }; #[allow(missing_docs)] @@ -41,7 +41,7 @@ pub enum CreateCipherError { } /// Request to add a cipher. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -78,6 +78,12 @@ impl CipherCreateRequest { self.key = Some(ctx.wrap_symmetric_key(key, new_key)?); Ok(()) } + + fn generate_checksums(&mut self) { + if let Some(login) = &mut self.type_data.as_login_view_mut() { + login.generate_checksums(); + } + } } impl CompositeEncryptable for CipherCreateRequest { @@ -86,56 +92,60 @@ impl CompositeEncryptable for Cipher ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + // Clone self so we can generating the checksums before encrypting. + let mut cipher_data = (*self).clone(); + cipher_data.generate_checksums(); + + let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &cipher_data.key)?; let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: self + r#type: cipher_data .type_data .as_ref() .map(CipherViewType::get_cipher_type) .map(<_ as Into<_>>::into), - organization_id: self.organization_id.map(|id| id.to_string()), - folder_id: self.folder_id.map(|id| id.to_string()), - favorite: Some(self.favorite), - reprompt: Some(self.reprompt.into()), + organization_id: cipher_data.organization_id.map(|id| id.to_string()), + folder_id: cipher_data.folder_id.map(|id| id.to_string()), + favorite: Some(cipher_data.favorite), + reprompt: Some(cipher_data.reprompt.into()), key: None, - name: self.name.encrypt(ctx, cipher_key)?.to_string(), - notes: self + name: cipher_data.name.encrypt(ctx, cipher_key)?.to_string(), + notes: cipher_data .notes .as_ref() .map(|n| n.encrypt(ctx, cipher_key)) .transpose()? .map(|n| n.to_string()), - login: self + login: cipher_data .type_data .as_login_view() .as_ref() .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), - card: self + card: cipher_data .type_data .as_card_view() .as_ref() .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), - identity: self + identity: cipher_data .type_data .as_identity_view() .as_ref() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|i| Box::new(i.into())), - secure_note: self + secure_note: cipher_data .type_data .as_secure_note_view() .as_ref() .map(|s| s.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|s| Box::new(s.into())), - ssh_key: self + ssh_key: cipher_data .type_data .as_ssh_key_view() .as_ref() @@ -143,7 +153,8 @@ impl CompositeEncryptable for Cipher .transpose()? .map(|s| Box::new(s.into())), fields: Some( - self.fields + cipher_data + .fields .iter() .map(|f| f.encrypt_composite(ctx, cipher_key)) .map(|f| f.map(|f| f.into())) From 4a0c45cacd1048021c96f2fd0901f26a6af2e31a Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:37:27 -0700 Subject: [PATCH 36/60] Remove list operations in favor of list_with_failures --- .../src/cipher/cipher_client/get.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs index 73b09b625..7184049a3 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs @@ -32,15 +32,6 @@ async fn get_cipher( Ok(store.decrypt(&cipher)?) } -async fn list_ciphers( - store: &KeyStore, - repository: &dyn Repository, -) -> Result, GetCipherError> { - let ciphers = repository.list().await?; - let views = store.decrypt_list(&ciphers)?; - Ok(views) -} - async fn list_ciphers_with_failures( store: &KeyStore, repository: &dyn Repository, @@ -54,14 +45,6 @@ async fn list_ciphers_with_failures( } impl CiphersClient { - /// Get all ciphers from state and decrypt them to a list of [CipherView]. - pub async fn list(&self) -> Result, GetCipherError> { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - list_ciphers(key_store, repository.as_ref()).await - } - /// Get all ciphers from state and decrypt them, returning both successes and failures. /// This method will not fail when some ciphers fail to decrypt, allowing for graceful /// handling of corrupted or problematic cipher data. From 1cf65142db77e3ab148eaacd4f48b9b2f37cdd1f Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:37:51 -0700 Subject: [PATCH 37/60] Add docstring for get_cipher_type --- crates/bitwarden-vault/src/cipher/cipher_view_type.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index 543dc93f5..1987b8625 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -19,6 +19,7 @@ pub enum CipherViewType { } impl CipherViewType { + /// Returns the corresponding [crate::CipherType] for this view type. pub fn get_cipher_type(&self) -> crate::CipherType { match self { CipherViewType::Login(_) => crate::CipherType::Login, From 86b0b56c45114cb53f91384b9787f9c08c4063b3 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 16:55:20 -0700 Subject: [PATCH 38/60] Fix clippy errors. --- crates/bitwarden-vault/src/cipher/cipher.rs | 4 ++- .../src/cipher/cipher_view_type.rs | 35 ++++++------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index d080d767c..488b55d90 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -470,7 +470,9 @@ impl CipherView { #[allow(missing_docs)] pub fn generate_checksums(&mut self) { - self.login.as_mut().map(|l| l.generate_checksums()); + if let Some(l) = self.login.as_mut() { + l.generate_checksums(); + } } #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index 1987b8625..5ea06a174 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -9,7 +9,7 @@ use crate::{CardView, IdentityView, LoginView, SecureNoteView, SshKeyView}; #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -#[allow(missing_docs)] +#[allow(missing_docs, clippy::large_enum_variant)] pub enum CipherViewType { Login(LoginView), Card(CardView), @@ -51,56 +51,43 @@ where impl CipherViewTypeExt for Option { fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { - self.as_mut() - .map(|inner| inner.as_login_view_mut()) - .flatten() + self.as_mut().and_then(|inner| inner.as_login_view_mut()) } fn as_card_view_mut(&mut self) -> Option<&mut CardView> { - self.as_mut() - .map(|inner| inner.as_card_view_mut()) - .flatten() + self.as_mut().and_then(|inner| inner.as_card_view_mut()) } fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { - self.as_mut() - .map(|inner| inner.as_identity_view_mut()) - .flatten() + self.as_mut().and_then(|inner| inner.as_identity_view_mut()) } fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { self.as_mut() - .map(|inner| inner.as_secure_note_view_mut()) - .flatten() + .and_then(|inner| inner.as_secure_note_view_mut()) } fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { - self.as_mut() - .map(|inner| inner.as_ssh_key_view_mut()) - .flatten() + self.as_mut().and_then(|inner| inner.as_ssh_key_view_mut()) } fn as_login_view(&self) -> Option<&LoginView> { - self.as_ref().map(|inner| inner.as_login_view()).flatten() + self.as_ref().and_then(|inner| inner.as_login_view()) } fn as_card_view(&self) -> Option<&CardView> { - self.as_ref().map(|inner| inner.as_card_view()).flatten() + self.as_ref().and_then(|inner| inner.as_card_view()) } fn as_identity_view(&self) -> Option<&IdentityView> { - self.as_ref() - .map(|inner| inner.as_identity_view()) - .flatten() + self.as_ref().and_then(|inner| inner.as_identity_view()) } fn as_secure_note_view(&self) -> Option<&SecureNoteView> { - self.as_ref() - .map(|inner| inner.as_secure_note_view()) - .flatten() + self.as_ref().and_then(|inner| inner.as_secure_note_view()) } fn as_ssh_key_view(&self) -> Option<&SshKeyView> { - self.as_ref().map(|inner| inner.as_ssh_key_view()).flatten() + self.as_ref().and_then(|inner| inner.as_ssh_key_view()) } } From 69978d02f9e3e564d294e3009f86face52fd3d72 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 17:25:23 -0700 Subject: [PATCH 39/60] Bugfix: Store cipher key when encrypting CipherCreateRequest --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 3 ++- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index ec02c36e2..15e50cdf2 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -54,6 +54,7 @@ pub struct CipherCreateRequest { pub reprompt: CipherRepromptType, pub type_data: Option, pub fields: Vec, + /// For internal use only. Do not set this from clients. pub key: Option, } @@ -109,7 +110,7 @@ impl CompositeEncryptable for Cipher folder_id: cipher_data.folder_id.map(|id| id.to_string()), favorite: Some(cipher_data.favorite), reprompt: Some(cipher_data.reprompt.into()), - key: None, + key: cipher_data.key.map(|k| k.to_string()), name: cipher_data.name.encrypt(ctx, cipher_key)?.to_string(), notes: cipher_data .notes diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index f0d5232cf..0de1c6b61 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -74,7 +74,7 @@ pub struct CipherEditRequest { pub revision_date: DateTime, pub archived_date: Option>, /// For internal use only. Do not set this from clients. - key: Option, + pub key: Option, } impl TryFrom for CipherEditRequest { From ef2f7ca48119469d97e90b6ca8a2cbf471c6b124 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 26 Sep 2025 17:31:41 -0700 Subject: [PATCH 40/60] Remove unnecessary comments --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 0de1c6b61..671f7ac46 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -73,7 +73,6 @@ pub struct CipherEditRequest { pub type_data: Option, pub revision_date: DateTime, pub archived_date: Option>, - /// For internal use only. Do not set this from clients. pub key: Option, } @@ -210,7 +209,6 @@ impl CompositeEncryptable for Cipher let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - // let encrypted_cipher = cipher_view.encrypt_composite(ctx, key)?; let cipher_request = CipherRequestModel { encrypted_for: None, r#type: cipher_data From 2b57bcf8d1950d8d5cb27ef455bdb87ad82cb843 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Mon, 29 Sep 2025 10:12:59 -0700 Subject: [PATCH 41/60] Remove unused import in create_cipher tests --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 15e50cdf2..e5619b707 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -250,7 +250,7 @@ mod tests { use bitwarden_test::MemoryRepository; use super::*; - use crate::{CipherId, CipherType, LoginView}; + use crate::{CipherId, LoginView}; const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097"; const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000"; From 5e13d4cfff1df863cc1f292138613a3f3fdc2477 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 1 Oct 2025 17:06:17 -0700 Subject: [PATCH 42/60] Remove unneeded fiels from cipher_client edit and create requests --- .../src/cipher/cipher_client/create.rs | 5 +- .../src/cipher/cipher_client/edit.rs | 53 ++++++------------- .../src/cipher/cipher_client/get.rs | 8 +-- 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index e5619b707..a5941dcf4 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -1,7 +1,8 @@ use bitwarden_api_api::models::CipherRequestModel; use bitwarden_core::{ + ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, key_management::{KeyIds, SymmetricKeyId}, - require, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, + require, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, EncString, IdentifyKey, KeyStore, KeyStoreContext, @@ -18,8 +19,8 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - cipher_view_type::{CipherViewType, CipherViewTypeExt}, Cipher, CipherRepromptType, CipherView, FieldView, FolderId, VaultParseError, + cipher_view_type::{CipherViewType, CipherViewTypeExt}, }; #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 671f7ac46..62a80efe9 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use bitwarden_api_api::models::CipherRequestModel; use bitwarden_core::{ - key_management::{KeyIds, SymmetricKeyId}, ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, + key_management::{KeyIds, SymmetricKeyId}, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, EncString, IdentifyKey, KeyStore, KeyStoreContext, @@ -21,10 +21,10 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - cipher_view_type::{CipherViewType, CipherViewTypeExt}, - password_history::PasswordChange, AttachmentView, Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, ItemNotFoundError, PasswordHistoryView, VaultParseError, + cipher_view_type::{CipherViewType, CipherViewTypeExt}, + password_history::PasswordChange, }; /// Maximum number of password history entries to retain @@ -67,10 +67,7 @@ pub struct CipherEditRequest { pub name: String, pub notes: Option, pub fields: Vec, - pub password_history: Option>, - pub attachments: Option>, - - pub type_data: Option, + pub r#type: CipherViewType, pub revision_date: DateTime, pub archived_date: Option>, pub key: Option, @@ -97,9 +94,7 @@ impl TryFrom for CipherEditRequest { name: value.name, notes: value.notes, fields: value.fields.unwrap_or_default(), - password_history: value.password_history, - attachments: value.attachments, - type_data, + r#type: type_data, revision_date: value.revision_date, archived_date: value.archived_date, }) @@ -126,7 +121,7 @@ impl CipherEditRequest { &mut self, original_cipher: &CipherView, ) -> Vec { - if !matches!(self.type_data, Some(CipherViewType::Login(_))) + if !matches!(self.r#type, Some(CipherViewType::Login(_))) || original_cipher.r#type != CipherType::Login { return vec![]; @@ -134,7 +129,7 @@ impl CipherEditRequest { let (Some(original_login), Some(current_login)) = ( original_cipher.login.as_ref(), - self.type_data.as_login_view_mut(), + self.r#type.as_login_view_mut(), ) else { return vec![]; }; @@ -192,7 +187,7 @@ impl CipherEditRequest { } fn generate_checksums(&mut self) { - if let Some(login) = &mut self.type_data.as_login_view_mut() { + if let Some(login) = &mut self.r#type.as_login_view_mut() { login.generate_checksums(); } } @@ -212,7 +207,7 @@ impl CompositeEncryptable for Cipher let cipher_request = CipherRequestModel { encrypted_for: None, r#type: cipher_data - .type_data + .r#type .as_ref() .map(CipherViewType::get_cipher_type) .map(<_ as Into<_>>::into), @@ -246,52 +241,34 @@ impl CompositeEncryptable for Cipher .collect(), ), attachments: None, - attachments2: Some( - cipher_data - .attachments - .encrypt_composite(ctx, cipher_key)? - .into_iter() - .flatten() - .filter_map(|a| { - a.id.map(|id| { - ( - id, - bitwarden_api_api::models::CipherAttachmentModel { - file_name: a.file_name.map(|n| n.to_string()), - key: a.key.map(|k| k.to_string()), - }, - ) - }) - }) - .collect(), - ), + attachments2: None, login: cipher_data - .type_data + .r#type .as_login_view() .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), card: cipher_data - .type_data + .r#type .as_card_view() .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data - .type_data + .r#type .as_identity_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), secure_note: cipher_data - .type_data + .r#type .as_secure_note_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), ssh_key: cipher_data - .type_data + .r#type .as_ssh_key_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs index 7184049a3..cad74d2bc 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs @@ -5,7 +5,7 @@ use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; use super::CiphersClient; -use crate::{cipher::cipher::DecryptCipherListResult, Cipher, CipherView, ItemNotFoundError}; +use crate::{Cipher, CipherView, ItemNotFoundError, cipher::cipher::DecryptCipherListResult}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -32,7 +32,7 @@ async fn get_cipher( Ok(store.decrypt(&cipher)?) } -async fn list_ciphers_with_failures( +async fn list( store: &KeyStore, repository: &dyn Repository, ) -> Result { @@ -48,11 +48,11 @@ impl CiphersClient { /// Get all ciphers from state and decrypt them, returning both successes and failures. /// This method will not fail when some ciphers fail to decrypt, allowing for graceful /// handling of corrupted or problematic cipher data. - pub async fn list_with_failures(&self) -> Result { + pub async fn list(&self) -> Result { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; - list_ciphers_with_failures(key_store, repository.as_ref()).await + list(key_store, repository.as_ref()).await } /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. From 365534ed71fb1ee06ee773ee130bfa92cbc4bb44 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 2 Oct 2025 14:32:00 -0700 Subject: [PATCH 43/60] Remove password_history from CipherEditRequest --- .../src/cipher/cipher_client/edit.rs | 64 +++++++++---------- .../bitwarden-vault/src/password_history.rs | 27 ++------ 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 62a80efe9..bcbd15a34 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -4,6 +4,7 @@ use bitwarden_api_api::models::CipherRequestModel; use bitwarden_core::{ ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, key_management::{KeyIds, SymmetricKeyId}, + require, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, EncString, IdentifyKey, KeyStore, KeyStoreContext, @@ -21,10 +22,9 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - AttachmentView, Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, - FieldView, FolderId, ItemNotFoundError, PasswordHistoryView, VaultParseError, + Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, + ItemNotFoundError, PasswordHistoryView, VaultParseError, cipher_view_type::{CipherViewType, CipherViewTypeExt}, - password_history::PasswordChange, }; /// Maximum number of password history entries to retain @@ -94,7 +94,7 @@ impl TryFrom for CipherEditRequest { name: value.name, notes: value.notes, fields: value.fields.unwrap_or_default(), - r#type: type_data, + r#type: require!(type_data), revision_date: value.revision_date, archived_date: value.archived_date, }) @@ -102,26 +102,28 @@ impl TryFrom for CipherEditRequest { } impl CipherEditRequest { - fn update_password_history(&mut self, original_cipher: &CipherView) { + fn generate_password_history( + &mut self, + original_cipher: &CipherView, + ) -> Vec { let changes = self .detect_login_password_changes(original_cipher) .into_iter() .chain(self.detect_hidden_field_changes(original_cipher)); let history: Vec<_> = changes .rev() - .map(|change| change.into_history_entry()) .chain(original_cipher.password_history.iter().flatten().cloned()) .take(MAX_PASSWORD_HISTORY_ENTRIES) .collect(); - self.password_history = (!history.is_empty()).then_some(history); + history } fn detect_login_password_changes( &mut self, original_cipher: &CipherView, - ) -> Vec { - if !matches!(self.r#type, Some(CipherViewType::Login(_))) + ) -> Vec { + if !matches!(self.r#type, CipherViewType::Login(_)) || original_cipher.r#type != CipherType::Login { return vec![]; @@ -150,11 +152,14 @@ impl CipherEditRequest { } else { // Password changed - update revision date and track change current_login.password_revision_date = Some(Utc::now()); - vec![PasswordChange::new_password(original_password)] + vec![PasswordHistoryView::new_password(original_password)] } } - fn detect_hidden_field_changes(&self, original_cipher: &CipherView) -> Vec { + fn detect_hidden_field_changes( + &self, + original_cipher: &CipherView, + ) -> Vec { let original_fields = Self::extract_hidden_fields(original_cipher.fields.as_deref().unwrap_or_default()); let current_fields = Self::extract_hidden_fields(&self.fields); @@ -164,7 +169,7 @@ impl CipherEditRequest { .filter_map(|(field_name, original_value)| { let current_value = current_fields.get(&field_name); if current_value != Some(&original_value) { - Some(PasswordChange::new_field(&field_name, &original_value)) + Some(PasswordHistoryView::new_field(&field_name, &original_value)) } else { None } @@ -206,11 +211,7 @@ impl CompositeEncryptable for Cipher let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: cipher_data - .r#type - .as_ref() - .map(CipherViewType::get_cipher_type) - .map(<_ as Into<_>>::into), + r#type: Some(cipher_data.r#type.get_cipher_type().into()), organization_id: cipher_data.organization_id.map(|id| id.to_string()), folder_id: cipher_data.folder_id.map(|id| id.to_string()), favorite: Some(cipher_data.favorite), @@ -231,15 +232,7 @@ impl CompositeEncryptable for Cipher .map(|f| f.into()) .collect(), ), - password_history: Some( - cipher_data - .password_history - .encrypt_composite(ctx, cipher_key)? - .into_iter() - .flatten() - .map(|ph| ph.into()) - .collect(), - ), + password_history: None, // TODO: Need to calculate this and re-encrypt after encryption. attachments: None, attachments2: None, login: cipher_data @@ -308,10 +301,14 @@ async fn edit_cipher + ?Sized>( let original_cipher_view: CipherView = key_store.decrypt(&original_cipher)?; // Update password history - request.update_password_history(&original_cipher_view); + let password_history = request.generate_password_history(&original_cipher_view); + let enc_password_history = + password_history.encrypt_composite(&mut key_store.context(), request.key_identifier())?; let mut cipher_request = key_store.encrypt(request)?; cipher_request.encrypted_for = Some(encrypted_for.into()); + cipher_request.password_history = + Some(enc_password_history.into_iter().map(Into::into).collect()); let response = api_client .ciphers_api() @@ -622,9 +619,8 @@ mod tests { let mut edit_request = CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); - edit_request.update_password_history(&original_cipher); + let history = edit_request.generate_password_history(&original_cipher); - let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].password, "old_password"); } @@ -635,9 +631,9 @@ mod tests { let mut edit_request = CipherEditRequest::try_from(create_test_login_cipher("same_password")).unwrap(); - edit_request.update_password_history(&original_cipher); + let password_history = edit_request.generate_password_history(&original_cipher); - assert!(edit_request.password_history.is_none()); + assert!(password_history.is_empty()); } #[test] @@ -660,9 +656,8 @@ mod tests { let mut edit_request = CipherEditRequest::try_from(new_cipher).unwrap(); - edit_request.update_password_history(&original_cipher); + let history = edit_request.generate_password_history(&original_cipher); - let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].password, "Secret Key: old_secret_value"); } @@ -683,9 +678,8 @@ mod tests { let mut edit_request = CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); - edit_request.update_password_history(&original_cipher); + let history = edit_request.generate_password_history(&original_cipher); - let history = edit_request.password_history.unwrap(); assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES); // Most recent change (original password) should be first assert_eq!(history[0].password, "password"); diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index 48a2c77a1..08d7e09ec 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -88,34 +88,19 @@ impl From for CipherPasswordHistoryModel { } } -/// Tracks password and sensitive field changes for history purposes. -#[derive(Debug, Clone)] -pub struct PasswordChange { - /// Display name for the changed item - display_name: String, - /// When this change occurred - changed_at: DateTime, -} - -impl PasswordChange { +#[allow(missing_docs)] +impl PasswordHistoryView { pub fn new_password(old_password: &str) -> Self { Self { - display_name: old_password.to_string(), - changed_at: Utc::now(), + password: old_password.to_string(), + last_used_date: Utc::now(), } } pub fn new_field(field_name: &str, old_value: &str) -> Self { Self { - display_name: format!("{field_name}: {old_value}"), - changed_at: Utc::now(), - } - } - - pub fn into_history_entry(self) -> PasswordHistoryView { - PasswordHistoryView { - password: self.display_name, - last_used_date: self.changed_at, + password: format!("{field_name}: {old_value}"), + last_used_date: Utc::now(), } } } From a1f39f213e1383b44987dd20fb55692f193f012e Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 2 Oct 2025 14:45:26 -0700 Subject: [PATCH 44/60] Rename cipher_client::list() to cipher_client::list_ciphers() --- crates/bitwarden-vault/src/cipher/cipher_client/get.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs index cad74d2bc..dfe222a02 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/get.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/get.rs @@ -32,7 +32,7 @@ async fn get_cipher( Ok(store.decrypt(&cipher)?) } -async fn list( +async fn list_ciphers( store: &KeyStore, repository: &dyn Repository, ) -> Result { @@ -52,7 +52,7 @@ impl CiphersClient { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; - list(key_store, repository.as_ref()).await + list_ciphers(key_store, repository.as_ref()).await } /// Get [Cipher] by ID from state and decrypt it to a [CipherView]. From 1b897a2acad9ebf71f14e625a66a08c64b1a97d0 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 2 Oct 2025 14:45:48 -0700 Subject: [PATCH 45/60] Revert formatting changes to Cargo.toml --- Cargo.toml | 44 +++++++++++++++---------------- crates/bitwarden-vault/Cargo.toml | 4 +-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25705cd20..00d90641d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,6 @@ keywords = ["bitwarden"] # Define dependencies that are expected to be consistent across all crates [workspace.dependencies] - -# External crates that are expected to maintain a consistent version across all crates -async-trait = ">=0.1.80, <0.2" bitwarden = { path = "crates/bitwarden", version = "=1.0.0" } bitwarden-api-api = { path = "crates/bitwarden-api-api", version = "=1.0.0" } bitwarden-api-identity = { path = "crates/bitwarden-api-identity", version = "=1.0.0" } @@ -49,6 +46,9 @@ bitwarden-uniffi-error = { path = "crates/bitwarden-uniffi-error", version = "=1 bitwarden-uuid = { path = "crates/bitwarden-uuid", version = "=1.0.0" } bitwarden-uuid-macro = { path = "crates/bitwarden-uuid-macro", version = "=1.0.0" } bitwarden-vault = { path = "crates/bitwarden-vault", version = "=1.0.0" } + +# External crates that are expected to maintain a consistent version across all crates +async-trait = ">=0.1.80, <0.2" chrono = { version = ">=0.4.26, <0.5", features = [ "clock", "serde", @@ -67,11 +67,11 @@ reqwest = { version = ">=0.12.5, <0.13", features = [ ], default-features = false } schemars = { version = ">=1.0.0, <2.0", features = ["uuid1", "chrono04"] } serde = { version = ">=1.0, <2.0", features = ["derive"] } -serde-wasm-bindgen = ">=0.6.0, <0.7" serde_bytes = { version = ">=0.11.17, <0.12.0" } serde_json = ">=1.0.96, <2.0" serde_qs = ">=0.12.0, <0.16" serde_repr = ">=0.1.12, <0.2" +serde-wasm-bindgen = ">=0.6.0, <0.7" syn = ">=2.0.87, <3" thiserror = ">=1.0.40, <3" tokio = { version = "1.36.0", features = ["macros"] } @@ -86,20 +86,6 @@ wasm-bindgen-futures = "0.4.41" wasm-bindgen-test = "0.3.45" wiremock = ">=0.6.0, <0.7" -[workspace.lints.clippy] -unused_async = "deny" -unwrap_used = "deny" -string_slice = "warn" - -[workspace.lints.rust] -missing_docs = "warn" -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(feature, values("uniffi", "wasm"))', -] } - -[workspace.metadata.dylint] -libraries = [{ path = "support/lints" }] - # There is an incompatibility when using pkcs5 and chacha20 on wasm builds. This can be removed once a new # rustcrypto-formats crate version is released since the fix has been upstreamed. # https://github.com/RustCrypto/formats/pull/1625 @@ -113,10 +99,19 @@ uniffi_internal_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = " uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "6d46b3f756dde3213357c477d86771a0fc5da7b4" } uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "6d46b3f756dde3213357c477d86771a0fc5da7b4" } -# Turn on a small amount of optimisation in development mode. This might interfere when trying to use a debugger -# if the compiler decides to optimize some code away, if that's the case, it can be set to 0 or commented out -[profile.dev] -opt-level = 1 +[workspace.lints.clippy] +unused_async = "deny" +unwrap_used = "deny" +string_slice = "warn" + +[workspace.lints.rust] +missing_docs = "warn" +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(feature, values("uniffi", "wasm"))', +] } + +[workspace.metadata.dylint] +libraries = [{ path = "support/lints" }] # Compile all dependencies with some optimizations when building this crate on debug # This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster @@ -124,6 +119,11 @@ opt-level = 1 [profile.dev.package."*"] opt-level = 2 +# Turn on a small amount of optimisation in development mode. This might interfere when trying to use a debugger +# if the compiler decides to optimize some code away, if that's the case, it can be set to 0 or commented out +[profile.dev] +opt-level = 1 + # Turn on LTO on release mode [profile.release] codegen-units = 1 diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 6955681e2..9ae609694 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -19,7 +19,7 @@ uniffi = [ "bitwarden-collections/uniffi", "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", - "dep:uniffi", + "dep:uniffi" ] # Uniffi bindings wasm = [ "bitwarden-collections/wasm", @@ -27,7 +27,7 @@ wasm = [ "bitwarden-encoding/wasm", "dep:tsify", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] From 7c423e9e359d8cc7769dbdb9fd617e6369f84520 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 2 Oct 2025 15:21:05 -0700 Subject: [PATCH 46/60] Add test for preserving password history without edits --- .../src/cipher/cipher_client/edit.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index bcbd15a34..c6962d6a8 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -357,6 +357,7 @@ mod tests { use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; use bitwarden_test::MemoryRepository; + use chrono::TimeZone; use super::*; use crate::{ @@ -636,6 +637,45 @@ mod tests { assert!(password_history.is_empty()); } + #[test] + fn test_password_history_is_preserved() { + let mut original_cipher = create_test_login_cipher("same_password"); + original_cipher.password_history = Some( + (0..4) + .map(|i| PasswordHistoryView { + password: format!("old_password_{}", i), + last_used_date: Utc.with_ymd_and_hms(2025, i + 1, i + 1, i, i, i).unwrap(), + }) + .collect(), + ); + + let mut edit_request = + CipherEditRequest::try_from(create_test_login_cipher("same_password")).unwrap(); + + let history = edit_request.generate_password_history(&original_cipher); + + assert_eq!(history[0].password, "old_password_0"); + assert_eq!( + history[0].last_used_date, + Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap() + ); + assert_eq!(history[1].password, "old_password_1"); + assert_eq!( + history[1].last_used_date, + Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap() + ); + assert_eq!(history[2].password, "old_password_2"); + assert_eq!( + history[2].last_used_date, + Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap() + ); + assert_eq!(history[3].password, "old_password_3"); + assert_eq!( + history[3].last_used_date, + Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap() + ); + } + #[test] fn test_password_history_with_hidden_fields() { let mut original_cipher = create_test_login_cipher("password"); From c22d5195292b478e3b6a97d7a934fafa88d663bd Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 2 Oct 2025 15:25:15 -0700 Subject: [PATCH 47/60] Add check for last_used_date on new passwords --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index c6962d6a8..05b1a3fa4 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -620,9 +620,15 @@ mod tests { let mut edit_request = CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); + let start = Utc::now(); let history = edit_request.generate_password_history(&original_cipher); + let end = Utc::now(); assert_eq!(history.len(), 1); + assert!( + history[0].last_used_date > start && history[0].last_used_date < end, + "last_used_date was not set properly" + ); assert_eq!(history[0].password, "old_password"); } From 8600bd4ee1e973b72aa1d7ed2cf4b8a40c639cd0 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Tue, 7 Oct 2025 14:50:17 -0700 Subject: [PATCH 48/60] Update CipherViewType for CipherCreateRequest --- .../src/cipher/cipher_client/create.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index a5941dcf4..06bf7c690 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -42,7 +42,7 @@ pub enum CreateCipherError { } /// Request to add a cipher. -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -53,7 +53,7 @@ pub struct CipherCreateRequest { pub notes: Option, pub favorite: bool, pub reprompt: CipherRepromptType, - pub type_data: Option, + pub r#type: CipherViewType, pub fields: Vec, /// For internal use only. Do not set this from clients. pub key: Option, @@ -72,7 +72,7 @@ impl CipherCreateRequest { const NEW_KEY_ID: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key"); let new_key = ctx.generate_symmetric_key(NEW_KEY_ID)?; - self.type_data + self.r#type .as_login_view_mut() .map(|l| l.reencrypt_fido2_credentials(ctx, old_key, new_key)) .transpose()?; @@ -82,7 +82,7 @@ impl CipherCreateRequest { } fn generate_checksums(&mut self) { - if let Some(login) = &mut self.type_data.as_login_view_mut() { + if let Some(login) = &mut self.r#type.as_login_view_mut() { login.generate_checksums(); } } @@ -102,11 +102,7 @@ impl CompositeEncryptable for Cipher let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: cipher_data - .type_data - .as_ref() - .map(CipherViewType::get_cipher_type) - .map(<_ as Into<_>>::into), + r#type: Some(cipher_data.r#type.get_cipher_type().into()), organization_id: cipher_data.organization_id.map(|id| id.to_string()), folder_id: cipher_data.folder_id.map(|id| id.to_string()), favorite: Some(cipher_data.favorite), @@ -120,35 +116,35 @@ impl CompositeEncryptable for Cipher .transpose()? .map(|n| n.to_string()), login: cipher_data - .type_data + .r#type .as_login_view() .as_ref() .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), card: cipher_data - .type_data + .r#type .as_card_view() .as_ref() .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data - .type_data + .r#type .as_identity_view() .as_ref() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|i| Box::new(i.into())), secure_note: cipher_data - .type_data + .r#type .as_secure_note_view() .as_ref() .map(|s| s.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|s| Box::new(s.into())), ssh_key: cipher_data - .type_data + .r#type .as_ssh_key_view() .as_ref() .map(|s| s.encrypt_composite(ctx, cipher_key)) @@ -260,7 +256,7 @@ mod tests { CipherCreateRequest { name: "Test Login".to_string(), notes: Some("Test notes".to_string()), - type_data: Some(CipherViewType::Login(LoginView { + r#type: CipherViewType::Login(LoginView { username: Some("test@example.com".to_string()), password: Some("password123".to_string()), password_revision_date: None, @@ -268,8 +264,13 @@ mod tests { totp: None, autofill_on_page_load: None, fido2_credentials: None, - })), - ..Default::default() + }), + organization_id: Default::default(), + folder_id: Default::default(), + favorite: Default::default(), + reprompt: Default::default(), + fields: Default::default(), + key: Default::default(), } } From b665cab6fb3d70e0b7c5ccbeba4c527d3c0ea291 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Tue, 7 Oct 2025 14:59:39 -0700 Subject: [PATCH 49/60] Add wasm_bindgen attribute to CiphersClient::edit and ::create --- crates/bitwarden-vault/src/cipher/cipher_client/create.rs | 1 + crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 06bf7c690..6694a3296 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -201,6 +201,7 @@ async fn create_cipher + ?Sized>( Ok(key_store.decrypt(&cipher)?) } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl CiphersClient { /// Create a new [Cipher] and save it to the server. pub async fn create( diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 05b1a3fa4..25f09a355 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -327,6 +327,7 @@ async fn edit_cipher + ?Sized>( Ok(key_store.decrypt(&cipher)?) } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl CiphersClient { /// Edit an existing [Cipher] and save it to the server. pub async fn edit(&self, request: CipherEditRequest) -> Result { From 9713beb3da44f43d0125dbfd2d02b5545c61ccc9 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 8 Oct 2025 17:27:08 -0700 Subject: [PATCH 50/60] Remove CipherViewTypeExt and impl the methods directly --- .../src/cipher/cipher_client/create.rs | 2 +- .../src/cipher/cipher_client/edit.rs | 3 +- .../src/cipher/cipher_view_type.rs | 86 +++---------------- 3 files changed, 14 insertions(+), 77 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 6694a3296..230a97c24 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -20,7 +20,7 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ Cipher, CipherRepromptType, CipherView, FieldView, FolderId, VaultParseError, - cipher_view_type::{CipherViewType, CipherViewTypeExt}, + cipher_view_type::CipherViewType, }; #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 25f09a355..a0c32fb5b 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -23,8 +23,7 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, - ItemNotFoundError, PasswordHistoryView, VaultParseError, - cipher_view_type::{CipherViewType, CipherViewTypeExt}, + ItemNotFoundError, PasswordHistoryView, VaultParseError, cipher_view_type::CipherViewType, }; /// Maximum number of password history entries to retain diff --git a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs index 5ea06a174..53e622784 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_view_type.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_view_type.rs @@ -31,133 +31,71 @@ impl CipherViewType { } } -/// Extension trait to provide type-safe accessors for the different cipher view types. -#[allow(private_bounds)] -pub trait CipherViewTypeExt -where - Self: LockedTrait, -{ - fn as_login_view_mut(&mut self) -> Option<&mut LoginView>; - fn as_card_view_mut(&mut self) -> Option<&mut CardView>; - fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView>; - fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView>; - fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView>; - fn as_login_view(&self) -> Option<&LoginView>; - fn as_card_view(&self) -> Option<&CardView>; - fn as_identity_view(&self) -> Option<&IdentityView>; - fn as_secure_note_view(&self) -> Option<&SecureNoteView>; - fn as_ssh_key_view(&self) -> Option<&SshKeyView>; -} - -impl CipherViewTypeExt for Option { - fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { - self.as_mut().and_then(|inner| inner.as_login_view_mut()) - } - fn as_card_view_mut(&mut self) -> Option<&mut CardView> { - self.as_mut().and_then(|inner| inner.as_card_view_mut()) - } - - fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { - self.as_mut().and_then(|inner| inner.as_identity_view_mut()) - } - - fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { - self.as_mut() - .and_then(|inner| inner.as_secure_note_view_mut()) - } - - fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { - self.as_mut().and_then(|inner| inner.as_ssh_key_view_mut()) - } - - fn as_login_view(&self) -> Option<&LoginView> { - self.as_ref().and_then(|inner| inner.as_login_view()) - } - - fn as_card_view(&self) -> Option<&CardView> { - self.as_ref().and_then(|inner| inner.as_card_view()) - } - - fn as_identity_view(&self) -> Option<&IdentityView> { - self.as_ref().and_then(|inner| inner.as_identity_view()) - } - - fn as_secure_note_view(&self) -> Option<&SecureNoteView> { - self.as_ref().and_then(|inner| inner.as_secure_note_view()) - } - - fn as_ssh_key_view(&self) -> Option<&SshKeyView> { - self.as_ref().and_then(|inner| inner.as_ssh_key_view()) - } -} - -trait LockedTrait {} -impl LockedTrait for CipherViewType {} -impl LockedTrait for Option {} -impl CipherViewTypeExt for CipherViewType { - fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { +#[allow(unused)] +impl CipherViewType { + pub(crate) fn as_login_view_mut(&mut self) -> Option<&mut LoginView> { match self { CipherViewType::Login(l) => Some(l), _ => None, } } - fn as_card_view_mut(&mut self) -> Option<&mut CardView> { + pub(crate) fn as_card_view_mut(&mut self) -> Option<&mut CardView> { match self { CipherViewType::Card(c) => Some(c), _ => None, } } - fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { + pub(crate) fn as_identity_view_mut(&mut self) -> Option<&mut IdentityView> { match self { CipherViewType::Identity(i) => Some(i), _ => None, } } - fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { + pub(crate) fn as_secure_note_view_mut(&mut self) -> Option<&mut SecureNoteView> { match self { CipherViewType::SecureNote(s) => Some(s), _ => None, } } - fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { + pub(crate) fn as_ssh_key_view_mut(&mut self) -> Option<&mut SshKeyView> { match self { CipherViewType::SshKey(s) => Some(s), _ => None, } } - fn as_login_view(&self) -> Option<&LoginView> { + pub(crate) fn as_login_view(&self) -> Option<&LoginView> { match self { CipherViewType::Login(l) => Some(l), _ => None, } } - fn as_card_view(&self) -> Option<&CardView> { + pub(crate) fn as_card_view(&self) -> Option<&CardView> { match self { CipherViewType::Card(c) => Some(c), _ => None, } } - fn as_identity_view(&self) -> Option<&IdentityView> { + pub(crate) fn as_identity_view(&self) -> Option<&IdentityView> { match self { CipherViewType::Identity(i) => Some(i), _ => None, } } - fn as_secure_note_view(&self) -> Option<&SecureNoteView> { + pub(crate) fn as_secure_note_view(&self) -> Option<&SecureNoteView> { match self { CipherViewType::SecureNote(s) => Some(s), _ => None, } } - fn as_ssh_key_view(&self) -> Option<&SshKeyView> { + pub(crate) fn as_ssh_key_view(&self) -> Option<&SshKeyView> { match self { CipherViewType::SshKey(s) => Some(s), _ => None, From 1e0ed6cd28702e149f73094299fd3f7e8ef79f56 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 8 Oct 2025 17:28:14 -0700 Subject: [PATCH 51/60] Change visibility on PasswordHistoryView creation methods --- crates/bitwarden-vault/src/password_history.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index 08d7e09ec..b72905ea3 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -88,16 +88,15 @@ impl From for CipherPasswordHistoryModel { } } -#[allow(missing_docs)] impl PasswordHistoryView { - pub fn new_password(old_password: &str) -> Self { + pub(crate) fn new_password(old_password: &str) -> Self { Self { password: old_password.to_string(), last_used_date: Utc::now(), } } - pub fn new_field(field_name: &str, old_value: &str) -> Self { + pub(crate) fn new_field(field_name: &str, old_value: &str) -> Self { Self { password: format!("{field_name}: {old_value}"), last_used_date: Utc::now(), From 0ccac3de32058b21d41ea6f989740fe15f42102b Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 8 Oct 2025 18:22:32 -0700 Subject: [PATCH 52/60] Add intermediary CipherEditRequestInternal to prevent accidental misuse of CipherEditRequest --- .../src/cipher/cipher_client/edit.rs | 97 ++++++++++++++----- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index a0c32fb5b..6913710ca 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -100,11 +100,46 @@ impl TryFrom for CipherEditRequest { } } -impl CipherEditRequest { - fn generate_password_history( - &mut self, - original_cipher: &CipherView, - ) -> Vec { +/// Used as an intermediary between the public-facing [CipherEditRequest], and the encrypted +/// value. This allows us to calculate password history safely, without risking misuse. +#[derive(Clone, Debug)] +struct CipherEditRequestInternal { + pub organization_id: Option, + pub folder_id: Option, + pub favorite: bool, + pub reprompt: CipherRepromptType, + pub name: String, + pub notes: Option, + pub fields: Vec, + pub r#type: CipherViewType, + pub revision_date: DateTime, + pub archived_date: Option>, + pub password_history: Vec, + pub key: Option, +} + +impl CipherEditRequestInternal { + fn new(req: CipherEditRequest, orig_cipher: &CipherView) -> Self { + let mut internal_req = Self { + organization_id: req.organization_id, + folder_id: req.folder_id, + favorite: req.favorite, + reprompt: req.reprompt, + name: req.name, + notes: req.notes, + fields: req.fields, + r#type: req.r#type, + revision_date: req.revision_date, + archived_date: req.archived_date, + key: req.key, + password_history: vec![], + }; + internal_req.update_password_history(&orig_cipher); + + internal_req + } + + fn update_password_history(&mut self, original_cipher: &CipherView) { let changes = self .detect_login_password_changes(original_cipher) .into_iter() @@ -115,7 +150,7 @@ impl CipherEditRequest { .take(MAX_PASSWORD_HISTORY_ENTRIES) .collect(); - history + self.password_history = history; } fn detect_login_password_changes( @@ -197,7 +232,9 @@ impl CipherEditRequest { } } -impl CompositeEncryptable for CipherEditRequest { +impl CompositeEncryptable + for CipherEditRequestInternal +{ fn encrypt_composite( &self, ctx: &mut KeyStoreContext, @@ -231,7 +268,14 @@ impl CompositeEncryptable for Cipher .map(|f| f.into()) .collect(), ), - password_history: None, // TODO: Need to calculate this and re-encrypt after encryption. + password_history: Some( + cipher_data + .password_history + .encrypt_composite(ctx, cipher_key)? + .into_iter() + .map(Into::into) + .collect(), + ), attachments: None, attachments2: None, login: cipher_data @@ -275,7 +319,7 @@ impl CompositeEncryptable for Cipher } } -impl IdentifyKey for CipherEditRequest { +impl IdentifyKey for CipherEditRequestInternal { fn key_identifier(&self) -> SymmetricKeyId { match self.organization_id { Some(organization_id) => SymmetricKeyId::Organization(organization_id), @@ -289,7 +333,7 @@ async fn edit_cipher + ?Sized>( api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, encrypted_for: UserId, - mut request: CipherEditRequest, + request: CipherEditRequest, ) -> Result { let cipher_id = request.id; @@ -300,14 +344,10 @@ async fn edit_cipher + ?Sized>( let original_cipher_view: CipherView = key_store.decrypt(&original_cipher)?; // Update password history - let password_history = request.generate_password_history(&original_cipher_view); - let enc_password_history = - password_history.encrypt_composite(&mut key_store.context(), request.key_identifier())?; + let request = CipherEditRequestInternal::new(request, &original_cipher_view); let mut cipher_request = key_store.encrypt(request)?; cipher_request.encrypted_for = Some(encrypted_for.into()); - cipher_request.password_history = - Some(enc_password_history.into_iter().map(Into::into).collect()); let response = api_client .ciphers_api() @@ -617,11 +657,12 @@ mod tests { #[test] fn test_password_history_on_password_change() { let original_cipher = create_test_login_cipher("old_password"); - let mut edit_request = + let edit_request = CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); let start = Utc::now(); - let history = edit_request.generate_password_history(&original_cipher); + let internal_req = CipherEditRequestInternal::new(edit_request, &original_cipher); + let history = internal_req.password_history; let end = Utc::now(); assert_eq!(history.len(), 1); @@ -635,10 +676,11 @@ mod tests { #[test] fn test_password_history_on_unchanged_password() { let original_cipher = create_test_login_cipher("same_password"); - let mut edit_request = + let edit_request = CipherEditRequest::try_from(create_test_login_cipher("same_password")).unwrap(); - let password_history = edit_request.generate_password_history(&original_cipher); + let internal_req = CipherEditRequestInternal::new(edit_request, &original_cipher); + let password_history = internal_req.password_history; assert!(password_history.is_empty()); } @@ -655,12 +697,13 @@ mod tests { .collect(), ); - let mut edit_request = + let edit_request = CipherEditRequest::try_from(create_test_login_cipher("same_password")).unwrap(); - - let history = edit_request.generate_password_history(&original_cipher); + let internal_req = CipherEditRequestInternal::new(edit_request, &original_cipher); + let history = internal_req.password_history; assert_eq!(history[0].password, "old_password_0"); + assert_eq!( history[0].last_used_date, Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap() @@ -700,9 +743,10 @@ mod tests { linked_id: None, }]); - let mut edit_request = CipherEditRequest::try_from(new_cipher).unwrap(); + let edit_request = CipherEditRequest::try_from(new_cipher).unwrap(); - let history = edit_request.generate_password_history(&original_cipher); + let internal_req = CipherEditRequestInternal::new(edit_request, &original_cipher); + let history = internal_req.password_history; assert_eq!(history.len(), 1); assert_eq!(history[0].password, "Secret Key: old_secret_value"); @@ -721,10 +765,11 @@ mod tests { ); // Create edit request with new password (no existing history) - let mut edit_request = + let edit_request = CipherEditRequest::try_from(create_test_login_cipher("new_password")).unwrap(); - let history = edit_request.generate_password_history(&original_cipher); + let internal_req = CipherEditRequestInternal::new(edit_request, &original_cipher); + let history = internal_req.password_history; assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES); // Most recent change (original password) should be first From bed6f11c5b341de3c89be57cc2ee38f6860ca7d7 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 8 Oct 2025 18:33:24 -0700 Subject: [PATCH 53/60] Adjust checks to account for very fast test execution --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 6913710ca..a4b4a7a96 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -343,7 +343,6 @@ async fn edit_cipher + ?Sized>( .ok_or(ItemNotFoundError)?; let original_cipher_view: CipherView = key_store.decrypt(&original_cipher)?; - // Update password history let request = CipherEditRequestInternal::new(request, &original_cipher_view); let mut cipher_request = key_store.encrypt(request)?; @@ -393,6 +392,8 @@ impl CiphersClient { #[cfg(test)] mod tests { + use std::{thread::sleep, time::Duration}; + use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel}; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; @@ -667,7 +668,7 @@ mod tests { assert_eq!(history.len(), 1); assert!( - history[0].last_used_date > start && history[0].last_used_date < end, + history[0].last_used_date >= start && history[0].last_used_date <= end, "last_used_date was not set properly" ); assert_eq!(history[0].password, "old_password"); From 08dbf06093a0ac53944210962f41e3b79a1b3249 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 8 Oct 2025 18:44:24 -0700 Subject: [PATCH 54/60] Fix clippy errors --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index a4b4a7a96..093a31fd6 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -134,7 +134,7 @@ impl CipherEditRequestInternal { key: req.key, password_history: vec![], }; - internal_req.update_password_history(&orig_cipher); + internal_req.update_password_history(orig_cipher); internal_req } @@ -392,8 +392,6 @@ impl CiphersClient { #[cfg(test)] mod tests { - use std::{thread::sleep, time::Duration}; - use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel}; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; From ea7d98af8c5c7d13b3e134d2f0c19e39d46d56ae Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 9 Oct 2025 14:32:43 -0700 Subject: [PATCH 55/60] Remove unnecessary pub from CipherEditREquestInternal fields --- .../src/cipher/cipher_client/edit.rs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 093a31fd6..d28db2d14 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -104,18 +104,18 @@ impl TryFrom for CipherEditRequest { /// value. This allows us to calculate password history safely, without risking misuse. #[derive(Clone, Debug)] struct CipherEditRequestInternal { - pub organization_id: Option, - pub folder_id: Option, - pub favorite: bool, - pub reprompt: CipherRepromptType, - pub name: String, - pub notes: Option, - pub fields: Vec, - pub r#type: CipherViewType, - pub revision_date: DateTime, - pub archived_date: Option>, - pub password_history: Vec, - pub key: Option, + organization_id: Option, + folder_id: Option, + favorite: bool, + reprompt: CipherRepromptType, + name: String, + notes: Option, + fields: Vec, + r#type: CipherViewType, + revision_date: DateTime, + archived_date: Option>, + password_history: Vec, + key: Option, } impl CipherEditRequestInternal { From ed2f8e727e0bbfff409b7c3e294aa438be228b75 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Thu, 9 Oct 2025 14:33:11 -0700 Subject: [PATCH 56/60] Create CipherCreateRequestInternal to abstract key attr from consumer --- .../src/cipher/cipher_client/create.rs | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 230a97c24..5f2a212ba 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -55,11 +55,40 @@ pub struct CipherCreateRequest { pub reprompt: CipherRepromptType, pub r#type: CipherViewType, pub fields: Vec, - /// For internal use only. Do not set this from clients. - pub key: Option, } -impl CipherCreateRequest { +/// Used as an intermediary between the public-facing [CipherCreateRequest], and the encrypted +/// value. This allows us to manage the cipher key creation internally. +#[derive(Clone, Debug)] +struct CipherCreateRequestInternal { + organization_id: Option, + folder_id: Option, + name: String, + notes: Option, + favorite: bool, + reprompt: CipherRepromptType, + r#type: CipherViewType, + fields: Vec, + key: Option, +} + +impl From for CipherCreateRequestInternal { + fn from(req: CipherCreateRequest) -> Self { + Self { + organization_id: req.organization_id, + folder_id: req.folder_id, + name: req.name, + notes: req.notes, + favorite: req.favorite, + reprompt: req.reprompt, + r#type: req.r#type, + fields: req.fields, + key: None, + } + } +} + +impl CipherCreateRequestInternal { /// Generate a new key for the cipher, re-encrypting internal data, if necessary, and stores the /// encrypted key to the cipher data. fn generate_cipher_key( @@ -88,7 +117,9 @@ impl CipherCreateRequest { } } -impl CompositeEncryptable for CipherCreateRequest { +impl CompositeEncryptable + for CipherCreateRequestInternal +{ fn encrypt_composite( &self, ctx: &mut KeyStoreContext, @@ -170,7 +201,7 @@ impl CompositeEncryptable for Cipher } } -impl IdentifyKey for CipherCreateRequest { +impl IdentifyKey for CipherCreateRequestInternal { fn key_identifier(&self) -> SymmetricKeyId { match self.organization_id { Some(organization_id) => SymmetricKeyId::Organization(organization_id), @@ -184,7 +215,7 @@ async fn create_cipher + ?Sized>( api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, encrypted_for: UserId, - request: CipherCreateRequest, + request: CipherCreateRequestInternal, ) -> Result { let mut cipher_request = key_store.encrypt(request)?; cipher_request.encrypted_for = Some(encrypted_for.into()); @@ -206,11 +237,12 @@ impl CiphersClient { /// Create a new [Cipher] and save it to the server. pub async fn create( &self, - mut request: CipherCreateRequest, + request: CipherCreateRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; + let mut internal_request: CipherCreateRequestInternal = request.into(); let user_id = self .client @@ -226,8 +258,8 @@ impl CiphersClient { .get_flags() .enable_cipher_key_encryption { - let key = request.key_identifier(); - request.generate_cipher_key(&mut key_store.context(), key)?; + let key = internal_request.key_identifier(); + internal_request.generate_cipher_key(&mut key_store.context(), key)?; } create_cipher( @@ -235,7 +267,7 @@ impl CiphersClient { &config.api_client, repository.as_ref(), user_id, - request, + internal_request, ) .await } @@ -271,7 +303,6 @@ mod tests { favorite: Default::default(), reprompt: Default::default(), fields: Default::default(), - key: Default::default(), } } @@ -338,7 +369,7 @@ mod tests { &api_client, &repository, TEST_USER_ID.parse().unwrap(), - request, + request.into(), ) .await .unwrap(); @@ -401,7 +432,7 @@ mod tests { &api_client, &repository, TEST_USER_ID.parse().unwrap(), - request, + request.into(), ) .await; From 7a7acd5bc262b0e4a8037445946a6c808fe41d66 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Mon, 13 Oct 2025 17:32:42 -0700 Subject: [PATCH 57/60] Move CipherCreateREquest into CipherCreateRequestInternal --- .../src/cipher/cipher_client/create.rs | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 5f2a212ba..e42430fee 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -61,28 +61,14 @@ pub struct CipherCreateRequest { /// value. This allows us to manage the cipher key creation internally. #[derive(Clone, Debug)] struct CipherCreateRequestInternal { - organization_id: Option, - folder_id: Option, - name: String, - notes: Option, - favorite: bool, - reprompt: CipherRepromptType, - r#type: CipherViewType, - fields: Vec, + create_request: CipherCreateRequest, key: Option, } impl From for CipherCreateRequestInternal { - fn from(req: CipherCreateRequest) -> Self { + fn from(create_request: CipherCreateRequest) -> Self { Self { - organization_id: req.organization_id, - folder_id: req.folder_id, - name: req.name, - notes: req.notes, - favorite: req.favorite, - reprompt: req.reprompt, - r#type: req.r#type, - fields: req.fields, + create_request, key: None, } } @@ -101,7 +87,8 @@ impl CipherCreateRequestInternal { const NEW_KEY_ID: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key"); let new_key = ctx.generate_symmetric_key(NEW_KEY_ID)?; - self.r#type + self.create_request + .r#type .as_login_view_mut() .map(|l| l.reencrypt_fido2_credentials(ctx, old_key, new_key)) .transpose()?; @@ -111,7 +98,7 @@ impl CipherCreateRequestInternal { } fn generate_checksums(&mut self) { - if let Some(login) = &mut self.r#type.as_login_view_mut() { + if let Some(login) = &mut self.create_request.r#type.as_login_view_mut() { login.generate_checksums(); } } @@ -133,20 +120,32 @@ impl CompositeEncryptable let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: Some(cipher_data.r#type.get_cipher_type().into()), - organization_id: cipher_data.organization_id.map(|id| id.to_string()), - folder_id: cipher_data.folder_id.map(|id| id.to_string()), - favorite: Some(cipher_data.favorite), - reprompt: Some(cipher_data.reprompt.into()), + r#type: Some(cipher_data.create_request.r#type.get_cipher_type().into()), + organization_id: cipher_data + .create_request + .organization_id + .map(|id| id.to_string()), + folder_id: cipher_data + .create_request + .folder_id + .map(|id| id.to_string()), + favorite: Some(cipher_data.create_request.favorite), + reprompt: Some(cipher_data.create_request.reprompt.into()), key: cipher_data.key.map(|k| k.to_string()), - name: cipher_data.name.encrypt(ctx, cipher_key)?.to_string(), + name: cipher_data + .create_request + .name + .encrypt(ctx, cipher_key)? + .to_string(), notes: cipher_data + .create_request .notes .as_ref() .map(|n| n.encrypt(ctx, cipher_key)) .transpose()? .map(|n| n.to_string()), login: cipher_data + .create_request .r#type .as_login_view() .as_ref() @@ -154,6 +153,7 @@ impl CompositeEncryptable .transpose()? .map(|l| Box::new(l.into())), card: cipher_data + .create_request .r#type .as_card_view() .as_ref() @@ -161,6 +161,7 @@ impl CompositeEncryptable .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data + .create_request .r#type .as_identity_view() .as_ref() @@ -168,6 +169,7 @@ impl CompositeEncryptable .transpose()? .map(|i| Box::new(i.into())), secure_note: cipher_data + .create_request .r#type .as_secure_note_view() .as_ref() @@ -175,6 +177,7 @@ impl CompositeEncryptable .transpose()? .map(|s| Box::new(s.into())), ssh_key: cipher_data + .create_request .r#type .as_ssh_key_view() .as_ref() @@ -183,6 +186,7 @@ impl CompositeEncryptable .map(|s| Box::new(s.into())), fields: Some( cipher_data + .create_request .fields .iter() .map(|f| f.encrypt_composite(ctx, cipher_key)) @@ -203,7 +207,7 @@ impl CompositeEncryptable impl IdentifyKey for CipherCreateRequestInternal { fn key_identifier(&self) -> SymmetricKeyId { - match self.organization_id { + match self.create_request.organization_id { Some(organization_id) => SymmetricKeyId::Organization(organization_id), None => SymmetricKeyId::User, } From 0894b42dbb297c836a51628ec6aca53eaa5e4ed6 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Mon, 13 Oct 2025 17:37:30 -0700 Subject: [PATCH 58/60] Move CipherEditRequest into CipherEditRequestInternal --- .../src/cipher/cipher_client/edit.rs | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index d28db2d14..60fa84c69 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -104,34 +104,14 @@ impl TryFrom for CipherEditRequest { /// value. This allows us to calculate password history safely, without risking misuse. #[derive(Clone, Debug)] struct CipherEditRequestInternal { - organization_id: Option, - folder_id: Option, - favorite: bool, - reprompt: CipherRepromptType, - name: String, - notes: Option, - fields: Vec, - r#type: CipherViewType, - revision_date: DateTime, - archived_date: Option>, + edit_request: CipherEditRequest, password_history: Vec, - key: Option, } impl CipherEditRequestInternal { - fn new(req: CipherEditRequest, orig_cipher: &CipherView) -> Self { + fn new(edit_request: CipherEditRequest, orig_cipher: &CipherView) -> Self { let mut internal_req = Self { - organization_id: req.organization_id, - folder_id: req.folder_id, - favorite: req.favorite, - reprompt: req.reprompt, - name: req.name, - notes: req.notes, - fields: req.fields, - r#type: req.r#type, - revision_date: req.revision_date, - archived_date: req.archived_date, - key: req.key, + edit_request, password_history: vec![], }; internal_req.update_password_history(orig_cipher); @@ -157,7 +137,7 @@ impl CipherEditRequestInternal { &mut self, original_cipher: &CipherView, ) -> Vec { - if !matches!(self.r#type, CipherViewType::Login(_)) + if !matches!(self.edit_request.r#type, CipherViewType::Login(_)) || original_cipher.r#type != CipherType::Login { return vec![]; @@ -165,7 +145,7 @@ impl CipherEditRequestInternal { let (Some(original_login), Some(current_login)) = ( original_cipher.login.as_ref(), - self.r#type.as_login_view_mut(), + self.edit_request.r#type.as_login_view_mut(), ) else { return vec![]; }; @@ -196,7 +176,7 @@ impl CipherEditRequestInternal { ) -> Vec { let original_fields = Self::extract_hidden_fields(original_cipher.fields.as_deref().unwrap_or_default()); - let current_fields = Self::extract_hidden_fields(&self.fields); + let current_fields = Self::extract_hidden_fields(&self.edit_request.fields); original_fields .into_iter() @@ -226,7 +206,7 @@ impl CipherEditRequestInternal { } fn generate_checksums(&mut self) { - if let Some(login) = &mut self.r#type.as_login_view_mut() { + if let Some(login) = &mut self.edit_request.r#type.as_login_view_mut() { login.generate_checksums(); } } @@ -243,18 +223,26 @@ impl CompositeEncryptable let mut cipher_data = (*self).clone(); cipher_data.generate_checksums(); - let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + let cipher_key = Cipher::decrypt_cipher_key(ctx, key, &self.edit_request.key)?; let cipher_request = CipherRequestModel { encrypted_for: None, - r#type: Some(cipher_data.r#type.get_cipher_type().into()), - organization_id: cipher_data.organization_id.map(|id| id.to_string()), - folder_id: cipher_data.folder_id.map(|id| id.to_string()), - favorite: Some(cipher_data.favorite), - reprompt: Some(cipher_data.reprompt.into()), - key: cipher_data.key.map(|k| k.to_string()), - name: cipher_data.name.encrypt(ctx, cipher_key)?.to_string(), + r#type: Some(cipher_data.edit_request.r#type.get_cipher_type().into()), + organization_id: cipher_data + .edit_request + .organization_id + .map(|id| id.to_string()), + folder_id: cipher_data.edit_request.folder_id.map(|id| id.to_string()), + favorite: Some(cipher_data.edit_request.favorite), + reprompt: Some(cipher_data.edit_request.reprompt.into()), + key: cipher_data.edit_request.key.map(|k| k.to_string()), + name: cipher_data + .edit_request + .name + .encrypt(ctx, cipher_key)? + .to_string(), notes: cipher_data + .edit_request .notes .as_ref() .map(|n| n.encrypt(ctx, cipher_key)) @@ -262,6 +250,7 @@ impl CompositeEncryptable .map(|n| n.to_string()), fields: Some( cipher_data + .edit_request .fields .encrypt_composite(ctx, cipher_key)? .into_iter() @@ -279,18 +268,21 @@ impl CompositeEncryptable attachments: None, attachments2: None, login: cipher_data + .edit_request .r#type .as_login_view() .map(|l| l.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|l| Box::new(l.into())), card: cipher_data + .edit_request .r#type .as_card_view() .map(|c| c.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), identity: cipher_data + .edit_request .r#type .as_identity_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) @@ -298,20 +290,25 @@ impl CompositeEncryptable .map(|c| Box::new(c.into())), secure_note: cipher_data + .edit_request .r#type .as_secure_note_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), ssh_key: cipher_data + .edit_request .r#type .as_ssh_key_view() .map(|i| i.encrypt_composite(ctx, cipher_key)) .transpose()? .map(|c| Box::new(c.into())), - last_known_revision_date: Some(cipher_data.revision_date.to_rfc3339()), - archived_date: cipher_data.archived_date.map(|d| d.to_rfc3339()), + last_known_revision_date: Some(cipher_data.edit_request.revision_date.to_rfc3339()), + archived_date: cipher_data + .edit_request + .archived_date + .map(|d| d.to_rfc3339()), data: None, }; @@ -321,7 +318,7 @@ impl CompositeEncryptable impl IdentifyKey for CipherEditRequestInternal { fn key_identifier(&self) -> SymmetricKeyId { - match self.organization_id { + match self.edit_request.organization_id { Some(organization_id) => SymmetricKeyId::Organization(organization_id), None => SymmetricKeyId::User, } From f565d3402626eaf5515e6d3e9cf5fff52b35ff5b Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 15 Oct 2025 15:49:34 -0700 Subject: [PATCH 59/60] Generate new cipher_key when editing a cipher without one. --- .../bitwarden-vault/src/cipher/attachment.rs | 38 ++++++++++ crates/bitwarden-vault/src/cipher/cipher.rs | 12 +-- .../src/cipher/cipher_client/edit.rs | 73 +++++++++++++++++-- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index ba8c9bb5d..01b28d786 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::CipherAttachmentModel; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext, @@ -25,6 +26,15 @@ pub struct Attachment { pub key: Option, } +impl From for CipherAttachmentModel { + fn from(attachment: Attachment) -> Self { + Self { + file_name: attachment.file_name.map(|f| f.to_string()), + key: attachment.key.map(|k| k.to_string()), + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -51,6 +61,34 @@ pub struct AttachmentView { pub decrypted_key: Option, } +impl AttachmentView { + pub(crate) fn reencrypt_key( + &mut self, + ctx: &mut KeyStoreContext, + old_key: SymmetricKeyId, + new_key: SymmetricKeyId, + ) -> Result<(), CryptoError> { + if let Some(attachment_key) = &mut self.key { + let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key"); + ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?; + *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?; + } + Ok(()) + } + + pub(crate) fn reencrypt_keys( + attachment_views: &mut Vec, + ctx: &mut KeyStoreContext, + old_key: SymmetricKeyId, + new_key: SymmetricKeyId, + ) -> Result<(), CryptoError> { + for attachment in attachment_views { + attachment.reencrypt_key(ctx, old_key, new_key)?; + } + Ok(()) + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index b74e1e9d9..b7594e4df 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -30,8 +30,8 @@ use super::{ secure_note, ssh_key, }; use crate::{ - EncryptError, Fido2CredentialFullView, Fido2CredentialView, FolderId, Login, LoginView, - VaultParseError, password_history, + AttachmentView, EncryptError, Fido2CredentialFullView, Fido2CredentialView, FolderId, Login, + LoginView, VaultParseError, password_history, }; uuid_newtype!(pub CipherId); @@ -492,13 +492,7 @@ impl CipherView { new_key: SymmetricKeyId, ) -> Result<(), CryptoError> { if let Some(attachments) = &mut self.attachments { - for attachment in attachments { - if let Some(attachment_key) = &mut attachment.key { - let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key"); - ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?; - *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?; - } - } + AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?; } Ok(()) } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 60fa84c69..6876c01ef 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -22,8 +22,9 @@ use wasm_bindgen::prelude::*; use super::CiphersClient; use crate::{ - Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, FieldView, FolderId, - ItemNotFoundError, PasswordHistoryView, VaultParseError, cipher_view_type::CipherViewType, + AttachmentView, Cipher, CipherId, CipherRepromptType, CipherType, CipherView, FieldType, + FieldView, FolderId, ItemNotFoundError, PasswordHistoryView, VaultParseError, + cipher_view_type::CipherViewType, }; /// Maximum number of password history entries to retain @@ -69,6 +70,7 @@ pub struct CipherEditRequest { pub r#type: CipherViewType, pub revision_date: DateTime, pub archived_date: Option>, + pub attachments: Vec, pub key: Option, } @@ -94,15 +96,38 @@ impl TryFrom for CipherEditRequest { notes: value.notes, fields: value.fields.unwrap_or_default(), r#type: require!(type_data), + attachments: value.attachments.unwrap_or_default(), revision_date: value.revision_date, archived_date: value.archived_date, }) } } +impl CipherEditRequest { + fn generate_cipher_key( + &mut self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result<(), CryptoError> { + let old_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + + const NEW_KEY_ID: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key"); + let new_key = ctx.generate_symmetric_key(NEW_KEY_ID)?; + + // Re-encrypt the internal fields with the new key + self.r#type + .as_login_view_mut() + .map(|l| l.reencrypt_fido2_credentials(ctx, old_key, new_key)) + .transpose()?; + AttachmentView::reencrypt_keys(&mut self.attachments, ctx, old_key, new_key)?; + Ok(()) + } +} + /// Used as an intermediary between the public-facing [CipherEditRequest], and the encrypted /// value. This allows us to calculate password history safely, without risking misuse. #[derive(Clone, Debug)] + struct CipherEditRequestInternal { edit_request: CipherEditRequest, password_history: Vec, @@ -266,7 +291,21 @@ impl CompositeEncryptable .collect(), ), attachments: None, - attachments2: None, + attachments2: Some( + cipher_data + .edit_request + .attachments + .encrypt_composite(ctx, cipher_key)? + .into_iter() + // .map(|a| Ok((require!(a.id), a.into())) as Result<_, CryptoError>) + .map(|a| { + Ok(( + a.id.clone().ok_or(CryptoError::MissingField("id"))?, + a.into(), + )) as Result<_, CryptoError> + }) + .collect::>()?, + ), login: cipher_data .edit_request .r#type @@ -316,15 +355,21 @@ impl CompositeEncryptable } } -impl IdentifyKey for CipherEditRequestInternal { +impl IdentifyKey for CipherEditRequest { fn key_identifier(&self) -> SymmetricKeyId { - match self.edit_request.organization_id { + match self.organization_id { Some(organization_id) => SymmetricKeyId::Organization(organization_id), None => SymmetricKeyId::User, } } } +impl IdentifyKey for CipherEditRequestInternal { + fn key_identifier(&self) -> SymmetricKeyId { + self.edit_request.key_identifier() + } +} + async fn edit_cipher + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, @@ -365,7 +410,10 @@ async fn edit_cipher + ?Sized>( #[cfg_attr(feature = "wasm", wasm_bindgen)] impl CiphersClient { /// Edit an existing [Cipher] and save it to the server. - pub async fn edit(&self, request: CipherEditRequest) -> Result { + pub async fn edit( + &self, + mut request: CipherEditRequest, + ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; @@ -376,6 +424,19 @@ impl CiphersClient { .get_user_id() .ok_or(NotAuthenticatedError)?; + // TODO: Once this flag is removed, the key generation logic should + // be moved closer to the actual encryption logic. + if request.key.is_none() + && self + .client + .internal + .get_flags() + .enable_cipher_key_encryption + { + let key = request.key_identifier(); + request.generate_cipher_key(&mut key_store.context(), key)?; + } + edit_cipher( key_store, &config.api_client, From 11d08c09e2efd4537b771ff87089ae1cc4eb3681 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 15 Oct 2025 15:53:35 -0700 Subject: [PATCH 60/60] Remove unneeded comments --- crates/bitwarden-vault/src/cipher/cipher_client/edit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index 6876c01ef..86cd4c34f 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -297,7 +297,6 @@ impl CompositeEncryptable .attachments .encrypt_composite(ctx, cipher_key)? .into_iter() - // .map(|a| Ok((require!(a.id), a.into())) as Result<_, CryptoError>) .map(|a| { Ok(( a.id.clone().ok_or(CryptoError::MissingField("id"))?,