bitwarden_exporters/cxf/
login.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
7use bitwarden_core::MissingFieldError;
8use bitwarden_fido::{string_to_guid_bytes, InvalidGuid};
9use chrono::{DateTime, Utc};
10use credential_exchange_format::{BasicAuthCredential, CredentialScope, PasskeyCredential};
11use thiserror::Error;
12
13use crate::{Fido2Credential, Login, LoginUri};
14
15pub(super) fn to_login(
16 creation_date: DateTime<Utc>,
17 basic_auth: Option<&BasicAuthCredential>,
18 passkey: Option<&PasskeyCredential>,
19 scope: Option<CredentialScope>,
20) -> Login {
21 let login = Login {
22 username: basic_auth.and_then(|v| v.username.clone().map(|v| v.into())),
23 password: basic_auth.and_then(|v| v.password.clone().map(|u| u.into())),
24 login_uris: scope
25 .map(|v| {
26 v.urls
27 .iter()
28 .map(|u| LoginUri {
29 uri: Some(u.clone()),
30 r#match: None,
31 })
32 .collect()
33 })
34 .unwrap_or_default(),
35 totp: None,
36 fido2_credentials: passkey.map(|p| {
37 vec![Fido2Credential {
38 credential_id: format!("b64.{}", p.credential_id),
39 key_type: "public-key".to_string(),
40 key_algorithm: "ECDSA".to_string(),
41 key_curve: "P-256".to_string(),
42 key_value: URL_SAFE_NO_PAD.encode(&p.key),
43 rp_id: p.rp_id.clone(),
44 user_handle: Some(p.user_handle.to_string()),
45 user_name: Some(p.username.clone()),
46 counter: 0,
47 rp_name: Some(p.rp_id.clone()),
48 user_display_name: Some(p.user_display_name.clone()),
49 discoverable: "true".to_string(),
50 creation_date,
51 }]
52 }),
53 };
54 login
55}
56
57impl From<Login> for BasicAuthCredential {
58 fn from(login: Login) -> Self {
59 BasicAuthCredential {
60 username: login.username.map(|v| v.into()),
61 password: login.password.map(|v| v.into()),
62 }
63 }
64}
65
66impl From<Login> for CredentialScope {
67 fn from(login: Login) -> Self {
68 CredentialScope {
69 urls: login.login_uris.into_iter().filter_map(|u| u.uri).collect(),
70 android_apps: vec![],
71 }
72 }
73}
74
75#[derive(Error, Debug)]
76pub enum PasskeyError {
77 #[error("Counter is not zero")]
78 CounterNotZero,
79 #[error(transparent)]
80 InvalidGuid(InvalidGuid),
81 #[error(transparent)]
82 MissingField(MissingFieldError),
83 #[error(transparent)]
84 InvalidBase64(#[from] base64::DecodeError),
85}
86
87impl TryFrom<Fido2Credential> for PasskeyCredential {
88 type Error = PasskeyError;
89
90 fn try_from(value: Fido2Credential) -> Result<Self, Self::Error> {
91 if value.counter > 0 {
92 return Err(PasskeyError::CounterNotZero);
93 }
94
95 Ok(PasskeyCredential {
96 credential_id: string_to_guid_bytes(&value.credential_id)
97 .map_err(PasskeyError::InvalidGuid)?
98 .into(),
99 rp_id: value.rp_id,
100 username: value.user_name.unwrap_or_default(),
101 user_display_name: value.user_display_name.unwrap_or_default(),
102 user_handle: value
103 .user_handle
104 .map(|v| URL_SAFE_NO_PAD.decode(v))
105 .transpose()?
106 .map(|v| v.into())
107 .ok_or(PasskeyError::MissingField(MissingFieldError("user_handle")))?,
108 key: URL_SAFE_NO_PAD.decode(value.key_value)?.into(),
109 fido2_extensions: None,
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::LoginUri;
118
119 #[test]
120 fn test_basic_auth() {
121 let login = Login {
122 username: Some("[email protected]".to_string()),
123 password: Some("asdfasdfasdf".to_string()),
124 login_uris: vec![LoginUri {
125 uri: Some("https://vault.bitwarden.com".to_string()),
126 r#match: None,
127 }],
128 totp: None,
129 fido2_credentials: None,
130 };
131
132 let basic_auth: BasicAuthCredential = login.into();
133
134 let username = basic_auth.username.as_ref().unwrap();
135 assert_eq!(username.value.0, "[email protected]");
136 assert!(username.label.is_none());
137
138 let password = basic_auth.password.as_ref().unwrap();
139 assert_eq!(password.value.0, "asdfasdfasdf");
140 assert!(password.label.is_none());
141 }
142
143 #[test]
144 fn test_credential_scope() {
145 let login = Login {
146 username: None,
147 password: None,
148 login_uris: vec![LoginUri {
149 uri: Some("https://vault.bitwarden.com".to_string()),
150 r#match: None,
151 }],
152 totp: None,
153 fido2_credentials: None,
154 };
155
156 let scope: CredentialScope = login.into();
157
158 assert_eq!(scope.urls, vec!["https://vault.bitwarden.com".to_string()]);
159 }
160
161 #[test]
162 fn test_passkey() {
163 let credential = Fido2Credential {
164 credential_id: "e8d88789-e916-e196-3cbd-81dafae71bbc".to_string(),
165 key_type: "public-key".to_string(),
166 key_algorithm: "ECDSA".to_string(),
167 key_curve: "P-256".to_string(),
168 key_value: "AAECAwQFBg".to_string(),
169 rp_id: "123".to_string(),
170 user_handle: Some("AAECAwQFBg".to_string()),
171 user_name: None,
172 counter: 0,
173 rp_name: None,
174 user_display_name: None,
175 discoverable: "true".to_string(),
176 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
177 };
178
179 let passkey: PasskeyCredential = credential.try_into().unwrap();
180
181 assert_eq!(passkey.credential_id.to_string(), "6NiHiekW4ZY8vYHa-ucbvA");
182 assert_eq!(passkey.rp_id, "123");
183 assert_eq!(passkey.username, "");
184 assert_eq!(passkey.user_display_name, "");
185 assert_eq!(String::from(passkey.user_handle.clone()), "AAECAwQFBg");
186 assert_eq!(String::from(passkey.key.clone()), "AAECAwQFBg");
187 assert!(passkey.fido2_extensions.is_none());
188 }
189}