Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion crates/bitwarden-exporters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ wasm = [
] # WebAssembly bindings

[dependencies]
base64 = ">=0.22.1, <0.23"
bitwarden-collections = { workspace = true }
bitwarden-core = { workspace = true }
bitwarden-crypto = { workspace = true }
bitwarden-encoding = { workspace = true }
bitwarden-error = { workspace = true }
bitwarden-fido = { workspace = true }
bitwarden-ssh = { workspace = true }
Expand Down
85 changes: 23 additions & 62 deletions crates/bitwarden-exporters/src/cxf/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,8 @@ struct GroupedCredentials {

#[cfg(test)]
mod tests {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::{Duration, Month};
use credential_exchange_format::{CreditCardCredential, EditableFieldYearMonth};
use credential_exchange_format::{B64Url, CreditCardCredential, EditableFieldYearMonth};

use super::*;

Expand Down Expand Up @@ -320,35 +319,22 @@ mod tests {
#[test]
fn test_parse_passkey() {
let item = Item {
id: URL_SAFE_NO_PAD
.decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
.unwrap()
.as_slice()
.into(),
id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
.unwrap(),
creation_at: Some(1732181986),
modified_at: Some(1732182026),
title: "example.com".to_string(),
subtitle: None,
favorite: None,
credentials: vec![Credential::Passkey(Box::new(PasskeyCredential {
credential_id: URL_SAFE_NO_PAD
.decode("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap()
.as_slice()
.into(),
credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap(),
rp_id: "example.com".to_string(),
username: "pj-fry".to_string(),
user_display_name: "Philip J. Fry".to_string(),
user_handle: URL_SAFE_NO_PAD
.decode("YWxleCBtdWxsZXI")
.unwrap()
.as_slice()
.into(),
key: URL_SAFE_NO_PAD
.decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap()
.as_slice()
.into(),
user_handle: B64Url::try_from("YWxleCBtdWxsZXI").unwrap(),
key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap(),
fido2_extensions: None,
}))],
tags: None,
Expand Down Expand Up @@ -407,11 +393,8 @@ mod tests {
use credential_exchange_format::{BasicAuthCredential, CredentialScope};

let item = Item {
id: URL_SAFE_NO_PAD
.decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
.unwrap()
.as_slice()
.into(),
id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
.unwrap(),
creation_at: Some(1732181986),
modified_at: Some(1732182026),
title: "Combined Login".to_string(),
Expand All @@ -423,24 +406,15 @@ mod tests {
password: Some("basic_password".to_string().into()),
})),
Credential::Passkey(Box::new(PasskeyCredential {
credential_id: URL_SAFE_NO_PAD
.decode("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap()
.as_slice()
.into(),
credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap(),
rp_id: "passkey-domain.com".to_string(),
username: "passkey_username".to_string(),
user_display_name: "Passkey User".to_string(),
user_handle: URL_SAFE_NO_PAD
.decode("YWxleCBtdWxsZXI")
.unwrap()
.as_slice()
.into(),
key: URL_SAFE_NO_PAD
.decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap()
.as_slice()
.into(),
user_handle: B64Url::try_from("YWxleCBtdWxsZXI")
.unwrap(),
key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap(),
fido2_extensions: None,
}))
],
Expand Down Expand Up @@ -479,35 +453,22 @@ mod tests {
#[test]
fn test_passkey_with_empty_username() {
let item = Item {
id: URL_SAFE_NO_PAD
.decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
.unwrap()
.as_slice()
.into(),
id: B64Url::try_from("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF").unwrap(),
creation_at: Some(1732181986),
modified_at: Some(1732182026),
title: "Empty Username Passkey".to_string(),
subtitle: None,
favorite: None,
credentials: vec![Credential::Passkey(Box::new(PasskeyCredential {
credential_id: URL_SAFE_NO_PAD
.decode("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap()
.as_slice()
.into(),
credential_id: B64Url::try_from("6NiHiekW4ZY8vYHa-ucbvA")
.unwrap(),
rp_id: "example.com".to_string(),
username: "".to_string(), // Empty username
user_display_name: "User Display".to_string(),
user_handle: URL_SAFE_NO_PAD
.decode("YWxleCBtdWxsZXI")
.unwrap()
.as_slice()
.into(),
key: URL_SAFE_NO_PAD
.decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap()
.as_slice()
.into(),
user_handle: B64Url::try_from("YWxleCBtdWxsZXI")
.unwrap(),
key: B64Url::try_from("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
.unwrap(),
fido2_extensions: None,
}))],
tags: None,
Expand Down
19 changes: 9 additions & 10 deletions crates/bitwarden-exporters/src/cxf/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
//! Handles conversion between internal [Login] and credential exchange [BasicAuthCredential] and
//! [PasskeyCredential].

use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bitwarden_core::MissingFieldError;
use bitwarden_fido::{string_to_guid_bytes, InvalidGuid};
use bitwarden_vault::{FieldType, Totp, TotpAlgorithm};
use chrono::{DateTime, Utc};
use credential_exchange_format::{
AndroidAppIdCredential, BasicAuthCredential, CredentialScope, OTPHashAlgorithm,
PasskeyCredential, TotpCredential,
AndroidAppIdCredential, B64Url, BasicAuthCredential, CredentialScope, NotB64UrlEncoded,
OTPHashAlgorithm, PasskeyCredential, TotpCredential,
};
use thiserror::Error;

Expand Down Expand Up @@ -82,7 +81,7 @@ pub(super) fn to_login(
key_type: "public-key".to_string(),
key_algorithm: "ECDSA".to_string(),
key_curve: "P-256".to_string(),
key_value: URL_SAFE_NO_PAD.encode(&p.key),
key_value: p.key.to_string(),
rp_id: p.rp_id.clone(),
user_handle: Some(p.user_handle.to_string()),
user_name: Some(p.username.clone()),
Expand Down Expand Up @@ -191,8 +190,8 @@ pub enum PasskeyError {
InvalidGuid(InvalidGuid),
#[error(transparent)]
MissingField(MissingFieldError),
#[error(transparent)]
InvalidBase64(#[from] base64::DecodeError),
#[error("Data isn't base64url encoded")]
InvalidBase64(NotB64UrlEncoded),
}

impl TryFrom<Fido2Credential> for PasskeyCredential {
Expand All @@ -212,11 +211,11 @@ impl TryFrom<Fido2Credential> for PasskeyCredential {
user_display_name: value.user_display_name.unwrap_or_default(),
user_handle: value
.user_handle
.map(|v| URL_SAFE_NO_PAD.decode(v))
.transpose()?
.map(|v| v.into())
.map(|v| B64Url::try_from(v.as_str()))
.transpose()
.map_err(PasskeyError::InvalidBase64)?
.ok_or(PasskeyError::MissingField(MissingFieldError("user_handle")))?,
key: URL_SAFE_NO_PAD.decode(value.key_value)?.into(),
key: B64Url::try_from(value.key_value.as_str()).map_err(PasskeyError::InvalidBase64)?,
fido2_extensions: None,
})
}
Expand Down
6 changes: 3 additions & 3 deletions crates/bitwarden-exporters/src/encrypted_json.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{generate_random_bytes, Kdf, KeyEncryptable, PinKey};
use bitwarden_encoding::B64;
use serde::Serialize;
use thiserror::Error;
use uuid::Uuid;
Expand Down Expand Up @@ -44,15 +44,15 @@ pub(crate) fn export_encrypted_json(
};

let salt = generate_random_bytes::<[u8; 16]>();
let salt = STANDARD.encode(salt);
let salt = B64::from(salt.as_slice());
let key = PinKey::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,
salt: salt.to_string(),
kdf_type,
kdf_iterations,
kdf_memory,
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-fido/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ uniffi = ["dep:uniffi", "bitwarden-core/uniffi", "bitwarden-vault/uniffi"]

[dependencies]
async-trait = { workspace = true }
base64 = ">=0.22.1, <0.23"
bitwarden-core = { workspace = true }
bitwarden-crypto = { workspace = true }
bitwarden-encoding = { workspace = true }
bitwarden-vault = { workspace = true }
chrono = { workspace = true }
coset = ">=0.3.7, <0.4"
Expand Down
31 changes: 13 additions & 18 deletions crates/bitwarden-fido/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![doc = include_str!("../README.md")]

use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bitwarden_core::key_management::KeyIds;
use bitwarden_crypto::KeyStoreContext;
use bitwarden_encoding::{B64Url, NotB64UrlEncoded};
use bitwarden_vault::{
CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView,
};
Expand Down Expand Up @@ -78,7 +78,7 @@ impl CipherViewContainer {
#[derive(Debug, Error)]
pub enum Fido2Error {
#[error(transparent)]
DecodeError(#[from] base64::DecodeError),
DecodeError(#[from] NotB64UrlEncoded),

#[error(transparent)]
UnknownEnum(#[from] UnknownEnum),
Expand Down Expand Up @@ -115,19 +115,16 @@ fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result<Passk
.parse()
.map_err(|_| Fido2Error::InvalidCounter)?;
let counter = (counter != 0).then_some(counter);
let key_value = URL_SAFE_NO_PAD.decode(value.key_value)?;
let user_handle = value
.user_handle
.map(|u| URL_SAFE_NO_PAD.decode(u))
.transpose()?;
let key_value = B64Url::try_from(value.key_value)?;
let user_handle = value.user_handle.map(B64Url::try_from).transpose()?;

let key = pkcs8_to_cose_key(&key_value)?;
let key = pkcs8_to_cose_key(key_value.as_bytes())?;

Ok(Passkey {
key,
credential_id: string_to_guid_bytes(&value.credential_id)?.into(),
rp_id: value.rp_id.clone(),
user_handle: user_handle.map(|u| u.into()),
user_handle: user_handle.map(|u| u.as_bytes().to_vec().into()),
counter,
})
}
Expand All @@ -149,8 +146,8 @@ pub fn fill_with_credential(
let cred_id: Vec<u8> = value.credential_id.into();
let user_handle = value
.user_handle
.map(|u| URL_SAFE_NO_PAD.encode(u.to_vec()));
let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?);
.map(|u| B64Url::from(u.to_vec()).to_string());
let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();

Ok(Fido2CredentialFullView {
credential_id: guid_bytes_to_string(&cred_id)?,
Expand All @@ -175,7 +172,7 @@ pub(crate) fn try_from_credential_new_view(
rp: &passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
) -> Result<Fido2CredentialNewView, InvalidInputLength> {
let cred_id: Vec<u8> = vec![0; 16];
let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec());
let user_handle = B64Url::from(user.id.to_vec()).to_string();

Ok(Fido2CredentialNewView {
// TODO: Why do we have a credential id here?
Expand All @@ -201,8 +198,8 @@ pub(crate) fn try_from_credential_full(
options: passkey::types::ctap2::get_assertion::Options,
) -> Result<Fido2CredentialFullView, FillCredentialError> {
let cred_id: Vec<u8> = value.credential_id.into();
let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?);
let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec());
let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();
let user_handle = B64Url::from(user.id.to_vec()).to_string();

Ok(Fido2CredentialFullView {
credential_id: guid_bytes_to_string(&cred_id)?,
Expand Down Expand Up @@ -243,10 +240,8 @@ pub struct InvalidGuid;
#[allow(missing_docs)]
pub fn string_to_guid_bytes(source: &str) -> Result<Vec<u8>, InvalidGuid> {
if source.starts_with("b64.") {
let bytes = URL_SAFE_NO_PAD
.decode(source.trim_start_matches("b64."))
.map_err(|_| InvalidGuid)?;
Ok(bytes)
let bytes = B64Url::try_from(source.trim_start_matches("b64.")).map_err(|_| InvalidGuid)?;
Ok(bytes.as_bytes().to_vec())
} else {
let Ok(uuid) = uuid::Uuid::try_parse(source) else {
return Err(InvalidGuid);
Expand Down
Loading
Loading