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 {
226 subdomain: "my".into(),
227 domain: SignInDomain::Global,
228 },
229 }
230 }
231
232 fn account_key() -> AccountKey {
233 AccountKey::parse(&credentials().account_key).expect("valid account key")
234 }
235
236 fn start_response(status: &str) -> serde_json::Value {
237 json!({"status": status, "sessionID": "SESSION"})
238 }
239
240 fn ok_start_response() -> serde_json::Value {
241 json!({
242 "status": "ok",
243 "sessionID": "SESSION",
244 "accountKeyFormat": "A3",
245 "accountKeyUuid": "RTN9SA",
246 "userAuth": {
247 "method": "SRPg-4096",
248 "alg": "PBES2g-HS256",
249 "iterations": 100000,
250 "salt": "c2FsdHNhbHRzYWx0",
251 },
252 })
253 }
254
255 #[tokio::test]
256 async fn fetches_the_auth_methods() {
257 let server = MockServer::start().await;
258 server
259 .register(
260 Mock::given(matchers::path("/api/v2/auth/methods"))
261 .and(matchers::body_json(json!({"email": "[email protected]"})))
262 .respond_with(
263 ResponseTemplate::new(200)
264 .set_body_json(json!({"authMethods": [{"type": "PASSWORD+SK"}]})),
265 )
266 .expect(1),
267 )
268 .await;
269
270 let info = fetch_auth_methods("[email protected]", &client(&server))
271 .await
272 .expect("methods are listed");
273
274 assert_eq!(info.auth_methods[0].kind, "PASSWORD+SK");
275 server.verify().await;
276 }
277
278 #[tokio::test]
279 async fn start_registers_an_unknown_device_then_retries() {
280 let server = MockServer::start().await;
281 server
284 .register(
285 Mock::given(matchers::path("/api/v3/auth/start"))
286 .respond_with(
287 ResponseTemplate::new(200)
288 .set_body_json(start_response("device-not-registered")),
289 )
290 .up_to_n_times(1)
291 .expect(1),
292 )
293 .await;
294 server
295 .register(
296 Mock::given(matchers::path("/api/v1/device"))
297 .and(matchers::method("POST"))
298 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
299 .expect(1),
300 )
301 .await;
302 server
303 .register(
304 Mock::given(matchers::path("/api/v3/auth/start"))
305 .respond_with(ResponseTemplate::new(200).set_body_json(ok_start_response()))
306 .expect(1),
307 )
308 .await;
309
310 let (session_id, srp_info) = start_new_session(
311 &credentials(),
312 &account_key(),
313 &ClientInfo::for_desktop("device-uuid"),
314 &client(&server),
315 )
316 .await
317 .expect("session starts after registering the device");
318
319 assert_eq!(session_id, "SESSION");
320 assert_eq!(
321 srp_info,
322 SrpInfo::new(
323 "SRPg-4096".into(),
324 "PBES2g-HS256".into(),
325 100000,
326 b"saltsaltsalt".to_vec(),
327 )
328 .expect("supported parameters")
329 );
330 server.verify().await;
331 }
332
333 #[tokio::test]
334 async fn start_gives_up_when_the_device_never_registers() {
335 let server = MockServer::start().await;
336 server
337 .register(
338 Mock::given(matchers::path("/api/v3/auth/start"))
339 .respond_with(
340 ResponseTemplate::new(200)
341 .set_body_json(start_response("device-not-registered")),
342 )
343 .expect(u64::from(MAX_DEVICE_ATTEMPTS) + 1),
344 )
345 .await;
346 server
347 .register(
348 Mock::given(matchers::path("/api/v1/device"))
349 .and(matchers::method("POST"))
350 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
351 .expect(u64::from(MAX_DEVICE_ATTEMPTS)),
352 )
353 .await;
354
355 let error = start_new_session(
356 &credentials(),
357 &account_key(),
358 &ClientInfo::for_desktop("device-uuid"),
359 &client(&server),
360 )
361 .await
362 .expect_err("gives up instead of registering the device forever");
363
364 assert!(
365 error.to_string().contains("device-not-registered"),
366 "unexpected error: {error}"
367 );
368 server.verify().await;
369 }
370
371 #[tokio::test]
372 async fn start_rejects_a_mismatching_account_key() {
373 let server = MockServer::start().await;
374 server
375 .register(
376 Mock::given(matchers::path("/api/v3/auth/start"))
377 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
378 "status": "ok",
379 "sessionID": "SESSION",
380 "accountKeyFormat": "A3",
381 "accountKeyUuid": "OTHERS",
382 })))
383 .expect(1),
384 )
385 .await;
386
387 let error = start_new_session(
388 &credentials(),
389 &account_key(),
390 &ClientInfo::for_desktop("device-uuid"),
391 &client(&server),
392 )
393 .await
394 .expect_err("the server knows a different Secret Key");
395
396 assert!(matches!(error, OnePasswordError::BadCredentials));
397 server.verify().await;
398 }
399
400 #[tokio::test]
401 async fn start_reports_an_unknown_status() {
402 let server = MockServer::start().await;
403 server
404 .register(
405 Mock::given(matchers::path("/api/v3/auth/start"))
406 .respond_with(
407 ResponseTemplate::new(200).set_body_json(start_response("who-knows")),
408 )
409 .expect(1),
410 )
411 .await;
412
413 let error = start_new_session(
414 &credentials(),
415 &account_key(),
416 &ClientInfo::for_desktop("device-uuid"),
417 &client(&server),
418 )
419 .await
420 .expect_err("unknown status");
421
422 assert!(error.to_string().contains("who-knows"));
423 server.verify().await;
424 }
425}