bitwarden_core/key_management/
webauthn_prf.rs1use bitwarden_api_api::models::WebAuthnPrfDecryptionOption;
7use bitwarden_crypto::{EncString, UnsignedSharedKey};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::{MissingFieldError, require};
12
13#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
17#[bitwarden_ffi::wasm_record]
18pub struct WebAuthnPrfUnlockOption {
19 pub encrypted_private_key: EncString,
22 pub encrypted_user_key: UnsignedSharedKey,
24 pub credential_id: Option<String>,
26 pub transports: Option<Vec<String>>,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
35#[bitwarden_ffi::wasm_record]
36pub struct WebAuthnPrfUnlockData {
37 pub options: Vec<WebAuthnPrfUnlockOption>,
39}
40
41#[cfg(feature = "wasm")]
42impl TryFrom<wasm_bindgen::JsValue> for WebAuthnPrfUnlockData {
43 type Error = serde_wasm_bindgen::Error;
44
45 fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
46 serde_wasm_bindgen::from_value(value)
47 }
48}
49
50impl TryFrom<&WebAuthnPrfDecryptionOption> for WebAuthnPrfUnlockOption {
51 type Error = WebAuthnPrfError;
52
53 fn try_from(response: &WebAuthnPrfDecryptionOption) -> Result<Self, Self::Error> {
54 let encrypted_private_key = require!(&response.encrypted_private_key)
55 .parse()
56 .map_err(|_| WebAuthnPrfError::ResponseModelMalformed)?;
57 let encrypted_user_key = require!(&response.encrypted_user_key)
58 .parse()
59 .map_err(|_| WebAuthnPrfError::ResponseModelMalformed)?;
60
61 Ok(WebAuthnPrfUnlockOption {
62 encrypted_private_key,
63 encrypted_user_key,
64 credential_id: response.credential_id.clone(),
65 transports: response.transports.clone(),
66 })
67 }
68}
69
70#[derive(Debug, Error)]
72pub enum WebAuthnPrfError {
73 #[error("Response model malformed")]
75 ResponseModelMalformed,
76 #[error(transparent)]
78 MissingField(#[from] MissingFieldError),
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 const ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
86 const ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
87
88 fn build_response_model() -> WebAuthnPrfDecryptionOption {
89 WebAuthnPrfDecryptionOption {
90 encrypted_private_key: Some(ENCRYPTED_PRIVATE_KEY.to_string()),
91 encrypted_user_key: Some(ENCRYPTED_USER_KEY.to_string()),
92 credential_id: None,
93 transports: None,
94 }
95 }
96
97 #[test]
98 fn test_from_response_model() {
99 let response = build_response_model();
100
101 let option = WebAuthnPrfUnlockOption::try_from(&response).unwrap();
102
103 assert_eq!(
104 option.encrypted_private_key,
105 ENCRYPTED_PRIVATE_KEY.parse().unwrap()
106 );
107 assert_eq!(
108 option.encrypted_user_key.to_string(),
109 ENCRYPTED_USER_KEY.to_string()
110 );
111 assert_eq!(option.credential_id, None);
112 assert_eq!(option.transports, None);
113 }
114
115 #[test]
116 fn test_from_response_model_with_optional_fields() {
117 let mut response = build_response_model();
118 response.credential_id = Some("test-credential-id".to_string());
119 response.transports = Some(vec!["usb".to_string(), "nfc".to_string()]);
120
121 let option = WebAuthnPrfUnlockOption::try_from(&response).unwrap();
122
123 assert_eq!(option.credential_id, Some("test-credential-id".to_string()));
124 assert_eq!(
125 option.transports,
126 Some(vec!["usb".to_string(), "nfc".to_string()])
127 );
128 }
129
130 #[test]
131 fn test_from_response_model_missing_encrypted_private_key() {
132 let mut response = build_response_model();
133 response.encrypted_private_key = None;
134
135 assert!(matches!(
136 WebAuthnPrfUnlockOption::try_from(&response),
137 Err(WebAuthnPrfError::MissingField(_))
138 ));
139 }
140
141 #[test]
142 fn test_from_response_model_missing_encrypted_user_key() {
143 let mut response = build_response_model();
144 response.encrypted_user_key = None;
145
146 assert!(matches!(
147 WebAuthnPrfUnlockOption::try_from(&response),
148 Err(WebAuthnPrfError::MissingField(_))
149 ));
150 }
151
152 #[test]
153 fn test_from_response_model_unparseable_encrypted_user_key() {
154 let mut response = build_response_model();
155 response.encrypted_user_key = Some("not an unsigned shared key".to_string());
156
157 assert!(matches!(
158 WebAuthnPrfUnlockOption::try_from(&response),
159 Err(WebAuthnPrfError::ResponseModelMalformed)
160 ));
161 }
162
163 #[test]
164 fn test_unlock_data_serde_round_trip() {
165 let data = WebAuthnPrfUnlockData {
166 options: vec![WebAuthnPrfUnlockOption::try_from(&build_response_model()).unwrap()],
167 };
168
169 let serialized = serde_json::to_string(&data).unwrap();
170 let deserialized: WebAuthnPrfUnlockData = serde_json::from_str(&serialized).unwrap();
171
172 assert_eq!(data, deserialized);
173 }
174}