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