Skip to content

Commit

Permalink
[PM-5348] Encrypt/decrypt attachment files (#490)
Browse files Browse the repository at this point in the history
## Type of change
```
- [ ] Bug fix
- [x] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
Add encryption/decryption support for attachment files
  • Loading branch information
dani-garcia authored Jan 18, 2024
1 parent 8b435e5 commit d70deac
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 2 deletions.
94 changes: 94 additions & 0 deletions crates/bitwarden-uniffi/src/vault/attachments.rs
Original file line number Diff line number Diff line change
@@ -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<Client>);

#[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<u8>,
) -> Result<AttachmentEncryptResult> {
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<Attachment> {
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<u8>,
) -> Result<Vec<u8>> {
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?)
}
}
6 changes: 6 additions & 0 deletions crates/bitwarden-uniffi/src/vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,11 @@ impl ClientVault {
Arc::new(sends::ClientSends(self.0.clone()))
}

/// Attachment file operations
pub fn attachments(self: Arc<Self>) -> Arc<attachments::ClientAttachments> {
Arc::new(attachments::ClientAttachments(self.0.clone()))
}

/// Generate a TOTP code from a provided key.
///
/// The key can be either:
Expand Down
89 changes: 89 additions & 0 deletions crates/bitwarden/src/mobile/vault/client_attachments.rs
Original file line number Diff line number Diff line change
@@ -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<AttachmentEncryptResult> {
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<Attachment> {
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<Vec<u8>> {
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,
}
}
}
2 changes: 2 additions & 0 deletions crates/bitwarden/src/mobile/vault/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod client_attachments;
mod client_ciphers;
mod client_collection;
mod client_folders;
Expand All @@ -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;
Expand Down
127 changes: 127 additions & 0 deletions crates/bitwarden/src/vault/cipher/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand All @@ -31,6 +33,65 @@ pub struct AttachmentView {
pub key: Option<EncString>,
}

#[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<u8>,
}

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<SymmetricCryptoKey, AttachmentEncryptResult> for AttachmentFileView<'a> {
fn encrypt_with_key(
self,
key: &SymmetricCryptoKey,
) -> Result<AttachmentEncryptResult, CryptoError> {
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<SymmetricCryptoKey, Vec<u8>> for AttachmentFile {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>, 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<u8> = 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<SymmetricCryptoKey, Attachment> for AttachmentView {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<Attachment, CryptoError> {
Ok(Attachment {
Expand Down Expand Up @@ -71,3 +132,69 @@ impl TryFrom<bitwarden_api_api::models::AttachmentResponseModel> 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);
}
}
2 changes: 1 addition & 1 deletion crates/bitwarden/src/vault/cipher/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncString>,
) -> Result<Option<SymmetricCryptoKey>, CryptoError> {
Expand Down
7 changes: 6 additions & 1 deletion crates/bitwarden/src/vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading

0 comments on commit d70deac

Please sign in to comment.