Skip to main content

bitwarden_importers/importers/onepassword/access/
credentials.rs

1//! The credentials a password login needs.
2
3use zeroize::Zeroize;
4
5use super::{error::OnePasswordError, sign_in::SignInAddress};
6
7/// The credentials for a password + Secret Key login.
8///
9/// Deliberately neither `Debug` nor `Serialize`: it holds the master password and Secret Key.
10#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
11#[cfg_attr(feature = "wasm", derive(serde::Deserialize))]
12#[bitwarden_ffi::wasm_record(input)]
13#[derive(Clone)]
14pub struct Credentials {
15    /// The account's email address.
16    pub username: String,
17    /// The account's master password.
18    pub password: String,
19    /// The account's Secret Key (Account Key), such as `A3-XXXXXX-...`.
20    pub account_key: String,
21    /// Where the account signs in, such as `my.1password.com`.
22    pub sign_in_address: SignInAddress,
23}
24
25impl Zeroize for Credentials {
26    fn zeroize(&mut self) {
27        self.password.zeroize();
28        self.account_key.zeroize();
29    }
30}
31
32// UniFFI records are lowered by moving out their fields, which Rust does not allow for a type that
33// implements `Drop`. The access client owns the lowered record in a zeroizing guard instead.
34#[cfg(not(feature = "uniffi"))]
35impl Drop for Credentials {
36    fn drop(&mut self) {
37        self.zeroize();
38    }
39}
40
41/// The credentials for a single sign-on login.
42///
43/// Unlike [`Credentials`] there is no secret in here: the identity provider authenticates the
44/// user and the enrolled device supplies the account keys.
45#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
46#[cfg_attr(feature = "wasm", derive(serde::Deserialize))]
47#[bitwarden_ffi::wasm_record(input)]
48#[derive(Debug, Clone)]
49pub struct SsoCredentials {
50    /// The account's email address.
51    pub username: String,
52    /// Where the account signs in, such as `my.1password.com`.
53    pub sign_in_address: SignInAddress,
54}
55
56impl SsoCredentials {
57    /// Rejects a blank username and normalizes the sign-in address.
58    pub(super) fn validate(&mut self) -> Result<(), OnePasswordError> {
59        if self.username.trim().is_empty() {
60            return Err(OnePasswordError::Internal(
61                "username (email) is required".into(),
62            ));
63        }
64        self.sign_in_address.normalize()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::{super::sign_in::SignInDomain, *};
71
72    fn credentials() -> SsoCredentials {
73        SsoCredentials {
74            username: "[email protected]".into(),
75            sign_in_address: SignInAddress {
76                subdomain: "  ACME ".into(),
77                domain: SignInDomain::Global,
78            },
79        }
80    }
81
82    #[test]
83    fn validation_normalizes_the_sign_in_address() {
84        let mut credentials = credentials();
85
86        credentials.validate().expect("valid credentials");
87
88        assert_eq!(
89            credentials.sign_in_address.to_string(),
90            "acme.1password.com"
91        );
92        assert_eq!(credentials.username, "[email protected]");
93    }
94
95    #[test]
96    fn validation_rejects_a_blank_username() {
97        for username in ["", "   \t"] {
98            let mut credentials = credentials();
99            credentials.username = username.into();
100
101            assert!(matches!(
102                credentials.validate(),
103                Err(OnePasswordError::Internal(_))
104            ));
105        }
106    }
107
108    #[test]
109    fn validation_rejects_a_bad_sign_in_address() {
110        let mut credentials = credentials();
111        credentials.sign_in_address.subdomain = "evil.com/x".into();
112
113        assert!(matches!(
114            credentials.validate(),
115            Err(OnePasswordError::InvalidSignInAddress(_))
116        ));
117    }
118}