bitwarden_vault/cipher/
login.rs

1use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
2use bitwarden_core::{
3    key_management::{KeyIds, SymmetricKeyId},
4    require,
5};
6use bitwarden_crypto::{
7    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
8    PrimitiveEncryptable,
9};
10use bitwarden_encoding::B64;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14#[cfg(feature = "wasm")]
15use tsify::Tsify;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::wasm_bindgen;
18
19use super::cipher::CipherKind;
20use crate::{Cipher, VaultParseError, cipher::cipher::CopyableCipherFields};
21
22#[allow(missing_docs)]
23#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
24#[repr(u8)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
27#[cfg_attr(feature = "wasm", wasm_bindgen)]
28pub enum UriMatchType {
29    Domain = 0,
30    Host = 1,
31    StartsWith = 2,
32    Exact = 3,
33    RegularExpression = 4,
34    Never = 5,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41pub struct LoginUri {
42    pub uri: Option<EncString>,
43    pub r#match: Option<UriMatchType>,
44    pub uri_checksum: Option<EncString>,
45}
46
47#[allow(missing_docs)]
48#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
51#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
52pub struct LoginUriView {
53    pub uri: Option<String>,
54    pub r#match: Option<UriMatchType>,
55    pub uri_checksum: Option<String>,
56}
57
58impl LoginUriView {
59    pub(crate) fn is_checksum_valid(&self) -> bool {
60        let Some(uri) = &self.uri else {
61            return false;
62        };
63        let Some(cs) = &self.uri_checksum else {
64            return false;
65        };
66        let Ok(cs) = B64::try_from(cs.as_str()) else {
67            return false;
68        };
69
70        use sha2::Digest;
71        let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
72
73        uri_hash.as_slice() == cs.as_bytes()
74    }
75
76    pub(crate) fn generate_checksum(&mut self) {
77        if let Some(uri) = &self.uri {
78            use sha2::Digest;
79            let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
80            let uri_hash = B64::from(uri_hash.as_slice()).to_string();
81            self.uri_checksum = Some(uri_hash);
82        }
83    }
84}
85
86#[allow(missing_docs)]
87#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
91pub struct Fido2Credential {
92    pub credential_id: EncString,
93    pub key_type: EncString,
94    pub key_algorithm: EncString,
95    pub key_curve: EncString,
96    pub key_value: EncString,
97    pub rp_id: EncString,
98    pub user_handle: Option<EncString>,
99    pub user_name: Option<EncString>,
100    pub counter: EncString,
101    pub rp_name: Option<EncString>,
102    pub user_display_name: Option<EncString>,
103    pub discoverable: EncString,
104    pub creation_date: DateTime<Utc>,
105}
106
107#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
110#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
111pub struct Fido2CredentialListView {
112    pub credential_id: String,
113    pub rp_id: String,
114    pub user_handle: Option<String>,
115    pub user_name: Option<String>,
116    pub user_display_name: Option<String>,
117    pub counter: String,
118}
119
120#[allow(missing_docs)]
121#[derive(Serialize, Deserialize, Debug, Clone)]
122#[serde(rename_all = "camelCase", deny_unknown_fields)]
123#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
124#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
125pub struct Fido2CredentialView {
126    pub credential_id: String,
127    pub key_type: String,
128    pub key_algorithm: String,
129    pub key_curve: String,
130    // This value doesn't need to be returned to the client
131    // so we keep it encrypted until we need it
132    pub key_value: EncString,
133    pub rp_id: String,
134    pub user_handle: Option<String>,
135    pub user_name: Option<String>,
136    pub counter: String,
137    pub rp_name: Option<String>,
138    pub user_display_name: Option<String>,
139    pub discoverable: String,
140    pub creation_date: DateTime<Utc>,
141}
142
143// This is mostly a copy of the Fido2CredentialView, but with the key exposed
144// Only meant to be used internally and not exposed to the outside world
145#[allow(missing_docs)]
146#[derive(Serialize, Deserialize, Debug, Clone)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
149pub struct Fido2CredentialFullView {
150    pub credential_id: String,
151    pub key_type: String,
152    pub key_algorithm: String,
153    pub key_curve: String,
154    pub key_value: String,
155    pub rp_id: String,
156    pub user_handle: Option<String>,
157    pub user_name: Option<String>,
158    pub counter: String,
159    pub rp_name: Option<String>,
160    pub user_display_name: Option<String>,
161    pub discoverable: String,
162    pub creation_date: DateTime<Utc>,
163}
164
165// This is mostly a copy of the Fido2CredentialView, meant to be exposed to the clients
166// to let them select where to store the new credential. Note that it doesn't contain
167// the encrypted key as that is only filled when the cipher is selected
168#[allow(missing_docs)]
169#[derive(Serialize, Deserialize, Debug, Clone)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
172#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
173pub struct Fido2CredentialNewView {
174    pub credential_id: String,
175    pub key_type: String,
176    pub key_algorithm: String,
177    pub key_curve: String,
178    pub rp_id: String,
179    pub user_handle: Option<String>,
180    pub user_name: Option<String>,
181    pub counter: String,
182    pub rp_name: Option<String>,
183    pub user_display_name: Option<String>,
184    pub creation_date: DateTime<Utc>,
185}
186
187impl From<Fido2CredentialFullView> for Fido2CredentialNewView {
188    fn from(value: Fido2CredentialFullView) -> Self {
189        Fido2CredentialNewView {
190            credential_id: value.credential_id,
191            key_type: value.key_type,
192            key_algorithm: value.key_algorithm,
193            key_curve: value.key_curve,
194            rp_id: value.rp_id,
195            user_handle: value.user_handle,
196            user_name: value.user_name,
197            counter: value.counter,
198            rp_name: value.rp_name,
199            user_display_name: value.user_display_name,
200            creation_date: value.creation_date,
201        }
202    }
203}
204
205impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialFullView {
206    fn encrypt_composite(
207        &self,
208        ctx: &mut KeyStoreContext<KeyIds>,
209        key: SymmetricKeyId,
210    ) -> Result<Fido2Credential, CryptoError> {
211        Ok(Fido2Credential {
212            credential_id: self.credential_id.encrypt(ctx, key)?,
213            key_type: self.key_type.encrypt(ctx, key)?,
214            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
215            key_curve: self.key_curve.encrypt(ctx, key)?,
216            key_value: self.key_value.encrypt(ctx, key)?,
217            rp_id: self.rp_id.encrypt(ctx, key)?,
218            user_handle: self
219                .user_handle
220                .as_ref()
221                .map(|h| h.encrypt(ctx, key))
222                .transpose()?,
223            user_name: self.user_name.encrypt(ctx, key)?,
224            counter: self.counter.encrypt(ctx, key)?,
225            rp_name: self.rp_name.encrypt(ctx, key)?,
226            user_display_name: self.user_display_name.encrypt(ctx, key)?,
227            discoverable: self.discoverable.encrypt(ctx, key)?,
228            creation_date: self.creation_date,
229        })
230    }
231}
232
233impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2Credential {
234    fn decrypt(
235        &self,
236        ctx: &mut KeyStoreContext<KeyIds>,
237        key: SymmetricKeyId,
238    ) -> Result<Fido2CredentialFullView, CryptoError> {
239        Ok(Fido2CredentialFullView {
240            credential_id: self.credential_id.decrypt(ctx, key)?,
241            key_type: self.key_type.decrypt(ctx, key)?,
242            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
243            key_curve: self.key_curve.decrypt(ctx, key)?,
244            key_value: self.key_value.decrypt(ctx, key)?,
245            rp_id: self.rp_id.decrypt(ctx, key)?,
246            user_handle: self.user_handle.decrypt(ctx, key)?,
247            user_name: self.user_name.decrypt(ctx, key)?,
248            counter: self.counter.decrypt(ctx, key)?,
249            rp_name: self.rp_name.decrypt(ctx, key)?,
250            user_display_name: self.user_display_name.decrypt(ctx, key)?,
251            discoverable: self.discoverable.decrypt(ctx, key)?,
252            creation_date: self.creation_date,
253        })
254    }
255}
256
257impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2CredentialView {
258    fn decrypt(
259        &self,
260        ctx: &mut KeyStoreContext<KeyIds>,
261        key: SymmetricKeyId,
262    ) -> Result<Fido2CredentialFullView, CryptoError> {
263        Ok(Fido2CredentialFullView {
264            credential_id: self.credential_id.clone(),
265            key_type: self.key_type.clone(),
266            key_algorithm: self.key_algorithm.clone(),
267            key_curve: self.key_curve.clone(),
268            key_value: self.key_value.decrypt(ctx, key)?,
269            rp_id: self.rp_id.clone(),
270            user_handle: self.user_handle.clone(),
271            user_name: self.user_name.clone(),
272            counter: self.counter.clone(),
273            rp_name: self.rp_name.clone(),
274            user_display_name: self.user_display_name.clone(),
275            discoverable: self.discoverable.clone(),
276            creation_date: self.creation_date,
277        })
278    }
279}
280
281#[allow(missing_docs)]
282#[derive(Serialize, Deserialize, Debug, Clone)]
283#[serde(rename_all = "camelCase", deny_unknown_fields)]
284#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
285#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
286pub struct Login {
287    pub username: Option<EncString>,
288    pub password: Option<EncString>,
289    pub password_revision_date: Option<DateTime<Utc>>,
290
291    pub uris: Option<Vec<LoginUri>>,
292    pub totp: Option<EncString>,
293    pub autofill_on_page_load: Option<bool>,
294
295    pub fido2_credentials: Option<Vec<Fido2Credential>>,
296}
297
298#[allow(missing_docs)]
299#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
300#[serde(rename_all = "camelCase", deny_unknown_fields)]
301#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
302#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
303pub struct LoginView {
304    pub username: Option<String>,
305    pub password: Option<String>,
306    pub password_revision_date: Option<DateTime<Utc>>,
307
308    pub uris: Option<Vec<LoginUriView>>,
309    pub totp: Option<String>,
310    pub autofill_on_page_load: Option<bool>,
311
312    // TODO: Remove this once the SDK supports state
313    pub fido2_credentials: Option<Vec<Fido2Credential>>,
314}
315
316impl LoginView {
317    /// Generate checksums for all URIs in the login view
318    pub fn generate_checksums(&mut self) {
319        if let Some(uris) = &mut self.uris {
320            for uri in uris {
321                uri.generate_checksum();
322            }
323        }
324    }
325
326    /// Re-encrypts the fido2 credentials with a new key, replacing the old encrypted values.
327    pub fn reencrypt_fido2_credentials(
328        &mut self,
329        ctx: &mut KeyStoreContext<KeyIds>,
330        old_key: SymmetricKeyId,
331        new_key: SymmetricKeyId,
332    ) -> Result<(), CryptoError> {
333        if let Some(creds) = &mut self.fido2_credentials {
334            let decrypted_creds: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, old_key)?;
335            *creds = decrypted_creds.encrypt_composite(ctx, new_key)?;
336        }
337        Ok(())
338    }
339}
340
341#[allow(missing_docs)]
342#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
343#[serde(rename_all = "camelCase", deny_unknown_fields)]
344#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
345#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
346pub struct LoginListView {
347    pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
348    pub has_fido2: bool,
349    pub username: Option<String>,
350    /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`].
351    pub totp: Option<EncString>,
352    pub uris: Option<Vec<LoginUriView>>,
353}
354
355impl CompositeEncryptable<KeyIds, SymmetricKeyId, LoginUri> for LoginUriView {
356    fn encrypt_composite(
357        &self,
358        ctx: &mut KeyStoreContext<KeyIds>,
359        key: SymmetricKeyId,
360    ) -> Result<LoginUri, CryptoError> {
361        Ok(LoginUri {
362            uri: self.uri.encrypt(ctx, key)?,
363            r#match: self.r#match,
364            uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
365        })
366    }
367}
368
369impl CompositeEncryptable<KeyIds, SymmetricKeyId, Login> for LoginView {
370    fn encrypt_composite(
371        &self,
372        ctx: &mut KeyStoreContext<KeyIds>,
373        key: SymmetricKeyId,
374    ) -> Result<Login, CryptoError> {
375        Ok(Login {
376            username: self.username.encrypt(ctx, key)?,
377            password: self.password.encrypt(ctx, key)?,
378            password_revision_date: self.password_revision_date,
379            uris: self.uris.encrypt_composite(ctx, key)?,
380            totp: self.totp.encrypt(ctx, key)?,
381            autofill_on_page_load: self.autofill_on_page_load,
382            fido2_credentials: self.fido2_credentials.clone(),
383        })
384    }
385}
386
387impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
388    fn decrypt(
389        &self,
390        ctx: &mut KeyStoreContext<KeyIds>,
391        key: SymmetricKeyId,
392    ) -> Result<LoginUriView, CryptoError> {
393        Ok(LoginUriView {
394            uri: self.uri.decrypt(ctx, key)?,
395            r#match: self.r#match,
396            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
397        })
398    }
399}
400
401impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
402    fn decrypt(
403        &self,
404        ctx: &mut KeyStoreContext<KeyIds>,
405        key: SymmetricKeyId,
406    ) -> Result<LoginView, CryptoError> {
407        Ok(LoginView {
408            username: self.username.decrypt(ctx, key).ok().flatten(),
409            password: self.password.decrypt(ctx, key).ok().flatten(),
410            password_revision_date: self.password_revision_date,
411            uris: self.uris.decrypt(ctx, key).ok().flatten(),
412            totp: self.totp.decrypt(ctx, key).ok().flatten(),
413            autofill_on_page_load: self.autofill_on_page_load,
414            fido2_credentials: self.fido2_credentials.clone(),
415        })
416    }
417}
418
419impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
420    fn decrypt(
421        &self,
422        ctx: &mut KeyStoreContext<KeyIds>,
423        key: SymmetricKeyId,
424    ) -> Result<LoginListView, CryptoError> {
425        Ok(LoginListView {
426            fido2_credentials: self
427                .fido2_credentials
428                .as_ref()
429                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
430                .transpose()?,
431            has_fido2: self.fido2_credentials.is_some(),
432            username: self.username.decrypt(ctx, key).ok().flatten(),
433            totp: self.totp.clone(),
434            uris: self.uris.decrypt(ctx, key).ok().flatten(),
435        })
436    }
437}
438
439impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
440    fn encrypt_composite(
441        &self,
442        ctx: &mut KeyStoreContext<KeyIds>,
443        key: SymmetricKeyId,
444    ) -> Result<Fido2Credential, CryptoError> {
445        Ok(Fido2Credential {
446            credential_id: self.credential_id.encrypt(ctx, key)?,
447            key_type: self.key_type.encrypt(ctx, key)?,
448            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
449            key_curve: self.key_curve.encrypt(ctx, key)?,
450            key_value: self.key_value.clone(),
451            rp_id: self.rp_id.encrypt(ctx, key)?,
452            user_handle: self
453                .user_handle
454                .as_ref()
455                .map(|h| h.encrypt(ctx, key))
456                .transpose()?,
457            user_name: self
458                .user_name
459                .as_ref()
460                .map(|n| n.encrypt(ctx, key))
461                .transpose()?,
462            counter: self.counter.encrypt(ctx, key)?,
463            rp_name: self.rp_name.encrypt(ctx, key)?,
464            user_display_name: self.user_display_name.encrypt(ctx, key)?,
465            discoverable: self.discoverable.encrypt(ctx, key)?,
466            creation_date: self.creation_date,
467        })
468    }
469}
470
471impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
472    fn decrypt(
473        &self,
474        ctx: &mut KeyStoreContext<KeyIds>,
475        key: SymmetricKeyId,
476    ) -> Result<Fido2CredentialView, CryptoError> {
477        Ok(Fido2CredentialView {
478            credential_id: self.credential_id.decrypt(ctx, key)?,
479            key_type: self.key_type.decrypt(ctx, key)?,
480            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
481            key_curve: self.key_curve.decrypt(ctx, key)?,
482            key_value: self.key_value.clone(),
483            rp_id: self.rp_id.decrypt(ctx, key)?,
484            user_handle: self.user_handle.decrypt(ctx, key)?,
485            user_name: self.user_name.decrypt(ctx, key)?,
486            counter: self.counter.decrypt(ctx, key)?,
487            rp_name: self.rp_name.decrypt(ctx, key)?,
488            user_display_name: self.user_display_name.decrypt(ctx, key)?,
489            discoverable: self.discoverable.decrypt(ctx, key)?,
490            creation_date: self.creation_date,
491        })
492    }
493}
494
495impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
496    fn decrypt(
497        &self,
498        ctx: &mut KeyStoreContext<KeyIds>,
499        key: SymmetricKeyId,
500    ) -> Result<Fido2CredentialListView, CryptoError> {
501        Ok(Fido2CredentialListView {
502            credential_id: self.credential_id.decrypt(ctx, key)?,
503            rp_id: self.rp_id.decrypt(ctx, key)?,
504            user_handle: self.user_handle.decrypt(ctx, key)?,
505            user_name: self.user_name.decrypt(ctx, key)?,
506            user_display_name: self.user_display_name.decrypt(ctx, key)?,
507            counter: self.counter.decrypt(ctx, key)?,
508        })
509    }
510}
511
512impl TryFrom<CipherLoginModel> for Login {
513    type Error = VaultParseError;
514
515    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
516        Ok(Self {
517            username: EncString::try_from_optional(login.username)?,
518            password: EncString::try_from_optional(login.password)?,
519            password_revision_date: login
520                .password_revision_date
521                .map(|d| d.parse())
522                .transpose()?,
523            uris: login
524                .uris
525                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
526                .transpose()?,
527            totp: EncString::try_from_optional(login.totp)?,
528            autofill_on_page_load: login.autofill_on_page_load,
529            fido2_credentials: login
530                .fido2_credentials
531                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
532                .transpose()?,
533        })
534    }
535}
536
537impl TryFrom<CipherLoginUriModel> for LoginUri {
538    type Error = VaultParseError;
539
540    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
541        Ok(Self {
542            uri: EncString::try_from_optional(uri.uri)?,
543            r#match: uri.r#match.map(|m| m.into()),
544            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
545        })
546    }
547}
548
549impl From<bitwarden_api_api::models::UriMatchType> for UriMatchType {
550    fn from(value: bitwarden_api_api::models::UriMatchType) -> Self {
551        match value {
552            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
553            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
554            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
555            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
556            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
557            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
558        }
559    }
560}
561
562impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
563    type Error = VaultParseError;
564
565    fn try_from(
566        value: bitwarden_api_api::models::CipherFido2CredentialModel,
567    ) -> Result<Self, Self::Error> {
568        Ok(Self {
569            credential_id: require!(value.credential_id).parse()?,
570            key_type: require!(value.key_type).parse()?,
571            key_algorithm: require!(value.key_algorithm).parse()?,
572            key_curve: require!(value.key_curve).parse()?,
573            key_value: require!(value.key_value).parse()?,
574            rp_id: require!(value.rp_id).parse()?,
575            user_handle: EncString::try_from_optional(value.user_handle)
576                .ok()
577                .flatten(),
578            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
579            counter: require!(value.counter).parse()?,
580            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
581            user_display_name: EncString::try_from_optional(value.user_display_name)
582                .ok()
583                .flatten(),
584            discoverable: require!(value.discoverable).parse()?,
585            creation_date: value.creation_date.parse()?,
586        })
587    }
588}
589
590impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
591    fn from(uri: LoginUri) -> Self {
592        bitwarden_api_api::models::CipherLoginUriModel {
593            uri: uri.uri.map(|u| u.to_string()),
594            uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
595            r#match: uri.r#match.map(|m| m.into()),
596        }
597    }
598}
599
600impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
601    fn from(match_type: UriMatchType) -> Self {
602        match match_type {
603            UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
604            UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
605            UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
606            UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
607            UriMatchType::RegularExpression => {
608                bitwarden_api_api::models::UriMatchType::RegularExpression
609            }
610            UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
611        }
612    }
613}
614
615impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
616    fn from(cred: Fido2Credential) -> Self {
617        bitwarden_api_api::models::CipherFido2CredentialModel {
618            credential_id: Some(cred.credential_id.to_string()),
619            key_type: Some(cred.key_type.to_string()),
620            key_algorithm: Some(cred.key_algorithm.to_string()),
621            key_curve: Some(cred.key_curve.to_string()),
622            key_value: Some(cred.key_value.to_string()),
623            rp_id: Some(cred.rp_id.to_string()),
624            user_handle: cred.user_handle.map(|h| h.to_string()),
625            user_name: cred.user_name.map(|n| n.to_string()),
626            counter: Some(cred.counter.to_string()),
627            rp_name: cred.rp_name.map(|n| n.to_string()),
628            user_display_name: cred.user_display_name.map(|n| n.to_string()),
629            discoverable: Some(cred.discoverable.to_string()),
630            creation_date: cred.creation_date.to_rfc3339(),
631        }
632    }
633}
634
635impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
636    fn from(login: Login) -> Self {
637        bitwarden_api_api::models::CipherLoginModel {
638            uri: None,
639            uris: login
640                .uris
641                .map(|u| u.into_iter().map(|u| u.into()).collect()),
642            username: login.username.map(|u| u.to_string()),
643            password: login.password.map(|p| p.to_string()),
644            password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
645            totp: login.totp.map(|t| t.to_string()),
646            autofill_on_page_load: login.autofill_on_page_load,
647            fido2_credentials: login
648                .fido2_credentials
649                .map(|c| c.into_iter().map(|c| c.into()).collect()),
650        }
651    }
652}
653
654impl CipherKind for Login {
655    fn decrypt_subtitle(
656        &self,
657        ctx: &mut KeyStoreContext<KeyIds>,
658        key: SymmetricKeyId,
659    ) -> Result<String, CryptoError> {
660        let username: Option<String> = self.username.decrypt(ctx, key)?;
661
662        Ok(username.unwrap_or_default())
663    }
664
665    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
666        [
667            self.username
668                .as_ref()
669                .map(|_| CopyableCipherFields::LoginUsername),
670            self.password
671                .as_ref()
672                .map(|_| CopyableCipherFields::LoginPassword),
673            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
674        ]
675        .into_iter()
676        .flatten()
677        .collect()
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use crate::{
684        Login,
685        cipher::cipher::{CipherKind, CopyableCipherFields},
686    };
687
688    #[test]
689    fn test_valid_checksum() {
690        let uri = super::LoginUriView {
691            uri: Some("https://example.com".to_string()),
692            r#match: Some(super::UriMatchType::Domain),
693            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
694        };
695        assert!(uri.is_checksum_valid());
696    }
697
698    #[test]
699    fn test_invalid_checksum() {
700        let uri = super::LoginUriView {
701            uri: Some("https://example.com".to_string()),
702            r#match: Some(super::UriMatchType::Domain),
703            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
704        };
705        assert!(!uri.is_checksum_valid());
706    }
707
708    #[test]
709    fn test_missing_checksum() {
710        let uri = super::LoginUriView {
711            uri: Some("https://example.com".to_string()),
712            r#match: Some(super::UriMatchType::Domain),
713            uri_checksum: None,
714        };
715        assert!(!uri.is_checksum_valid());
716    }
717
718    #[test]
719    fn test_generate_checksum() {
720        let mut uri = super::LoginUriView {
721            uri: Some("https://test.com".to_string()),
722            r#match: Some(super::UriMatchType::Domain),
723            uri_checksum: None,
724        };
725
726        uri.generate_checksum();
727
728        assert_eq!(
729            uri.uri_checksum.unwrap().as_str(),
730            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
731        );
732    }
733
734    #[test]
735    fn test_get_copyable_fields_login_password() {
736        let login_with_password = Login {
737            username: None,
738            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
739            password_revision_date: None,
740            uris: None,
741            totp: None,
742            autofill_on_page_load: None,
743            fido2_credentials: None,
744        };
745
746        let copyable_fields = login_with_password.get_copyable_fields(None);
747        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
748    }
749
750    #[test]
751    fn test_get_copyable_fields_login_username() {
752        let login_with_username = Login {
753            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
754            password: None,
755            password_revision_date: None,
756            uris: None,
757            totp: None,
758            autofill_on_page_load: None,
759            fido2_credentials: None,
760        };
761
762        let copyable_fields = login_with_username.get_copyable_fields(None);
763        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
764    }
765
766    #[test]
767    fn test_get_copyable_fields_login_everything() {
768        let login = Login {
769            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
770            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
771            password_revision_date: None,
772            uris: None,
773            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
774            autofill_on_page_load: None,
775            fido2_credentials: None,
776        };
777
778        let copyable_fields = login.get_copyable_fields(None);
779        assert_eq!(
780            copyable_fields,
781            vec![
782                CopyableCipherFields::LoginUsername,
783                CopyableCipherFields::LoginPassword,
784                CopyableCipherFields::LoginTotp
785            ]
786        );
787    }
788}