Skip to main content

bitwarden_auth/login/models/
webauthn_prf_user_decryption_option.rs

1use bitwarden_crypto::{EncString, UnsignedSharedKey};
2use serde::{Deserialize, Serialize};
3
4use crate::login::api::response::WebAuthnPrfUserDecryptionOptionApiResponse;
5
6/// SDK domain model for WebAuthn PRF user decryption option.
7#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
8#[serde(rename_all = "camelCase")]
9#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
10#[bitwarden_ffi::wasm_record]
11pub struct WebAuthnPrfUserDecryptionOption {
12    /// PRF key encrypted private key
13    pub encrypted_private_key: EncString,
14
15    /// Public Key encrypted user key
16    pub encrypted_user_key: UnsignedSharedKey,
17
18    /// Credential ID for this WebAuthn PRF credential.
19    /// TODO: PM-32163 - can remove `Option<T>` after 3 releases from server v2026.1.1
20    pub credential_id: Option<String>,
21
22    /// Transport methods available for this credential (e.g., "usb", "nfc", "ble", "internal",
23    /// "hybrid").
24    /// TODO: PM-32163 - can remove `Option<T>` after 3 releases from server v2026.1.1
25    pub transports: Option<Vec<String>>,
26}
27
28impl From<WebAuthnPrfUserDecryptionOptionApiResponse> for WebAuthnPrfUserDecryptionOption {
29    fn from(api: WebAuthnPrfUserDecryptionOptionApiResponse) -> Self {
30        Self {
31            encrypted_private_key: api.encrypted_private_key,
32            encrypted_user_key: api.encrypted_user_key,
33            credential_id: api.credential_id,
34            transports: api.transports,
35        }
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn test_webauthn_prf_conversion() {
45        let api = WebAuthnPrfUserDecryptionOptionApiResponse {
46            encrypted_private_key: "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=".parse().unwrap(),
47            encrypted_user_key: "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==".parse().unwrap(),
48            credential_id: None,
49            transports: None,
50        };
51
52        let domain: WebAuthnPrfUserDecryptionOption = api.clone().into();
53
54        assert_eq!(domain.encrypted_private_key, api.encrypted_private_key);
55        assert_eq!(domain.encrypted_user_key, api.encrypted_user_key);
56        assert_eq!(domain.credential_id, None);
57        assert_eq!(domain.transports, None);
58    }
59
60    #[test]
61    fn test_webauthn_prf_conversion_with_optional_fields() {
62        let api = WebAuthnPrfUserDecryptionOptionApiResponse {
63            encrypted_private_key: "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=".parse().unwrap(),
64            encrypted_user_key: "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==".parse().unwrap(),
65            credential_id: Some("test-credential-id".to_string()),
66            transports: Some(vec!["usb".to_string(), "nfc".to_string()]),
67        };
68
69        let domain: WebAuthnPrfUserDecryptionOption = api.clone().into();
70
71        assert_eq!(domain.encrypted_private_key, api.encrypted_private_key);
72        assert_eq!(domain.encrypted_user_key, api.encrypted_user_key);
73        assert_eq!(domain.credential_id, Some("test-credential-id".to_string()));
74        assert_eq!(
75            domain.transports,
76            Some(vec!["usb".to_string(), "nfc".to_string()])
77        );
78    }
79}