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
17const DEVICE_NAME: &str = "Bitwarden";
19
20const SSO_DEVICE_MODEL: &str = env!("CARGO_PKG_VERSION");
22
23pub(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
33pub(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 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 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 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 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
100pub(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
111pub(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}