Skip to content

Commit

Permalink
Initial work on encrypted export
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Feb 6, 2024
1 parent 714c2d4 commit 61b4e47
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 20 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions crates/bitwarden-crypto/src/keys/master_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ impl MasterKey {
&stretched_key.key,
)
}

pub fn encrypt(&self, data: &[u8]) -> Result<EncString> {
let stretched_key = stretch_master_key(self)?;

EncString::encrypt_aes256_hmac(
data,
stretched_key.mac_key.as_ref().unwrap(),
&stretched_key.key,
)
}
}

/// Generate a new random user key and encrypt it with the master key.
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden-exporters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ rust-version = "1.57"
exclude = ["/resources"]

[dependencies]
base64 = ">=0.21.2, <0.22"
bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0" }
chrono = { version = ">=0.4.26, <0.5", features = [
"clock",
"serde",
Expand All @@ -23,4 +25,4 @@ csv = "1.3.0"
serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
thiserror = ">=1.0.40, <2.0"
uuid = { version = ">=1.3.3, <2.0", features = ["serde"] }
uuid = { version = ">=1.3.3, <2.0", features = ["serde", "v4"] }
246 changes: 246 additions & 0 deletions crates/bitwarden-exporters/src/encrypted_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{generate_random_bytes, Kdf, MasterKey};
use serde::Serialize;
use thiserror::Error;
use uuid::Uuid;

use crate::{
json::{self, export_json},
Cipher, Folder,
};

#[derive(Error, Debug)]

Check warning on line 12 in crates/bitwarden-exporters/src/encrypted_json.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/encrypted_json.rs#L12

Added line #L12 was not covered by tests
pub enum EncryptedJsonError {
#[error(transparent)]
JsonExport(#[from] json::JsonError),

#[error("JSON error: {0}")]
Serde(#[from] serde_json::Error),

#[error("Cryptography error, {0}")]
Crypto(#[from] bitwarden_crypto::CryptoError),
}

pub(crate) fn export_encrypted_json(
folders: Vec<Folder>,
ciphers: Vec<Cipher>,
password: String,
kdf: Kdf,
) -> Result<String, EncryptedJsonError> {
let decrypted_export = export_json(folders, ciphers)?;

let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) = match kdf {
Kdf::PBKDF2 { iterations } => (0, iterations.get(), None, None),
Kdf::Argon2id {
iterations,
memory,
parallelism,
} => (
1,
iterations.get(),
Some(memory.get()),
Some(parallelism.get()),
),

Check warning on line 43 in crates/bitwarden-exporters/src/encrypted_json.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/encrypted_json.rs#L35-L43

Added lines #L35 - L43 were not covered by tests
};

let salt: [u8; 16] = generate_random_bytes();
let salt = STANDARD.encode(salt);
let key = MasterKey::derive(password.as_bytes(), salt.as_bytes(), &kdf)?;

let enc_key_validation = Uuid::new_v4().to_string();

let encrypted_export = EncryptedJsonExport {
encrypted: true,
password_protected: true,
salt,
kdf_type,
kdf_iterations,
kdf_memory,
kdf_parallelism,
enc_key_validation: key.encrypt(enc_key_validation.as_bytes())?.to_string(),
data: key.encrypt(decrypted_export.as_bytes())?.to_string(),
};

Ok(serde_json::to_string_pretty(&encrypted_export)?)
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EncryptedJsonExport {
encrypted: bool,
password_protected: bool,
salt: String,
kdf_type: u32,
kdf_iterations: u32,
kdf_memory: Option<u32>,
kdf_parallelism: Option<u32>,
#[serde(rename = "encKeyValidation_DO_NOT_EDIT")]
enc_key_validation: String,
data: String,
}

#[cfg(test)]
mod tests {
use std::num::NonZeroU32;

use super::*;
use crate::{
Card, Cipher, CipherType, Field, Identity, Login, LoginUri, SecureNote, SecureNoteType,
};

#[test]
pub fn test_export() {
let export = export_encrypted_json(
vec![Folder {
id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
name: "Important".to_string(),
}],
vec![
Cipher {
id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),

name: "Bitwarden".to_string(),
notes: Some("My note".to_string()),

r#type: CipherType::Login(Box::new(Login {
username: Some("[email protected]".to_string()),
password: Some("asdfasdfasdf".to_string()),
login_uris: vec![LoginUri {
uri: Some("https://vault.bitwarden.com".to_string()),
r#match: None,
}],
totp: Some("ABC".to_string()),
})),

favorite: true,
reprompt: 0,

fields: vec![
Field {
name: Some("Text".to_string()),
value: Some("A".to_string()),
r#type: 0,
linked_id: None,
},
Field {
name: Some("Hidden".to_string()),
value: Some("B".to_string()),
r#type: 1,
linked_id: None,
},
Field {
name: Some("Boolean (true)".to_string()),
value: Some("true".to_string()),
r#type: 2,
linked_id: None,
},
Field {
name: Some("Boolean (false)".to_string()),
value: Some("false".to_string()),
r#type: 2,
linked_id: None,
},
Field {
name: Some("Linked".to_string()),
value: None,
r#type: 3,
linked_id: Some(101),
},
],

revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
deleted_date: None,
},
Cipher {
id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
folder_id: None,

name: "My secure note".to_string(),
notes: Some("Very secure!".to_string()),

r#type: CipherType::SecureNote(Box::new(SecureNote {
r#type: SecureNoteType::Generic,
})),

favorite: false,
reprompt: 0,

fields: vec![],

revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
deleted_date: None,
},
Cipher {
id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
folder_id: None,

name: "My card".to_string(),
notes: None,

r#type: CipherType::Card(Box::new(Card {
cardholder_name: Some("John Doe".to_string()),
exp_month: Some("1".to_string()),
exp_year: Some("2032".to_string()),
code: Some("123".to_string()),
brand: Some("Visa".to_string()),
number: Some("4111111111111111".to_string()),
})),

favorite: false,
reprompt: 0,

fields: vec![],

revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
deleted_date: None,
},
Cipher {
id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),

name: "My identity".to_string(),
notes: None,

r#type: CipherType::Identity(Box::new(Identity {
title: Some("Mr".to_string()),
first_name: Some("John".to_string()),
middle_name: None,
last_name: Some("Doe".to_string()),
address1: None,
address2: None,
address3: None,
city: None,
state: None,
postal_code: None,
country: None,
company: Some("Bitwarden".to_string()),
email: None,
phone: None,
ssn: None,
username: Some("JDoe".to_string()),
passport_number: None,
license_number: None,
})),

favorite: false,
reprompt: 0,

fields: vec![],

revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
deleted_date: None,
},
],
"password".to_string(),
Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
)
.unwrap();
}
}
11 changes: 9 additions & 2 deletions crates/bitwarden-exporters/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use bitwarden_crypto::Kdf;
use chrono::{DateTime, Utc};
use thiserror::Error;
use uuid::Uuid;
Expand All @@ -6,11 +7,13 @@ mod csv;
use csv::export_csv;
mod json;
use json::export_json;
mod encrypted_json;
use encrypted_json::export_encrypted_json;

pub enum Format {
Csv,
Json,
EncryptedJson { password: String },
EncryptedJson { password: String, kdf: Kdf },
}

/// Export representation of a Bitwarden folder.
Expand Down Expand Up @@ -127,6 +130,8 @@ pub enum ExportError {
Csv(#[from] csv::CsvError),
#[error("JSON error: {0}")]
Json(#[from] json::JsonError),
#[error("Encrypted JSON error: {0}")]
EncryptedJsonError(#[from] encrypted_json::EncryptedJsonError),
}

pub fn export(
Expand All @@ -137,6 +142,8 @@ pub fn export(
match format {
Format::Csv => Ok(export_csv(folders, ciphers)?),
Format::Json => Ok(export_json(folders, ciphers)?),
Format::EncryptedJson { password: _ } => todo!(),
Format::EncryptedJson { password, kdf } => {
Ok(export_encrypted_json(folders, ciphers, password, kdf)?)

Check warning on line 146 in crates/bitwarden-exporters/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/lib.rs#L145-L146

Added lines #L145 - L146 were not covered by tests
}
}
}
Loading

0 comments on commit 61b4e47

Please sign in to comment.