pake-cpace 0.1.7

A simple implementation of CPace, a balanced PAKE.
Documentation
#![no_std]
#![forbid(unsafe_code)]

use core::fmt;
use curve25519_dalek::{
    ristretto::{CompressedRistretto, RistrettoPoint},
    scalar::Scalar,
    traits::IsIdentity,
};
use getrandom::getrandom;
use hmac_sha512::{Hash, BLOCKBYTES};

pub const SESSION_ID_BYTES: usize = 16;
pub const STEP1_PACKET_BYTES: usize = 16 + 32;
pub const STEP2_PACKET_BYTES: usize = 32;
pub const SHARED_KEY_BYTES: usize = 32;

const DSI1: &str = "CPaceRistretto255-1";
const DSI2: &str = "CPaceRistretto255-2";

#[derive(Debug)]
pub enum Error {
    Overflow(&'static str),
    Random(getrandom::Error),
    InvalidPublicKey,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", &self)
    }
}

impl From<getrandom::Error> for Error {
    fn from(e: getrandom::Error) -> Self {
        Error::Random(e)
    }
}

#[derive(Debug, Copy, Clone)]
pub struct SharedKeys {
    pub k1: [u8; SHARED_KEY_BYTES],
    pub k2: [u8; SHARED_KEY_BYTES],
}

#[derive(Debug, Clone)]
pub struct CPace {
    session_id: [u8; SESSION_ID_BYTES],
    p: RistrettoPoint,
    r: Scalar,
}

pub struct Step1Out {
    ctx: CPace,
    step1_packet: [u8; STEP1_PACKET_BYTES],
}

impl Step1Out {
    pub fn packet(&self) -> [u8; STEP1_PACKET_BYTES] {
        self.step1_packet
    }

    pub fn step3(&self, step2_packet: &[u8; STEP2_PACKET_BYTES]) -> Result<SharedKeys, Error> {
        self.ctx.step3(step2_packet)
    }
}

pub struct Step2Out {
    shared_keys: SharedKeys,
    step2_packet: [u8; STEP2_PACKET_BYTES],
}

impl Step2Out {
    pub fn shared_keys(&self) -> SharedKeys {
        self.shared_keys
    }

    pub fn packet(&self) -> [u8; STEP2_PACKET_BYTES] {
        self.step2_packet
    }
}

impl CPace {
    fn new<T: AsRef<[u8]>>(
        session_id: [u8; SESSION_ID_BYTES],
        password: &str,
        id_a: &str,
        id_b: &str,
        ad: Option<T>,
    ) -> Result<Self, Error> {
        if id_a.len() > 0xff || id_b.len() > 0xff {
            return Err(Error::Overflow(
                "Identifiers must be at most 255 bytes long",
            ));
        }
        let zpad = [0u8; BLOCKBYTES];
        let pad_len = zpad.len().wrapping_sub(DSI1.len() + password.len()) & (zpad.len() - 1);
        let mut st = Hash::new();
        st.update(DSI1);
        st.update(password);
        st.update(&zpad[..pad_len]);
        st.update(session_id);
        st.update([id_a.len() as u8]);
        st.update(id_a);
        st.update([id_b.len() as u8]);
        st.update(id_b);
        st.update(ad.as_ref().map(|ad| ad.as_ref()).unwrap_or_default());
        let h = st.finalize();
        let mut p = RistrettoPoint::from_uniform_bytes(&h);
        let mut r = [0u8; 64];
        getrandom(&mut r)?;
        let r = Scalar::from_bytes_mod_order_wide(&r);
        p *= r;
        Ok(CPace { session_id, p, r })
    }

    fn finalize(
        &self,
        op: RistrettoPoint,
        ya: RistrettoPoint,
        yb: RistrettoPoint,
    ) -> Result<SharedKeys, Error> {
        if op.is_identity() {
            return Err(Error::InvalidPublicKey);
        }
        let p = op * self.r;
        let mut st = Hash::new();
        st.update(DSI2);
        st.update(p.compress().as_bytes());
        st.update(ya.compress().as_bytes());
        st.update(yb.compress().as_bytes());
        let h = st.finalize();
        let (mut k1, mut k2) = ([0u8; SHARED_KEY_BYTES], [0u8; SHARED_KEY_BYTES]);
        k1.copy_from_slice(&h[..SHARED_KEY_BYTES]);
        k2.copy_from_slice(&h[SHARED_KEY_BYTES..]);
        Ok(SharedKeys { k1, k2 })
    }

    pub fn step1<T: AsRef<[u8]>>(
        password: &str,
        id_a: &str,
        id_b: &str,
        ad: Option<T>,
    ) -> Result<Step1Out, Error> {
        let mut session_id = [0u8; SESSION_ID_BYTES];
        getrandom(&mut session_id)?;
        let ctx = CPace::new(session_id, password, id_a, id_b, ad)?;
        let mut step1_packet = [0u8; STEP1_PACKET_BYTES];
        step1_packet[..SESSION_ID_BYTES].copy_from_slice(&ctx.session_id);
        step1_packet[SESSION_ID_BYTES..].copy_from_slice(ctx.p.compress().as_bytes());
        Ok(Step1Out { ctx, step1_packet })
    }

    pub fn step2<T: AsRef<[u8]>>(
        step1_packet: &[u8; STEP1_PACKET_BYTES],
        password: &str,
        id_a: &str,
        id_b: &str,
        ad: Option<T>,
    ) -> Result<Step2Out, Error> {
        let mut session_id = [0u8; SESSION_ID_BYTES];
        session_id.copy_from_slice(&step1_packet[..SESSION_ID_BYTES]);
        let ya = &step1_packet[SESSION_ID_BYTES..];
        let ctx = CPace::new(session_id, password, id_a, id_b, ad)?;
        let mut step2_packet = [0u8; STEP2_PACKET_BYTES];
        step2_packet.copy_from_slice(ctx.p.compress().as_bytes());
        let ya = CompressedRistretto::from_slice(ya)
            .map_err(|_| Error::InvalidPublicKey)?
            .decompress()
            .ok_or(Error::InvalidPublicKey)?;
        let shared_keys = ctx.finalize(ya, ya, ctx.p)?;
        Ok(Step2Out {
            shared_keys,
            step2_packet,
        })
    }

    pub fn step3(&self, step2_packet: &[u8; STEP2_PACKET_BYTES]) -> Result<SharedKeys, Error> {
        let yb = CompressedRistretto::from_slice(step2_packet)
            .map_err(|_| Error::InvalidPublicKey)?
            .decompress()
            .ok_or(Error::InvalidPublicKey)?;
        self.finalize(yb, self.p, yb)
    }
}

#[test]
fn test_cpace() {
    let client = CPace::step1("password", "client", "server", Some("ad")).unwrap();

    let step2 = CPace::step2(&client.packet(), "password", "client", "server", Some("ad")).unwrap();

    let shared_keys = client.step3(&step2.packet()).unwrap();

    assert_eq!(shared_keys.k1, step2.shared_keys.k1);
    assert_eq!(shared_keys.k2, step2.shared_keys.k2);
}