bitwarden_importers/importers/onepassword/access/
device.rs1use 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
17pub fn generate_device_uuid() -> String {
22 let mut rng = bitwarden_random::rng();
23 (0..DEVICE_UUID_LENGTH)
24 .map(|_| BASE32_ALPHABET[(rng.next_u32() % 32) as usize] as char)
25 .collect()
26}
27
28pub(super) struct ClientInfo {
30 client_name: String,
31 client_version: String,
32 pub user_agent: String,
33 pub op_user_agent: String,
34 pub device_uuid: String,
35}
36
37impl ClientInfo {
38 pub(super) fn for_desktop(device_uuid: &str) -> ClientInfo {
40 let platform = PLATFORM;
41
42 ClientInfo {
43 client_name: format!("1Password for {}", platform.os),
44 client_version: VERSION.to_string(),
45 user_agent: format!("1Password for {}/{VERSION}", platform.os),
46 op_user_agent: format!(
47 "1|{}|{VERSION}|{device_uuid}|||{HTTP_LIB}|{}",
48 platform.op_code, platform.os_suffix
49 ),
50 device_uuid: device_uuid.to_string(),
51 }
52 }
53
54 pub(super) fn client_id(&self) -> String {
55 format!("{}/{}", self.client_name, self.client_version)
56 }
57
58 pub(super) fn device_body(&self) -> Value {
64 json!({
65 "uuid": self.device_uuid,
66 "clientName": self.client_name,
67 "clientVersion": self.client_version,
68 "name": "Bitwarden",
70 "osName": PLATFORM.os_name,
71 "userAgent": self.user_agent,
72 })
73 }
74}
75
76pub(super) async fn register_device(
78 client_info: &ClientInfo,
79 rest: &RestClient,
80) -> Result<(), OnePasswordError> {
81 let response: SuccessStatus = rest
82 .post_json(DEVICE_ENDPOINT, client_info.device_body())
83 .await?;
84 check_success(response, "register", client_info)
85}
86
87pub(super) async fn reauthorize_device(
89 client_info: &ClientInfo,
90 rest: &RestClient,
91) -> Result<(), OnePasswordError> {
92 let response: SuccessStatus = rest
93 .put(&format!(
94 "{DEVICE_ENDPOINT}/{}/reauthorize",
95 client_info.device_uuid
96 ))
97 .await?;
98 check_success(response, "reauthorize", client_info)
99}
100
101fn check_success(
102 response: SuccessStatus,
103 action: &str,
104 client_info: &ClientInfo,
105) -> Result<(), OnePasswordError> {
106 if response.success != 1 {
107 return Err(OnePasswordError::Internal(format!(
108 "failed to {action} the device '{}'",
109 client_info.device_uuid
110 )));
111 }
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use bitwarden_api_base::new_http_client;
118 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
119
120 use super::*;
121
122 fn client(server: &MockServer) -> RestClient {
123 let info = ClientInfo::for_desktop("device-uuid");
124 RestClient::new(
125 new_http_client(),
126 format!("http://{}/api", server.address()),
127 &info.client_id(),
128 &info.user_agent,
129 &info.op_user_agent,
130 )
131 .expect("valid headers")
132 }
133
134 #[test]
135 fn generated_uuid_has_expected_shape() {
136 let uuid = generate_device_uuid();
137 assert_eq!(uuid.len(), DEVICE_UUID_LENGTH);
138 assert!(uuid.bytes().all(|b| BASE32_ALPHABET.contains(&b)));
139 assert_ne!(uuid, generate_device_uuid());
140 }
141
142 #[test]
143 fn client_info_builds_identity_headers() {
144 let info = ClientInfo::for_desktop("device-uuid");
145 assert_eq!(info.client_id(), format!("{}/81210036", info.client_name));
146 assert!(info.op_user_agent.contains("device-uuid"));
147 assert!(info.user_agent.starts_with("1Password for "));
148 }
149
150 #[test]
151 fn device_body_carries_the_device_descriptor() {
152 let info = ClientInfo::for_desktop("device-uuid");
153 let body = info.device_body();
154 assert_eq!(body["uuid"], "device-uuid");
155 assert_eq!(body["clientVersion"], "81210036");
156 assert_eq!(body["osName"], PLATFORM.os_name);
157 assert_eq!(body["clientName"], format!("1Password for {}", PLATFORM.os));
158 }
159
160 #[tokio::test]
161 async fn registers_and_reauthorizes_the_device() {
162 let server = MockServer::start().await;
163 server
164 .register(
165 Mock::given(matchers::path("/api/v1/device"))
166 .and(matchers::method("POST"))
167 .and(matchers::body_partial_json(json!({"uuid": "device-uuid"})))
168 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
169 .expect(1),
170 )
171 .await;
172 server
173 .register(
174 Mock::given(matchers::path("/api/v1/device/device-uuid/reauthorize"))
175 .and(matchers::method("PUT"))
176 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
177 .expect(1),
178 )
179 .await;
180
181 let rest = client(&server);
182 let info = ClientInfo::for_desktop("device-uuid");
183 register_device(&info, &rest).await.expect("registers");
184 reauthorize_device(&info, &rest)
185 .await
186 .expect("reauthorizes");
187
188 server.verify().await;
189 }
190
191 #[tokio::test]
192 async fn reports_a_failed_registration() {
193 let server = MockServer::start().await;
194 server
195 .register(
196 Mock::given(matchers::path("/api/v1/device"))
197 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 0})))
198 .expect(1),
199 )
200 .await;
201
202 let error = register_device(&ClientInfo::for_desktop("device-uuid"), &client(&server))
203 .await
204 .expect_err("registration is rejected");
205
206 assert!(error.to_string().contains("failed to register the device"));
207 server.verify().await;
208 }
209}