From 78996ea64fbe1f720e7bcc11ce08e5f8dca5496b Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 25 Sep 2023 11:12:51 +0200 Subject: [PATCH] WIP attachment export --- .../src/models/attachment_response_model.rs | 2 +- crates/bitwarden/src/crypto/enc_string.rs | 6 ++ crates/bitwarden/src/platform/sync.rs | 21 +---- .../src/tool/exporters/client_exporter.rs | 10 ++- crates/bitwarden/src/tool/exporters/mod.rs | 48 ++++++++++- crates/bitwarden/src/vault/api/attachment.rs | 7 ++ crates/bitwarden/src/vault/api/mod.rs | 2 + .../bitwarden/src/vault/cipher/attachment.rs | 81 +++++++++++++++-- crates/bitwarden/src/vault/cipher/cipher.rs | 86 ++++++++++++++++--- crates/bitwarden/src/vault/cipher/mod.rs | 1 + crates/bitwarden/src/vault/mod.rs | 3 +- crates/bw/Cargo.toml | 1 + crates/bw/src/auth/login.rs | 2 +- crates/bw/src/main.rs | 29 ++++++- 14 files changed, 258 insertions(+), 41 deletions(-) create mode 100644 crates/bitwarden/src/vault/api/attachment.rs create mode 100644 crates/bitwarden/src/vault/api/mod.rs diff --git a/crates/bitwarden-api-api/src/models/attachment_response_model.rs b/crates/bitwarden-api-api/src/models/attachment_response_model.rs index 86637a161..c9066488d 100644 --- a/crates/bitwarden-api-api/src/models/attachment_response_model.rs +++ b/crates/bitwarden-api-api/src/models/attachment_response_model.rs @@ -21,7 +21,7 @@ pub struct AttachmentResponseModel { #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, #[serde(rename = "size", skip_serializing_if = "Option::is_none")] - pub size: Option, + pub size: Option, #[serde(rename = "sizeName", skip_serializing_if = "Option::is_none")] pub size_name: Option, } diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index b701aaf8f..308a013ec 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -330,6 +330,12 @@ impl Encryptable for String { } } +impl Encryptable for &[u8] { + fn encrypt(self, enc: &EncryptionSettings, org_id: &Option) -> Result { + enc.encrypt(self, org_id) + } +} + impl Decryptable for EncString { fn decrypt(&self, enc: &EncryptionSettings, org_id: &Option) -> Result { enc.decrypt(self, org_id) diff --git a/crates/bitwarden/src/platform/sync.rs b/crates/bitwarden/src/platform/sync.rs index fd33e4343..b3a8d4fdf 100644 --- a/crates/bitwarden/src/platform/sync.rs +++ b/crates/bitwarden/src/platform/sync.rs @@ -1,6 +1,5 @@ use bitwarden_api_api::models::{ - CipherDetailsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, - SyncResponseModel, + ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,6 +8,7 @@ use uuid::Uuid; use crate::{ client::{encryption_settings::EncryptionSettings, Client}, error::{Error, Result}, + vault::Cipher, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -57,17 +57,13 @@ pub struct ProfileOrganizationResponse { pub id: Uuid, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CipherDetailsResponse {} - #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SyncResponse { /// Data about the user, including their encryption keys and the organizations they are a part of pub profile: ProfileResponse, /// List of ciphers accesible by the user - pub ciphers: Vec, + pub ciphers: Vec, } impl SyncResponse { @@ -80,20 +76,11 @@ impl SyncResponse { Ok(SyncResponse { profile: ProfileResponse::process_response(profile, enc)?, - ciphers: ciphers - .into_iter() - .map(CipherDetailsResponse::process_response) - .collect::>()?, + ciphers: ciphers.into_iter().map(|c| c.into()).collect(), }) } } -impl CipherDetailsResponse { - fn process_response(_response: CipherDetailsResponseModel) -> Result { - Ok(CipherDetailsResponse {}) - } -} - impl ProfileOrganizationResponse { fn process_response( response: ProfileOrganizationResponseModel, diff --git a/crates/bitwarden/src/tool/exporters/client_exporter.rs b/crates/bitwarden/src/tool/exporters/client_exporter.rs index f14cbc846..ae34e0d41 100644 --- a/crates/bitwarden/src/tool/exporters/client_exporter.rs +++ b/crates/bitwarden/src/tool/exporters/client_exporter.rs @@ -5,8 +5,10 @@ use crate::{ Client, }; +use super::export_vault_attachments; + pub struct ClientExporters<'a> { - pub(crate) _client: &'a crate::Client, + pub(crate) _client: &'a mut crate::Client, } impl<'a> ClientExporters<'a> { @@ -27,10 +29,14 @@ impl<'a> ClientExporters<'a> { ) -> Result { export_organization_vault(collections, ciphers, format) } + + pub async fn export_vault_attachments(&mut self) -> Result<()> { + export_vault_attachments(self._client).await + } } impl<'a> Client { - pub fn exporters(&'a self) -> ClientExporters<'a> { + pub fn exporters(&'a mut self) -> ClientExporters<'a> { ClientExporters { _client: self } } } diff --git a/crates/bitwarden/src/tool/exporters/mod.rs b/crates/bitwarden/src/tool/exporters/mod.rs index 508aae8fb..60bc3f25d 100644 --- a/crates/bitwarden/src/tool/exporters/mod.rs +++ b/crates/bitwarden/src/tool/exporters/mod.rs @@ -1,8 +1,12 @@ +use log::{debug, info}; use schemars::JsonSchema; use crate::{ + crypto::Decryptable, error::Result, - vault::{Cipher, Collection, Folder}, + platform::SyncRequest, + vault::{download_attachment, Cipher, CipherView, Collection, Folder}, + Client, }; mod client_exporter; @@ -31,3 +35,45 @@ pub(super) fn export_organization_vault( ) -> Result { todo!(); } + +pub(super) async fn export_vault_attachments(client: &mut Client) -> Result<()> { + info!("Syncing vault"); + let sync = client + .sync(&SyncRequest { + exclude_subdomains: None, + }) + .await?; + + debug!("{:?}", sync); + + info!("Vault synced got {} ciphers", sync.ciphers.len()); + + let ciphers_with_attachments = sync.ciphers.iter().filter(|c| !c.attachments.is_empty()); + + info!( + "Found {} ciphers with attachments", + ciphers_with_attachments.count() + ); + + info!("Decrypting ciphers"); + + let enc_settings = client.get_encryption_settings()?; + + let decrypted: Vec = sync + .ciphers + .iter() + .map(|c| c.decrypt(enc_settings, &None).unwrap()) + .collect(); + + let num_attachments = decrypted.iter().flat_map(|c| &c.attachments).count(); + + info!("Found {} attachments, starting export", num_attachments); + + for cipher in decrypted { + for attachment in cipher.attachments { + download_attachment(client, cipher.id.unwrap(), &attachment.id.unwrap()).await?; + } + } + + Ok(()) +} diff --git a/crates/bitwarden/src/vault/api/attachment.rs b/crates/bitwarden/src/vault/api/attachment.rs new file mode 100644 index 000000000..d341899e9 --- /dev/null +++ b/crates/bitwarden/src/vault/api/attachment.rs @@ -0,0 +1,7 @@ +use reqwest::Response; + +use crate::error::Result; + +pub(crate) async fn attachment_get(url: &str) -> Result { + reqwest::get(url).await.map_err(|e| e.into()) +} diff --git a/crates/bitwarden/src/vault/api/mod.rs b/crates/bitwarden/src/vault/api/mod.rs new file mode 100644 index 000000000..dc88fec7a --- /dev/null +++ b/crates/bitwarden/src/vault/api/mod.rs @@ -0,0 +1,2 @@ +mod attachment; +pub(super) use attachment::attachment_get; diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index 143e2a4f9..407791621 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -1,11 +1,22 @@ +use std::{ + fs::{create_dir_all, File}, + io::Write, + path::Path, + str::FromStr, +}; + +use bitwarden_api_api::apis::ciphers_api::ciphers_id_attachment_attachment_id_get; +use log::debug; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{Decryptable, EncString, Encryptable}, + crypto::{Decryptable, EncString, Encryptable, SymmetricCryptoKey}, error::Result, + vault::api::attachment_get, + Client, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -30,7 +41,48 @@ pub struct AttachmentView { pub size: Option, pub size_name: Option, pub file_name: Option, - pub key: Option, + pub key: Option>, // TODO: Should be made into SymmetricCryptoKey +} + +pub async fn download_attachment( + client: &mut Client, + cipher_id: Uuid, + attachment_id: &str, +) -> Result> { + // The attachments from sync doesn't contain the correct url + let configuration = &client.get_api_configurations().await.api; + let response = ciphers_id_attachment_attachment_id_get( + configuration, + cipher_id.to_string().as_str(), + attachment_id.to_string().as_str(), + ) + .await?; + + let attachment: Attachment = response.into(); + let enc = client.get_encryption_settings()?; + let view = attachment.decrypt(enc, &None)?; + + let key = SymmetricCryptoKey::try_from(view.key.unwrap().as_slice())?; + let enc = EncryptionSettings::new_single_key(key); + + let response = attachment_get(&view.url.unwrap()).await?; + let bytes = response.bytes().await?; + + let buf = EncString::from_buffer(&bytes)?; + let dec = enc.decrypt_bytes(&buf, &None)?; + + let path = Path::new("attachments") + .join(cipher_id.to_string()) + .join(attachment_id) + .join(view.file_name.unwrap()); + + create_dir_all(path.parent().unwrap())?; + let mut file = File::create(path)?; + file.write_all(&dec)?; + + debug!("{:?}", bytes.len()); + + todo!() } impl Encryptable for AttachmentView { @@ -41,7 +93,7 @@ impl Encryptable for AttachmentView { size: self.size, size_name: self.size_name, file_name: self.file_name.encrypt(enc, org_id)?, - key: self.key.encrypt(enc, org_id)?, + key: self.key.map(|k| k.encrypt(enc, org_id)).transpose()?, }) } } @@ -53,8 +105,27 @@ impl Decryptable for Attachment { url: self.url.clone(), size: self.size.clone(), size_name: self.size_name.clone(), - file_name: self.file_name.decrypt(enc, org_id)?, - key: self.key.decrypt(enc, org_id)?, + file_name: self.file_name.decrypt(enc, org_id).unwrap(), + key: self + .key + .as_ref() + .map(|key| enc.decrypt_bytes(key, org_id).unwrap()), }) } } + +impl From for Attachment { + fn from(attachment: bitwarden_api_api::models::AttachmentResponseModel) -> Self { + debug!("{:?}", attachment); + Self { + id: attachment.id, + url: attachment.url, + size: attachment.size.map(|s| s.to_string()), + size_name: attachment.size_name, + file_name: attachment + .file_name + .map(|s| EncString::from_str(&s).unwrap()), + key: attachment.key.map(|s| EncString::from_str(&s).unwrap()), + } + } +} diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 49586028d..8f77fa153 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -1,4 +1,8 @@ +use std::str::FromStr; + +use bitwarden_api_api::models::CipherDetailsResponseModel; use chrono::{DateTime, Utc}; +use log::debug; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -162,28 +166,29 @@ impl Encryptable for CipherView { impl Decryptable for Cipher { fn decrypt(&self, enc: &EncryptionSettings, _: &Option) -> Result { + debug!("{:?}", self); let org_id = &self.organization_id; Ok(CipherView { id: self.id, organization_id: self.organization_id, folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), - name: self.name.decrypt(enc, org_id)?, - notes: self.notes.decrypt(enc, org_id)?, + name: self.name.decrypt(enc, org_id).unwrap(), + notes: self.notes.decrypt(enc, org_id).unwrap(), r#type: self.r#type, - login: self.login.decrypt(enc, org_id)?, - identity: self.identity.decrypt(enc, org_id)?, - card: self.card.decrypt(enc, org_id)?, - secure_note: self.secure_note.decrypt(enc, org_id)?, + login: self.login.decrypt(enc, org_id).unwrap(), + identity: self.identity.decrypt(enc, org_id).unwrap(), + card: self.card.decrypt(enc, org_id).unwrap(), + secure_note: self.secure_note.decrypt(enc, org_id).unwrap(), favorite: self.favorite, reprompt: self.reprompt, organization_use_totp: self.organization_use_totp, edit: self.edit, view_password: self.view_password, - local_data: self.local_data.decrypt(enc, org_id)?, - attachments: self.attachments.decrypt(enc, org_id)?, - fields: self.fields.decrypt(enc, org_id)?, - password_history: self.password_history.decrypt(enc, org_id)?, + local_data: self.local_data.decrypt(enc, org_id).unwrap(), + attachments: self.attachments.decrypt(enc, org_id).unwrap(), + fields: self.fields.decrypt(enc, org_id).unwrap(), + password_history: self.password_history.decrypt(enc, org_id).unwrap(), creation_date: self.creation_date, deleted_date: self.deleted_date, revision_date: self.revision_date, @@ -280,3 +285,64 @@ impl Decryptable for Cipher { }) } } + +/// Impl from bitwarden_api_api::models::CipherDetailsResponseModel to Cipher + +impl From for Cipher { + fn from(cipher: CipherDetailsResponseModel) -> Self { + debug!("{:?}", cipher); + Cipher { + id: cipher.id, + organization_id: cipher.organization_id, + folder_id: cipher.folder_id, + collection_ids: cipher.collection_ids.unwrap_or_default(), + name: EncString::from_str(&cipher.name.unwrap()).unwrap(), + notes: cipher.notes.map(|s| EncString::from_str(&s).unwrap()), + r#type: cipher.r#type.unwrap().into(), + login: None, + identity: None, + card: None, + secure_note: 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), + view_password: cipher.view_password.unwrap_or(true), + local_data: None, + attachments: cipher + .attachments + .unwrap_or_default() + .into_iter() + .map(|a| a.into()) + .collect(), + fields: vec![], + password_history: vec![], + creation_date: cipher.creation_date.unwrap().parse().unwrap(), + deleted_date: cipher.deleted_date.map(|d| d.parse().unwrap()), + revision_date: cipher.revision_date.unwrap().parse().unwrap(), + } + } +} + +impl From for CipherType { + fn from(t: bitwarden_api_api::models::CipherType) -> Self { + match t { + bitwarden_api_api::models::CipherType::Variant1 => CipherType::Login, + bitwarden_api_api::models::CipherType::Variant2 => CipherType::SecureNote, + bitwarden_api_api::models::CipherType::Variant3 => CipherType::Card, + bitwarden_api_api::models::CipherType::Variant4 => CipherType::Identity, + } + } +} + +impl From for CipherRepromptType { + fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self { + match t { + bitwarden_api_api::models::CipherRepromptType::Variant0 => CipherRepromptType::None, + bitwarden_api_api::models::CipherRepromptType::Variant1 => CipherRepromptType::Password, + } + } +} diff --git a/crates/bitwarden/src/vault/cipher/mod.rs b/crates/bitwarden/src/vault/cipher/mod.rs index c891f439d..570b1e812 100644 --- a/crates/bitwarden/src/vault/cipher/mod.rs +++ b/crates/bitwarden/src/vault/cipher/mod.rs @@ -9,4 +9,5 @@ pub(crate) mod local_data; pub(crate) mod login; pub(crate) mod secure_note; +pub use attachment::download_attachment; pub use cipher::{Cipher, CipherListView, CipherView}; diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index 9eecd6620..54cc9d2b8 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -1,10 +1,11 @@ +mod api; mod cipher; mod collection; mod folder; mod password_history; mod send; -pub use cipher::{Cipher, CipherListView, CipherView}; +pub use cipher::{download_attachment, Cipher, CipherListView, CipherView}; pub use collection::{Collection, CollectionView}; pub use folder::{Folder, FolderView}; pub use password_history::{PasswordHistory, PasswordHistoryView}; diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index bb1621184..29252d133 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -24,6 +24,7 @@ inquire = "0.6.2" bitwarden = { path = "../bitwarden", version = "0.3.0", features = [ "internal", + "mobile", ] } bitwarden-cli = { path = "../bitwarden-cli", version = "0.1.0" } diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 6ba3c7929..594270978 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -10,7 +10,7 @@ use color_eyre::eyre::{bail, Result}; use inquire::{Password, Text}; use log::{debug, error, info}; -pub(crate) async fn password_login(mut client: Client, email: Option) -> Result<()> { +pub(crate) async fn password_login(client: &mut Client, email: Option) -> Result<()> { let email = text_prompt_when_none("Email", email)?; let password = Password::new("Password").without_confirmation().prompt()?; diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 73e64a4aa..569b0612e 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -55,6 +55,14 @@ enum Commands { #[command(subcommand)] command: GeneratorCommands, }, + + Export { + // TODO: Remove these + #[arg(short = 'e', long, help = "Email address")] + email: Option, + #[arg(short = 's', long, global = true, help = "Server URL")] + server: Option, + }, } #[derive(Args, Clone)] @@ -138,12 +146,12 @@ async fn process_commands() -> Result<()> { identity_url: format!("{}/identity", server), ..Default::default() }); - let client = bitwarden::Client::new(settings); + let mut client = bitwarden::Client::new(settings); match args.command { // FIXME: Rust CLI will not support password login! LoginCommands::Password { email } => { - auth::password_login(client, email).await?; + auth::password_login(&mut client, email).await?; } LoginCommands::ApiKey { client_id, @@ -178,9 +186,23 @@ async fn process_commands() -> Result<()> { }) .await?; } + Commands::Export { email, server } => { + let settings = server.map(|server| ClientSettings { + api_url: format!("{}/api", server), + identity_url: format!("{}/identity", server), + ..Default::default() + }); + let mut client = bitwarden::Client::new(settings); + + auth::password_login(&mut client, email).await?; + + client.exporters().export_vault_attachments().await?; + + return Ok(()); + // Export vault + } _ => {} } - // Not login, assuming we have a config let client = bitwarden::Client::new(None); @@ -190,6 +212,7 @@ async fn process_commands() -> Result<()> { Commands::Register { .. } => unreachable!(), Commands::Item { command: _ } => todo!(), Commands::Sync {} => todo!(), + Commands::Export { .. } => unreachable!(), Commands::Generate { command } => match command { GeneratorCommands::Password(args) => { let password = client