1use bitwarden_threading::time;
8use curve25519_dalek::{
9 ristretto::{CompressedRistretto, RistrettoPoint},
10 scalar::Scalar,
11 traits::Identity,
12};
13use data_encoding::BASE64URL_NOPAD;
14use hmac::{Hmac, KeyInit, Mac};
15use rand::Rng;
16use serde::de::DeserializeOwned;
17use serde_json::{Value, json};
18use sha2::{Digest, Sha384, Sha512};
19use zeroize::Zeroizing;
20
21use super::{
22 super::{
23 error::OnePasswordError,
24 kdf,
25 opdata::{AesKey, Encrypted, decode64_loose},
26 rest::RestClient,
27 wire::{self, CredentialBundle, EncryptedEnvelope, SuccessStatus},
28 },
29 Timing,
30};
31
32const PASSCODE_ITERATIONS: u32 = 100_000;
33
34const PADDING_LENGTH: usize = 76;
36
37const TRANSPORT_KEY_ID: &str = "1P_SSO_ENROLLMENT_TRANSPORT_KEY";
39const EXCHANGE_KEY_INFO: &[u8] = b"1P_SSO_CREDENTIAL_BUNDLE_KEY:ENROLL\x01";
40const CREDENTIAL_BUNDLE_AAD: &[u8] = b"1P_SSO_CREDENTIAL_BUNDLE:ENROLL";
41
42#[derive(Debug)]
44struct MsgA {
45 ya: RistrettoPoint,
47 ad: Ad,
48 original_message: Vec<u8>,
50}
51
52#[derive(Debug)]
54struct Ad {
55 #[cfg_attr(not(test), allow(dead_code))]
56 version: i32,
57 salt: Vec<u8>,
58 session_id: Vec<u8>,
59}
60
61fn parse_msg_a(json: &[u8]) -> Result<MsgA, OnePasswordError> {
66 let msg_a: wire::MsgA = serde_json::from_slice(json).map_err(|_| OnePasswordError::Parse)?;
67 let ya = decompress_point(&decode64_loose(&msg_a.ya)?)?;
68 if ya == RistrettoPoint::identity() {
69 return Err(OnePasswordError::Internal(
70 "the CPace point must not be the identity".into(),
71 ));
72 }
73
74 Ok(MsgA {
75 ya,
76 ad: Ad {
77 version: msg_a.ad.version,
78 salt: decode64_loose(&msg_a.ad.salt)?,
79 session_id: msg_a.ad.session_id,
80 },
81 original_message: json.to_vec(),
82 })
83}
84
85fn decompress_point(bytes: &[u8]) -> Result<RistrettoPoint, OnePasswordError> {
87 CompressedRistretto::from_slice(bytes)
88 .ok()
89 .and_then(|point| point.decompress())
90 .ok_or_else(|| OnePasswordError::Internal("invalid CPace point".into()))
91}
92
93fn calculate_msg_b(
95 username: &str,
96 sign_in_address: &str,
97 enrollment_uuid: &str,
98 verification_code: &str,
99 msg_a: &MsgA,
100 client_secret: &[u8; 64],
101) -> [u8; 32] {
102 let passcode_hash = calculate_passcode_hash(verification_code, &msg_a.ad.salt);
103 let first_hash = calculate_first_hash(username, sign_in_address, enrollment_uuid, msg_a);
104 let second_hash = Zeroizing::new(calculate_second_hash(
105 &first_hash,
106 passcode_hash.as_slice(),
107 msg_a,
108 ));
109 let generator = RistrettoPoint::from_uniform_bytes(&second_hash);
110 multiply_by_client_scalar(&generator, client_secret)
111}
112
113fn calculate_passcode_hash(passcode: &str, salt: &[u8]) -> Zeroizing<[u8; 32]> {
115 Zeroizing::new(kdf::pbes2(passcode, salt, PASSCODE_ITERATIONS))
116}
117
118fn calculate_first_hash(
120 email: &str,
121 sign_in_address: &str,
122 enrollment_uuid: &str,
123 msg_a: &MsgA,
124) -> [u8; 64] {
125 sha512(&leb128_prefixed(&[
126 b"1P-SSO-TRUSTED-DEVICE",
127 b"1P-SSO-NEW-DEVICE",
128 email.as_bytes(),
129 sign_in_address.as_bytes(),
130 enrollment_uuid.as_bytes(),
131 &msg_a.ad.salt,
132 &[1],
133 ]))
134}
135
136fn calculate_second_hash(first_hash: &[u8], passcode_hash: &[u8], msg_a: &MsgA) -> [u8; 64] {
138 sha512(&leb128_prefixed(&[
139 b"CPaceRistretto255",
140 passcode_hash,
141 &[0; PADDING_LENGTH],
142 first_hash,
143 &msg_a.ad.session_id,
144 ]))
145}
146
147fn multiply_by_client_scalar(point: &RistrettoPoint, client_secret: &[u8; 64]) -> [u8; 32] {
149 let scalar = Zeroizing::new(Scalar::from_bytes_mod_order_wide(client_secret));
150 (point * *scalar).compress().to_bytes()
151}
152
153fn leb128_prefixed(fields: &[&[u8]]) -> Zeroizing<Vec<u8>> {
155 let mut buffer = Zeroizing::new(Vec::new());
156 for field in fields {
157 write_leb128_prefixed(&mut buffer, field);
158 }
159 buffer
160}
161
162fn write_leb128_prefixed(buffer: &mut Vec<u8>, data: &[u8]) {
164 let mut length = data.len();
165 loop {
166 let byte = (length & 0x7f) as u8;
167 length >>= 7;
168 if length == 0 {
169 buffer.push(byte);
170 break;
171 }
172 buffer.push(byte | 0x80);
173 }
174 buffer.extend_from_slice(data);
175}
176
177fn verify_tag_a(_tag_a: &[u8]) {}
181
182fn calculate_auth_b_tag_b(
184 client_secret: &[u8; 64],
185 msg_a: &MsgA,
186 msg_b: &[u8],
187) -> (Zeroizing<[u8; 64]>, [u8; 64]) {
188 let auth_a = calculate_auth_a_hash(msg_a, client_secret);
189 let auth_b = calculate_auth_b_hash(auth_a.as_slice(), msg_a, msg_b);
190 let tag_b = calculate_tag_b(auth_b.as_slice(), msg_b);
191 (auth_b, tag_b)
192}
193
194fn calculate_auth_a_hash(msg_a: &MsgA, client_secret: &[u8; 64]) -> Zeroizing<[u8; 32]> {
196 Zeroizing::new(multiply_by_client_scalar(&msg_a.ya, client_secret))
197}
198
199fn calculate_auth_b_hash(auth_a: &[u8], msg_a: &MsgA, msg_b: &[u8]) -> Zeroizing<[u8; 64]> {
201 let mut buffer = leb128_prefixed(&[b"CPaceRistretto255_ISK", &msg_a.ad.session_id, auth_a]);
202 buffer.extend_from_slice(&msg_a.original_message);
203 buffer.extend_from_slice(msg_b);
204 Zeroizing::new(sha512(&buffer))
205}
206
207fn calculate_tag_b(auth_b: &[u8], msg_b: &[u8]) -> [u8; 64] {
209 let mac_key = Zeroizing::new(<[u8; 64]>::from(
210 Sha512::new()
211 .chain_update(b"CPaceMac")
212 .chain_update(auth_b)
213 .finalize(),
214 ));
215 hmac_sha512(mac_key.as_slice(), msg_b)
216}
217
218fn derive_exchange_key(auth_b: &[u8]) -> Zeroizing<[u8; 32]> {
220 let transport_key = hmac_sha384(TRANSPORT_KEY_ID.as_bytes(), auth_b);
221 let exchange_key = hmac_sha384(transport_key.as_slice(), EXCHANGE_KEY_INFO);
222
223 let mut key = Zeroizing::new([0; 32]);
224 key.copy_from_slice(&exchange_key[..32]);
225 key
226}
227
228fn decrypt_credentials(
230 encrypted_credentials: &EncryptedEnvelope,
231 exchange_key: &[u8],
232) -> Result<CredentialBundle, OnePasswordError> {
233 let key = AesKey::new(TRANSPORT_KEY_ID, exchange_key.to_vec());
234 let encrypted = Encrypted::parse(encrypted_credentials)?;
235 let credentials = Zeroizing::new(key.decrypt_with_aad(&encrypted, CREDENTIAL_BUNDLE_AAD)?);
236
237 serde_json::from_slice(&credentials).map_err(|_| OnePasswordError::Parse)
239}
240
241fn sha512(data: &[u8]) -> [u8; 64] {
242 Sha512::digest(data).into()
243}
244
245fn hmac_sha384(key: &[u8], message: &[u8]) -> Zeroizing<[u8; 48]> {
246 let mut mac =
247 <Hmac<Sha384> as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
248 mac.update(message);
249 Zeroizing::new(mac.finalize().into_bytes().into())
250}
251
252fn hmac_sha512(key: &[u8], message: &[u8]) -> [u8; 64] {
253 let mut mac =
254 <Hmac<Sha512> as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
255 mac.update(message);
256 mac.finalize().into_bytes().into()
257}
258
259pub(super) async fn perform_cpace(
262 username: &str,
263 sign_in_address: &str,
264 sign_in_token: &str,
265 enrollment_uuid: &str,
266 verification_code: &str,
267 rest: &RestClient,
268 timing: &Timing,
269) -> Result<CredentialBundle, OnePasswordError> {
270 let mut client_secret = Zeroizing::new([0u8; 64]);
271 bitwarden_random::rng().fill_bytes(&mut *client_secret);
272
273 perform_cpace_with_client_secret(
274 username,
275 sign_in_address,
276 sign_in_token,
277 enrollment_uuid,
278 verification_code,
279 &client_secret,
280 rest,
281 timing,
282 )
283 .await
284}
285
286#[allow(clippy::too_many_arguments)]
288async fn perform_cpace_with_client_secret(
289 username: &str,
290 sign_in_address: &str,
291 sign_in_token: &str,
292 enrollment_uuid: &str,
293 verification_code: &str,
294 client_secret: &[u8; 64],
295 rest: &RestClient,
296 timing: &Timing,
297) -> Result<CredentialBundle, OnePasswordError> {
298 let msg_a = request_msg_a(sign_in_token, enrollment_uuid, rest).await?;
299
300 let msg_b = calculate_msg_b(
301 username,
302 sign_in_address,
303 enrollment_uuid,
304 verification_code,
305 &msg_a,
306 client_secret,
307 );
308 send_msg_b(&msg_b, sign_in_token, enrollment_uuid, rest).await?;
309
310 let tag_a = get_tag_a(sign_in_token, enrollment_uuid, rest, timing).await?;
311 verify_tag_a(&tag_a);
312
313 let (auth_b, tag_b) = calculate_auth_b_tag_b(client_secret, &msg_a, &msg_b);
314 send_tag_b(&tag_b, sign_in_token, enrollment_uuid, rest).await?;
315
316 let encrypted_credentials =
317 request_encrypted_credentials(sign_in_token, enrollment_uuid, rest, timing).await?;
318 let exchange_key = derive_exchange_key(auth_b.as_slice());
319
320 decrypt_credentials(&encrypted_credentials, exchange_key.as_slice())
321}
322
323async fn request_msg_a(
325 sign_in_token: &str,
326 enrollment_uuid: &str,
327 rest: &RestClient,
328) -> Result<MsgA, OnePasswordError> {
329 let response: wire::CpaceMsgA = rest
330 .post_json(
331 &cpace_endpoint(enrollment_uuid, "msga"),
332 sign_in_body(sign_in_token),
333 )
334 .await?;
335
336 parse_msg_a(&decode64_loose(&response.msga)?)
337}
338
339async fn send_msg_b(
341 msg_b: &[u8],
342 sign_in_token: &str,
343 enrollment_uuid: &str,
344 rest: &RestClient,
345) -> Result<(), OnePasswordError> {
346 let response: SuccessStatus = rest
347 .put_json(
348 &cpace_endpoint(enrollment_uuid, "msgb"),
349 json!({
350 "signInToken": sign_in_token,
351 "msgb": BASE64URL_NOPAD.encode(msg_b),
352 }),
353 )
354 .await?;
355
356 expect_success(&response, "exchange credentials")
357}
358
359async fn get_tag_a(
361 sign_in_token: &str,
362 enrollment_uuid: &str,
363 rest: &RestClient,
364 timing: &Timing,
365) -> Result<Vec<u8>, OnePasswordError> {
366 let result: wire::CpaceTagA = post_json_until_result(
367 &cpace_endpoint(enrollment_uuid, "taga"),
368 sign_in_body(sign_in_token),
369 rest,
370 timing,
371 )
372 .await?;
373
374 decode64_loose(&result.taga)
375}
376
377async fn send_tag_b(
379 tag_b: &[u8],
380 sign_in_token: &str,
381 enrollment_uuid: &str,
382 rest: &RestClient,
383) -> Result<(), OnePasswordError> {
384 let response: SuccessStatus = rest
385 .put_json(
386 &cpace_endpoint(enrollment_uuid, "tagb"),
387 json!({
388 "signInToken": sign_in_token,
389 "tagb": BASE64URL_NOPAD.encode(tag_b),
390 }),
391 )
392 .await?;
393
394 expect_success(&response, "submit tagb")
395}
396
397async fn request_encrypted_credentials(
400 sign_in_token: &str,
401 enrollment_uuid: &str,
402 rest: &RestClient,
403 timing: &Timing,
404) -> Result<EncryptedEnvelope, OnePasswordError> {
405 let result: wire::SharedCredentials = post_json_until_result(
406 &enrollment_endpoint(enrollment_uuid, "share/credentials"),
407 sign_in_body(sign_in_token),
408 rest,
409 timing,
410 )
411 .await?;
412
413 Ok(result.encrypted_credentials)
414}
415
416async fn post_json_until_result<T: DeserializeOwned>(
419 endpoint: &str,
420 body: Value,
421 rest: &RestClient,
422 timing: &Timing,
423) -> Result<T, OnePasswordError> {
424 for _ in 0..timing.until_result_attempts {
425 match rest.post_json_or_empty(endpoint, body.clone()).await? {
426 Some(result) => return Ok(result),
427 None => time::sleep(timing.until_result_delay).await,
428 }
429 }
430
431 Err(OnePasswordError::Internal(format!(
432 "failed to get a response from the server (endpoint: {endpoint})"
433 )))
434}
435
436fn sign_in_body(sign_in_token: &str) -> Value {
438 json!({ "signInToken": sign_in_token })
439}
440
441fn expect_success(response: &SuccessStatus, action: &str) -> Result<(), OnePasswordError> {
443 if response.success != 1 {
444 return Err(OnePasswordError::Internal(format!("failed to {action}")));
445 }
446 Ok(())
447}
448
449fn enrollment_endpoint(enrollment_uuid: &str, method: &str) -> String {
451 format!("v3/device/enrollments/{enrollment_uuid}/{method}")
452}
453
454fn cpace_endpoint(enrollment_uuid: &str, method: &str) -> String {
456 enrollment_endpoint(enrollment_uuid, &format!("cpace/{method}"))
457}
458
459#[cfg(test)]
460mod tests {
461 use data_encoding::HEXLOWER;
462 use serde_json::{Value, json};
463 use wiremock::{Mock, MockServer, ResponseTemplate, matchers};
464
465 use super::{
466 super::test_support::{
467 CpaceVectors, SIGN_IN_TOKEN, hex, hex_array, mock_enrolled_device, mock_msga, path,
468 quick_timing, rest_client, vectors,
469 },
470 *,
471 };
472
473 fn msg_a() -> MsgA {
474 parse_msg_a(vectors().cpace.msga_json.as_bytes()).expect("valid msga")
475 }
476
477 #[test]
478 fn parses_msg_a() {
479 let vectors = vectors().cpace;
480 let msg_a = msg_a();
481
482 assert_eq!(msg_a.original_message, vectors.msga_json.as_bytes());
483 assert_eq!(msg_a.ad.version, 1);
484 assert_eq!(msg_a.ad.salt, (1..=16).collect::<Vec<u8>>());
485 assert_eq!(msg_a.ad.session_id, (100..116).collect::<Vec<u8>>());
486 assert_eq!(
487 BASE64URL_NOPAD.encode(&msg_a.ya.compress().to_bytes()),
488 "IrCugkEa6t7w1_c8A8rYXtk4flTBrMStgtMSEf7qFUE"
489 );
490 }
491
492 fn edited_msg_a(pointer: &str, replacement: Option<Value>) -> Vec<u8> {
494 let mut msg_a: Value =
495 serde_json::from_str(&vectors().cpace.msga_json).expect("valid json");
496 let (parent, key) = pointer.rsplit_once('/').expect("a pointer to a member");
497 let parent = msg_a
498 .pointer_mut(parent)
499 .and_then(Value::as_object_mut)
500 .expect("an existing object");
501 match replacement {
502 Some(value) => parent.insert(key.into(), value),
503 None => parent.remove(key),
504 };
505 serde_json::to_vec(&msg_a).expect("serializes")
506 }
507
508 #[test]
509 fn rejects_malformed_msg_a() {
510 let point = |bytes: &[u8]| Some(json!(BASE64URL_NOPAD.encode(bytes)));
511
512 let cases = [
513 ("not json", b"not json".to_vec()),
514 ("empty", Vec::new()),
515 ("ya is not base64", edited_msg_a("/ya", Some(json!("***")))),
516 ("ya is null", edited_msg_a("/ya", Some(Value::Null))),
517 (
518 "ya is not canonical",
519 edited_msg_a("/ya", point(&[0xff; 32])),
520 ),
521 ("ya is too short", edited_msg_a("/ya", point(&[1; 31]))),
522 ("ya is too long", edited_msg_a("/ya", point(&[1; 33]))),
523 ("ya is the identity", edited_msg_a("/ya", point(&[0; 32]))),
524 (
525 "salt is not base64",
526 edited_msg_a("/ad/salt", Some(json!("***"))),
527 ),
528 (
529 "session id is above 255",
530 edited_msg_a("/ad/session_id", Some(json!([100, 256]))),
531 ),
532 (
533 "session id is negative",
534 edited_msg_a("/ad/session_id", Some(json!([-1]))),
535 ),
536 (
537 "session id is not an array",
538 edited_msg_a("/ad/session_id", Some(json!("abc"))),
539 ),
540 ("ya is missing", edited_msg_a("/ya", None)),
541 ("ad is missing", edited_msg_a("/ad", None)),
542 ("version is missing", edited_msg_a("/ad/version", None)),
543 ("salt is missing", edited_msg_a("/ad/salt", None)),
544 (
545 "session id is missing",
546 edited_msg_a("/ad/session_id", None),
547 ),
548 ];
549
550 for (name, json) in cases {
551 assert!(parse_msg_a(&json).is_err(), "{name}");
552 }
553 }
554
555 #[test]
556 fn rejects_an_invalid_point_with_an_internal_error() {
557 let json = edited_msg_a("/ya", Some(json!(BASE64URL_NOPAD.encode(&[0xff; 32]))));
558
559 assert!(matches!(
560 parse_msg_a(&json),
561 Err(OnePasswordError::Internal(_))
562 ));
563 }
564
565 #[test]
566 fn writes_leb128_length_prefixes() {
567 let table = vectors().leb128_prefix;
568 assert!(!table.is_empty());
569
570 for (length, prefix) in table {
571 let data = vec![0xab; length];
572 let mut buffer = Vec::new();
573 write_leb128_prefixed(&mut buffer, &data);
574
575 let (written_prefix, written_data) = buffer.split_at(buffer.len() - length);
576 assert_eq!(HEXLOWER.encode(written_prefix), prefix, "length {length}");
577 assert_eq!(written_data, data, "length {length}");
578 }
579 }
580
581 #[test]
582 fn calculates_passcode_hash() {
583 let vectors = vectors().cpace;
584
585 assert_eq!(
586 HEXLOWER.encode(
587 calculate_passcode_hash(&vectors.verification_code, &msg_a().ad.salt).as_slice()
588 ),
589 vectors.passcode_hash
590 );
591 }
592
593 #[test]
594 fn calculates_first_hash() {
595 let vectors = vectors().cpace;
596
597 let hash = calculate_first_hash(
598 &vectors.username,
599 &vectors.sign_in_address,
600 &vectors.enrollment_uuid,
601 &msg_a(),
602 );
603
604 assert_eq!(HEXLOWER.encode(&hash), vectors.first_hash);
605 }
606
607 #[test]
608 fn calculates_second_hash() {
609 let vectors = vectors().cpace;
610
611 let hash = calculate_second_hash(
612 &hex(&vectors.first_hash),
613 &hex(&vectors.passcode_hash),
614 &msg_a(),
615 );
616
617 assert_eq!(HEXLOWER.encode(&hash), vectors.second_hash);
618 }
619
620 #[test]
621 fn calculates_msg_b() {
622 let vectors = vectors().cpace;
623
624 let msg_b = calculate_msg_b(
625 &vectors.username,
626 &vectors.sign_in_address,
627 &vectors.enrollment_uuid,
628 &vectors.verification_code,
629 &msg_a(),
630 &hex_array(&vectors.client_secret),
631 );
632
633 assert_eq!(HEXLOWER.encode(&msg_b), vectors.msgb);
634 }
635
636 #[test]
637 fn calculates_auth_a() {
638 let vectors = vectors().cpace;
639
640 let auth_a = calculate_auth_a_hash(&msg_a(), &hex_array(&vectors.client_secret));
641
642 assert_eq!(HEXLOWER.encode(auth_a.as_slice()), vectors.auth_a);
643 }
644
645 #[test]
646 fn calculates_auth_b() {
647 let vectors = vectors().cpace;
648
649 let auth_b = calculate_auth_b_hash(&hex(&vectors.auth_a), &msg_a(), &hex(&vectors.msgb));
650
651 assert_eq!(HEXLOWER.encode(auth_b.as_slice()), vectors.auth_b);
652 }
653
654 #[test]
655 fn calculates_tag_b() {
656 let vectors = vectors().cpace;
657
658 let tag_b = calculate_tag_b(&hex(&vectors.auth_b), &hex(&vectors.msgb));
659
660 assert_eq!(HEXLOWER.encode(&tag_b), vectors.tag_b);
661 }
662
663 #[test]
664 fn calculates_auth_b_and_tag_b_together() {
665 let vectors = vectors().cpace;
666
667 let (auth_b, tag_b) = calculate_auth_b_tag_b(
668 &hex_array(&vectors.client_secret),
669 &msg_a(),
670 &hex(&vectors.msgb),
671 );
672
673 assert_eq!(HEXLOWER.encode(auth_b.as_slice()), vectors.auth_b);
674 assert_eq!(HEXLOWER.encode(&tag_b), vectors.tag_b);
675 }
676
677 #[test]
678 fn derives_exchange_key() {
679 let vectors = vectors().cpace;
680
681 let key = derive_exchange_key(&hex(&vectors.auth_b));
682
683 assert_eq!(HEXLOWER.encode(key.as_slice()), vectors.exchange_key);
684 }
685
686 #[test]
687 fn decrypts_credentials() {
688 let vectors = vectors().cpace;
689
690 let bundle =
691 decrypt_credentials(&vectors.encrypted_credentials, &hex(&vectors.exchange_key))
692 .expect("decrypts");
693
694 assert_eq!(*bundle.srpx, "oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8");
695 assert_eq!(*bundle.auk.k, "WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM");
696 }
697
698 #[test]
699 fn decrypting_credentials_fails_with_another_key() {
700 let vectors = vectors().cpace;
701
702 assert!(decrypt_credentials(&vectors.encrypted_credentials, &[0; 32]).is_err());
703 }
704
705 #[test]
706 fn decrypting_credentials_fails_with_another_associated_data() {
707 let vectors = vectors().cpace;
708 let key = AesKey::new(TRANSPORT_KEY_ID, hex(&vectors.exchange_key));
709 let envelope = key
710 .encrypt_with_aad(
711 vectors.credential_bundle_json.as_bytes(),
712 &[7; 12],
713 b"other",
714 )
715 .expect("encrypts");
716
717 assert!(decrypt_credentials(&envelope, &key.key).is_err());
718 }
719
720 #[test]
721 fn decrypting_credentials_fails_on_malformed_bundle() {
722 let vectors = vectors().cpace;
723 let key = AesKey::new(TRANSPORT_KEY_ID, hex(&vectors.exchange_key));
724 let envelope = key
725 .encrypt_with_aad(br#"{"srpx":"x"}"#, &[7; 12], CREDENTIAL_BUNDLE_AAD)
726 .expect("encrypts");
727
728 assert!(matches!(
729 decrypt_credentials(&envelope, &key.key),
730 Err(OnePasswordError::Parse)
731 ));
732 }
733
734 #[test]
736 fn ya_is_the_generator_times_the_enrolled_secret() {
737 let vectors = vectors().cpace;
738 let ya_scalar = Scalar::from_bytes_mod_order_wide(&hex_array(&vectors.ya_secret));
739 let generator = RistrettoPoint::from_uniform_bytes(&hex_array(&vectors.second_hash));
740
741 assert_eq!((generator * ya_scalar).compress(), msg_a().ya.compress());
742 }
743
744 #[test]
745 fn enrolled_device_derives_the_same_auth_a() {
746 let vectors = vectors().cpace;
747 let ya_scalar = Scalar::from_bytes_mod_order_wide(&hex_array(&vectors.ya_secret));
748 let msg_b = decompress_point(&hex(&vectors.msgb)).expect("a valid point");
749
750 assert_eq!(
751 HEXLOWER.encode(&(msg_b * ya_scalar).compress().to_bytes()),
752 vectors.auth_a
753 );
754 }
755
756 async fn run_cpace(
757 server: &MockServer,
758 vectors: &CpaceVectors,
759 timing: &Timing,
760 ) -> Result<CredentialBundle, OnePasswordError> {
761 perform_cpace_with_client_secret(
762 &vectors.username,
763 &vectors.sign_in_address,
764 SIGN_IN_TOKEN,
765 &vectors.enrollment_uuid,
766 &vectors.verification_code,
767 &hex_array(&vectors.client_secret),
768 &rest_client(server),
769 timing,
770 )
771 .await
772 }
773
774 #[tokio::test]
775 async fn perform_cpace_sends_the_vector_messages_and_returns_the_bundle() {
776 let vectors = vectors().cpace;
777 let server = MockServer::start().await;
778 mock_enrolled_device(&server, &vectors).await;
779
780 let bundle = run_cpace(&server, &vectors, &quick_timing())
781 .await
782 .expect("the exchange succeeds");
783
784 assert_eq!(*bundle.srpx, "oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8");
785 assert_eq!(*bundle.auk.k, "WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM");
786 server.verify().await;
787 }
788
789 #[tokio::test]
790 async fn a_rejected_msgb_is_an_error() {
791 let vectors = vectors().cpace;
792 let server = MockServer::start().await;
793 mock_msga(&server, &vectors).await;
794 server
795 .register(
796 Mock::given(matchers::method("PUT"))
797 .and(matchers::path(path(&vectors, "cpace/msgb")))
798 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 0})))
799 .expect(1),
800 )
801 .await;
802
803 let error = run_cpace(&server, &vectors, &quick_timing())
804 .await
805 .err()
806 .expect("the exchange fails");
807
808 assert!(matches!(
809 &error,
810 OnePasswordError::Internal(message) if message.contains("exchange credentials")
811 ));
812 server.verify().await;
813 }
814
815 #[tokio::test]
816 async fn an_error_status_on_msga_is_returned_as_is() {
817 let vectors = vectors().cpace;
818 let server = MockServer::start().await;
819 server
820 .register(
821 Mock::given(matchers::path(path(&vectors, "cpace/msga")))
822 .respond_with(ResponseTemplate::new(503))
823 .expect(1),
824 )
825 .await;
826
827 let error = run_cpace(&server, &vectors, &quick_timing())
828 .await
829 .err()
830 .expect("the exchange fails");
831
832 assert!(matches!(
833 &error,
834 OnePasswordError::UnexpectedStatus { endpoint, status: 503 }
835 if *endpoint == format!("v3/device/enrollments/{}/cpace/msga", vectors.enrollment_uuid)
836 ));
837 server.verify().await;
838 }
839
840 #[tokio::test]
841 async fn exhausting_the_empty_answers_is_an_error() {
842 let vectors = vectors().cpace;
843 let server = MockServer::start().await;
844 mock_msga(&server, &vectors).await;
845 server
846 .register(
847 Mock::given(matchers::method("PUT"))
848 .and(matchers::path(path(&vectors, "cpace/msgb")))
849 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 1})))
850 .expect(1),
851 )
852 .await;
853 server
854 .register(
855 Mock::given(matchers::method("POST"))
856 .and(matchers::path(path(&vectors, "cpace/taga")))
857 .respond_with(ResponseTemplate::new(200))
858 .expect(3),
859 )
860 .await;
861
862 let timing = Timing {
863 until_result_attempts: 3,
864 ..quick_timing()
865 };
866 let error = run_cpace(&server, &vectors, &timing)
867 .await
868 .err()
869 .expect("the exchange fails");
870
871 assert!(matches!(
872 &error,
873 OnePasswordError::Internal(message)
874 if message.contains(&format!("v3/device/enrollments/{}/cpace/taga", vectors.enrollment_uuid))
875 ));
876 server.verify().await;
877 }
878}