Skip to main content

bitwarden_exporters/
export.rs

1use 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
55/// See [crate::ExporterClient::export_cxf] for more documentation.
56pub(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
77/// Encrypts a parsed/imported cipher for the user's vault, or for an organization when
78/// `organization_id` is set. Shared by the importers (`import_kdbx`) and by CXF import; lives here
79/// alongside the `ImportingCipher` interchange model and the `From<ImportingCipher> for CipherView`
80/// bridge.
81///
82/// `user_id` identifies the user performing the import and is recorded on the returned
83/// [`EncryptionContext`] alongside the id of the key the cipher was wrapped under.
84pub 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    // Get passkey from cipher if cipher is type login
94    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    // Capture the id of the wrapping key - the organization key for org-owned ciphers, the user key
106    // otherwise - before encrypting, since that borrows the context mutably.
107    let key = view.key_identifier();
108    let encrypted_by_key_id = ctx.get_symmetric_key_id(key).map(|id| id.to_string());
109
110    // Select the encryption format based on the account's current security state, matching how
111    // regular cipher saves choose between the blob and legacy field-level formats.
112    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
126/// See [crate::ExporterClient::import_cxf] for more documentation.
127pub(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    /// The imported ciphers are attributed to the importing user and are actually encrypted.
159    #[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        // "adobe.com" is one of the plaintext titles in the fixture; it must not survive as-is.
170        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    /// The user key's id is recorded so the server can reject writes made under a stale key. Both
178    /// the V1 (AES-CBC-HMAC) and V2 (XAES-256-GCM) user keys carry one.
179    #[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}