diff --git a/crates/bitwarden-uniffi/src/tool/mod.rs b/crates/bitwarden-uniffi/src/tool/mod.rs index 4a4ea2401..0dc875ef9 100644 --- a/crates/bitwarden-uniffi/src/tool/mod.rs +++ b/crates/bitwarden-uniffi/src/tool/mod.rs @@ -65,7 +65,7 @@ impl ClientExporters { Ok(self .0 .0 - .read() + .write() .await .exporters() .export_vault(folders, ciphers, format) @@ -82,7 +82,7 @@ impl ClientExporters { Ok(self .0 .0 - .read() + .write() .await .exporters() .export_organization_vault(collections, ciphers, format) diff --git a/crates/bitwarden/src/tool/exporters/client_exporter.rs b/crates/bitwarden/src/tool/exporters/client_exporter.rs index 05eb737f3..08eacd8a6 100644 --- a/crates/bitwarden/src/tool/exporters/client_exporter.rs +++ b/crates/bitwarden/src/tool/exporters/client_exporter.rs @@ -1,3 +1,4 @@ +use super::export_vault_attachments; use crate::{ error::Result, tool::exporters::{export_organization_vault, export_vault, ExportFormat}, @@ -6,7 +7,7 @@ use crate::{ }; pub struct ClientExporters<'a> { - pub(crate) client: &'a crate::Client, + pub(crate) client: &'a mut crate::Client, } impl<'a> ClientExporters<'a> { @@ -28,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 9e9e99ed5..a83c9bf0b 100644 --- a/crates/bitwarden/src/tool/exporters/mod.rs +++ b/crates/bitwarden/src/tool/exporters/mod.rs @@ -1,13 +1,17 @@ +use std::{fs::File, io::Write}; + use bitwarden_crypto::Decryptable; use bitwarden_exporters::export; +use log::{debug, info}; use schemars::JsonSchema; use crate::{ client::{LoginMethod, UserLoginMethod}, error::{Error, Result}, + platform::SyncRequest, vault::{ - login::LoginUriView, Cipher, CipherType, CipherView, Collection, FieldView, Folder, - FolderView, SecureNoteType, + download_attachment, login::LoginUriView, Cipher, CipherType, CipherView, Collection, + FieldView, Folder, FolderView, SecureNoteType, }, Client, }; @@ -78,6 +82,60 @@ pub(super) fn export_organization_vault( 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 folders = sync.folders; + let ciphers = sync.ciphers; + + let mut f = File::create("export.csv")?; + f.write_all( + export_vault(client, folders.clone(), ciphers.clone(), ExportFormat::Csv)?.as_bytes(), + )?; + + let mut f = File::create("export.json")?; + f.write_all(export_vault(client, folders, ciphers.clone(), ExportFormat::Json)?.as_bytes())?; + + let ciphers_with_attachments = ciphers + .iter() + .filter(|c| c.attachments.as_ref().is_some_and(|a| !a.is_empty())); + + info!( + "Found {} ciphers with attachments", + ciphers_with_attachments.count() + ); + + info!("Decrypting ciphers"); + + let enc_settings = client.get_encryption_settings()?; + + let decrypted: Vec = 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.unwrap_or_default() { + download_attachment(client, cipher.id.unwrap(), &attachment.id.unwrap()).await?; + } + } + + Ok(()) +} + impl TryFrom for bitwarden_exporters::Folder { type Error = Error; 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 1ec6be6fe..724ab1c99 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -1,13 +1,26 @@ +use std::{ + fs::{create_dir_all, File}, + io::Write, + path::Path, +}; + +use bitwarden_api_api::apis::ciphers_api::ciphers_id_attachment_attachment_id_get; use bitwarden_crypto::{ CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, }; +use log::debug; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use super::Cipher; -use crate::error::{Error, Result}; +use crate::{ + error::{Error, Result}, + vault::api::attachment_get, + Client, +}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Attachment { @@ -32,6 +45,49 @@ pub struct AttachmentView { pub key: Option, } +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, + attachment_id.to_string().as_str(), + ) + .await?; + + let attachment: Attachment = response.try_into()?; + let enc = client.get_encryption_settings()?; + let k = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + let view = attachment.decrypt_with_key(k)?; + let mut cipher_key: Vec = view.key.unwrap().decrypt_with_key(k)?; + + let key = SymmetricCryptoKey::try_from(cipher_key.as_mut_slice())?; + + let response = attachment_get(&view.url.unwrap()).await?; + let bytes = response.bytes().await?; + + let buf = EncString::from_buffer(&bytes)?; + let dec: Vec = buf.decrypt_with_key(&key)?; + + 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!() +} + #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] diff --git a/crates/bitwarden/src/vault/cipher/card.rs b/crates/bitwarden/src/vault/cipher/card.rs index cd61a17d8..06c38d441 100644 --- a/crates/bitwarden/src/vault/cipher/card.rs +++ b/crates/bitwarden/src/vault/cipher/card.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Card { diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 9223462a1..ce996f5ae 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -37,7 +37,7 @@ pub enum CipherRepromptType { Password = 1, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Cipher { diff --git a/crates/bitwarden/src/vault/cipher/field.rs b/crates/bitwarden/src/vault/cipher/field.rs index 25a318d6f..10fe4fdc6 100644 --- a/crates/bitwarden/src/vault/cipher/field.rs +++ b/crates/bitwarden/src/vault/cipher/field.rs @@ -19,7 +19,7 @@ pub enum FieldType { Linked = 3, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Field { diff --git a/crates/bitwarden/src/vault/cipher/identity.rs b/crates/bitwarden/src/vault/cipher/identity.rs index f59166eec..3c3221311 100644 --- a/crates/bitwarden/src/vault/cipher/identity.rs +++ b/crates/bitwarden/src/vault/cipher/identity.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Identity { diff --git a/crates/bitwarden/src/vault/cipher/local_data.rs b/crates/bitwarden/src/vault/cipher/local_data.rs index 6a85512c2..c3950e7c7 100644 --- a/crates/bitwarden/src/vault/cipher/local_data.rs +++ b/crates/bitwarden/src/vault/cipher/local_data.rs @@ -2,7 +2,7 @@ use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCry use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LocalData { diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index 5e2156a83..445b06b52 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -22,7 +22,7 @@ pub enum UriMatchType { Never = 5, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginUri { @@ -38,7 +38,7 @@ pub struct LoginUriView { pub r#match: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Login { diff --git a/crates/bitwarden/src/vault/cipher/mod.rs b/crates/bitwarden/src/vault/cipher/mod.rs index c2b49eb37..8527220a5 100644 --- a/crates/bitwarden/src/vault/cipher/mod.rs +++ b/crates/bitwarden/src/vault/cipher/mod.rs @@ -10,7 +10,8 @@ pub(crate) mod login; pub(crate) mod secure_note; pub use attachment::{ - Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView, + download_attachment, Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, + AttachmentView, }; pub use cipher::{Cipher, CipherListView, CipherRepromptType, CipherType, CipherView}; pub use field::FieldView; diff --git a/crates/bitwarden/src/vault/folder.rs b/crates/bitwarden/src/vault/folder.rs index edd1cac42..ad0cd5654 100644 --- a/crates/bitwarden/src/vault/folder.rs +++ b/crates/bitwarden/src/vault/folder.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Folder { diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index 2addfec6b..4495688f1 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -1,3 +1,4 @@ +mod api; mod cipher; mod collection; mod folder; diff --git a/crates/bitwarden/src/vault/password_history.rs b/crates/bitwarden/src/vault/password_history.rs index 2ec20116b..8267e9533 100644 --- a/crates/bitwarden/src/vault/password_history.rs +++ b/crates/bitwarden/src/vault/password_history.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordHistory { diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index e0195f5aa..3e2ad8c12 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -11,7 +11,7 @@ use color_eyre::eyre::{bail, Result}; use inquire::{Password, Text}; use log::{debug, error, info}; -pub(crate) async fn login_password(mut client: Client, email: Option) -> Result<()> { +pub(crate) async fn login_password(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 6674bda1e..84e09fe5c 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -57,6 +57,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)] @@ -157,12 +165,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::login_password(client, email).await?; + auth::login_password(&mut client, email).await?; } LoginCommands::ApiKey { client_id, @@ -203,9 +211,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::login_password(&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); @@ -215,6 +237,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