From aaa6466c60f50d52781baa0be1fc301e23b125e3 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 14 Mar 2024 12:31:52 +0100 Subject: [PATCH] [PM-6761] Add fido credentials to data model (#657) Add `Fido2Credentials` to the `Login` struct to support storing passkeys. Currently we don't decrypt passkeys but rather pass them along encrypted to ensure we can re-encrypt the login without data loss. --- crates/bitwarden/src/tool/exporters/mod.rs | 1 + crates/bitwarden/src/vault/cipher/cipher.rs | 1 + crates/bitwarden/src/vault/cipher/login.rs | 56 +++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/crates/bitwarden/src/tool/exporters/mod.rs b/crates/bitwarden/src/tool/exporters/mod.rs index 9e9e99ed5..45bdfd3fd 100644 --- a/crates/bitwarden/src/tool/exporters/mod.rs +++ b/crates/bitwarden/src/tool/exporters/mod.rs @@ -240,6 +240,7 @@ mod tests { uris: None, totp: None, autofill_on_page_load: None, + fido2_credentials: None, }), id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(), organization_id: None, diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 24207251f..47b58694d 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -433,6 +433,7 @@ mod tests { uris: None, totp: None, autofill_on_page_load: None, + fido2_credentials: None, }), id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(), organization_id: None, diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index 5e2156a83..5d00c41e1 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -38,6 +38,25 @@ pub struct LoginUriView { pub r#match: Option, } +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct Fido2Credential { + pub credential_id: EncString, + pub key_type: EncString, + pub key_algorithm: EncString, + pub key_curve: EncString, + pub key_value: EncString, + pub rp_id: EncString, + pub user_handle: Option, + pub user_name: Option, + pub counter: EncString, + pub rp_name: Option, + pub user_display_name: Option, + pub discoverable: EncString, + pub creation_date: DateTime, +} + #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] @@ -49,6 +68,8 @@ pub struct Login { pub uris: Option>, pub totp: Option, pub autofill_on_page_load: Option, + + pub fido2_credentials: Option>, } #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -62,6 +83,9 @@ pub struct LoginView { pub uris: Option>, pub totp: Option, pub autofill_on_page_load: Option, + + // TODO: Remove this once the SDK supports state + pub fido2_credentials: Option>, } impl KeyEncryptable for LoginUriView { @@ -82,6 +106,7 @@ impl KeyEncryptable for LoginView { uris: self.uris.encrypt_with_key(key)?, totp: self.totp.encrypt_with_key(key)?, autofill_on_page_load: self.autofill_on_page_load, + fido2_credentials: self.fido2_credentials, }) } } @@ -104,6 +129,7 @@ impl KeyDecryptable for Login { uris: self.uris.decrypt_with_key(key).ok().flatten(), totp: self.totp.decrypt_with_key(key).ok().flatten(), autofill_on_page_load: self.autofill_on_page_load, + fido2_credentials: self.fido2_credentials.clone(), }) } } @@ -125,6 +151,10 @@ impl TryFrom for Login { .transpose()?, totp: EncString::try_from_optional(login.totp)?, autofill_on_page_load: login.autofill_on_page_load, + fido2_credentials: login + .fido2_credentials + .map(|v| v.into_iter().map(|c| c.try_into()).collect()) + .transpose()?, }) } } @@ -152,3 +182,29 @@ impl From for UriMatchType { } } } + +impl TryFrom for Fido2Credential { + type Error = Error; + + fn try_from(value: bitwarden_api_api::models::CipherFido2CredentialModel) -> Result { + Ok(Self { + credential_id: value.credential_id.ok_or(Error::MissingFields)?.parse()?, + key_type: value.key_type.ok_or(Error::MissingFields)?.parse()?, + key_algorithm: value.key_algorithm.ok_or(Error::MissingFields)?.parse()?, + key_curve: value.key_curve.ok_or(Error::MissingFields)?.parse()?, + key_value: value.key_value.ok_or(Error::MissingFields)?.parse()?, + rp_id: value.rp_id.ok_or(Error::MissingFields)?.parse()?, + user_handle: EncString::try_from_optional(value.user_handle) + .ok() + .flatten(), + user_name: EncString::try_from_optional(value.user_name).ok().flatten(), + counter: value.counter.ok_or(Error::MissingFields)?.parse()?, + rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(), + user_display_name: EncString::try_from_optional(value.user_display_name) + .ok() + .flatten(), + discoverable: value.discoverable.ok_or(Error::MissingFields)?.parse()?, + creation_date: value.creation_date.parse().unwrap(), + }) + } +}