aucpace/
client.rs

1use crate::{
2    errors::{Error, Result},
3    utils::{
4        compute_authenticator_messages, compute_first_session_key, compute_session_key,
5        compute_ssid, generate_keypair, generate_nonce, scalar_from_hash, H0,
6    },
7};
8
9use crate::constants::MIN_SSID_LEN;
10use core::marker::PhantomData;
11use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
12use curve25519_dalek::traits::IsIdentity;
13use curve25519_dalek::{
14    digest::consts::U64,
15    digest::{Digest, Output},
16    ristretto::RistrettoPoint,
17    scalar::Scalar,
18};
19use password_hash::{ParamsString, PasswordHash, PasswordHasher, Salt, SaltString};
20use rand_core::CryptoRngCore;
21use subtle::ConstantTimeEq;
22
23#[cfg(feature = "strong_aucpace")]
24use crate::utils::H1;
25
26#[cfg(feature = "alloc")]
27extern crate alloc;
28
29#[cfg(feature = "serde")]
30use crate::utils::{serde_paramsstring, serde_saltstring};
31
32#[cfg(feature = "serde")]
33use serde::{Deserialize, Serialize};
34
35/// Implementation of the client side of the AuCPace protocol
36pub struct AuCPaceClient<D, H, CSPRNG, const K1: usize>
37where
38    D: Digest<OutputSize = U64> + Default,
39    H: PasswordHasher,
40    CSPRNG: CryptoRngCore,
41{
42    rng: CSPRNG,
43    d: PhantomData<D>,
44    h: PhantomData<H>,
45}
46
47impl<D, H, CSPRNG, const K1: usize> AuCPaceClient<D, H, CSPRNG, K1>
48where
49    D: Digest<OutputSize = U64> + Default,
50    H: PasswordHasher,
51    CSPRNG: CryptoRngCore,
52{
53    /// Create new server
54    pub fn new(rng: CSPRNG) -> Self {
55        Self {
56            rng,
57            d: Default::default(),
58            h: Default::default(),
59        }
60    }
61
62    /// Create a new client in the SSID agreement phase
63    ///
64    /// # Return:
65    /// ([`next_step`](AuCPaceClientSsidEstablish), [`message`](ClientMessage::Nonce))
66    /// - [`next_step`](AuCPaceClientSsidEstablish): the client in the SSID establishment stage
67    /// - [`message`](ClientMessage::Nonce): the message to send to the server
68    ///
69    pub fn begin(&mut self) -> (AuCPaceClientSsidEstablish<D, H, K1>, ClientMessage<'_, K1>) {
70        let next_step = AuCPaceClientSsidEstablish::new(&mut self.rng);
71        let message = ClientMessage::Nonce(next_step.nonce);
72
73        (next_step, message)
74    }
75
76    /// Create a new client in the pre-augmentation layer phase, provided an SSID
77    ///
78    /// # Argument:
79    /// - `ssid`: Some data to be hashed and act as the sub-session ID
80    ///
81    /// # Return:
82    /// - Ok([`next_step`](AuCPaceClientSsidEstablish)): the server in the SSID establishment stage
83    /// - Err([`Error::InsecureSsid`](Error::InsecureSsid)): the SSID provided was not long enough to be secure
84    ///
85    pub fn begin_prestablished_ssid<S>(&mut self, ssid: S) -> Result<AuCPaceClientPreAug<D, H, K1>>
86    where
87        S: AsRef<[u8]>,
88    {
89        // if the SSID isn't long enough return an error
90        if ssid.as_ref().len() < MIN_SSID_LEN {
91            return Err(Error::InsecureSsid);
92        }
93
94        // hash the SSID and begin the next step
95        let mut hasher: D = H0();
96        hasher.update(ssid);
97        let ssid_hash = hasher.finalize();
98        let next_step = AuCPaceClientPreAug::new(ssid_hash);
99        Ok(next_step)
100    }
101
102    /// Register a username/password
103    ///
104    /// # Arguments:
105    /// - `username` - the username to register with
106    /// - `password` - the password for the user
107    /// - `params` - the parameters of the PBKDF used
108    /// - `hasher` - the hasher to use for hashing the username and password.
109    ///
110    /// # Const Parameters
111    /// - `BUFSIZ` - the size of the buffer to use while hashing
112    ///   it should be enough to store the maximum length of a username + password + 1 for your use case
113    ///   e.g. if you have a username limit of 20 and password limit of 60, 81 would be the right value.
114    ///
115    /// # Return:
116    /// - Ok([`messsage`](ClientMessage::Registration)): the message to send to the server
117    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
118    ///   one of the three error variants that can result from the password hashing process
119    ///
120    pub fn register<'a, P, const BUFSIZ: usize>(
121        &mut self,
122        username: &'a [u8],
123        password: P,
124        params: H::Params,
125        hasher: H,
126    ) -> Result<ClientMessage<'a, K1>>
127    where
128        P: AsRef<[u8]>,
129    {
130        let salt_string = SaltString::generate(&mut self.rng);
131
132        // compute the verifier W
133        let pw_hash = hash_password::<&[u8], P, &SaltString, H, BUFSIZ>(
134            username,
135            password,
136            &salt_string,
137            params.clone(),
138            hasher,
139        )?;
140
141        let cofactor = Scalar::ONE;
142        let w = scalar_from_hash(pw_hash)?;
143        let verifier = RISTRETTO_BASEPOINT_POINT * (w * cofactor);
144
145        // attempt to convert the parameters to a ParamsString
146        let params_string = params.try_into().map_err(Error::PasswordHashing)?;
147
148        Ok(ClientMessage::Registration {
149            username,
150            salt: salt_string,
151            params: params_string,
152            verifier,
153        })
154    }
155
156    /// Register a username/password in the strong variant of the protocol
157    ///
158    /// # Arguments:
159    /// - `username` - the username to register with
160    /// - `password` - the password for the user
161    /// - `params` - the parameters of the PBKDF used
162    /// - `hasher` - the hasher to use for hashing the username and password.
163    ///
164    /// # Const Parameters
165    /// - `BUFSIZ` - the size of the buffer to use while hashing
166    ///   it should be enough to store the maximum length of a username + password + 1 for your use case
167    ///   e.g. if you have a username limit of 20 and password limit of 60, 81 would be the right value.
168    ///
169    /// # Return:
170    /// - Ok([`messsage`](ClientMessage::Registration)): the message to send to the server
171    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
172    ///   one of the three error variants that can result from the password hashing process
173    ///
174    #[cfg(feature = "strong_aucpace")]
175    pub fn register_strong<'a, P, const BUFSIZ: usize>(
176        &mut self,
177        username: &'a [u8],
178        password: P,
179        params: H::Params,
180        hasher: H,
181    ) -> Result<ClientMessage<'a, K1>>
182    where
183        P: AsRef<[u8]>,
184    {
185        // generate a secret exponent and salt
186        let (q, salt_string) =
187            Self::generate_salt_strong(username, password.as_ref(), &mut self.rng)?;
188
189        // compute the verifier W
190        let pw_hash = hash_password::<&[u8], P, &SaltString, H, BUFSIZ>(
191            username,
192            password,
193            &salt_string,
194            params.clone(),
195            hasher,
196        )?;
197        let cofactor = Scalar::ONE;
198        let w = scalar_from_hash(pw_hash)?;
199        let verifier = RISTRETTO_BASEPOINT_POINT * (w * cofactor);
200
201        // attempt to convert the parameters to a ParamsString
202        let params_string = params.try_into().map_err(Error::PasswordHashing)?;
203
204        Ok(ClientMessage::StrongRegistration {
205            username,
206            secret_exponent: q,
207            params: params_string,
208            verifier,
209        })
210    }
211
212    /// Register a username/password
213    ///
214    /// Allocates space for user:pass string on the heap, instead of a constant size buffer.
215    ///
216    /// # Arguments:
217    /// - `username` - the username to register with
218    /// - `password` - the password for the user
219    /// - `params` - the parameters of the PBKDF used
220    /// - `hasher` - the hasher to use for hashing the username and password.
221    ///
222    /// # Return:
223    /// - Ok([`messsage`](ClientMessage::Registration)): the message to send to the server
224    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
225    ///   one of the three error variants that can result from the password hashing process
226    ///
227    #[cfg(feature = "alloc")]
228    pub fn register_alloc<'a, P>(
229        &mut self,
230        username: &'a [u8],
231        password: P,
232        params: H::Params,
233        hasher: H,
234    ) -> Result<ClientMessage<'a, K1>>
235    where
236        P: AsRef<[u8]>,
237    {
238        // adapted from SaltString::generate, which we cannot use due to curve25519 versions of rand_core
239        let salt_string = SaltString::generate(&mut self.rng);
240
241        // compute the verifier W
242        let pw_hash =
243            hash_password_alloc(username, password, &salt_string, params.clone(), hasher)?;
244        let cofactor = Scalar::ONE;
245        let w = scalar_from_hash(pw_hash)?;
246        let verifier = RISTRETTO_BASEPOINT_POINT * (w * cofactor);
247
248        // attempt to convert the parameters to a ParamsString
249        let params_string = params.try_into().map_err(Error::PasswordHashing)?;
250
251        Ok(ClientMessage::Registration {
252            username,
253            salt: salt_string,
254            params: params_string,
255            verifier,
256        })
257    }
258
259    /// Register a username/password in the strong variant of the protocol
260    ///
261    /// Allocates space for user:pass string on the heap, instead of a constant size buffer.
262    ///
263    /// # Arguments:
264    /// - `username` - the username to register with
265    /// - `password` - the password for the user
266    /// - `params` - the parameters of the PBKDF used
267    /// - `hasher` - the hasher to use for hashing the username and password.
268    ///
269    /// # Const Parameters
270    /// - `BUFSIZ` - the size of the buffer to use while hashing
271    ///   it should be enough to store the maximum length of a username + password + 1 for your use case
272    ///   e.g. if you have a username limit of 20 and password limit of 60, 81 would be the right value.
273    ///
274    /// # Return:
275    /// - Ok([`messsage`](ClientMessage::Registration)): the message to send to the server
276    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
277    ///   one of the three error variants that can result from the password hashing process
278    ///
279    #[cfg(all(feature = "strong_aucpace", feature = "alloc"))]
280    pub fn register_alloc_strong<'a, P>(
281        &mut self,
282        username: &'a [u8],
283        password: P,
284        params: H::Params,
285        hasher: H,
286    ) -> Result<ClientMessage<'a, K1>>
287    where
288        P: AsRef<[u8]>,
289    {
290        // generate a secret exponent and salt
291        let (q, salt_string) =
292            Self::generate_salt_strong(username, password.as_ref(), &mut self.rng)?;
293
294        // compute the verifier W
295        let pw_hash = hash_password_alloc(
296            username,
297            password,
298            salt_string.as_salt(),
299            params.clone(),
300            hasher,
301        )?;
302        let cofactor = Scalar::ONE;
303        let w = scalar_from_hash(pw_hash)?;
304        let verifier = RISTRETTO_BASEPOINT_POINT * (w * cofactor);
305
306        // attempt to convert the parameters to a ParamsString
307        let params_string = params.try_into().map_err(Error::PasswordHashing)?;
308
309        Ok(ClientMessage::StrongRegistration {
310            username,
311            secret_exponent: q,
312            params: params_string,
313            verifier,
314        })
315    }
316
317    /// generate a secret exponent and a salt value for the strong variant of the protocol
318    #[cfg(feature = "strong_aucpace")]
319    fn generate_salt_strong(
320        user: &[u8],
321        pass: &[u8],
322        rng: &mut CSPRNG,
323    ) -> Result<(Scalar, SaltString)> {
324        // choose a random q
325        let q = Scalar::random(rng);
326
327        // compute z
328        let mut hasher: D = H1();
329        hasher.update(user);
330        hasher.update(pass);
331        let z = RistrettoPoint::from_hash(hasher);
332
333        // compute the salt value
334        let cofactor = Scalar::ONE;
335        let salt_point = z * (q * cofactor);
336        let salt = salt_point.compress().to_bytes();
337        let salt_string = SaltString::encode_b64(&salt).map_err(Error::PasswordHashing)?;
338
339        Ok((q, salt_string))
340    }
341}
342
343/// Client in the SSID agreement phase
344pub struct AuCPaceClientSsidEstablish<D, H, const K1: usize>
345where
346    D: Digest<OutputSize = U64> + Default,
347    H: PasswordHasher,
348{
349    nonce: [u8; K1],
350    d: PhantomData<D>,
351    h: PhantomData<H>,
352}
353
354impl<D, H, const K1: usize> AuCPaceClientSsidEstablish<D, H, K1>
355where
356    D: Digest<OutputSize = U64> + Default,
357    H: PasswordHasher,
358{
359    fn new<CSPRNG>(rng: &mut CSPRNG) -> Self
360    where
361        CSPRNG: CryptoRngCore,
362    {
363        Self {
364            nonce: generate_nonce(rng),
365            d: Default::default(),
366            h: Default::default(),
367        }
368    }
369
370    /// Consume the server's nonce - `s` and progress to the augmentation layer
371    ///
372    /// # arguments:
373    /// - `server_nonce` - the nonce received from the server
374    ///
375    /// # return:
376    /// [`next_step`](AuCPaceClientPreAug): the client in the pre-augmentation stage
377    ///
378    pub fn agree_ssid(self, server_nonce: [u8; K1]) -> AuCPaceClientPreAug<D, H, K1> {
379        let ssid = compute_ssid::<D, K1>(server_nonce, self.nonce);
380        AuCPaceClientPreAug::new(ssid)
381    }
382}
383
384/// Client in the pre-augmentation phase
385pub struct AuCPaceClientPreAug<D, H, const K1: usize>
386where
387    D: Digest<OutputSize = U64> + Default,
388    H: PasswordHasher,
389{
390    ssid: Output<D>,
391    h: PhantomData<H>,
392}
393
394impl<D, H, const K1: usize> AuCPaceClientPreAug<D, H, K1>
395where
396    D: Digest<OutputSize = U64> + Default,
397    H: PasswordHasher,
398{
399    fn new(ssid: Output<D>) -> Self {
400        Self {
401            ssid,
402            h: Default::default(),
403        }
404    }
405
406    /// Consume the client's username and begin the augmentation layer
407    ///
408    /// # Arguments:
409    /// - `username` - a reference to the client's username
410    ///
411    /// # Return:
412    /// ([`next_step`](AuCPaceClientAugLayer), [`message`](ClientMessage::Username))
413    /// - [`next_step`](AuCPaceClientAugLayer): the client in the augmentation layer
414    /// - [`message`](ClientMessage::Username): the message to send to the server
415    ///
416    pub fn start_augmentation<'a>(
417        self,
418        username: &'a [u8],
419        password: &'a [u8],
420    ) -> (AuCPaceClientAugLayer<'a, D, H, K1>, ClientMessage<'a, K1>) {
421        let next_step = AuCPaceClientAugLayer::new(self.ssid, username, password);
422        let message = ClientMessage::Username(username);
423
424        (next_step, message)
425    }
426
427    /// Consume the client's username and begin the augmentation layer.
428    /// This variant performs the strong version of the protocol.
429    ///
430    /// # Arguments:
431    /// - `username` - a reference to the client's username
432    ///
433    /// # Return:
434    /// ([`next_step`](StrongAuCPaceClientAugLayer), [`message`](ClientMessage::Username))
435    /// - [`next_step`](StrongAuCPaceClientAugLayer): the client in the augmentation layer
436    /// - [`message`](ClientMessage::Username): the message to send to the server
437    ///
438    #[cfg(feature = "strong_aucpace")]
439    pub fn start_augmentation_strong<'a, CSPRNG>(
440        self,
441        username: &'a [u8],
442        password: &'a [u8],
443        rng: &mut CSPRNG,
444    ) -> (
445        StrongAuCPaceClientAugLayer<'a, D, H, K1>,
446        ClientMessage<'a, K1>,
447    )
448    where
449        CSPRNG: CryptoRngCore,
450    {
451        // compute the blinding value and blind the hash of the username and password
452        // ensuring that it is non-zero as required by `invert`
453        let blinding_value = loop {
454            let val = Scalar::random(rng);
455            if val != Scalar::ZERO {
456                break val;
457            }
458        };
459        let mut hasher: D = H1();
460        hasher.update(username);
461        hasher.update(password);
462        let z = RistrettoPoint::from_hash(hasher);
463        let cofactor = Scalar::ONE;
464        let blinded = z * (blinding_value * cofactor);
465
466        let next_step =
467            StrongAuCPaceClientAugLayer::new(self.ssid, username, password, blinding_value);
468        let message = ClientMessage::StrongUsername { username, blinded };
469
470        (next_step, message)
471    }
472}
473
474/// Client in the augmentation layer
475pub struct AuCPaceClientAugLayer<'a, D, H, const K1: usize>
476where
477    D: Digest<OutputSize = U64> + Default,
478    H: PasswordHasher,
479{
480    ssid: Output<D>,
481    username: &'a [u8],
482    password: &'a [u8],
483    h: PhantomData<H>,
484}
485
486impl<'a, D, H, const K1: usize> AuCPaceClientAugLayer<'a, D, H, K1>
487where
488    D: Digest<OutputSize = U64> + Default,
489    H: PasswordHasher,
490{
491    fn new(ssid: Output<D>, username: &'a [u8], password: &'a [u8]) -> Self {
492        Self {
493            ssid,
494            username,
495            password,
496            h: Default::default(),
497        }
498    }
499
500    /// Process the augmentation layer information from the server, hashes the user's password
501    /// together with their username, then computes `w` and `PRS`.
502    ///
503    /// # Arguments:
504    /// - `x_pub` - `x` from the protocol definition, used in generating the password related string (prs)
505    /// - `salt` - the salt value sent by the server
506    /// - `params` - the parameters used by the hasher
507    /// - `hasher` - the hasher to use when computing `w`
508    ///
509    /// # Const Parameters
510    /// - `BUFSIZ` - the size of the buffer to use while hashing
511    ///   it should be enough to store the maximum length of a username + password + 1 for your use case
512    ///   e.g. if you have a username limit of 20 and password limit of 60, 81 would be the right value.
513    ///
514    /// This version requires the alloc feature and allocates space for
515    /// the username and password on the heap using Vec.
516    ///
517    /// # Return:
518    /// - Ok([`next_step`](AuCPaceClientCPaceSubstep)): the client in the cpace substep
519    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
520    ///   one of the three error variants that can result from the password hashing process
521    ///
522    pub fn generate_cpace<'salt, S, const BUFSIZ: usize>(
523        self,
524        x_pub: RistrettoPoint,
525        salt: S,
526        params: H::Params,
527        hasher: H,
528    ) -> Result<AuCPaceClientCPaceSubstep<D, K1>>
529    where
530        S: Into<Salt<'salt>>,
531    {
532        // check for the identity point
533        if x_pub.is_identity() {
534            return Err(Error::IllegalPointError);
535        }
536
537        let cofactor = Scalar::ONE;
538        let pw_hash = hash_password::<&[u8], &[u8], S, H, BUFSIZ>(
539            self.username,
540            self.password,
541            salt,
542            params,
543            hasher,
544        )?;
545        let w = scalar_from_hash(pw_hash)?;
546
547        let prs = (x_pub * (w * cofactor)).compress().to_bytes();
548
549        Ok(AuCPaceClientCPaceSubstep::new(self.ssid, prs))
550    }
551
552    /// Process the augmentation layer information from the server, hashes the user's password
553    /// together with their username, then computes `w` and `PRS`.
554    ///
555    /// This version requires the alloc feature and allocates space for
556    /// the username:password string on the heap.
557    ///
558    /// # Arguments:
559    /// - `x_pub` - `x` from the protocol definition, used in generating the password related string (prs)
560    /// - `salt` - the salt value sent by the server
561    /// - `params` - the parameters used by the hasher
562    /// - `hasher` - the hasher to use when computing `w`
563    ///
564    /// # Return:
565    /// - Ok([`next_step`](AuCPaceClientCPaceSubstep)): the client in the cpace substep
566    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
567    ///   one of the three error variants that can result from the password hashing process
568    ///
569    #[cfg(feature = "alloc")]
570    pub fn generate_cpace_alloc<'salt, S>(
571        self,
572        x_pub: RistrettoPoint,
573        salt: S,
574        params: H::Params,
575        hasher: H,
576    ) -> Result<AuCPaceClientCPaceSubstep<D, K1>>
577    where
578        S: Into<Salt<'a>>,
579    {
580        // check for the identity point
581        if x_pub.is_identity() {
582            return Err(Error::IllegalPointError);
583        }
584
585        let cofactor = Scalar::ONE;
586        let pw_hash = hash_password_alloc(self.username, self.password, salt, params, hasher)?;
587        let w = scalar_from_hash(pw_hash)?;
588
589        let prs = (x_pub * (w * cofactor)).compress().to_bytes();
590
591        Ok(AuCPaceClientCPaceSubstep::new(self.ssid, prs))
592    }
593}
594
595/// Client in the augmentation layer - strong version
596#[cfg(feature = "strong_aucpace")]
597pub struct StrongAuCPaceClientAugLayer<'a, D, H, const K1: usize>
598where
599    D: Digest<OutputSize = U64> + Default,
600    H: PasswordHasher,
601{
602    ssid: Output<D>,
603    username: &'a [u8],
604    password: &'a [u8],
605    blinding_value: Scalar,
606    h: PhantomData<H>,
607}
608
609#[cfg(feature = "strong_aucpace")]
610impl<'a, D, H, const K1: usize> StrongAuCPaceClientAugLayer<'a, D, H, K1>
611where
612    D: Digest<OutputSize = U64> + Default,
613    H: PasswordHasher,
614{
615    fn new(
616        ssid: Output<D>,
617        username: &'a [u8],
618        password: &'a [u8],
619        blinding_value: Scalar,
620    ) -> Self {
621        Self {
622            ssid,
623            username,
624            password,
625            blinding_value,
626            h: Default::default(),
627        }
628    }
629
630    /// Process the strong augmentation layer information from the server, unblinds the salt value,
631    /// hashes the user's password together with their username, then computes `w` and `PRS`.
632    ///
633    /// # Arguments:
634    /// - `x_pub` - `x` from the protocol definition, used in generating the password related string (prs)
635    /// - `salt_point` - our blinded point `U`, multiplied by the server's secret exponent `q`
636    /// - `params` - the parameters used by the hasher
637    /// - `hasher` - the hasher to use when computing `w`
638    ///
639    /// # Const Parameters
640    /// - `BUFSIZ` - the size of the buffer to use while hashing
641    ///   it should be enough to store the maximum length of a username + password + 1 for your use case
642    ///   e.g. if you have a username limit of 20 and password limit of 60, 81 would be the right value.
643    ///
644    /// This version requires the alloc feature and allocates space for
645    /// the username and password on the heap using Vec.
646    ///
647    /// # Return:
648    /// - Ok([`next_step`](AuCPaceClientCPaceSubstep)): the client in the cpace substep
649    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
650    ///   one of the three error variants that can result from the password hashing process
651    ///
652    pub fn generate_cpace<const BUFSIZ: usize>(
653        self,
654        x_pub: RistrettoPoint,
655        blinded_salt: RistrettoPoint,
656        params: H::Params,
657        hasher: H,
658    ) -> Result<AuCPaceClientCPaceSubstep<D, K1>> {
659        // check for the identity point
660        if x_pub.is_identity() {
661            return Err(Error::IllegalPointError);
662        }
663
664        // first recover the salt
665        let cofactor = Scalar::ONE;
666
667        // this is a tad funky, in the paper they write (1/(r * cj^2))*cj
668        // I have interpreted this as the multiplicative inverse of (r * cj^2)
669        // then multiplied by cj again.
670        let exponent = (self.blinding_value * cofactor * cofactor).invert() * cofactor;
671        let salt = (blinded_salt * exponent).compress().to_bytes();
672        let salt_string = SaltString::encode_b64(&salt).map_err(Error::PasswordHashing)?;
673
674        // compute the PRS
675        let pw_hash = hash_password::<&[u8], &[u8], &SaltString, H, BUFSIZ>(
676            self.username,
677            self.password,
678            &salt_string,
679            params,
680            hasher,
681        )?;
682        let w = scalar_from_hash(pw_hash)?;
683        let prs = (x_pub * (w * cofactor)).compress().to_bytes();
684
685        Ok(AuCPaceClientCPaceSubstep::new(self.ssid, prs))
686    }
687
688    /// Process the strong augmentation layer information from the server, unblinds the salt value,
689    /// hashes the user's password together with their username, then computes `w` and `PRS`.
690    ///
691    /// This version requires the alloc feature and allocates space for
692    /// the username:password string on the heap.
693    ///
694    /// # Arguments:
695    /// - `x_pub` - `x` from the protocol definition, used in generating the password related string (prs)
696    /// - `password` - the user's password
697    /// - `salt` - the salt value sent by the server
698    /// - `params` - the parameters used by the hasher
699    /// - `hasher` - the hasher to use when computing `w`
700    ///
701    /// # Return:
702    /// - Ok([`next_step`](AuCPaceClientCPaceSubstep)): the client in the cpace substep
703    /// - Err([`Error::PasswordHashing`](Error::PasswordHashing) | [`Error::HashEmpty`](Error::HashEmpty) | [`Error::HashSizeInvalid`](Error::HashSizeInvalid)):
704    ///   one of the three error variants that can result from the password hashing process
705    ///
706    #[cfg(feature = "alloc")]
707    pub fn generate_cpace_alloc(
708        self,
709        x_pub: RistrettoPoint,
710        blinded_salt: RistrettoPoint,
711        params: H::Params,
712        hasher: H,
713    ) -> Result<AuCPaceClientCPaceSubstep<D, K1>> {
714        // check for the identity point
715        if x_pub.is_identity() {
716            return Err(Error::IllegalPointError);
717        }
718
719        // first recover the salt
720        let cofactor = Scalar::ONE;
721
722        // this is a tad funky, in the paper they write (1/(r * cj^2))*cj
723        // I have interpreted this as the multiplicative inverse of (r * cj^2)
724        // then multiplied by cj again.
725        let exponent = (self.blinding_value * cofactor * cofactor).invert() * cofactor;
726
727        // check if the salt point is the neutral element
728        let salt_point = blinded_salt * exponent;
729        if salt_point.is_identity() {
730            return Err(Error::IllegalPointError);
731        }
732        let salt = salt_point.compress().to_bytes();
733        let salt_string = SaltString::encode_b64(&salt).map_err(Error::PasswordHashing)?;
734
735        // compute the PRS
736        let pw_hash = hash_password_alloc(
737            self.username,
738            self.password,
739            salt_string.as_salt(),
740            params,
741            hasher,
742        )?;
743        let w = scalar_from_hash(pw_hash)?;
744        let prs = (x_pub * (w * cofactor)).compress().to_bytes();
745
746        Ok(AuCPaceClientCPaceSubstep::new(self.ssid, prs))
747    }
748}
749
750/// Client in the CPace substep
751pub struct AuCPaceClientCPaceSubstep<D, const K1: usize>
752where
753    D: Digest<OutputSize = U64> + Default,
754{
755    ssid: Output<D>,
756    prs: [u8; 32],
757}
758
759impl<D, const K1: usize> AuCPaceClientCPaceSubstep<D, K1>
760where
761    D: Digest<OutputSize = U64> + Default,
762{
763    fn new(ssid: Output<D>, prs: [u8; 32]) -> Self {
764        Self { ssid, prs }
765    }
766
767    /// Generate a public key
768    /// moving the protocol onto the second half of the CPace substep - Receive Server Pubkey
769    ///
770    /// # Arguments:
771    /// - `channel_identifier` - `CI` from the protocol definition, in the context of TCP/IP this
772    ///     is usually some combination of the server and client's IP address and TCP port numbers.
773    ///     It's purpose is to prevent relay attacks.
774    /// - `rng` - the CSPRNG used when generating the public/private keypair
775    ///
776    /// # Return:
777    /// ([`next_step`](AuCPaceClientRecvServerKey), [`message`](ClientMessage::PublicKey))
778    /// - [`next_step`](AuCPaceClientRecvServerKey): the client waiting for the server's public key
779    /// - [`message`](ClientMessage::PublicKey): the message to send to the server
780    ///
781    pub fn generate_public_key<CI, CSPRNG>(
782        self,
783        channel_identifier: CI,
784        rng: &mut CSPRNG,
785    ) -> (
786        AuCPaceClientRecvServerKey<D, K1>,
787        ClientMessage<'static, K1>,
788    )
789    where
790        CI: AsRef<[u8]>,
791        CSPRNG: CryptoRngCore,
792    {
793        let (priv_key, pub_key) =
794            generate_keypair::<D, CSPRNG, CI>(rng, self.ssid, self.prs, channel_identifier);
795
796        let next_step = AuCPaceClientRecvServerKey::new(self.ssid, priv_key);
797        let message = ClientMessage::PublicKey(pub_key);
798
799        (next_step, message)
800    }
801}
802
803/// Client waiting to receive the server's public key
804pub struct AuCPaceClientRecvServerKey<D, const K1: usize>
805where
806    D: Digest<OutputSize = U64> + Default,
807{
808    ssid: Output<D>,
809    priv_key: Scalar,
810}
811
812impl<D, const K1: usize> AuCPaceClientRecvServerKey<D, K1>
813where
814    D: Digest<OutputSize = U64> + Default,
815{
816    fn new(ssid: Output<D>, priv_key: Scalar) -> Self {
817        Self { ssid, priv_key }
818    }
819
820    /// Receive the server's public key
821    /// This completes the CPace substep and moves the client on to explicit mutual authentication.
822    ///
823    /// # Arguments:
824    /// - `server_pubkey` - the server's public key
825    ///
826    /// # Return:
827    /// ([`next_step`](AuCPaceClientExpMutAuth), [`message`](ClientMessage::Authenticator))
828    /// - [`next_step`](AuCPaceClientExpMutAuth): the client in the Explicit Mutual Authentication phase
829    /// - [`message`](ClientMessage::Authenticator): the message to send to the server
830    ///
831    pub fn receive_server_pubkey(
832        self,
833        server_pubkey: RistrettoPoint,
834    ) -> Result<(AuCPaceClientExpMutAuth<D, K1>, ClientMessage<'static, K1>)> {
835        if server_pubkey.is_identity() {
836            return Err(Error::IllegalPointError);
837        }
838
839        let sk1 = compute_first_session_key::<D>(self.ssid, self.priv_key, server_pubkey);
840        let (ta, tb) = compute_authenticator_messages::<D>(self.ssid, sk1);
841        let next_step = AuCPaceClientExpMutAuth::new(self.ssid, sk1, ta);
842        let message = ClientMessage::Authenticator(
843            tb.as_slice()
844                .try_into()
845                .expect("array length invariant broken"),
846        );
847        Ok((next_step, message))
848    }
849
850    /// Allow the user to exit the protocol early in the case of implicit authentication
851    /// Note: this should only be used in special circumstances and the
852    ///       explicit mutual authentication stage should be used in all other cases
853    ///
854    /// # Arguments:
855    /// - `server_pubkey` - the server's public key
856    ///
857    /// # Return:
858    /// `sk`: the session key reached by the AuCPace protocol
859    ///
860    pub fn implicit_auth(self, server_pubkey: RistrettoPoint) -> Result<Output<D>> {
861        if server_pubkey.is_identity() {
862            return Err(Error::IllegalPointError);
863        }
864
865        let sk1 = compute_first_session_key::<D>(self.ssid, self.priv_key, server_pubkey);
866        Ok(compute_session_key::<D>(self.ssid, sk1))
867    }
868}
869
870/// Client in the Explicit Mutual Authenticaton phase
871pub struct AuCPaceClientExpMutAuth<D, const K1: usize>
872where
873    D: Digest<OutputSize = U64> + Default,
874{
875    ssid: Output<D>,
876    sk1: Output<D>,
877    server_authenticator: Output<D>,
878}
879
880impl<D, const K1: usize> AuCPaceClientExpMutAuth<D, K1>
881where
882    D: Digest<OutputSize = U64> + Default,
883{
884    fn new(ssid: Output<D>, sk1: Output<D>, server_authenticator: Output<D>) -> Self {
885        Self {
886            ssid,
887            sk1,
888            server_authenticator,
889        }
890    }
891
892    /// Receive the server's authenticator.
893    /// This completes the protocol and returns the derived key.
894    ///
895    /// # Arguments:
896    /// - `server_authenticator` - the server's authenticator
897    ///
898    /// # Return:
899    /// either:
900    /// - Ok(`sk`): the session key reached by the AuCPace protocol
901    /// - Err([`Error::MutualAuthFail`](Error::MutualAuthFail)): an error if the authenticator we computed doesn't match
902    ///     the server's authenticator, compared in constant time.
903    ///
904    pub fn receive_server_authenticator(self, server_authenticator: [u8; 64]) -> Result<Output<D>> {
905        if self
906            .server_authenticator
907            .ct_eq(&server_authenticator)
908            .into()
909        {
910            Ok(compute_session_key::<D>(self.ssid, self.sk1))
911        } else {
912            Err(Error::MutualAuthFail)
913        }
914    }
915}
916
917/// Hash a username and password with the given password hasher
918fn hash_password<'a, U, P, S, H, const BUFSIZ: usize>(
919    username: U,
920    password: P,
921    salt: S,
922    params: H::Params,
923    hasher: H,
924) -> Result<PasswordHash<'a>>
925where
926    H: PasswordHasher,
927    U: AsRef<[u8]>,
928    P: AsRef<[u8]>,
929    S: Into<Salt<'a>>,
930{
931    let user = username.as_ref();
932    let pass = password.as_ref();
933    let u = user.len();
934    let p = pass.len();
935
936    if u + p + 1 > BUFSIZ {
937        return Err(Error::UsernameOrPasswordTooLong);
938    }
939
940    let mut buf = [0u8; BUFSIZ];
941    buf[0..u].copy_from_slice(user);
942    buf[u] = b':';
943    buf[u + 1..u + p + 1].copy_from_slice(pass);
944
945    let hash = hasher
946        .hash_password_customized(&buf[0..u + p + 1], None, None, params, salt)
947        .map_err(Error::PasswordHashing);
948
949    hash
950}
951
952/// Hash a username and password with the given password hasher
953#[cfg(feature = "alloc")]
954fn hash_password_alloc<'a, U, P, S, H>(
955    username: U,
956    password: P,
957    salt: S,
958    params: H::Params,
959    hasher: H,
960) -> Result<PasswordHash<'a>>
961where
962    H: PasswordHasher,
963    U: AsRef<[u8]>,
964    P: AsRef<[u8]>,
965    S: Into<Salt<'a>>,
966{
967    let user = username.as_ref();
968    let pass = password.as_ref();
969
970    // hash "{username}:{password}"
971    let mut v = alloc::vec::Vec::with_capacity(user.len() + pass.len() + 1);
972    v.extend_from_slice(user);
973    v.push(b':');
974    v.extend_from_slice(pass);
975
976    hasher
977        .hash_password_customized(v.as_slice(), None, None, params, salt)
978        .map_err(Error::PasswordHashing)
979}
980
981/// An enum representing the different messages the client can send to the server
982#[derive(Debug)]
983#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
984pub enum ClientMessage<'a, const K1: usize> {
985    /// SSID establishment message - the client's nonce: `t`
986    Nonce(#[cfg_attr(feature = "serde", serde(with = "serde_byte_array"))] [u8; K1]),
987
988    /// Username - the client's username
989    Username(&'a [u8]),
990
991    /// StrongUsername - the strong AuCPace username message
992    /// also contains the blinded point `U`
993    #[cfg(feature = "strong_aucpace")]
994    StrongUsername {
995        /// The client's username
996        username: &'a [u8],
997        /// The blinded point `U`
998        blinded: RistrettoPoint,
999    },
1000
1001    /// PublicKey - the client's public key: `Ya`
1002    PublicKey(RistrettoPoint),
1003
1004    /// Explicit Mutual Authentication - the client's authenticator: `Tb`
1005    Authenticator(#[cfg_attr(feature = "serde", serde(with = "serde_byte_array"))] [u8; 64]),
1006
1007    /// Registration - the username, verifier, salt and parameters needed for registering a user
1008    /// NOTE: if the UAD field is desired this should be handled separately and sent at the same time
1009    Registration {
1010        /// The username of whoever is registering
1011        username: &'a [u8],
1012
1013        /// The salt used when computing the verifier
1014        #[cfg_attr(feature = "serde", serde(with = "serde_saltstring"))]
1015        salt: SaltString,
1016
1017        /// The password hasher's parameters used when computing the verifier
1018        #[cfg_attr(feature = "serde", serde(with = "serde_paramsstring"))]
1019        params: ParamsString,
1020
1021        /// The verifier computer from the user's password
1022        verifier: RistrettoPoint,
1023    },
1024
1025    /// Registration Strong version - the username, verifier, secret exponent and parameters needed for registering a user
1026    /// NOTE: if the UAD field is desired this should be handled separately and sent at the same time
1027    #[cfg(feature = "strong_aucpace")]
1028    StrongRegistration {
1029        /// The username of whoever is registering
1030        username: &'a [u8],
1031
1032        /// The salt used when computing the verifier
1033        secret_exponent: Scalar,
1034
1035        /// The password hasher's parameters used when computing the verifier
1036        #[cfg_attr(feature = "serde", serde(with = "serde_paramsstring"))]
1037        params: ParamsString,
1038
1039        /// The verifier computer from the user's password
1040        verifier: RistrettoPoint,
1041    },
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    #[allow(unused)]
1047    use super::*;
1048
1049    #[test]
1050    #[cfg(all(feature = "alloc", feature = "getrandom", feature = "scrypt"))]
1051    fn test_hash_password_no_std_and_alloc_agree() {
1052        use rand_core::{OsRng, RngCore};
1053        use scrypt::{Params, Scrypt};
1054
1055        let username = "worf@starship.enterprise";
1056        let password = "data_x_worf_4ever_<3";
1057        let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
1058        OsRng.fill_bytes(&mut bytes);
1059        let salt = SaltString::encode_b64(&bytes).expect("Salt length invariant broken.");
1060        // These are weak parameters, do not use them
1061        // they are used here to make the test run faster
1062        let params: Params = Default::default();
1063
1064        let no_std_res = hash_password::<&str, &str, &SaltString, Scrypt, 100>(
1065            username, password, &salt, params, Scrypt,
1066        )
1067        .unwrap();
1068        let alloc_res = hash_password_alloc(username, password, &salt, params, Scrypt).unwrap();
1069
1070        assert_eq!(alloc_res, no_std_res);
1071    }
1072
1073    #[test]
1074    #[cfg(all(feature = "getrandom", feature = "sha2"))]
1075    fn test_client_doesnt_accept_insecure_ssid() {
1076        use crate::Client;
1077        use rand_core::OsRng;
1078
1079        let mut client = Client::new(OsRng);
1080        let res = client.begin_prestablished_ssid("bad ssid");
1081        assert!(matches!(res, Err(Error::InsecureSsid)));
1082    }
1083
1084    #[test]
1085    #[cfg(all(feature = "sha2", feature = "scrypt"))]
1086    fn test_client_doesnt_accept_invalid_x_pub() {
1087        use crate::utils::H0;
1088        use curve25519_dalek::traits::Identity;
1089        let ssid = H0::<sha2::Sha512>().finalize();
1090        let aug_client: AuCPaceClientAugLayer<'_, sha2::Sha512, scrypt::Scrypt, 16> =
1091            AuCPaceClientAugLayer::new(
1092                ssid,
1093                b"bob",
1094                b"bob's very secure password that nobody knows about, honest",
1095            );
1096        let res = aug_client.generate_cpace::<'_, &SaltString, 100>(
1097            RistrettoPoint::identity(),
1098            &SaltString::encode_b64(b"saltyboi").unwrap(),
1099            scrypt::Params::recommended(),
1100            scrypt::Scrypt,
1101        );
1102
1103        if let Err(e) = res {
1104            assert_eq!(e, Error::IllegalPointError);
1105        } else {
1106            panic!("Client accepted illegal point.");
1107        }
1108    }
1109
1110    #[test]
1111    #[cfg(all(feature = "sha2", feature = "scrypt", feature = "alloc"))]
1112    fn test_alloc_client_doesnt_accept_invalid_x_pub() {
1113        use crate::utils::H0;
1114        use curve25519_dalek::traits::Identity;
1115        let ssid = H0::<sha2::Sha512>().finalize();
1116        let aug_client: AuCPaceClientAugLayer<'_, sha2::Sha512, scrypt::Scrypt, 16> =
1117            AuCPaceClientAugLayer::new(
1118                ssid,
1119                b"bob",
1120                b"bob's very secure password that nobody knows about, honest",
1121            );
1122        let res = aug_client.generate_cpace_alloc(
1123            RistrettoPoint::identity(),
1124            &SaltString::encode_b64(b"saltyboi").unwrap(),
1125            scrypt::Params::recommended(),
1126            scrypt::Scrypt,
1127        );
1128
1129        if let Err(e) = res {
1130            assert_eq!(e, Error::IllegalPointError);
1131        } else {
1132            panic!("Client accepted illegal point.");
1133        }
1134    }
1135
1136    #[test]
1137    #[cfg(all(feature = "sha2", feature = "scrypt", feature = "strong_aucpace"))]
1138    fn test_strong_client_doesnt_accept_invalid_x_pub() {
1139        use crate::utils::H0;
1140        use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
1141        use curve25519_dalek::traits::Identity;
1142
1143        let ssid = H0::<sha2::Sha512>().finalize();
1144        let aug_client: StrongAuCPaceClientAugLayer<'_, sha2::Sha512, scrypt::Scrypt, 16> =
1145            StrongAuCPaceClientAugLayer::new(
1146                ssid,
1147                b"bob",
1148                b"bob's very secure password that nobody knows about, honest",
1149                Scalar::from(69u32),
1150            );
1151        let res = aug_client.generate_cpace::<100>(
1152            RistrettoPoint::identity(),
1153            RISTRETTO_BASEPOINT_POINT,
1154            scrypt::Params::recommended(),
1155            scrypt::Scrypt,
1156        );
1157
1158        if let Err(e) = res {
1159            assert_eq!(e, Error::IllegalPointError);
1160        } else {
1161            panic!("Client accepted illegal point.");
1162        }
1163    }
1164
1165    #[test]
1166    #[cfg(all(
1167        feature = "sha2",
1168        feature = "scrypt",
1169        feature = "alloc",
1170        feature = "strong_aucpace"
1171    ))]
1172    fn test_strong_alloc_client_doesnt_accept_invalid_x_pub() {
1173        use crate::utils::H0;
1174        use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
1175        use curve25519_dalek::traits::Identity;
1176
1177        let ssid = H0::<sha2::Sha512>().finalize();
1178        let aug_client: StrongAuCPaceClientAugLayer<'_, sha2::Sha512, scrypt::Scrypt, 16> =
1179            StrongAuCPaceClientAugLayer::new(
1180                ssid,
1181                b"bob",
1182                b"bob's very secure password that nobody knows about, honest",
1183                Scalar::from(69u32),
1184            );
1185        let res = aug_client.generate_cpace_alloc(
1186            RistrettoPoint::identity(),
1187            RISTRETTO_BASEPOINT_POINT,
1188            scrypt::Params::recommended(),
1189            scrypt::Scrypt,
1190        );
1191
1192        if let Err(e) = res {
1193            assert_eq!(e, Error::IllegalPointError);
1194        } else {
1195            panic!("Client accepted illegal point.");
1196        }
1197    }
1198
1199    #[test]
1200    #[cfg(all(feature = "sha2", feature = "scrypt"))]
1201    fn test_client_doesnt_accept_invalid_pubkey() {
1202        use crate::utils::H0;
1203        use curve25519_dalek::traits::Identity;
1204        let ssid = H0::<sha2::Sha512>().finalize();
1205        let aug_client: AuCPaceClientRecvServerKey<sha2::Sha512, 16> =
1206            AuCPaceClientRecvServerKey::new(ssid, Scalar::from(420u32));
1207        let res = aug_client.receive_server_pubkey(RistrettoPoint::identity());
1208
1209        if let Err(e) = res {
1210            assert_eq!(e, Error::IllegalPointError);
1211        } else {
1212            panic!("Client accepted illegal point.");
1213        }
1214    }
1215
1216    #[test]
1217    #[cfg(all(feature = "sha2", feature = "scrypt"))]
1218    fn test_client_doesnt_accept_invalid_pubkey_implicit_auth() {
1219        use crate::utils::H0;
1220        use curve25519_dalek::traits::Identity;
1221        let ssid = H0::<sha2::Sha512>().finalize();
1222        let aug_client: AuCPaceClientRecvServerKey<sha2::Sha512, 16> =
1223            AuCPaceClientRecvServerKey::new(ssid, Scalar::from(420u32));
1224        let res = aug_client.implicit_auth(RistrettoPoint::identity());
1225
1226        if let Err(e) = res {
1227            assert_eq!(e, Error::IllegalPointError);
1228        } else {
1229            panic!("Client accepted illegal point.");
1230        }
1231    }
1232
1233    #[test]
1234    #[cfg(all(
1235        feature = "sha2",
1236        feature = "scrypt",
1237        feature = "alloc",
1238        feature = "strong_aucpace"
1239    ))]
1240    fn test_strong_alloc_client_doesnt_accept_invalid_salt() {
1241        use crate::utils::H0;
1242        use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
1243        use curve25519_dalek::traits::Identity;
1244
1245        let ssid = H0::<sha2::Sha512>().finalize();
1246        let aug_client: StrongAuCPaceClientAugLayer<'_, sha2::Sha512, scrypt::Scrypt, 16> =
1247            StrongAuCPaceClientAugLayer::new(
1248                ssid,
1249                b"bob",
1250                b"bob's very secure password that nobody knows about, honest",
1251                Scalar::from(69u32),
1252            );
1253        let res = aug_client.generate_cpace_alloc(
1254            RISTRETTO_BASEPOINT_POINT,
1255            RistrettoPoint::identity(),
1256            scrypt::Params::recommended(),
1257            scrypt::Scrypt,
1258        );
1259
1260        if let Err(e) = res {
1261            assert_eq!(e, Error::IllegalPointError);
1262        } else {
1263            panic!("Client accepted illegal point.");
1264        }
1265    }
1266}