bitwarden_exporters/
export.rs1use bitwarden_collections::collection::Collection;
2use bitwarden_core::{
3 Client, NotAuthenticatedError, OrganizationId, UserId, key_management::KeySlotIds,
4};
5use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, KeyStoreContext};
6use bitwarden_vault::{
7 Cipher, CipherView, EncryptMode, EncryptionContext, Folder, FolderView,
8 should_use_blob_encryption,
9};
10
11use crate::{
12 ExportError, ExportFormat, ImportingCipher,
13 csv::export_csv,
14 cxf::{Account, build_cxf, parse_cxf},
15 encrypted_json::export_encrypted_json,
16 json::export_json,
17};
18
19pub(crate) async fn export_vault(
20 client: &Client,
21 folders: Vec<Folder>,
22 ciphers: Vec<Cipher>,
23 format: ExportFormat,
24) -> Result<String, ExportError> {
25 let key_store = client.internal.get_key_store();
26
27 let folders: Vec<FolderView> = key_store.decrypt_list(&folders)?;
28 let folders: Vec<crate::Folder> = folders.into_iter().flat_map(|f| f.try_into()).collect();
29
30 let ciphers: Vec<crate::Cipher> = ciphers
31 .into_iter()
32 .flat_map(|c| crate::Cipher::from_cipher(key_store, c))
33 .collect();
34
35 match format {
36 ExportFormat::Csv => Ok(export_csv(folders, ciphers)?),
37 ExportFormat::Json => Ok(export_json(folders, ciphers)?),
38 ExportFormat::EncryptedJson { password } => Ok(export_encrypted_json(
39 folders,
40 ciphers,
41 password,
42 client.internal.get_kdf().await?,
43 )?),
44 }
45}
46
47pub(crate) fn export_organization_vault(
48 _collections: Vec<Collection>,
49 _ciphers: Vec<Cipher>,
50 _format: ExportFormat,
51) -> Result<String, ExportError> {
52 todo!();
53}
54
55pub(crate) fn export_cxf(
57 client: &Client,
58 account: Account,
59 ciphers: Vec<Cipher>,
60) -> Result<String, ExportError> {
61 let key_store = client.internal.get_key_store();
62
63 let mut ciphers: Vec<crate::Cipher> = ciphers
64 .into_iter()
65 .flat_map(|c| crate::Cipher::from_cipher(key_store, c))
66 .collect();
67
68 for cipher in &mut ciphers {
69 if let crate::CipherType::Login(login) = &mut cipher.r#type {
70 login.sanitize_uris();
71 }
72 }
73
74 Ok(build_cxf(account, ciphers)?)
75}
76
77pub fn encrypt_import(
85 ctx: &mut KeyStoreContext<KeySlotIds>,
86 cipher: ImportingCipher,
87 organization_id: Option<OrganizationId>,
88 user_id: UserId,
89) -> Result<EncryptionContext, ExportError> {
90 let mut view: CipherView = cipher.clone().into();
91 view.organization_id = organization_id;
92
93 let passkey = match cipher.r#type {
95 crate::CipherType::Login(login) => login.fido2_credentials,
96 _ => None,
97 };
98
99 if let Some(passkey) = passkey {
100 let passkeys = passkey.into_iter().map(|p| p.into()).collect();
101
102 view.set_new_fido2_credentials(ctx, passkeys)?;
103 }
104
105 let key = view.key_identifier();
108 let encrypted_by_key_id = ctx.get_symmetric_key_id(key).map(|id| id.to_string());
109
110 let mode = if should_use_blob_encryption(ctx, organization_id) {
113 EncryptMode::Blob(view)
114 } else {
115 EncryptMode::Legacy(view)
116 };
117 let new_cipher = mode.encrypt_composite(ctx, key)?;
118
119 Ok(EncryptionContext {
120 cipher: new_cipher,
121 encrypted_for: user_id,
122 encrypted_by_key_id,
123 })
124}
125
126pub(crate) fn import_cxf(
128 client: &Client,
129 payload: String,
130) -> Result<Vec<EncryptionContext>, ExportError> {
131 let user_id = client.internal.get_user_id().ok_or(NotAuthenticatedError)?;
132
133 let key_store = client.internal.get_key_store();
134 let mut ctx = key_store.context();
135
136 let ciphers = parse_cxf(payload)?;
137 let ciphers: Result<Vec<EncryptionContext>, _> = ciphers
138 .into_iter()
139 .map(|c| encrypt_import(&mut ctx, c, None, user_id))
140 .collect();
141
142 ciphers
143}
144
145#[cfg(test)]
146mod tests {
147 use bitwarden_core::{
148 client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
149 key_management::SymmetricKeySlotId,
150 };
151
152 use super::*;
153
154 fn dashlane_payload() -> String {
155 std::fs::read_to_string("resources/dashlane_export.json").unwrap()
156 }
157
158 #[tokio::test]
160 async fn import_cxf_returns_encryption_context() {
161 let client = Client::init_test_account(test_bitwarden_com_account()).await;
162 let user_id = client.internal.get_user_id().unwrap();
163
164 let imported = import_cxf(&client, dashlane_payload()).unwrap();
165
166 assert!(!imported.is_empty());
167 assert!(imported.iter().all(|c| c.encrypted_for == user_id));
168
169 let names: Vec<String> = imported
171 .iter()
172 .map(|c| c.cipher.name.as_ref().unwrap().to_string())
173 .collect();
174 assert!(!names.iter().any(|n| n == "adobe.com"));
175 }
176
177 #[tokio::test]
180 async fn import_cxf_records_user_key_id() {
181 for account in [
182 test_bitwarden_com_account(),
183 test_bitwarden_com_account_v2(),
184 ] {
185 let client = Client::init_test_account(account).await;
186 let expected = client
187 .internal
188 .get_key_store()
189 .context()
190 .get_symmetric_key_id(SymmetricKeySlotId::User)
191 .map(|id| id.to_string());
192 assert!(expected.is_some());
193
194 let imported = import_cxf(&client, dashlane_payload()).unwrap();
195
196 assert!(!imported.is_empty());
197 for context in &imported {
198 assert_eq!(context.encrypted_by_key_id, expected);
199 }
200 }
201 }
202}