diff --git a/crates/bitwarden-uniffi/src/vault/attachments.rs b/crates/bitwarden-uniffi/src/vault/attachments.rs new file mode 100644 index 000000000..0c099b779 --- /dev/null +++ b/crates/bitwarden-uniffi/src/vault/attachments.rs @@ -0,0 +1,94 @@ +use std::{path::Path, sync::Arc}; + +use bitwarden::vault::{Attachment, AttachmentEncryptResult, AttachmentView, Cipher}; + +use crate::{Client, Result}; + +#[derive(uniffi::Object)] +pub struct ClientAttachments(pub Arc); + +#[uniffi::export(async_runtime = "tokio")] +impl ClientAttachments { + /// Encrypt an attachment file in memory + pub async fn encrypt_buffer( + &self, + cipher: Cipher, + attachment: AttachmentView, + buffer: Vec, + ) -> Result { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .encrypt_buffer(cipher, attachment, &buffer) + .await?) + } + + /// Encrypt an attachment file located in the file system + pub async fn encrypt_file( + &self, + cipher: Cipher, + attachment: AttachmentView, + decrypted_file_path: String, + encrypted_file_path: String, + ) -> Result { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .encrypt_file( + cipher, + attachment, + Path::new(&decrypted_file_path), + Path::new(&encrypted_file_path), + ) + .await?) + } + /// Decrypt an attachment file in memory + pub async fn decrypt_buffer( + &self, + cipher: Cipher, + attachment: Attachment, + buffer: Vec, + ) -> Result> { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .decrypt_buffer(cipher, attachment, &buffer) + .await?) + } + + /// Decrypt an attachment file located in the file system + pub async fn decrypt_file( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_file_path: String, + decrypted_file_path: String, + ) -> Result<()> { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .decrypt_file( + cipher, + attachment, + Path::new(&encrypted_file_path), + Path::new(&decrypted_file_path), + ) + .await?) + } +} diff --git a/crates/bitwarden-uniffi/src/vault/mod.rs b/crates/bitwarden-uniffi/src/vault/mod.rs index 435e7f355..2205e0673 100644 --- a/crates/bitwarden-uniffi/src/vault/mod.rs +++ b/crates/bitwarden-uniffi/src/vault/mod.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use crate::{error::Result, Client}; +pub mod attachments; pub mod ciphers; pub mod collections; pub mod folders; @@ -41,6 +42,11 @@ impl ClientVault { Arc::new(sends::ClientSends(self.0.clone())) } + /// Attachment file operations + pub fn attachments(self: Arc) -> Arc { + Arc::new(attachments::ClientAttachments(self.0.clone())) + } + /// Generate a TOTP code from a provided key. /// /// The key can be either: diff --git a/crates/bitwarden/src/mobile/vault/client_attachments.rs b/crates/bitwarden/src/mobile/vault/client_attachments.rs new file mode 100644 index 000000000..c436f10fd --- /dev/null +++ b/crates/bitwarden/src/mobile/vault/client_attachments.rs @@ -0,0 +1,89 @@ +use std::path::Path; + +use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey}; + +use super::client_vault::ClientVault; +use crate::{ + error::{Error, Result}, + vault::{ + Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView, + Cipher, + }, + Client, +}; + +pub struct ClientAttachments<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientAttachments<'a> { + pub async fn encrypt_buffer( + &self, + cipher: Cipher, + attachment: AttachmentView, + buffer: &[u8], + ) -> Result { + let enc = self.client.get_encryption_settings()?; + let key = cipher.locate_key(enc, &None).ok_or(Error::VaultLocked)?; + + Ok(AttachmentFileView { + cipher, + attachment, + contents: buffer, + } + .encrypt_with_key(key)?) + } + pub async fn encrypt_file( + &self, + cipher: Cipher, + attachment: AttachmentView, + decrypted_file_path: &Path, + encrypted_file_path: &Path, + ) -> Result { + let data = std::fs::read(decrypted_file_path).unwrap(); + let AttachmentEncryptResult { + attachment, + contents, + } = self.encrypt_buffer(cipher, attachment, &data).await?; + std::fs::write(encrypted_file_path, contents)?; + Ok(attachment) + } + + pub async fn decrypt_buffer( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_buffer: &[u8], + ) -> Result> { + let enc = self.client.get_encryption_settings()?; + let key = cipher.locate_key(enc, &None).ok_or(Error::VaultLocked)?; + + AttachmentFile { + cipher, + attachment, + contents: EncString::from_buffer(encrypted_buffer)?, + } + .decrypt_with_key(key) + .map_err(Error::Crypto) + } + pub async fn decrypt_file( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_file_path: &Path, + decrypted_file_path: &Path, + ) -> Result<()> { + let data = std::fs::read(encrypted_file_path).unwrap(); + let decrypted = self.decrypt_buffer(cipher, attachment, &data).await?; + std::fs::write(decrypted_file_path, decrypted)?; + Ok(()) + } +} + +impl<'a> ClientVault<'a> { + pub fn attachments(&'a self) -> ClientAttachments<'a> { + ClientAttachments { + client: self.client, + } + } +} diff --git a/crates/bitwarden/src/mobile/vault/mod.rs b/crates/bitwarden/src/mobile/vault/mod.rs index 97f9556af..a2d4d91b3 100644 --- a/crates/bitwarden/src/mobile/vault/mod.rs +++ b/crates/bitwarden/src/mobile/vault/mod.rs @@ -1,3 +1,4 @@ +mod client_attachments; mod client_ciphers; mod client_collection; mod client_folders; @@ -6,6 +7,7 @@ mod client_sends; mod client_totp; mod client_vault; +pub use client_attachments::ClientAttachments; pub use client_ciphers::ClientCiphers; pub use client_collection::ClientCollections; pub use client_folders::ClientFolders; diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index fa4a35fc7..3e1e47025 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; +use super::Cipher; + #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] @@ -31,6 +33,65 @@ pub struct AttachmentView { pub key: Option, } +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AttachmentEncryptResult { + pub attachment: Attachment, + pub contents: Vec, +} + +pub struct AttachmentFile { + pub cipher: Cipher, + pub attachment: Attachment, + pub contents: EncString, +} + +pub struct AttachmentFileView<'a> { + pub cipher: Cipher, + pub attachment: AttachmentView, + pub contents: &'a [u8], +} + +impl<'a> KeyEncryptable for AttachmentFileView<'a> { + fn encrypt_with_key( + self, + key: &SymmetricCryptoKey, + ) -> Result { + let ciphers_key = Cipher::get_cipher_key(key, &self.cipher.key)?; + let ciphers_key = ciphers_key.as_ref().unwrap_or(key); + + let mut attachment = self.attachment; + + // Because this is a new attachment, we have to generate a key for it, encrypt the contents with it, and then encrypt the key with the cipher key + let attachment_key = SymmetricCryptoKey::generate(rand::thread_rng()); + let encrypted_contents = self.contents.encrypt_with_key(&attachment_key)?; + attachment.key = Some(attachment_key.to_vec().encrypt_with_key(ciphers_key)?); + + Ok(AttachmentEncryptResult { + attachment: attachment.encrypt_with_key(ciphers_key)?, + contents: encrypted_contents.to_buffer()?, + }) + } +} + +impl KeyDecryptable> for AttachmentFile { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result, CryptoError> { + let ciphers_key = Cipher::get_cipher_key(key, &self.cipher.key)?; + let ciphers_key = ciphers_key.as_ref().unwrap_or(key); + + let attachment_key: Vec = self + .attachment + .key + .as_ref() + .ok_or(CryptoError::MissingKey)? + .decrypt_with_key(ciphers_key)?; + let attachment_key = SymmetricCryptoKey::try_from(attachment_key.as_slice())?; + + self.contents.decrypt_with_key(&attachment_key) + } +} + impl KeyEncryptable for AttachmentView { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Attachment { @@ -71,3 +132,69 @@ impl TryFrom for Attachment }) } } + +#[cfg(test)] +mod tests { + use base64::{engine::general_purpose::STANDARD, Engine}; + + use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; + + use crate::vault::{ + cipher::cipher::{CipherRepromptType, CipherType}, + Attachment, AttachmentFile, Cipher, + }; + + #[test] + fn test_attachment_key() { + let user_key : SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".parse().unwrap(); + + let attachment = Attachment { + id: None, + url: None, + size: Some("161".into()), + size_name: Some("161 Bytes".into()), + file_name: Some("2.M3z1MOO9eBG9BWRTEUbPog==|jPw0By1AakHDfoaY8UOwOQ==|eP9/J1583OJpHsSM4ZnXZzdBHfqVTXnOXGlkkmAKSfA=".parse().unwrap()), + key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()) + }; + + let cipher = Cipher { + id: None, + organization_id: None, + folder_id: None, + collection_ids: Vec::new(), + key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()), + name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(), + notes: None, + r#type: CipherType::Login, + login: None, + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(), + deleted_date: None, + revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(), + }; + + let enc_file = STANDARD.decode(b"Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap(); + let original = STANDARD.decode(b"rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); + + let dec = AttachmentFile { + cipher, + attachment, + contents: EncString::from_buffer(&enc_file).unwrap(), + } + .decrypt_with_key(&user_key) + .unwrap(); + + assert_eq!(dec, original); + } +} diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 07d8da226..010f3cd8a 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -210,7 +210,7 @@ impl Cipher { /// Note that some ciphers do not have individual encryption keys, /// in which case this will return Ok(None) and the key associated /// with this cipher's user or organization must be used instead - fn get_cipher_key( + pub(super) fn get_cipher_key( key: &SymmetricCryptoKey, ciphers_key: &Option, ) -> Result, CryptoError> { diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index 6ac43a4d0..dce7b0e04 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -6,7 +6,12 @@ mod send; #[cfg(feature = "mobile")] mod totp; -pub use cipher::{Cipher, CipherListView, CipherView}; +pub use cipher::{ + attachment::{ + Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView, + }, + Cipher, CipherListView, CipherView, +}; pub use collection::{Collection, CollectionView}; pub use folder::{Folder, FolderView}; pub use password_history::{PasswordHistory, PasswordHistoryView}; diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index 896cf7a76..ccf193301 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -138,6 +138,62 @@ password, use the email OTP. **Output**: std::result::Result<,BitwardenError> +## ClientAttachments + +### `encrypt_buffer` + +Encrypt an attachment file in memory + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [AttachmentView](#attachmentview) +- buffer: Vec<> + +**Output**: std::result::Result + +### `encrypt_file` + +Encrypt an attachment file located in the file system + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [AttachmentView](#attachmentview) +- decrypted_file_path: String +- encrypted_file_path: String + +**Output**: std::result::Result + +### `decrypt_buffer` + +Decrypt an attachment file in memory + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [Attachment](#attachment) +- buffer: Vec<> + +**Output**: std::result::Result + +### `decrypt_file` + +Decrypt an attachment file located in the file system + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [Attachment](#attachment) +- encrypted_file_path: String +- decrypted_file_path: String + +**Output**: std::result::Result<,BitwardenError> + ## ClientCiphers ### `encrypt` @@ -541,6 +597,16 @@ Sends operations **Output**: Arc +### `attachments` + +Attachment file operations + +**Arguments**: + +- self: Arc + +**Output**: Arc + ### `generate_totp` Generate a TOTP code from a provided key. @@ -564,6 +630,86 @@ The key can be either: References are generated from the JSON schemas and should mostly match the kotlin and swift implementations. +## `Attachment` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
idstring,null
urlstring,null
sizestring,null
sizeNamestring,nullReadable size, ex: "4.2 KB" or "1.43 GB"
fileName
key
+ +## `AttachmentView` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
idstring,null
urlstring,null
sizestring,null
sizeNamestring,null
fileNamestring,null
key
+ ## `Cipher` diff --git a/support/docs/docs.ts b/support/docs/docs.ts index b547b4502..067ff0827 100644 --- a/support/docs/docs.ts +++ b/support/docs/docs.ts @@ -21,6 +21,7 @@ const template = Handlebars.compile( const rootElements = [ "Client", "ClientAuth", + "ClientAttachments", "ClientCiphers", "ClientCollections", "ClientCrypto",