Skip to main content

bitwarden_importers/importers/onepassword/access/sso/
cpace.rs

1//! The CPace exchange that hands the credentials of an enrolled device to a new one, reverse
2//! engineered from 1Password's WASM module: the local crypto, and the requests that carry it.
3//!
4//! The new device proves it knows the verification code the user reads off the enrolled device, and
5//! both sides end up with the same key to move the credential bundle through the server.
6
7use 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
34/// Pads the second hash input through the passcode hash to one 128 byte SHA-512 block.
35const PADDING_LENGTH: usize = 76;
36
37/// The key id of the exchange key, which also keys the first step of deriving it.
38const 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/// The first CPace message, from the enrolled device.
43#[derive(Debug)]
44struct MsgA {
45    /// The enrolled device's public point.
46    ya: RistrettoPoint,
47    ad: Ad,
48    /// The message as received, which goes into the transcript hash byte for byte.
49    original_message: Vec<u8>,
50}
51
52/// The associated data of a CPace message.
53#[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
61/// Parses the JSON of `msga`, after its base64 layer is removed.
62///
63/// Rejects a `ya` that is not the canonical encoding of a point, or is the identity, before
64/// anything is sent back.
65fn 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
85/// Fails for anything but the canonical encoding of a point.
86fn 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
93/// Calculates `msgb`, the new device's answer to `msga`.
94fn 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
113/// Stretches the verification code, salted by the enrolled device.
114fn calculate_passcode_hash(passcode: &str, salt: &[u8]) -> Zeroizing<[u8; 32]> {
115    Zeroizing::new(kdf::pbes2(passcode, salt, PASSCODE_ITERATIONS))
116}
117
118/// Binds the exchange to the user, the sign-in address and the enrollment.
119fn 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
136/// The input of the CPace generator, which binds the passcode to the first hash and the session.
137fn 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
147/// Multiplies by the scalar the client secret reduces to, and compresses the result.
148fn 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
153/// Prefixes every field with its length as LEB128, and concatenates them.
154fn 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
162/// Writes `data` after its length, as the little endian base 128 varint the WASM module uses.
163fn 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
177/// Would check `taga`, which proves the enrolled device knows the verification code too.
178///
179/// Not implemented: the server is trusted to relay an honest device.
180fn verify_tag_a(_tag_a: &[u8]) {}
181
182/// Calculates the shared secret `authB` and the `tagb` that proves knowing it.
183fn 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
194/// The point both sides agree on: `ya` times the client scalar.
195fn 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
199/// The session secret, hashed over the whole transcript.
200fn 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
207/// The MAC of `msgb` under a key derived from `authB`.
208fn 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
218/// Derives the key that encrypts the credential bundle in transit from `authB`.
219fn 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
228/// Decrypts the credential bundle the enrolled device shared.
229fn 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    // The error is dropped, serde echoes the offending value.
238    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
259/// Runs the whole exchange over `rest` with a fresh random client secret and returns the
260/// credentials the enrolled device shared.
261pub(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/// The CPace exchange with the client secret injected so the test vectors can pin it.
287#[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
323/// Fetches and parses the enrolled device's first message.
324async 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
339/// Replies with `msgb` and expects the server to accept it.
340async 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
359/// Waits for the enrolled device's `taga`, which only `verify_tag_a` looks at.
360async 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
377/// Replies with `tagb` and expects the server to accept it.
378async 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
397/// Waits for the credential bundle, still encrypted for the exchange key and so invisible to the
398/// server that relays it.
399async 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
416/// POSTs until the endpoint answers a body instead of the empty 200 it sends while the enrolled
417/// device has not replied yet. The web frontend retries the same way.
418async 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
436/// The body every CPace request carries besides the messages themselves.
437fn sign_in_body(sign_in_token: &str) -> Value {
438    json!({ "signInToken": sign_in_token })
439}
440
441/// The server confirms each CPace message with `success: 1`.
442fn 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
449/// `v3/device/enrollments/{enrollmentUuid}/{method}`
450fn enrollment_endpoint(enrollment_uuid: &str, method: &str) -> String {
451    format!("v3/device/enrollments/{enrollment_uuid}/{method}")
452}
453
454/// The CPace endpoints, under the enrollment's `cpace` prefix.
455fn 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    /// The vector's `msga_json` with the value at `pointer` replaced, or removed for `None`.
493    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    /// The enrolled device holds `ya_secret`, and the vectors have to be consistent with it.
735    #[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}