Skip to content

Commit

Permalink
Add more tests, use set_new_fido2_credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Dec 12, 2024
1 parent e199cdb commit 7a6827d
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 70 deletions.
43 changes: 35 additions & 8 deletions crates/bitwarden-exporters/src/cxp/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ pub(crate) fn parse_cxf(payload: String) -> Result<Vec<ImportingCipher>, CxpErro
Ok(items)
}

Check warning on line 15 in crates/bitwarden-exporters/src/cxp/import.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/import.rs#L12-L15

Added lines #L12 - L15 were not covered by tests

/// Convert a CXP timestamp to a DateTime<Utc>.
///
/// If the timestamp is None, the current time is used.
fn convert_date(ts: Option<u64>) -> DateTime<Utc> {
ts.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
.unwrap_or(Utc::now())
}

fn parse_item(value: Item) -> Vec<ImportingCipher> {
let grouped = group_credentials_by_type(value.credentials);

let creation_date = value
.creation_at
.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
.unwrap_or(Utc::now());
let revision_date = value
.modified_at
.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
.unwrap_or(Utc::now());
let creation_date = convert_date(value.creation_at);
let revision_date = convert_date(value.modified_at);

match value.ty {
ItemType::Login => {
Expand Down Expand Up @@ -82,6 +84,12 @@ fn parse_item(value: Item) -> Vec<ImportingCipher> {
}
}

/// Group credentials by type.
///
/// The Credential Exchange protocol allows multiple identical credentials to be stored in a single
/// item. Currently we only support one of each type and grouping allows an easy way to fetch the
/// first of each type. Eventually we should add support for handling multiple credentials of the
/// same type.
fn group_credentials_by_type(credentials: Vec<Credential>) -> GroupedCredentials {
GroupedCredentials {
basic_auth: credentials
Expand All @@ -108,8 +116,27 @@ struct GroupedCredentials {

#[cfg(test)]
mod tests {
use chrono::Duration;

use super::*;

#[test]
fn test_convert_date() {
let timestamp: u64 = 1706613834;
let datetime = convert_date(Some(timestamp));
assert_eq!(
datetime,
"2024-01-30T11:23:54Z".parse::<DateTime<Utc>>().unwrap()
);
}

#[test]
fn test_convert_date_none() {
let datetime = convert_date(None);
assert!(datetime > Utc::now() - Duration::seconds(1));
assert!(datetime < Utc::now());
}

#[test]
fn test_parse_item() {
let item = Item {
Expand Down
58 changes: 24 additions & 34 deletions crates/bitwarden-exporters/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bitwarden_core::Client;
use bitwarden_crypto::{KeyContainer, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey};
use bitwarden_crypto::{KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey};
use bitwarden_vault::{
Cipher, CipherView, Collection, Fido2CredentialFullView, Folder, FolderView,
};
Expand Down Expand Up @@ -65,62 +65,52 @@ pub(crate) fn export_cxf(
Ok(build_cxf(account, ciphers)?)
}

Check warning on line 66 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L60-L66

Added lines #L60 - L66 were not covered by tests

fn encrypt_import(
key: &SymmetricCryptoKey,
cipher: ImportingCipher,
) -> Result<Cipher, ExportError> {
let view: CipherView = cipher.clone().into();
fn encrypt_import(enc: &dyn KeyContainer, cipher: ImportingCipher) -> Result<Cipher, ExportError> {
let mut view: CipherView = cipher.clone().into();

Check warning on line 69 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L68-L69

Added lines #L68 - L69 were not covered by tests

let mut new_cipher = view.encrypt_with_key(key)?;

// Get passkey from cipher
// if cipher is typpe login
// Get passkey from cipher if cipher is type login
let passkey = match cipher.r#type {
crate::CipherType::Login(login) => login.fido2_credentials,
_ => None,

Check warning on line 74 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L72-L74

Added lines #L72 - L74 were not covered by tests
};

if let Some(passkey) = passkey {
let psk: Vec<bitwarden_vault::Fido2Credential> = passkey
let passkeys: Vec<Fido2CredentialFullView> = passkey
.into_iter()
.flat_map(|p| {
Fido2CredentialFullView {
credential_id: p.credential_id,
key_type: p.key_type,
key_algorithm: p.key_algorithm,
key_curve: p.key_curve,
key_value: p.key_value,
rp_id: p.rp_id,
user_handle: p.user_handle,
user_name: p.user_name,
counter: p.counter.to_string(),
rp_name: p.rp_name,
user_display_name: p.user_display_name,
discoverable: p.discoverable,
creation_date: p.creation_date,
}
.encrypt_with_key(key)
.map(|p| Fido2CredentialFullView {
credential_id: p.credential_id,
key_type: p.key_type,
key_algorithm: p.key_algorithm,
key_curve: p.key_curve,
key_value: p.key_value,
rp_id: p.rp_id,
user_handle: p.user_handle,
user_name: p.user_name,
counter: p.counter.to_string(),
rp_name: p.rp_name,
user_display_name: p.user_display_name,
discoverable: p.discoverable,
creation_date: p.creation_date,
})
.collect();

let login = new_cipher.login.as_mut().unwrap();
login.fido2_credentials = Some(psk);

new_cipher.login = Some(login.clone());
view.set_new_fido2_credentials(enc, passkeys)?;
}

Check warning on line 98 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L77-L98

Added lines #L77 - L98 were not covered by tests

let key = view.locate_key(enc, &None)?;
let new_cipher = view.encrypt_with_key(key)?;

Check warning on line 101 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L100-L101

Added lines #L100 - L101 were not covered by tests

Ok(new_cipher)
}

Check warning on line 104 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L103-L104

Added lines #L103 - L104 were not covered by tests

/// See [crate::ClientExporters::import_cxf] for more documentation.
pub(crate) fn import_cxf(client: &Client, payload: String) -> Result<Vec<Cipher>, ExportError> {
let enc = client.internal.get_encryption_settings()?;

Check warning on line 108 in crates/bitwarden-exporters/src/export.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L107-L108

Added lines #L107 - L108 were not covered by tests
let key = enc.get_key(&None)?;

let ciphers = parse_cxf(payload)?;
let ciphers: Result<Vec<Cipher>, _> = ciphers
.into_iter()
.map(|c| encrypt_import(key, c))
.map(|c| encrypt_import(&enc, c))
.collect();

ciphers
Expand Down
106 changes: 78 additions & 28 deletions crates/bitwarden-exporters/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,7 @@ impl crate::Cipher {
let view: CipherView = cipher.decrypt_with_key(key)?;

let r = match view.r#type {
CipherType::Login => {
let l = require!(view.login.clone());

crate::CipherType::Login(Box::new(crate::Login {
username: l.username,
password: l.password,
login_uris: l
.uris
.unwrap_or_default()
.into_iter()
.map(|u| u.into())
.collect(),
totp: l.totp,
fido2_credentials: {
if l.fido2_credentials.is_some() {
let credentials = view.get_fido2_credentials(enc)?;
if credentials.is_empty() {
None
} else {
Some(credentials.into_iter().map(|c| c.into()).collect())
}
} else {
None
}
},
}))
}
CipherType::Login => crate::CipherType::Login(Box::new(from_login(&view, enc)?)),
CipherType::SecureNote => {
let s = require!(view.secure_note);
crate::CipherType::SecureNote(Box::new(s.into()))

Check warning on line 31 in crates/bitwarden-exporters/src/models.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/models.rs#L30-L31

Added lines #L30 - L31 were not covered by tests
Expand Down Expand Up @@ -91,6 +65,34 @@ impl crate::Cipher {
}
}

/// Convert a `LoginView` into a `crate::Login`.
fn from_login(
view: &CipherView,
enc: &dyn KeyContainer,
) -> Result<crate::Login, MissingFieldError> {
let l = require!(view.login.clone());

Ok(crate::Login {
username: l.username,
password: l.password,
login_uris: l
.uris
.unwrap_or_default()
.into_iter()
.map(|u| u.into())
.collect(),
totp: l.totp,
fido2_credentials: l.fido2_credentials.as_ref().and_then(|_| {
let credentials = view.get_fido2_credentials(enc).ok()?;
if credentials.is_empty() {
None

Check warning on line 88 in crates/bitwarden-exporters/src/models.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/models.rs#L86-L88

Added lines #L86 - L88 were not covered by tests
} else {
Some(credentials.into_iter().map(|c| c.into()).collect())

Check warning on line 90 in crates/bitwarden-exporters/src/models.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/models.rs#L90

Added line #L90 was not covered by tests
}
}),
})
}

impl From<LoginUriView> for crate::LoginUri {
fn from(value: LoginUriView) -> Self {
Self {
Expand Down Expand Up @@ -229,7 +231,55 @@ mod tests {
}

#[test]
fn test_try_from_cipher_view_login() {
fn test_from_login() {
let enc = MockKeyContainer(SymmetricCryptoKey::generate(rand::thread_rng()));

let view = CipherView {
r#type: CipherType::Login,
login: Some(LoginView {
username: Some("test_username".to_string()),
password: Some("test_password".to_string()),
password_revision_date: None,
uris: None,
totp: None,
autofill_on_page_load: None,
fido2_credentials: None,
}),
id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(),
organization_id: None,
folder_id: None,
collection_ids: vec![],
key: None,
name: "My login".to_string(),
notes: None,
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
edit: true,
view_password: true,
local_data: None,
attachments: None,
fields: None,
password_history: None,
creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
deleted_date: None,
revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
};

let login = from_login(&view, &enc).unwrap();

assert_eq!(login.username, Some("test_username".to_string()));
assert_eq!(login.password, Some("test_password".to_string()));
assert!(login.login_uris.is_empty());
assert_eq!(login.totp, None);
}

#[test]
fn test_from_cipher_login() {
let enc = MockKeyContainer(SymmetricCryptoKey::generate(rand::thread_rng()));

let cipher_view = CipherView {
Expand Down

0 comments on commit 7a6827d

Please sign in to comment.