Skip to main content

bitwarden_importers/importers/onepassword/access/
device.rs

1//! The device identity presented to 1Password and its registration.
2
3use rand::Rng;
4use serde_json::{Value, json};
5
6use super::{
7    error::OnePasswordError,
8    identity::{HTTP_LIB, PLATFORM, VERSION},
9    rest::RestClient,
10    wire::SuccessStatus,
11};
12
13const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
14const DEVICE_UUID_LENGTH: usize = 26;
15const DEVICE_ENDPOINT: &str = "v1/device";
16
17/// Shown in the account's device list, so it names us rather than a 1Password client.
18const DEVICE_NAME: &str = "Bitwarden";
19
20/// The `model` of the SSO device descriptors is this crate's version.
21const SSO_DEVICE_MODEL: &str = env!("CARGO_PKG_VERSION");
22
23/// Generates a 26-character 1Password device id from the lowercase base32 alphabet.
24///
25/// Every login takes a fresh one: the account sees each import as a new device.
26pub(super) fn generate_device_uuid() -> String {
27    let mut rng = bitwarden_random::rng();
28    (0..DEVICE_UUID_LENGTH)
29        .map(|_| BASE32_ALPHABET[(rng.next_u32() % 32) as usize] as char)
30        .collect()
31}
32
33/// Client identity headers sent with every request.
34pub(super) struct ClientInfo {
35    client_name: String,
36    client_version: String,
37    pub user_agent: String,
38    pub op_user_agent: String,
39    pub device_uuid: String,
40}
41
42impl ClientInfo {
43    /// Impersonates the 1Password desktop client for the current platform.
44    pub(super) fn for_desktop(device_uuid: &str) -> ClientInfo {
45        let platform = PLATFORM;
46
47        ClientInfo {
48            client_name: format!("1Password for {}", platform.os),
49            client_version: VERSION.to_string(),
50            user_agent: format!("1Password for {}/{VERSION}", platform.os),
51            op_user_agent: format!(
52                "1|{}|{VERSION}|{device_uuid}|||{HTTP_LIB}|{}",
53                platform.op_code, platform.os_suffix
54            ),
55            device_uuid: device_uuid.to_string(),
56        }
57    }
58
59    pub(super) fn client_id(&self) -> String {
60        format!("{}/{}", self.client_name, self.client_version)
61    }
62
63    /// The device descriptor sent to `v1/device` and inside `v2/auth/complete`.
64    ///
65    /// The real 1Password clients also send `model` and `osVersion`. The server accepted their
66    /// removal when this was tested, so they are left out, but add them back if it starts
67    /// rejecting the request.
68    pub(super) fn device_body(&self) -> Value {
69        json!({
70            "uuid": self.device_uuid,
71            "clientName": self.client_name,
72            "clientVersion": self.client_version,
73            "name": DEVICE_NAME,
74            "osName": PLATFORM.os_name,
75            "userAgent": self.user_agent,
76        })
77    }
78
79    /// The device descriptor of `v3/auth/sso/oidc/verify`.
80    pub(super) fn sso_verify_device_body(&self) -> Value {
81        json!({
82            "uuid": self.device_uuid,
83            "clientName": self.client_name,
84            "clientVersion": self.client_version,
85            "osName": PLATFORM.os_name,
86            "osVersion": "",
87            "name": DEVICE_NAME,
88            "model": SSO_DEVICE_MODEL,
89        })
90    }
91
92    /// The device descriptor of `v2/auth/complete`: the verify one plus the user agent.
93    pub(super) fn sso_complete_device_body(&self) -> Value {
94        let mut body = self.sso_verify_device_body();
95        body["userAgent"] = self.user_agent.clone().into();
96        body
97    }
98}
99
100/// Registers the device with the server.
101pub(super) async fn register_device(
102    client_info: &ClientInfo,
103    rest: &RestClient,
104) -> Result<(), OnePasswordError> {
105    let response: SuccessStatus = rest
106        .post_json(DEVICE_ENDPOINT, client_info.device_body())
107        .await?;
108    check_success(response, "register", client_info)
109}
110
111/// Reauthorizes a previously deleted device.
112pub(super) async fn reauthorize_device(
113    client_info: &ClientInfo,
114    rest: &RestClient,
115) -> Result<(), OnePasswordError> {
116    let response: SuccessStatus = rest
117        .put(&format!(
118            "{DEVICE_ENDPOINT}/{}/reauthorize",
119            client_info.device_uuid
120        ))
121        .await?;
122    check_success(response, "reauthorize", client_info)
123}
124
125fn check_success(
126    response: SuccessStatus,
127    action: &str,
128    client_info: &ClientInfo,
129) -> Result<(), OnePasswordError> {
130    if response.success != 1 {
131        return Err(OnePasswordError::Internal(format!(
132            "failed to {action} the device '{}'",
133            client_info.device_uuid
134        )));
135    }
136    Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141    use bitwarden_api_base::new_http_client;
142    use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
143
144    use super::*;
145
146    fn client(server: &MockServer) -> RestClient {
147        let info = ClientInfo::for_desktop("device-uuid");
148        RestClient::new(
149            new_http_client(),
150            format!("http://{}/api", server.address()),
151            &info.client_id(),
152            &info.user_agent,
153            &info.op_user_agent,
154        )
155        .expect("valid headers")
156    }
157
158    #[test]
159    fn generated_uuid_has_expected_shape() {
160        let uuid = generate_device_uuid();
161        assert_eq!(uuid.len(), DEVICE_UUID_LENGTH);
162        assert!(uuid.bytes().all(|b| BASE32_ALPHABET.contains(&b)));
163        assert_ne!(uuid, generate_device_uuid());
164    }
165
166    #[test]
167    fn client_info_builds_identity_headers() {
168        let info = ClientInfo::for_desktop("device-uuid");
169        assert_eq!(info.client_id(), format!("{}/81210036", info.client_name));
170        assert!(info.op_user_agent.contains("device-uuid"));
171        assert!(info.user_agent.starts_with("1Password for "));
172    }
173
174    #[test]
175    fn device_body_carries_the_device_descriptor() {
176        let info = ClientInfo::for_desktop("device-uuid");
177        let body = info.device_body();
178        assert_eq!(body["uuid"], "device-uuid");
179        assert_eq!(body["clientVersion"], "81210036");
180        assert_eq!(body["osName"], PLATFORM.os_name);
181        assert_eq!(body["clientName"], format!("1Password for {}", PLATFORM.os));
182    }
183
184    #[tokio::test]
185    async fn registers_and_reauthorizes_the_device() {
186        let server = MockServer::start().await;
187        server
188            .register(
189                Mock::given(matchers::path("/api/v1/device"))
190                    .and(matchers::method("POST"))
191                    .and(matchers::body_partial_json(json!({"uuid": "device-uuid"})))
192                    .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
193                    .expect(1),
194            )
195            .await;
196        server
197            .register(
198                Mock::given(matchers::path("/api/v1/device/device-uuid/reauthorize"))
199                    .and(matchers::method("PUT"))
200                    .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
201                    .expect(1),
202            )
203            .await;
204
205        let rest = client(&server);
206        let info = ClientInfo::for_desktop("device-uuid");
207        register_device(&info, &rest).await.expect("registers");
208        reauthorize_device(&info, &rest)
209            .await
210            .expect("reauthorizes");
211
212        server.verify().await;
213    }
214
215    #[tokio::test]
216    async fn reports_a_failed_registration() {
217        let server = MockServer::start().await;
218        server
219            .register(
220                Mock::given(matchers::path("/api/v1/device"))
221                    .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 0})))
222                    .expect(1),
223            )
224            .await;
225
226        let error = register_device(&ClientInfo::for_desktop("device-uuid"), &client(&server))
227            .await
228            .expect_err("registration is rejected");
229
230        assert!(error.to_string().contains("failed to register the device"));
231        server.verify().await;
232    }
233}