soblendwa 0.1.0

SoblendWA: a blazing fast WhatsApp Web multi-device client library written in Rust. Crafted by Soblend Development Studio.
Documentation
use curve25519_dalek::edwards::EdwardsPoint;
use curve25519_dalek::montgomery::MontgomeryPoint;
use curve25519_dalek::scalar::Scalar;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512};

use crate::error::{Error, Result};
use crate::serde_b64;

pub const KEY_TYPE: u8 = 5;

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyPair {
    #[serde(with = "serde_b64::array")]
    pub public: [u8; 32],
    #[serde(with = "serde_b64::array")]
    pub private: [u8; 32],
}

impl std::fmt::Debug for KeyPair {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("KeyPair").field("public", &hex::encode(self.public)).finish_non_exhaustive()
    }
}

impl KeyPair {
    pub fn generate() -> Self {
        Self::from_private(super::random_array())
    }

    pub fn from_private(mut private: [u8; 32]) -> Self {
        private[0] &= 248;
        private[31] &= 127;
        private[31] |= 64;
        let public = MontgomeryPoint::mul_base_clamped(private).to_bytes();
        KeyPair { public, private }
    }

    pub fn shared_secret(&self, their_public: &[u8; 32]) -> [u8; 32] {
        MontgomeryPoint(*their_public).mul_clamped(self.private).to_bytes()
    }

    pub fn signal_public(&self) -> [u8; 33] {
        signal_key(&self.public)
    }

    pub fn sign(&self, message: &[u8]) -> [u8; 64] {
        sign(&self.private, message)
    }
}

pub fn sign(private: &[u8; 32], message: &[u8]) -> [u8; 64] {
    let nonce: [u8; 64] = super::random_array();
    let a = Scalar::from_bytes_mod_order(*private);
    let public_point = EdwardsPoint::mul_base(&a).compress();
    let sign_bit = public_point.as_bytes()[31] & 0x80;

    let mut prefix = [0xffu8; 32];
    prefix[0] = 0xfe;
    let r = Scalar::from_hash(
        Sha512::new().chain_update(prefix).chain_update(private).chain_update(message).chain_update(nonce),
    );
    let cap_r = EdwardsPoint::mul_base(&r).compress();
    let h = Scalar::from_hash(
        Sha512::new().chain_update(cap_r.as_bytes()).chain_update(public_point.as_bytes()).chain_update(message),
    );
    let s = h * a + r;

    let mut signature = [0u8; 64];
    signature[..32].copy_from_slice(cap_r.as_bytes());
    signature[32..].copy_from_slice(s.as_bytes());
    signature[63] &= 0x7f;
    signature[63] |= sign_bit;
    signature
}

pub fn verify(public: &[u8; 32], message: &[u8], signature: &[u8; 64]) -> bool {
    let Some(point) = MontgomeryPoint(*public).to_edwards((signature[63] & 0x80) >> 7) else {
        return false;
    };
    let mut s = [0u8; 32];
    s.copy_from_slice(&signature[32..]);
    s[31] &= 0x7f;
    if s[31] & 0xe0 != 0 {
        return false;
    }
    let cap_a = point.compress();
    let h = Scalar::from_hash(
        Sha512::new().chain_update(&signature[..32]).chain_update(cap_a.as_bytes()).chain_update(message),
    );
    let check = EdwardsPoint::vartime_double_scalar_mul_basepoint(&h, &-point, &Scalar::from_bytes_mod_order(s));
    super::constant_time_eq(check.compress().as_bytes(), &signature[..32])
}

pub fn signal_key(public: &[u8; 32]) -> [u8; 33] {
    let mut out = [0u8; 33];
    out[0] = KEY_TYPE;
    out[1..].copy_from_slice(public);
    out
}

pub fn strip_key_prefix(key: &[u8]) -> Result<[u8; 32]> {
    let raw = match key.len() {
        33 if key[0] == KEY_TYPE => &key[1..],
        32 => key,
        _ => return Err(Error::Crypto("invalid curve25519 public key")),
    };
    let mut out = [0u8; 32];
    out.copy_from_slice(raw);
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn agreement_is_symmetric() {
        let alice = KeyPair::generate();
        let bob = KeyPair::generate();
        assert_eq!(alice.shared_secret(&bob.public), bob.shared_secret(&alice.public));
    }

    #[test]
    fn xeddsa_sign_and_verify() {
        let pair = KeyPair::generate();
        for len in [0usize, 1, 33, 200] {
            let message = vec![0x42u8; len];
            let signature = pair.sign(&message);
            assert!(verify(&pair.public, &message, &signature));
            let mut tampered = signature;
            tampered[5] ^= 1;
            assert!(!verify(&pair.public, &message, &tampered));
        }
    }

    #[test]
    fn strips_signal_prefix() {
        let pair = KeyPair::generate();
        assert_eq!(strip_key_prefix(&pair.signal_public()).unwrap(), pair.public);
        assert_eq!(strip_key_prefix(&pair.public).unwrap(), pair.public);
        assert!(strip_key_prefix(&[1, 2, 3]).is_err());
    }
}