Skip to content

Commit

Permalink
Implement passkey export
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Nov 15, 2024
1 parent c6d6f10 commit f3f3e6d
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 82 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions crates/bitwarden-exporters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ uniffi = ["dep:uniffi"] # Uniffi bindings
base64 = ">=0.22.1, <0.23"
bitwarden-core = { workspace = true }
bitwarden-crypto = { workspace = true }
bitwarden-fido = { workspace = true }
bitwarden-vault = { workspace = true }
chrono = { workspace = true, features = ["std"] }
credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "b5b5fa3faab7a1aab4efba779f5f74c7ec0f3b35" }
Expand All @@ -32,5 +33,8 @@ thiserror = { workspace = true }
uniffi = { workspace = true, optional = true }
uuid = { workspace = true }

[dev-dependencies]
rand = ">=0.8.5, <0.9"

[lints]
workspace = true
2 changes: 2 additions & 0 deletions crates/bitwarden-exporters/src/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ mod tests {
r#match: None,
}],
totp: None,
fido2_credentials: None,
})),
favorite: false,
reprompt: 0,
Expand All @@ -160,6 +161,7 @@ mod tests {
r#match: None,
}],
totp: Some("steam://ABCD123".to_string()),
fido2_credentials: None,
})),
favorite: true,
reprompt: 0,
Expand Down
99 changes: 93 additions & 6 deletions crates/bitwarden-exporters/src/cxp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};

use bitwarden_core::MissingFieldError;
use bitwarden_crypto::generate_random_bytes;
use bitwarden_fido::{string_to_guid_bytes, InvalidGuid};
use credential_exchange_types::{
format::{
Account as CxpAccount, BasicAuthCredential, Credential, EditableField, FieldType, Item,
ItemType,
ItemType, PasskeyCredential,
},
B64Url,
};
use thiserror::Error;
use uuid::Uuid;

use crate::{Cipher, CipherType, Login};
use crate::{Cipher, CipherType, Fido2Credential, Login};

mod error;
pub use error::CxpError;
Expand Down Expand Up @@ -70,7 +75,26 @@ impl From<CipherType> for ItemType {

impl From<Login> for Vec<Credential> {
fn from(login: Login) -> Self {
vec![Credential::BasicAuth(BasicAuthCredential {
let mut credentials = vec![];

credentials.push(Credential::BasicAuth(login.clone().into()));

if let Some(fido2_credentials) = login.fido2_credentials {
for fido2_credential in fido2_credentials {
let c = fido2_credential.try_into();
if let Ok(c) = c {
credentials.push(Credential::Passkey(c))
}

Check warning on line 87 in crates/bitwarden-exporters/src/cxp/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/mod.rs#L87

Added line #L87 was not covered by tests
}
}

Check warning on line 89 in crates/bitwarden-exporters/src/cxp/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/mod.rs#L89

Added line #L89 was not covered by tests

credentials
}
}

impl From<Login> for BasicAuthCredential {
fn from(login: Login) -> Self {
BasicAuthCredential {
urls: login
.login_uris
.into_iter()
Expand All @@ -88,7 +112,46 @@ impl From<Login> for Vec<Credential> {
value,
label: None,
}),
})]
}
}
}

#[derive(Error, Debug)]
pub enum PasskeyError {
#[error("Counter is not zero")]
CounterNotZero,
#[error(transparent)]
InvalidGuid(InvalidGuid),
#[error(transparent)]
MissingField(MissingFieldError),
#[error(transparent)]
InvalidBase64(#[from] base64::DecodeError),
}

impl TryFrom<Fido2Credential> for PasskeyCredential {
type Error = PasskeyError;

fn try_from(value: Fido2Credential) -> Result<Self, Self::Error> {
if value.counter > 0 {
return Err(PasskeyError::CounterNotZero);

Check warning on line 136 in crates/bitwarden-exporters/src/cxp/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/mod.rs#L136

Added line #L136 was not covered by tests
}

Ok(PasskeyCredential {
credential_id: string_to_guid_bytes(&value.credential_id)
.map_err(PasskeyError::InvalidGuid)?
.into(),
rp_id: value.rp_id,
user_name: value.user_name.unwrap_or("".to_string()),
user_display_name: value.user_display_name.unwrap_or("".to_string()),
user_handle: value
.user_handle
.map(|v| URL_SAFE_NO_PAD.decode(v))
.transpose()?
.map(|v| v.into())
.ok_or(PasskeyError::MissingField(MissingFieldError("user_handle")))?,
key: URL_SAFE_NO_PAD.decode(value.key_value)?.into(),
fido2_extensions: vec![],
})
}
}

Expand All @@ -114,7 +177,7 @@ mod tests {
use chrono::{DateTime, Utc};

use super::*;
use crate::{CipherType, Field, Login, LoginUri};
use crate::{CipherType, Fido2Credential, Field, Login, LoginUri};

#[test]
fn test_login_to_item() {
Expand All @@ -133,6 +196,21 @@ mod tests {
r#match: None,
}],
totp: Some("ABC".to_string()),
fido2_credentials: Some(vec![Fido2Credential {
credential_id: "52217b91-73f1-4fea-b3f2-54a7959fd5aa".to_string(),
key_type: "public-key".to_string(),
key_algorithm: "ECDSA".to_string(),
key_curve: "P-256".to_string(),
key_value: URL_SAFE_NO_PAD.encode([0, 1, 2, 3, 4, 5, 6]),
rp_id: "123".to_string(),
user_handle: Some(URL_SAFE_NO_PAD.encode([0, 1, 2, 3, 4, 5, 6])),
user_name: None,
counter: 0,
rp_name: None,
user_display_name: None,
discoverable: "true".to_string(),
creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
}]),
})),

favorite: true,
Expand Down Expand Up @@ -192,7 +270,7 @@ mod tests {
assert_eq!(item.ty, ItemType::Login);
assert_eq!(item.title, "Bitwarden");
assert_eq!(item.subtitle, None);
assert_eq!(item.credentials.len(), 1);
assert_eq!(item.credentials.len(), 2);
assert_eq!(item.tags, None);
assert!(item.extensions.is_none());

Expand All @@ -217,5 +295,14 @@ mod tests {
}
_ => panic!("Expected Credential::BasicAuth"),

Check warning on line 296 in crates/bitwarden-exporters/src/cxp/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/mod.rs#L296

Added line #L296 was not covered by tests
}

let credential = &item.credentials[1];

match credential {
Credential::Passkey(passkey) => {
assert_eq!(passkey.credential_id.to_string(), "UiF7kXPxT-qz8lSnlZ_Vqg");
}
_ => panic!("Expected Credential::Passkey"),

Check warning on line 305 in crates/bitwarden-exporters/src/cxp/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/cxp/mod.rs#L305

Added line #L305 was not covered by tests
}
}
}
1 change: 1 addition & 0 deletions crates/bitwarden-exporters/src/encrypted_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ mod tests {
r#match: None,
}],
totp: Some("ABC".to_string()),
fido2_credentials: None,
})),

favorite: true,
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-exporters/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ pub enum ExportError {
BitwardenError(#[from] bitwarden_core::Error),
#[error(transparent)]
BitwardenCryptoError(#[from] bitwarden_crypto::CryptoError),
#[error(transparent)]
CipherError(#[from] bitwarden_vault::CipherError),
}
15 changes: 9 additions & 6 deletions crates/bitwarden-exporters/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bitwarden_core::Client;
use bitwarden_crypto::KeyDecryptable;
use bitwarden_vault::{Cipher, CipherView, Collection, Folder, FolderView};
use bitwarden_vault::{Cipher, Collection, Folder, FolderView};

use crate::{
csv::export_csv,
Expand All @@ -22,8 +22,10 @@ pub(crate) fn export_vault(
let folders: Vec<FolderView> = folders.decrypt_with_key(key)?;
let folders: Vec<crate::Folder> = folders.into_iter().flat_map(|f| f.try_into()).collect();

let ciphers: Vec<CipherView> = ciphers.decrypt_with_key(key)?;
let ciphers: Vec<crate::Cipher> = ciphers.into_iter().flat_map(|c| c.try_into()).collect();
let ciphers: Vec<crate::Cipher> = ciphers
.into_iter()
.flat_map(|c| crate::Cipher::from_cipher(&enc, c))
.collect();

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

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L25-L28

Added lines #L25 - L28 were not covered by tests

match format {
ExportFormat::Csv => Ok(export_csv(folders, ciphers)?),
Expand Down Expand Up @@ -51,10 +53,11 @@ pub(crate) fn export_cxf(
ciphers: Vec<Cipher>,
) -> Result<String, ExportError> {
let enc = client.internal.get_encryption_settings()?;

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

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L50-L55

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

let ciphers: Vec<CipherView> = ciphers.decrypt_with_key(key)?;
let ciphers: Vec<crate::Cipher> = ciphers.into_iter().flat_map(|c| c.try_into()).collect();
let ciphers: Vec<crate::Cipher> = ciphers
.into_iter()
.flat_map(|c| crate::Cipher::from_cipher(&enc, c))
.collect();

Ok(build_cxf(account, ciphers)?)
}

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

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-exporters/src/export.rs#L57-L63

Added lines #L57 - L63 were not covered by tests
2 changes: 2 additions & 0 deletions crates/bitwarden-exporters/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ mod tests {
r#match: None,
}],
totp: Some("ABC".to_string()),
fido2_credentials: None,
})),

favorite: true,
Expand Down Expand Up @@ -705,6 +706,7 @@ mod tests {
r#match: None,
}],
totp: Some("ABC".to_string()),
fido2_credentials: None,
})),

favorite: true,
Expand Down
19 changes: 19 additions & 0 deletions crates/bitwarden-exporters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ pub struct Login {
pub password: Option<String>,
pub login_uris: Vec<LoginUri>,
pub totp: Option<String>,

pub fido2_credentials: Option<Vec<Fido2Credential>>,
}

#[derive(Clone)]
Expand All @@ -105,6 +107,23 @@ pub struct LoginUri {
pub r#match: Option<u8>,
}

#[derive(Clone)]
pub struct Fido2Credential {
pub credential_id: String,
pub key_type: String,
pub key_algorithm: String,
pub key_curve: String,
pub key_value: String,
pub rp_id: String,
pub user_handle: Option<String>,
pub user_name: Option<String>,
pub counter: u32,
pub rp_name: Option<String>,
pub user_display_name: Option<String>,
pub discoverable: String,
pub creation_date: DateTime<Utc>,
}

#[derive(Clone)]
pub struct Card {
pub cardholder_name: Option<String>,
Expand Down
Loading

0 comments on commit f3f3e6d

Please sign in to comment.