diff --git a/.vscode/settings.json b/.vscode/settings.json index 0df252c66..49d117177 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "spki", "totp", "uniffi", + "wiremock", "wordlist", "XCHACHA", "Zeroize", diff --git a/Cargo.lock b/Cargo.lock index 4e3412708..89b6743bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -609,10 +609,21 @@ name = "bitwarden-state" version = "1.0.0" dependencies = [ "async-trait", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] +[[package]] +name = "bitwarden-test" +version = "1.0.0" +dependencies = [ + "async-trait", + "bitwarden-api-api", + "bitwarden-state", + "reqwest", + "wiremock", +] + [[package]] name = "bitwarden-threading" version = "1.0.0" @@ -691,6 +702,7 @@ dependencies = [ "bitwarden-crypto", "bitwarden-error", "bitwarden-state", + "bitwarden-test", "chrono", "data-encoding", "hmac", @@ -707,6 +719,8 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", + "wiremock", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c6bd4f340..3d43a62bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,11 @@ bitwarden-fido = { path = "crates/bitwarden-fido", version = "=1.0.0" } bitwarden-generators = { path = "crates/bitwarden-generators", version = "=1.0.0" } bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" } bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" } -bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" } -bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" } bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" } bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" } +bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" } +bitwarden-test = { path = "crates/bitwarden-test", version = "=1.0.0" } +bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" } 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" } @@ -60,6 +61,7 @@ serde = { version = ">=1.0, <2.0", features = ["derive"] } 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"] } @@ -72,7 +74,7 @@ validator = { version = ">=0.18.1, <0.21", features = ["derive"] } wasm-bindgen = { version = ">=0.2.91, <0.3", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" wasm-bindgen-test = "0.3.45" -serde-wasm-bindgen = ">=0.6.0, <0.7" +wiremock = ">=0.6.0, <0.7" # 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. diff --git a/bitwarden_license/bitwarden-sm/Cargo.toml b/bitwarden_license/bitwarden-sm/Cargo.toml index ce2521938..0eed312d1 100644 --- a/bitwarden_license/bitwarden-sm/Cargo.toml +++ b/bitwarden_license/bitwarden-sm/Cargo.toml @@ -28,7 +28,7 @@ validator = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } -wiremock = "0.6.0" +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index ccee89a3b..93f601cee 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -39,7 +39,7 @@ wasm-bindgen = { workspace = true, optional = true } [dev-dependencies] rand_chacha = "0.3.1" tokio = { workspace = true, features = ["rt"] } -wiremock = "0.6.0" +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-test/Cargo.toml b/crates/bitwarden-test/Cargo.toml new file mode 100644 index 000000000..0b7f8f481 --- /dev/null +++ b/crates/bitwarden-test/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bitwarden-test" +description = """ +Internal crate for the bitwarden crate. Do not use. +""" + +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +async-trait = { workspace = true } +bitwarden-api-api.workspace = true +bitwarden-state = { workspace = true } +reqwest = { workspace = true } +wiremock = { workspace = true } + +[lints] +workspace = true diff --git a/crates/bitwarden-test/README.md b/crates/bitwarden-test/README.md new file mode 100644 index 000000000..00f329a85 --- /dev/null +++ b/crates/bitwarden-test/README.md @@ -0,0 +1,7 @@ +# Bitwarden Test + +
+This crate should only be used in tests and should not be included in production builds. +
+ +Contains test utilities for Bitwarden. diff --git a/crates/bitwarden-test/src/api.rs b/crates/bitwarden-test/src/api.rs new file mode 100644 index 000000000..265dee15b --- /dev/null +++ b/crates/bitwarden-test/src/api.rs @@ -0,0 +1,24 @@ +use bitwarden_api_api::apis::configuration::Configuration; + +/// Helper for testing the Bitwarden API using wiremock. +/// +/// Warning: when using `Mock::expected` ensure `server` is not dropped before the test completes, +pub async fn start_api_mock(mocks: Vec) -> (wiremock::MockServer, Configuration) { + let server = wiremock::MockServer::start().await; + + for mock in mocks { + server.register(mock).await; + } + + let config = Configuration { + base_path: server.uri(), + user_agent: Some("test-agent".to_string()), + client: reqwest::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + }; + + (server, config) +} diff --git a/crates/bitwarden-test/src/lib.rs b/crates/bitwarden-test/src/lib.rs new file mode 100644 index 000000000..fcc1930a0 --- /dev/null +++ b/crates/bitwarden-test/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] + +mod api; +pub use api::*; + +mod repository; +pub use repository::*; diff --git a/crates/bitwarden-test/src/repository.rs b/crates/bitwarden-test/src/repository.rs new file mode 100644 index 000000000..31d4cb3ce --- /dev/null +++ b/crates/bitwarden-test/src/repository.rs @@ -0,0 +1,54 @@ +use bitwarden_state::repository::{Repository, RepositoryError, RepositoryItem}; + +/// A simple in-memory repository implementation. The data is only stored in memory and will not +/// persist beyond the lifetime of the repository instance. +/// +/// Primary use case is for unit and integration tests. +pub struct MemoryRepository { + store: std::sync::Mutex>, +} + +impl Default for MemoryRepository { + fn default() -> Self { + Self { + store: std::sync::Mutex::new(std::collections::HashMap::new()), + } + } +} + +#[async_trait::async_trait] +impl Repository for MemoryRepository { + async fn get(&self, key: String) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.get(&key).cloned()) + } + + async fn list(&self) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.values().cloned().collect()) + } + + async fn set(&self, key: String, value: V) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.insert(key, value); + Ok(()) + } + + async fn remove(&self, key: String) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.remove(&key); + Ok(()) + } +} diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 7b976f517..494e900f7 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,7 +23,8 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify", - "dep:wasm-bindgen" + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] @@ -48,9 +49,12 @@ tsify = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] +bitwarden-test = { workspace = true } tokio = { workspace = true, features = ["rt"] } +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-vault/src/folder/create.rs b/crates/bitwarden-vault/src/folder/create.rs new file mode 100644 index 000000000..115fbce9e --- /dev/null +++ b/crates/bitwarden-vault/src/folder/create.rs @@ -0,0 +1,184 @@ +use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; +use bitwarden_core::{ + key_management::{KeyIds, SymmetricKeyId}, + require, ApiError, MissingFieldError, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, +}; +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::{Folder, FolderView, VaultParseError}; + +/// Request to add or edit a folder. +#[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 FolderAddEditRequest { + /// The new name of the folder. + pub name: String, +} + +impl CompositeEncryptable for FolderAddEditRequest { + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + Ok(FolderRequestModel { + name: self.name.encrypt(ctx, key)?.to_string(), + }) + } +} + +impl IdentifyKey for FolderAddEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + SymmetricKeyId::User + } +} + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateFolderError { + #[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), +} + +pub(super) async fn create_folder + ?Sized>( + key_store: &KeyStore, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &R, + request: FolderAddEditRequest, +) -> Result { + let folder_request = key_store.encrypt(request)?; + let resp = folders_api::folders_post(api_config, Some(folder_request)) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + repository + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(key_store.decrypt(&folder)?) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::FolderResponseModel; + use bitwarden_crypto::SymmetricCryptoKey; + use bitwarden_test::{start_api_mock, MemoryRepository}; + use uuid::uuid; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; + + use super::*; + + #[tokio::test] + async fn test_create_folder() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders")) + .respond_with(move |req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(FolderResponseModel { + id: Some(folder_id), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("folder".to_string()), + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); + + let result = create_folder( + &store, + &api_config, + &repository, + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await + .unwrap(); + + assert_eq!( + result, + FolderView { + id: Some(folder_id), + name: "test".to_string(), + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + ); + + // Confirm the folder was stored in the repository + assert_eq!( + store + .decrypt( + &repository + .get(folder_id.to_string()) + .await + .unwrap() + .unwrap() + ) + .unwrap(), + result + ); + } + + #[tokio::test] + async fn test_create_folder_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("/folders")).respond_with(ResponseTemplate::new(500)) + ]) + .await; + + let repository = MemoryRepository::::default(); + + let result = create_folder( + &store, + &api_config, + &repository, + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_))); + } +} diff --git a/crates/bitwarden-vault/src/folder/edit.rs b/crates/bitwarden-vault/src/folder/edit.rs new file mode 100644 index 000000000..d93c637fd --- /dev/null +++ b/crates/bitwarden-vault/src/folder/edit.rs @@ -0,0 +1,209 @@ +use bitwarden_api_api::apis::folders_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; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{Folder, FolderAddEditRequest, FolderView, ItemNotFoundError, VaultParseError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum EditFolderError { + #[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_folder + ?Sized>( + key_store: &KeyStore, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &R, + folder_id: &str, + request: FolderAddEditRequest, +) -> Result { + // Verify the folder we're updating exists + repository + .get(folder_id.to_owned()) + .await? + .ok_or(ItemNotFoundError)?; + + let folder_request = key_store.encrypt(request)?; + + let resp = folders_api::folders_id_put(api_config, folder_id, Some(folder_request)) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + debug_assert!(folder.id.unwrap_or_default().to_string() == folder_id); + + repository + .set(folder_id.to_string(), folder.clone()) + .await?; + + Ok(key_store.decrypt(&folder)?) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::{ + apis::configuration::Configuration, + models::{FolderRequestModel, FolderResponseModel}, + }; + use bitwarden_core::key_management::SymmetricKeyId; + use bitwarden_crypto::{PrimitiveEncryptable, SymmetricCryptoKey}; + use bitwarden_test::{start_api_mock, MemoryRepository}; + use uuid::uuid; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; + + use super::*; + + async fn repository_add_folder( + repository: &MemoryRepository, + store: &KeyStore, + folder_id: uuid::Uuid, + name: &str, + ) { + repository + .set( + folder_id.to_string(), + Folder { + id: Some(folder_id), + name: name + .encrypt(&mut store.context(), SymmetricKeyId::User) + .unwrap(), + revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_edit_folder() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/folders/{}", + folder_id + ))) + .respond_with(move |req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(200).set_body_json(FolderResponseModel { + object: Some("folder".to_string()), + id: Some(folder_id), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); + repository_add_folder(&repository, &store, folder_id, "old_name").await; + + let result = edit_folder( + &store, + &api_config, + &repository, + &folder_id.to_string(), + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await + .unwrap(); + + assert_eq!( + result, + FolderView { + id: Some(folder_id), + name: "test".to_string(), + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + ); + } + + #[tokio::test] + async fn test_edit_folder_does_not_exist() { + let store: KeyStore = KeyStore::default(); + + let repository = MemoryRepository::::default(); + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let result = edit_folder( + &store, + &Configuration::default(), + &repository, + &folder_id.to_string(), + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + EditFolderError::ItemNotFound(_) + )); + } + + #[tokio::test] + async fn test_edit_folder_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 folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/folders/{}", + folder_id + ))) + .respond_with(ResponseTemplate::new(500))]) + .await; + + let repository = MemoryRepository::::default(); + repository_add_folder(&repository, &store, folder_id, "old_name").await; + + let result = edit_folder( + &store, + &api_config, + &repository, + &folder_id.to_string(), + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), EditFolderError::Api(_))); + } +} diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs new file mode 100644 index 000000000..63370bab6 --- /dev/null +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; + +use bitwarden_core::Client; +use bitwarden_state::repository::{Repository, RepositoryError}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{ + error::{DecryptError, EncryptError}, + folder::{create_folder, edit_folder, get_folder, list_folders}, + CreateFolderError, EditFolderError, Folder, FolderAddEditRequest, FolderView, GetFolderError, +}; + +/// Wrapper for folder specific functionality. +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub struct FoldersClient { + pub(crate) client: Client, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl FoldersClient { + /// Encrypt a [FolderView] to a [Folder]. + pub fn encrypt(&self, folder_view: FolderView) -> Result { + let key_store = self.client.internal.get_key_store(); + let folder = key_store.encrypt(folder_view)?; + Ok(folder) + } + + /// Encrypt a [Folder] to [FolderView]. + pub fn decrypt(&self, folder: Folder) -> Result { + let key_store = self.client.internal.get_key_store(); + let folder_view = key_store.decrypt(&folder)?; + Ok(folder_view) + } + + /// Decrypt a list of [Folder]s to a list of [FolderView]s. + pub fn decrypt_list(&self, folders: Vec) -> Result, DecryptError> { + let key_store = self.client.internal.get_key_store(); + let views = key_store.decrypt_list(&folders)?; + Ok(views) + } + + /// Get all folders from state and decrypt them to a list of [FolderView]. + pub async fn list(&self) -> Result, GetFolderError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_folders(key_store, repository.as_ref()).await + } + + /// Get a specific [Folder] by its ID from state and decrypt it to a [FolderView]. + pub async fn get(&self, folder_id: &str) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_folder(key_store, repository.as_ref(), folder_id).await + } + + /// Create a new [Folder] and save it to the server. + pub async fn create( + &self, + request: FolderAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + create_folder(key_store, &config.api, repository.as_ref(), request).await + } + + /// Edit the [Folder] and save it to the server. + pub async fn edit( + &self, + folder_id: &str, + request: FolderAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + edit_folder( + key_store, + &config.api, + repository.as_ref(), + folder_id, + request, + ) + .await + } +} + +impl FoldersClient { + /// Helper for getting the repository for folders. + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self + .client + .platform() + .state() + .get_client_managed::()?) + } +} diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder/folder_models.rs similarity index 81% rename from crates/bitwarden-vault/src/folder.rs rename to crates/bitwarden-vault/src/folder/folder_models.rs index 677adbfba..f1d882546 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder/folder_models.rs @@ -16,18 +16,20 @@ use {tsify::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[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 Folder { - id: Option, - name: EncString, - revision_date: DateTime, + pub id: Option, + pub name: EncString, + pub revision_date: DateTime, } +bitwarden_state::register_repository_item!(Folder, "Folder"); + #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -37,6 +39,15 @@ pub struct FolderView { pub revision_date: DateTime, } +#[cfg(feature = "wasm")] +impl wasm_bindgen::__rt::VectorIntoJsValue for FolderView { + 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 Folder { fn key_identifier(&self) -> SymmetricKeyId { SymmetricKeyId::User diff --git a/crates/bitwarden-vault/src/folder/get_list.rs b/crates/bitwarden-vault/src/folder/get_list.rs new file mode 100644 index 000000000..65ac6b2fa --- /dev/null +++ b/crates/bitwarden-vault/src/folder/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::{Folder, FolderView, ItemNotFoundError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum GetFolderError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +pub(super) async fn get_folder( + store: &KeyStore, + repository: &dyn Repository, + id: &str, +) -> Result { + let folder = repository + .get(id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(store.decrypt(&folder)?) +} + +pub(super) async fn list_folders( + store: &KeyStore, + repository: &dyn Repository, +) -> Result, GetFolderError> { + let folders = repository.list().await?; + let views = store.decrypt_list(&folders)?; + Ok(views) +} diff --git a/crates/bitwarden-vault/src/folder/mod.rs b/crates/bitwarden-vault/src/folder/mod.rs new file mode 100644 index 000000000..13fb151ef --- /dev/null +++ b/crates/bitwarden-vault/src/folder/mod.rs @@ -0,0 +1,16 @@ +mod create; +mod edit; +mod folder_client; +mod folder_models; +mod get_list; + +pub use create::*; +pub use edit::*; +pub use folder_client::*; +pub use folder_models::*; +pub use get_list::*; + +/// Item does not exist error. +#[derive(Debug, thiserror::Error)] +#[error("Item does not exist")] +pub struct ItemNotFoundError; diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs deleted file mode 100644 index 90f62bd0b..000000000 --- a/crates/bitwarden-vault/src/folder_client.rs +++ /dev/null @@ -1,38 +0,0 @@ -use bitwarden_core::Client; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::*; - -use crate::{ - error::{DecryptError, EncryptError}, - Folder, FolderView, -}; - -#[allow(missing_docs)] -#[cfg_attr(feature = "wasm", wasm_bindgen)] -pub struct FoldersClient { - pub(crate) client: Client, -} - -#[cfg_attr(feature = "wasm", wasm_bindgen)] -impl FoldersClient { - #[allow(missing_docs)] - pub fn encrypt(&self, folder_view: FolderView) -> Result { - let key_store = self.client.internal.get_key_store(); - let folder = key_store.encrypt(folder_view)?; - Ok(folder) - } - - #[allow(missing_docs)] - pub fn decrypt(&self, folder: Folder) -> Result { - let key_store = self.client.internal.get_key_store(); - let folder_view = key_store.decrypt(&folder)?; - Ok(folder_view) - } - - #[allow(missing_docs)] - pub fn decrypt_list(&self, folders: Vec) -> Result, DecryptError> { - let key_store = self.client.internal.get_key_store(); - let views = key_store.decrypt_list(&folders)?; - Ok(views) - } -} diff --git a/crates/bitwarden-vault/src/lib.rs b/crates/bitwarden-vault/src/lib.rs index e1d893cc2..6c7a691f0 100644 --- a/crates/bitwarden-vault/src/lib.rs +++ b/crates/bitwarden-vault/src/lib.rs @@ -12,9 +12,7 @@ pub use collection::{Collection, CollectionView}; mod collection_client; pub use collection_client::CollectionsClient; mod folder; -pub use folder::{Folder, FolderView}; -mod folder_client; -pub use folder_client::FoldersClient; +pub use folder::*; mod password_history; pub use password_history::{PasswordHistory, PasswordHistoryView}; mod password_history_client; diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 1bb5b551b..9d6c4274c 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::Cipher; +use bitwarden_vault::{Cipher, Folder}; use wasm_bindgen::prelude::wasm_bindgen; mod repository; @@ -31,6 +31,7 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); +repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -38,4 +39,9 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } + + pub fn register_folder_repository(&self, store: FolderRepository) { + let store = store.into_channel_impl(); + self.0.platform().state().register_client_managed(store) + } }