1use std::sync::LazyLock;
4
5use crypto_bigint::{
6 BoxedUint, ConcatenatingMul, Odd, Resize,
7 modular::{BoxedMontyForm, BoxedMontyParams},
8};
9use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
10use rand::Rng;
11use serde_json::json;
12use sha2::{Digest, Sha256};
13
14use super::{
15 account_key::AccountKey,
16 credentials::Credentials,
17 error::OnePasswordError,
18 kdf,
19 opdata::AesKey,
20 rest::RestClient,
21 wire::{AForB, ServerHash},
22};
23
24const SRP_METHOD: &str = "SRPg-4096";
25const AUTH_ENDPOINT: &str = "v2/auth";
26const CONFIRM_KEY_ENDPOINT: &str = "v2/auth/confirm-key";
27
28const N_BITS: u32 = 4096;
30
31const SCALAR_BITS: u32 = 256;
33
34static N: LazyLock<Odd<BoxedUint>> = LazyLock::new(|| {
36 Odd::new(BoxedUint::from_be_hex(N_HEX, N_BITS).expect("N_HEX is a compile-time hex constant"))
37 .expect("the SRP group prime is odd")
38});
39
40static N_PARAMS: LazyLock<BoxedMontyParams> =
42 LazyLock::new(|| BoxedMontyParams::new_vartime(N.clone()));
43
44static G: LazyLock<BoxedUint> = LazyLock::new(|| BoxedUint::from(5u32));
46
47const N_HEX: &str = concat!(
48 "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22",
49 "514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6",
50 "F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D",
51 "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB",
52 "9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E8603",
53 "9B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA0510",
54 "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D",
55 "B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864",
56 "D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB31",
57 "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C",
58 "1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D",
59 "99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9",
60 "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF",
61);
62
63#[derive(Debug, PartialEq)]
68pub(super) struct SrpInfo {
69 iterations: u32,
70 salt: Vec<u8>,
71}
72
73impl SrpInfo {
74 pub(super) fn new(
76 method: String,
77 key_method: String,
78 iterations: u32,
79 salt: Vec<u8>,
80 ) -> Result<SrpInfo, OnePasswordError> {
81 if method != SRP_METHOD {
84 return Err(OnePasswordError::Unsupported(format!(
85 "Method '{method}' is not supported"
86 )));
87 }
88 if iterations < kdf::MIN_PBKDF2_ITERATIONS {
89 return Err(OnePasswordError::Unsupported(format!(
90 "{iterations} iterations is below the minimum of {}",
91 kdf::MIN_PBKDF2_ITERATIONS
92 )));
93 }
94
95 kdf::validate_pbes2(&key_method)?;
96
97 Ok(SrpInfo { iterations, salt })
98 }
99
100 pub(super) fn salt(&self) -> &[u8] {
102 &self.salt
103 }
104}
105
106pub(super) async fn perform_and_verify_with_x(
109 srp_x: &[u8],
110 username: &str,
111 key_uuid: &str,
112 salt: &[u8],
113 session_id: &str,
114 rest: &RestClient,
115) -> Result<AesKey, OnePasswordError> {
116 let srp_x = scalar_bytes(srp_x)?;
117
118 let key = perform(&generate_secret_a(), srp_x, username, key_uuid, salt, rest).await?;
119 Ok(AesKey::new(session_id, key.to_vec()))
120}
121
122fn scalar_bytes(srp_x: &[u8]) -> Result<&[u8], OnePasswordError> {
125 let leading_zeros = srp_x.iter().take_while(|byte| **byte == 0).count();
126 let significant = &srp_x[leading_zeros..];
127 if significant.len() > SCALAR_BITS as usize / 8 {
128 return Err(OnePasswordError::Internal(
129 "the SRP x is longer than 32 bytes".into(),
130 ));
131 }
132
133 Ok(significant)
134}
135
136async fn perform(
138 secret_a: &BoxedUint,
139 srp_x: &[u8],
140 username: &str,
141 key_uuid: &str,
142 salt: &[u8],
143 rest: &RestClient,
144) -> Result<[u8; 32], OnePasswordError> {
145 let shared_a = compute_shared_a(secret_a);
147 let shared_b = exchange_a_for_b(&shared_a, rest).await?;
148
149 validate_b(&shared_b)?;
151
152 let session_key = compute_key(secret_a, &shared_a, &shared_b, srp_x);
154
155 verify_key(
157 &session_key,
158 username,
159 key_uuid,
160 salt,
161 &shared_a,
162 &shared_b,
163 rest,
164 )
165 .await?;
166
167 Ok(session_key)
168}
169
170fn generate_secret_a() -> BoxedUint {
172 let mut bytes = [0u8; 32];
173 bitwarden_random::rng().fill_bytes(&mut bytes);
174 scalar(&bytes)
175}
176
177fn compute_shared_a(secret_a: &BoxedUint) -> BoxedUint {
179 mod_pow(&G, secret_a)
180}
181
182fn validate_b(shared_b: &BoxedUint) -> Result<(), OnePasswordError> {
184 let reduced = shared_b.rem(N.as_nz_ref());
185 if bool::from(reduced.is_zero() | reduced.is_one()) {
186 return Err(OnePasswordError::Internal(
187 "Shared B validation failed".into(),
188 ));
189 }
190 Ok(())
191}
192
193async fn exchange_a_for_b(
195 shared_a: &BoxedUint,
196 rest: &RestClient,
197) -> Result<BoxedUint, OnePasswordError> {
198 let response: AForB = rest
199 .post_json(AUTH_ENDPOINT, json!({ "userA": to_server_hex(shared_a) }))
200 .await?;
201 from_server_hex(&response.b)
202}
203
204fn mod_pow(base: &BoxedUint, exponent: &BoxedUint) -> BoxedUint {
206 BoxedMontyForm::new(base.resize(N_BITS), &N_PARAMS)
207 .pow(exponent)
208 .retrieve()
209}
210
211fn scalar(hash: &[u8]) -> BoxedUint {
213 BoxedUint::from_be_slice(hash, SCALAR_BITS).expect("an SRP scalar is a 32-byte hash")
214}
215
216fn compute_key(
218 secret_a: &BoxedUint,
219 shared_a: &BoxedUint,
220 shared_b: &BoxedUint,
221 srp_x: &[u8],
222) -> [u8; 32] {
223 let x = scalar(srp_x);
225 let exponent = scalar(&compute_u(shared_a, shared_b))
226 .concatenating_mul(&x)
227 .wrapping_add(secret_a);
228
229 let k_g_pow_x = mod_pow(&G, &x)
233 .concatenating_mul(&scalar(&compute_k()))
234 .rem(N.as_nz_ref());
235 let base = shared_b
236 .rem(N.as_nz_ref())
237 .sub_mod(&k_g_pow_x, N.as_nz_ref());
238
239 sha256(to_server_hex(&mod_pow(&base, &exponent)).as_bytes())
241}
242
243fn compute_k() -> [u8; 32] {
246 let mut input = to_compatible_byte_array(&N);
247 input.extend_from_slice(&mod_n_bytes(&G));
248 sha256(&input)
249}
250
251fn compute_u(shared_a: &BoxedUint, shared_b: &BoxedUint) -> [u8; 32] {
253 let mut ab = mod_n_bytes(shared_a);
254 ab.extend_from_slice(&mod_n_bytes(shared_b));
255 sha256(&ab)
256}
257
258fn to_server_hex(value: &BoxedUint) -> String {
266 let hex = HEXLOWER.encode(&value.to_be_bytes());
267 match hex.trim_start_matches('0') {
268 "" => "0".to_string(),
269 trimmed => trimmed.to_string(),
270 }
271}
272
273fn from_server_hex(hex: &str) -> Result<BoxedUint, OnePasswordError> {
275 let invalid = || OnePasswordError::Internal("invalid shared value from server".into());
276
277 if hex.is_empty() || hex.len() > N_HEX.len() || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
280 return Err(invalid());
281 }
282
283 let padded = format!("{hex:0>width$}", width = N_HEX.len());
285 BoxedUint::from_be_hex(&padded, N_BITS)
286 .into_option()
287 .ok_or_else(invalid)
288}
289
290fn mod_n_bytes(value: &BoxedUint) -> Vec<u8> {
292 value.rem(N.as_nz_ref()).to_be_bytes().into_vec()
293}
294
295fn to_compatible_byte_array(value: &BoxedUint) -> Vec<u8> {
297 let bytes = value.to_be_bytes();
298 match bytes.iter().position(|byte| *byte != 0) {
299 Some(first_significant) => bytes[first_significant..].to_vec(),
300 None => vec![0],
301 }
302}
303
304fn calculate_identity(username: &str, key_uuid: &str) -> String {
306 let username = kdf::normalize_identity_username(username);
307
308 let mut buffer = Vec::with_capacity(64);
309 buffer.extend_from_slice(&sha256(key_uuid.as_bytes()));
310 buffer.extend_from_slice(&sha256(username.as_bytes()));
311 BASE64URL_NOPAD.encode(&sha256(&buffer))
312}
313
314async fn verify_key(
316 session_key: &[u8],
317 username: &str,
318 key_uuid: &str,
319 salt: &[u8],
320 shared_a: &BoxedUint,
321 shared_b: &BoxedUint,
322 rest: &RestClient,
323) -> Result<(), OnePasswordError> {
324 let client_hash =
325 calculate_client_hash(session_key, username, key_uuid, salt, shared_a, shared_b);
326 let response: ServerHash = rest
327 .post_json(
328 CONFIRM_KEY_ENDPOINT,
329 json!({ "clientVerifyHash": BASE64URL_NOPAD.encode(&client_hash) }),
330 )
331 .await
332 .map_err(|error| match error {
333 OnePasswordError::UnexpectedStatus { status: 401, .. } => {
336 OnePasswordError::BadCredentials
337 }
338 error => error,
339 })?;
340
341 let expected = calculate_server_hash(shared_a, &client_hash, session_key);
344 if response.server_verify_hash != BASE64URL_NOPAD.encode(&expected) {
345 return Err(OnePasswordError::Internal(
346 "the server verification hash does not match".into(),
347 ));
348 }
349 Ok(())
350}
351
352fn calculate_server_hash(shared_a: &BoxedUint, client_hash: &[u8], session_key: &[u8]) -> [u8; 32] {
355 let mut buffer = to_compatible_byte_array(shared_a);
356 buffer.extend_from_slice(client_hash);
357 buffer.extend_from_slice(session_key);
358
359 sha256(&buffer)
360}
361
362fn calculate_client_hash(
365 session_key: &[u8],
366 username: &str,
367 key_uuid: &str,
368 salt: &[u8],
369 shared_a: &BoxedUint,
370 shared_b: &BoxedUint,
371) -> [u8; 32] {
372 let sirp_n = sha256(&to_compatible_byte_array(&N));
373 let sirp_g = sha256(&to_compatible_byte_array(&G));
374 let identity = sha256(calculate_identity(username, key_uuid).as_bytes());
375
376 let mut buffer = Vec::new();
378 for (a, b) in sirp_n.iter().zip(sirp_g.iter()) {
379 buffer.push(a ^ b);
380 }
381
382 buffer.extend_from_slice(&identity);
384 buffer.extend_from_slice(salt);
385 buffer.extend_from_slice(&to_compatible_byte_array(shared_a));
386 buffer.extend_from_slice(&to_compatible_byte_array(shared_b));
387 buffer.extend_from_slice(session_key);
388
389 sha256(&buffer)
390}
391
392pub(super) fn compute_x(
394 credentials: &Credentials,
395 account_key: &AccountKey,
396 srp_info: &SrpInfo,
397) -> Result<[u8; 32], OnePasswordError> {
398 let k1 = kdf::hkdf_sha256(
399 SRP_METHOD,
400 &srp_info.salt,
401 kdf::normalize_username(&credentials.username).as_bytes(),
402 );
403 let k2 = kdf::pbes2(
404 &kdf::normalize_password(&credentials.password),
405 &k1,
406 srp_info.iterations,
407 );
408 account_key.combine_with(&k2)
409}
410
411fn sha256(data: &[u8]) -> [u8; 32] {
412 Sha256::digest(data).into()
413}
414
415#[cfg(test)]
418pub(super) struct TestServer {
419 username: String,
420 key_uuid: String,
421 salt: Vec<u8>,
422 verifier: BoxedUint,
423 secret_b: BoxedUint,
424 shared_b: BoxedUint,
425}
426
427#[cfg(test)]
428impl TestServer {
429 pub(super) fn new(
432 srp_x: &[u8],
433 secret_b: &[u8; 32],
434 username: &str,
435 key_uuid: &str,
436 salt: &[u8],
437 ) -> TestServer {
438 let verifier = mod_pow(&G, &scalar(srp_x));
439 let secret_b = scalar(secret_b);
440
441 let k_verifier = verifier
443 .concatenating_mul(&scalar(&compute_k()))
444 .rem(N.as_nz_ref());
445 let shared_b = k_verifier.add_mod(&mod_pow(&G, &secret_b), N.as_nz_ref());
446
447 TestServer {
448 username: username.to_string(),
449 key_uuid: key_uuid.to_string(),
450 salt: salt.to_vec(),
451 verifier,
452 secret_b,
453 shared_b,
454 }
455 }
456
457 pub(super) fn shared_b(&self) -> String {
459 to_server_hex(&self.shared_b)
460 }
461
462 pub(super) fn session_key(&self, shared_a: &str) -> Option<[u8; 32]> {
464 let shared_a = from_server_hex(shared_a).ok()?;
465
466 let u = scalar(&compute_u(&shared_a, &self.shared_b));
468 let base = mod_pow(&self.verifier, &u)
469 .concatenating_mul(&shared_a)
470 .rem(N.as_nz_ref());
471 Some(sha256(
472 to_server_hex(&mod_pow(&base, &self.secret_b)).as_bytes(),
473 ))
474 }
475
476 pub(super) fn confirm_key(&self, shared_a: &str, client_verify_hash: &str) -> Option<String> {
479 let session_key = self.session_key(shared_a)?;
480 let shared_a = from_server_hex(shared_a).ok()?;
481
482 let client_hash = calculate_client_hash(
483 &session_key,
484 &self.username,
485 &self.key_uuid,
486 &self.salt,
487 &shared_a,
488 &self.shared_b,
489 );
490 (BASE64URL_NOPAD.encode(&client_hash) == client_verify_hash).then(|| {
491 BASE64URL_NOPAD.encode(&calculate_server_hash(
492 &shared_a,
493 &client_hash,
494 &session_key,
495 ))
496 })
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use bitwarden_api_base::new_http_client;
503 use data_encoding::HEXLOWER;
504 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
505
506 use super::{
507 super::sign_in::{SignInAddress, SignInDomain},
508 *,
509 };
510
511 const SHARED_A: &str = "843c9c4977cf9c767452c90708c3dbdf3508c0016f8a56abc20c2e654dbd74c2c04b9412528a0927f499b245f9ad6742052662de2f725bf2a6c84913062842b4b2aaa8d41598c0d11424745bbae928d8e00e3c2c831c5ae90e128b719adb8be3845561186826462f0dbbdba272666c039f075b3da18c866c61a208cb9aed5ade03e6570818b7146c789f2e2928958ec7bebffbf2cc06cbb83b77ed80eae95e194502dead2e945e885d145d4521b74b8669211ffe718b20f04253d19550e0f9e8f1f0381caa2200223904a94d1e70f7db7cfa7d10d415bf7571f656a2e7bac3d142a2fa60b5a4e2fec4a82348fb46e03b65938f960373eefb95e50b1dd38134593b2f3ed0a19ae8684b4b54a04e0e022e01abc03072aa2e0096b209eaadb8dae57acd607a46e27bc5bfa66c3887e03441b4628135f830d1d78c7a60366d88cb42ed7ddd2dc32049f9dd3a1f459b610d41d25e8615f3271fcadcd37bf1b13c84c049d57d14ded500290b430c33d1d1dc3b04af66862ca3b4d501e2827355f68eaaf063a131c2436aa0a75519b7ac4d79845b6235898dcd9bef1093618b7c5bc5d73a7fc2a5ef8bca638e922152e459e89652b4a7d7d19cfd24de93f72f20e3a6f4325abf5ca1aec3ef3f392cc356c80b72e43a577775d2bf613b60d9f46d130e9881534e7548241e612901f61d5c5acb62100b8371c8dc42747437cd9ddcf9debf";
512 const SHARED_B: &str = "7112742e3035eca37656e1ad2171516e3e154bcbabbcb5f52787aa53ad882cffd8e952bd67dbc8059025be23a0b86914bf8ec4c08cac0b3448a99d8c097e4b0c6942870b2cd2c56a58499c81c294bf2f64de408535f0a36ba416177519dcb5a54b7a403459abb1bfe8aecb92048e84a55ba48f1672f6ee3f30abff81868e88c8bb25c7c17292e535f91debda167af8f12d1e1073a48a9257b443dacd8ba47270051b03940117d2cec29f6521a3e78e575634db5bc87d479a4327db1b30578c90553edd3de58af08e9157a11b352b0bd7fca70d469809b3d516fed4edc989b78c6f330e553947111c563cb8c8ff184179cf7b8733494e16f3e38ed7cd42651c5bb4d81548c4b320996445b6f1a4c34a6211b5f65e561c04009c7422e289d7035085e21258513040b16bea0d3e91304879fa61f48af4daefce65d0917e4af106d868c6189dfd9031c8a3b2d97fa2a50445d6a818341fed7ad2a986f5aa691626426dc2b1047e1db8a1984f8fda526f21e825df6b4cc60cd31300181a3782e53d039f85164e417b419cde581826b08887f25277f9f7c0933aa596f5a4bb27af7bffb095027e326d1c02544357eaa553ac93b564bb5953b8fc498044d65b8003ad93f95c319ce6af0a0327151935e860c3e5dad17cd65ae4318e76905ce2a3ae239c12ab207313af3c0c7744e7aee2584043ae71dfc3e376bf747f92fa5a94bd36cb";
513
514 fn big(hex: &str) -> BoxedUint {
515 from_server_hex(hex).expect("valid hex")
516 }
517
518 #[test]
519 fn to_server_hex_returns_hex_string() {
520 let cases: [(u32, &str); 8] = [
521 (0, "0"),
522 (1, "1"),
523 (0xD, "d"),
524 (0xDE, "de"),
525 (0xDEA, "dea"),
526 (0xDEAD, "dead"),
527 (0x80, "80"),
528 (0xFF, "ff"),
529 ];
530 for (number, expected) in cases {
531 assert_eq!(to_server_hex(&BoxedUint::from(number)), expected);
532 }
533 }
534
535 #[test]
536 fn from_server_hex_rejects_malformed_values() {
537 for input in ["", "not hex", "é", &"f".repeat(N_HEX.len() + 1)] {
538 from_server_hex(input).expect_err("malformed values are rejected");
539 }
540 }
541
542 #[test]
544 fn validate_b_rejects_zero_and_one_mod_n() {
545 let n = big(N_HEX);
546 for value in [big("0"), big("1"), n.clone(), n.wrapping_add(big("1"))] {
547 validate_b(&value).expect_err("B must not be 0 or 1 mod N");
548 }
549
550 validate_b(&big(SHARED_B)).expect("a normal B is accepted");
551 }
552
553 #[test]
554 fn compute_key_returns_key() {
555 let secret_a = BoxedUint::from_be_hex(
556 "37bbf7bf6a51f902673556ea6a2db91dd9987554ab74c3bc089b213693d9c06e",
557 SCALAR_BITS,
558 )
559 .expect("valid hex");
560 let srp_x = HEXLOWER
561 .decode(b"9559afc0581390b1190a57dd281729baa237760982c7369c4c14d42157703a0f")
562 .expect("valid hex");
563
564 let key = compute_key(&secret_a, &big(SHARED_A), &big(SHARED_B), &srp_x);
565
566 assert_eq!(
567 HEXLOWER.encode(&key),
568 "9d17458228928fc1107668113026390d502a40954e3e6a83513acbb2e1f8fedc"
569 );
570 }
571
572 #[test]
573 fn calculate_client_hash_returns_hash() {
574 let session_key = HEXLOWER
575 .decode(b"9d17458228928fc1107668113026390d502a40954e3e6a83513acbb2e1f8fedc")
576 .expect("valid hex");
577 let salt = HEXLOWER
578 .decode(b"c813e48eb6e88c7557c9a70fcbda0fbc")
579 .expect("valid hex");
580
581 let hash = calculate_client_hash(
582 &session_key,
583 "[email protected]",
584 "P9JQCW",
585 &salt,
586 &big(SHARED_A),
587 &big(SHARED_B),
588 );
589
590 assert_eq!(
591 HEXLOWER.encode(&hash),
592 "e74d30467ccdfdf7d61973b9a94f88bd2b7155ba304138f5d02e2078c3a124fa"
593 );
594 }
595
596 #[test]
597 fn compute_x_returns_x() {
598 let salt =
599 super::super::opdata::decode64_loose("-JLqTVQLjQg08LWZ0gyuUA").expect("valid salt");
600 let account_key =
601 AccountKey::parse("A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9").expect("valid account key");
602
603 let srp_info = SrpInfo::new("SRPg-4096".into(), "PBES2g-HS256".into(), 100000, salt)
604 .expect("supported parameters");
605
606 let credentials = Credentials {
607 username: "username".into(),
608 password: "password".into(),
609 account_key: "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9".into(),
610 sign_in_address: SignInAddress {
611 subdomain: "my".into(),
612 domain: SignInDomain::Global,
613 },
614 };
615
616 let x = compute_x(&credentials, &account_key, &srp_info).expect("derivation succeeds");
617
618 assert_eq!(
619 HEXLOWER.encode(&x),
620 "e7e14f282b01332cc193dc42f8501e3ffe8afdbf4b431ed4bfd885ff0bdfecf3"
621 );
622 }
623
624 #[tokio::test]
625 async fn rejects_a_precomputed_x_longer_than_a_scalar() {
626 let server = MockServer::start().await;
627 let rest = RestClient::new(
628 new_http_client(),
629 format!("http://{}/api", server.address()),
630 "client-id",
631 "user-agent",
632 "op-user-agent",
633 )
634 .expect("valid headers");
635
636 let error =
637 perform_and_verify_with_x(&[1; 33], "username", "key-uuid", b"salt", "SESSION", &rest)
638 .await
639 .map(|_| ())
640 .expect_err("33 bytes do not fit the scalar");
641
642 assert!(matches!(error, OnePasswordError::Internal(_)));
643 assert!(
644 server
645 .received_requests()
646 .await
647 .unwrap_or_default()
648 .is_empty()
649 );
650 }
651
652 #[test]
653 fn leading_zeros_do_not_count_towards_the_width_of_a_precomputed_x() {
654 let x = [0x5a; 32];
655 let mut padded = vec![0];
656 padded.extend_from_slice(&x);
657
658 assert_eq!(scalar_bytes(&x).expect("fits"), x);
659 assert_eq!(scalar_bytes(&padded).expect("fits"), x);
660 }
661
662 #[test]
663 fn a_precomputed_x_of_only_zeros_is_a_zero_scalar() {
664 let significant = scalar_bytes(&[0; 33]).expect("fits");
665
666 assert!(bool::from(scalar(significant).is_zero()));
667 }
668
669 #[tokio::test]
671 async fn a_precomputed_x_reaches_the_key_the_password_path_derives() {
672 let salt =
673 super::super::opdata::decode64_loose("-JLqTVQLjQg08LWZ0gyuUA").expect("valid salt");
674 let account_key =
675 AccountKey::parse("A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9").expect("valid account key");
676 let credentials = Credentials {
677 username: "username".into(),
678 password: "password".into(),
679 account_key: "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9".into(),
680 sign_in_address: SignInAddress {
681 subdomain: "my".into(),
682 domain: SignInDomain::Global,
683 },
684 };
685 let srp_info = SrpInfo::new("SRPg-4096".into(), "PBES2g-HS256".into(), 100000, salt)
686 .expect("supported parameters");
687
688 let srp_x = compute_x(&credentials, &account_key, &srp_info).expect("derivation succeeds");
690
691 let secret_a = BoxedUint::from_be_hex(
692 "37bbf7bf6a51f902673556ea6a2db91dd9987554ab74c3bc089b213693d9c06e",
693 SCALAR_BITS,
694 )
695 .expect("valid hex");
696 let shared_a = compute_shared_a(&secret_a);
697 let shared_b = big(SHARED_B);
698 let expected = compute_key(&secret_a, &shared_a, &shared_b, &srp_x);
699
700 let client_hash = calculate_client_hash(
702 &expected,
703 &credentials.username,
704 &account_key.uuid,
705 srp_info.salt(),
706 &shared_a,
707 &shared_b,
708 );
709 let server_hash =
710 BASE64URL_NOPAD.encode(&calculate_server_hash(&shared_a, &client_hash, &expected));
711
712 let server = MockServer::start().await;
713 server
714 .register(
715 Mock::given(matchers::path("/api/v2/auth"))
716 .respond_with(
717 ResponseTemplate::new(200).set_body_json(json!({ "userB": SHARED_B })),
718 )
719 .expect(1),
720 )
721 .await;
722 server
723 .register(
724 Mock::given(matchers::path("/api/v2/auth/confirm-key"))
725 .respond_with(
726 ResponseTemplate::new(200)
727 .set_body_json(json!({ "serverVerifyHash": server_hash })),
728 )
729 .expect(1),
730 )
731 .await;
732 let rest = RestClient::new(
733 new_http_client(),
734 format!("http://{}/api", server.address()),
735 "client-id",
736 "user-agent",
737 "op-user-agent",
738 )
739 .expect("valid headers");
740
741 let key = perform(
742 &secret_a,
743 &srp_x,
744 &credentials.username,
745 &account_key.uuid,
746 srp_info.salt(),
747 &rest,
748 )
749 .await
750 .expect("the exchange verifies against the mock server");
751
752 assert_eq!(HEXLOWER.encode(&key), HEXLOWER.encode(&expected));
753 server.verify().await;
754 }
755
756 #[test]
759 fn calculate_server_hash_returns_hash() {
760 let client_hash = HEXLOWER
761 .decode(b"e74d30467ccdfdf7d61973b9a94f88bd2b7155ba304138f5d02e2078c3a124fa")
762 .expect("valid hex");
763 let session_key = HEXLOWER
764 .decode(b"9d17458228928fc1107668113026390d502a40954e3e6a83513acbb2e1f8fedc")
765 .expect("valid hex");
766
767 let hash = calculate_server_hash(&big(SHARED_A), &client_hash, &session_key);
768
769 assert_eq!(
770 HEXLOWER.encode(&hash),
771 "e3ebd2e43efdc593960267f26b8a149e4368a5d5e963995ef9ceb6820307d6f9"
772 );
773 }
774
775 const VERIFY_SESSION_KEY: [u8; 32] = [0u8; 32];
776 const VERIFY_USERNAME: &str = "[email protected]";
777 const VERIFY_KEY_UUID: &str = "RTN9SA";
778 const VERIFY_SALT: &[u8] = b"salt";
779
780 fn verify_ephemerals() -> (BoxedUint, BoxedUint) {
781 (BoxedUint::from(2u32), BoxedUint::from(3u32))
782 }
783
784 async fn run_verify_key(server_hash: &str) -> Result<(), OnePasswordError> {
786 run_verify_key_against(
787 ResponseTemplate::new(200).set_body_json(json!({ "serverVerifyHash": server_hash })),
788 )
789 .await
790 }
791
792 async fn run_verify_key_against(response: ResponseTemplate) -> Result<(), OnePasswordError> {
794 let server = MockServer::start().await;
795 server
796 .register(
797 Mock::given(matchers::path("/api/v2/auth/confirm-key"))
798 .respond_with(response)
799 .expect(1),
800 )
801 .await;
802 let rest = RestClient::new(
803 new_http_client(),
804 format!("http://{}/api", server.address()),
805 "client-id",
806 "user-agent",
807 "op-user-agent",
808 )
809 .expect("valid headers");
810
811 let (shared_a, shared_b) = verify_ephemerals();
812 let result = verify_key(
813 &VERIFY_SESSION_KEY,
814 VERIFY_USERNAME,
815 VERIFY_KEY_UUID,
816 VERIFY_SALT,
817 &shared_a,
818 &shared_b,
819 &rest,
820 )
821 .await;
822
823 server.verify().await;
824 result
825 }
826
827 #[tokio::test]
828 async fn verify_key_reports_a_rejected_proof_as_bad_credentials() {
829 let error = run_verify_key_against(ResponseTemplate::new(401))
830 .await
831 .expect_err("the server rejects the proof");
832
833 assert!(matches!(error, OnePasswordError::BadCredentials));
834 }
835
836 #[tokio::test]
837 async fn verify_key_keeps_other_failures() {
838 let error = run_verify_key_against(ResponseTemplate::new(503))
839 .await
840 .expect_err("the server is unavailable");
841
842 assert!(matches!(
843 error,
844 OnePasswordError::UnexpectedStatus { status: 503, .. }
845 ));
846 }
847
848 #[tokio::test]
849 async fn verify_key_accepts_a_matching_server_hash() {
850 let (shared_a, shared_b) = verify_ephemerals();
851 let client_hash = calculate_client_hash(
852 &VERIFY_SESSION_KEY,
853 VERIFY_USERNAME,
854 VERIFY_KEY_UUID,
855 VERIFY_SALT,
856 &shared_a,
857 &shared_b,
858 );
859 let server_hash = BASE64URL_NOPAD.encode(&calculate_server_hash(
860 &shared_a,
861 &client_hash,
862 &VERIFY_SESSION_KEY,
863 ));
864
865 run_verify_key(&server_hash)
866 .await
867 .expect("the server proved it holds the session key");
868 }
869
870 #[tokio::test]
871 async fn verify_key_rejects_a_server_hash_it_did_not_expect() {
872 for server_hash in ["", "not-the-hash"] {
873 let error = run_verify_key(server_hash)
874 .await
875 .expect_err("the server did not prove it holds the session key");
876
877 assert!(
878 error.to_string().contains("does not match"),
879 "unexpected error: {error}"
880 );
881 }
882 }
883
884 fn test_server() -> TestServer {
885 TestServer::new(
886 &[0x5a; 32],
887 &[0x33; 32],
888 VERIFY_USERNAME,
889 VERIFY_KEY_UUID,
890 VERIFY_SALT,
891 )
892 }
893
894 fn client_proof(server: &TestServer, srp_x: &[u8]) -> (String, [u8; 32], [u8; 32]) {
896 let secret_a = scalar(&[0x77; 32]);
897 let shared_a = compute_shared_a(&secret_a);
898 let shared_b = from_server_hex(&server.shared_b()).expect("valid B");
899
900 let key = compute_key(&secret_a, &shared_a, &shared_b, srp_x);
901 let proof = calculate_client_hash(
902 &key,
903 VERIFY_USERNAME,
904 VERIFY_KEY_UUID,
905 VERIFY_SALT,
906 &shared_a,
907 &shared_b,
908 );
909 (to_server_hex(&shared_a), key, proof)
910 }
911
912 #[test]
914 fn the_test_server_agrees_with_the_client() {
915 let server = test_server();
916 let (shared_a, key, proof) = client_proof(&server, &[0x5a; 32]);
917
918 assert_eq!(server.session_key(&shared_a), Some(key));
919 assert_eq!(
920 server.confirm_key(&shared_a, &BASE64URL_NOPAD.encode(&proof)),
921 Some(BASE64URL_NOPAD.encode(&calculate_server_hash(&big(&shared_a), &proof, &key)))
922 );
923 }
924
925 #[test]
926 fn the_test_server_rejects_a_client_that_does_not_know_the_account() {
927 let server = test_server();
928 let (shared_a, _, proof) = client_proof(&server, &[0x5b; 32]);
929
930 assert!(
931 server
932 .confirm_key(&shared_a, &BASE64URL_NOPAD.encode(&proof))
933 .is_none()
934 );
935 }
936
937 #[test]
938 fn srp_info_rejects_unsupported_parameters() {
939 let bad_method = SrpInfo::new("SRPg-2048".into(), "PBES2g-HS256".into(), 100000, vec![])
940 .expect_err("only SRPg-4096 is supported");
941 assert!(bad_method.to_string().contains("SRPg-2048"));
942
943 let too_few = SrpInfo::new("SRPg-4096".into(), "PBES2g-HS256".into(), 9999, vec![])
944 .expect_err("a count below the floor is rejected");
945 assert!(too_few.to_string().contains("9999 iterations"));
946
947 let bad_key_method =
948 SrpInfo::new("SRPg-4096".into(), "PBES2g-HS512".into(), 100000, vec![])
949 .expect_err("only the SHA-256 variants are supported");
950 assert!(bad_key_method.to_string().contains("PBES2g-HS512"));
951 }
952}