bitwarden_importers/importers/onepassword/access/
login.rs1use serde_json::json;
8
9use super::{
10 account_key::AccountKey,
11 credentials::Credentials,
12 device::{ClientInfo, reauthorize_device, register_device},
13 error::OnePasswordError,
14 mac::MacSigner,
15 opdata::{AesKey, decode64_loose},
16 rest::RestClient,
17 session::Session,
18 srp::{self, SrpInfo},
19 two_factor::{MfaOutcome, TwoFactorUi, perform_second_factor_authentication},
20 wire::{AuthComplete, LoginInfo, MfaInfo, NewSession},
21};
22
23const MAX_DEVICE_ATTEMPTS: u32 = 2;
26const AUTH_METHODS_ENDPOINT: &str = "v2/auth/methods";
27const AUTH_START_ENDPOINT: &str = "v3/auth/start";
28const AUTH_COMPLETE_ENDPOINT: &str = "v2/auth/complete";
29
30pub(super) enum LoginOutcome {
33 Success(Box<Session>),
35 BadOtp,
37}
38
39pub(super) async fn fetch_auth_methods(
41 username: &str,
42 rest: &RestClient,
43) -> Result<LoginInfo, OnePasswordError> {
44 rest.post_json(AUTH_METHODS_ENDPOINT, json!({ "email": username }))
45 .await
46}
47
48pub(super) async fn login_attempt(
51 credentials: &Credentials,
52 account_key: &AccountKey,
53 client_info: &ClientInfo,
54 attempt: u32,
55 ui: &dyn TwoFactorUi,
56 rest: &RestClient,
57) -> Result<LoginOutcome, OnePasswordError> {
58 let (session_id, srp_info) =
60 start_new_session(credentials, account_key, client_info, rest).await?;
61
62 let session_rest = rest.with_session_id(&session_id)?;
65
66 let session_key = srp::perform_and_verify(
68 credentials,
69 account_key,
70 &srp_info,
71 &session_id,
72 &session_rest,
73 )
74 .await?;
75
76 let mac_rest = session_rest.with_signer(MacSigner::new(&session_key));
79
80 let mfa = verify_session_key(client_info, &session_key, &mac_rest).await?;
82
83 if let Some(mfa) = mfa {
85 let outcome = perform_second_factor_authentication(
86 &mfa,
87 client_info,
88 &session_key,
89 attempt,
90 ui,
91 &mac_rest,
92 )
93 .await?;
94
95 match outcome {
96 MfaOutcome::Verified => {}
97 MfaOutcome::BadOtp => return Ok(LoginOutcome::BadOtp),
98 }
99 }
100
101 Ok(LoginOutcome::Success(Box::new(Session::new(
102 session_key,
103 mac_rest,
104 ))))
105}
106
107async fn start_new_session(
110 credentials: &Credentials,
111 account_key: &AccountKey,
112 client_info: &ClientInfo,
113 rest: &RestClient,
114) -> Result<(String, SrpInfo), OnePasswordError> {
115 let mut device_attempts = 0;
116 loop {
117 let response: NewSession = rest
119 .post_json(
120 AUTH_START_ENDPOINT,
121 json!({
122 "email": credentials.username,
123 "skformat": account_key.format,
124 "skid": account_key.uuid,
125 "deviceUuid": client_info.device_uuid,
126 }),
127 )
128 .await?;
129
130 match response.status.as_str() {
133 "ok" => {
136 if response.key_format.as_deref() != Some(account_key.format.as_str())
137 || response.key_uuid.as_deref() != Some(account_key.uuid.as_str())
138 {
139 return Err(OnePasswordError::BadCredentials);
140 }
141
142 let auth = response.auth.ok_or_else(|| {
143 OnePasswordError::Internal(
144 "missing SRP parameters in the start response".into(),
145 )
146 })?;
147 let srp_info = SrpInfo::new(
148 auth.method,
149 auth.algorithm,
150 auth.iterations,
151 decode64_loose(&auth.salt)?,
152 )?;
153 return Ok((response.session_id, srp_info));
154 }
155 status @ ("device-not-registered" | "device-deleted") => {
158 device_attempts += 1;
159 if device_attempts > MAX_DEVICE_ATTEMPTS {
160 return Err(OnePasswordError::Internal(format!(
161 "the server still reports the device as '{status}' after \
162 {MAX_DEVICE_ATTEMPTS} attempts"
163 )));
164 }
165
166 let session_rest = rest.with_session_id(&response.session_id)?;
167 if status == "device-not-registered" {
168 register_device(client_info, &session_rest).await?;
169 } else {
170 reauthorize_device(client_info, &session_rest).await?;
171 }
172 }
173 other => {
174 return Err(OnePasswordError::Internal(format!(
175 "failed to start a new session, unsupported status '{other}'"
176 )));
177 }
178 }
179 }
180}
181
182async fn verify_session_key(
185 client_info: &ClientInfo,
186 session_key: &AesKey,
187 rest: &RestClient,
188) -> Result<Option<MfaInfo>, OnePasswordError> {
189 let params = json!({
190 "client": client_info.client_id(),
191 "device": client_info.device_body(),
192 });
193 let response: AuthComplete = rest
194 .post_encrypted_json(AUTH_COMPLETE_ENDPOINT, params, session_key)
195 .await?;
196 Ok(response.mfa)
197}
198
199#[cfg(test)]
200mod tests {
201 use bitwarden_api_base::new_http_client;
202 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
203
204 use super::{
205 super::sign_in::{SignInAddress, SignInDomain},
206 *,
207 };
208
209 fn client(server: &MockServer) -> RestClient {
210 RestClient::new(
211 new_http_client(),
212 format!("http://{}/api", server.address()),
213 "client-id",
214 "user-agent",
215 "op-user-agent",
216 )
217 .expect("valid headers")
218 }
219
220 fn credentials() -> Credentials {
221 Credentials {
222 username: "[email protected]".into(),
223 password: "password".into(),
224 account_key: "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9".into(),
225 sign_in_address: SignInAddress::new("my", SignInDomain::Global)
226 .expect("a valid subdomain"),
227 device_uuid: "device-uuid".into(),
228 }
229 }
230
231 fn account_key() -> AccountKey {
232 AccountKey::parse(&credentials().account_key).expect("valid account key")
233 }
234
235 fn start_response(status: &str) -> serde_json::Value {
236 json!({"status": status, "sessionID": "SESSION"})
237 }
238
239 fn ok_start_response() -> serde_json::Value {
240 json!({
241 "status": "ok",
242 "sessionID": "SESSION",
243 "accountKeyFormat": "A3",
244 "accountKeyUuid": "RTN9SA",
245 "userAuth": {
246 "method": "SRPg-4096",
247 "alg": "PBES2g-HS256",
248 "iterations": 100000,
249 "salt": "c2FsdHNhbHRzYWx0",
250 },
251 })
252 }
253
254 #[tokio::test]
255 async fn fetches_the_auth_methods() {
256 let server = MockServer::start().await;
257 server
258 .register(
259 Mock::given(matchers::path("/api/v2/auth/methods"))
260 .and(matchers::body_json(json!({"email": "[email protected]"})))
261 .respond_with(
262 ResponseTemplate::new(200)
263 .set_body_json(json!({"authMethods": [{"type": "PASSWORD+SK"}]})),
264 )
265 .expect(1),
266 )
267 .await;
268
269 let info = fetch_auth_methods("[email protected]", &client(&server))
270 .await
271 .expect("methods are listed");
272
273 assert_eq!(info.auth_methods[0].kind, "PASSWORD+SK");
274 server.verify().await;
275 }
276
277 #[tokio::test]
278 async fn start_registers_an_unknown_device_then_retries() {
279 let server = MockServer::start().await;
280 server
283 .register(
284 Mock::given(matchers::path("/api/v3/auth/start"))
285 .respond_with(
286 ResponseTemplate::new(200)
287 .set_body_json(start_response("device-not-registered")),
288 )
289 .up_to_n_times(1)
290 .expect(1),
291 )
292 .await;
293 server
294 .register(
295 Mock::given(matchers::path("/api/v1/device"))
296 .and(matchers::method("POST"))
297 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
298 .expect(1),
299 )
300 .await;
301 server
302 .register(
303 Mock::given(matchers::path("/api/v3/auth/start"))
304 .respond_with(ResponseTemplate::new(200).set_body_json(ok_start_response()))
305 .expect(1),
306 )
307 .await;
308
309 let (session_id, srp_info) = start_new_session(
310 &credentials(),
311 &account_key(),
312 &ClientInfo::for_desktop("device-uuid"),
313 &client(&server),
314 )
315 .await
316 .expect("session starts after registering the device");
317
318 assert_eq!(session_id, "SESSION");
319 assert_eq!(
320 srp_info,
321 SrpInfo::new(
322 "SRPg-4096".into(),
323 "PBES2g-HS256".into(),
324 100000,
325 b"saltsaltsalt".to_vec(),
326 )
327 .expect("supported parameters")
328 );
329 server.verify().await;
330 }
331
332 #[tokio::test]
333 async fn start_gives_up_when_the_device_never_registers() {
334 let server = MockServer::start().await;
335 server
336 .register(
337 Mock::given(matchers::path("/api/v3/auth/start"))
338 .respond_with(
339 ResponseTemplate::new(200)
340 .set_body_json(start_response("device-not-registered")),
341 )
342 .expect(u64::from(MAX_DEVICE_ATTEMPTS) + 1),
343 )
344 .await;
345 server
346 .register(
347 Mock::given(matchers::path("/api/v1/device"))
348 .and(matchers::method("POST"))
349 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
350 .expect(u64::from(MAX_DEVICE_ATTEMPTS)),
351 )
352 .await;
353
354 let error = start_new_session(
355 &credentials(),
356 &account_key(),
357 &ClientInfo::for_desktop("device-uuid"),
358 &client(&server),
359 )
360 .await
361 .expect_err("gives up instead of registering the device forever");
362
363 assert!(
364 error.to_string().contains("device-not-registered"),
365 "unexpected error: {error}"
366 );
367 server.verify().await;
368 }
369
370 #[tokio::test]
371 async fn start_rejects_a_mismatching_account_key() {
372 let server = MockServer::start().await;
373 server
374 .register(
375 Mock::given(matchers::path("/api/v3/auth/start"))
376 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
377 "status": "ok",
378 "sessionID": "SESSION",
379 "accountKeyFormat": "A3",
380 "accountKeyUuid": "OTHERS",
381 })))
382 .expect(1),
383 )
384 .await;
385
386 let error = start_new_session(
387 &credentials(),
388 &account_key(),
389 &ClientInfo::for_desktop("device-uuid"),
390 &client(&server),
391 )
392 .await
393 .expect_err("the server knows a different Secret Key");
394
395 assert!(matches!(error, OnePasswordError::BadCredentials));
396 server.verify().await;
397 }
398
399 #[tokio::test]
400 async fn start_reports_an_unknown_status() {
401 let server = MockServer::start().await;
402 server
403 .register(
404 Mock::given(matchers::path("/api/v3/auth/start"))
405 .respond_with(
406 ResponseTemplate::new(200).set_body_json(start_response("who-knows")),
407 )
408 .expect(1),
409 )
410 .await;
411
412 let error = start_new_session(
413 &credentials(),
414 &account_key(),
415 &ClientInfo::for_desktop("device-uuid"),
416 &client(&server),
417 )
418 .await
419 .expect_err("unknown status");
420
421 assert!(error.to_string().contains("who-knows"));
422 server.verify().await;
423 }
424}