soblendwa 0.1.0

SoblendWA: a blazing fast WhatsApp Web multi-device client library written in Rust. Crafted by Soblend Development Studio.
Documentation
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use prost::Message as _;

use crate::crypto::{self, KeyPair};
use crate::error::{Error, Result};
use crate::proto::{CertChain, CertificateDetails, ServerHello};
#[cfg(any(test, feature = "testing"))]
use crate::proto::{ClientFinish, NoiseCertificate};

const NOISE_MODE: &[u8; 32] = b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0";
pub(crate) const WA_HEADER: [u8; 4] = [b'W', b'A', 6, 3];
pub(crate) const WA_ROOT_KEY: [u8; 32] = [
    0x14, 0x23, 0x75, 0x57, 0x4d, 0x0a, 0x58, 0x71, 0x66, 0xaa, 0xe7, 0x1e, 0xbe, 0x51, 0x64, 0x37, 0xc4, 0xa2, 0x8b,
    0x73, 0xe3, 0x69, 0x5c, 0x6c, 0xe1, 0xf7, 0xf9, 0x54, 0x5d, 0xa8, 0xee, 0x6b,
];
const WA_ROOT_SERIAL: u32 = 0;
const CERTIFICATE_SKEW_SECS: u64 = 24 * 60 * 60;
pub(crate) const MAX_FRAME: usize = (1 << 24) - 1;

pub(crate) fn intro_header(routing_info: Option<&[u8]>) -> Vec<u8> {
    match routing_info {
        Some(routing) => {
            let mut header = Vec::with_capacity(7 + routing.len() + WA_HEADER.len());
            header.extend_from_slice(b"ED");
            header.extend_from_slice(&[0, 1]);
            header.push((routing.len() >> 16) as u8);
            header.extend_from_slice(&(routing.len() as u16).to_be_bytes());
            header.extend_from_slice(routing);
            header.extend_from_slice(&WA_HEADER);
            header
        }
        None => WA_HEADER.to_vec(),
    }
}

pub(crate) struct Handshake {
    hash: [u8; 32],
    salt: [u8; 32],
    cipher: Aes256Gcm,
    counter: u32,
}

impl Handshake {
    pub(crate) fn new(ephemeral_public: &[u8; 32]) -> Self {
        let mut handshake =
            Handshake { hash: *NOISE_MODE, salt: *NOISE_MODE, cipher: crypto::gcm(NOISE_MODE), counter: 0 };
        handshake.authenticate(&WA_HEADER);
        handshake.authenticate(ephemeral_public);
        handshake
    }

    fn authenticate(&mut self, data: &[u8]) {
        self.hash = crypto::sha256_concat(&[&self.hash, data]);
    }

    fn mix_key(&mut self, input: &[u8]) {
        let derived: [u8; 64] = crypto::hkdf_array(input, Some(&self.salt), b"");
        self.salt.copy_from_slice(&derived[..32]);
        self.cipher = crypto::gcm(&derived[32..]);
        self.counter = 0;
    }

    fn next_iv(&mut self) -> [u8; 12] {
        let iv = crypto::gcm_iv(self.counter);
        self.counter += 1;
        iv
    }

    pub(crate) fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
        let iv = self.next_iv();
        let ciphertext = self
            .cipher
            .encrypt(Nonce::from_slice(&iv), Payload { msg: plaintext, aad: &self.hash })
            .map_err(|_| Error::Crypto("noise encryption failed"))?;
        self.authenticate(&ciphertext);
        Ok(ciphertext)
    }

    fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>> {
        let iv = self.next_iv();
        let plaintext = self
            .cipher
            .decrypt(Nonce::from_slice(&iv), Payload { msg: ciphertext, aad: &self.hash })
            .map_err(|_| Error::Handshake("server hello failed authentication".into()))?;
        self.authenticate(ciphertext);
        Ok(plaintext)
    }

    pub(crate) fn process_server_hello(
        &mut self,
        hello: &ServerHello,
        ephemeral: &KeyPair,
        noise_key: &KeyPair,
        root: &[u8; 32],
    ) -> Result<Vec<u8>> {
        let field = |v: &Option<Vec<u8>>, name: &str| {
            v.clone().ok_or_else(|| Error::Handshake(format!("server hello is missing {name}")))
        };
        let server_ephemeral: [u8; 32] = field(&hello.ephemeral, "ephemeral")?
            .try_into()
            .map_err(|_| Error::Handshake("server ephemeral key has the wrong size".into()))?;

        self.authenticate(&server_ephemeral);
        self.mix_key(&ephemeral.shared_secret(&server_ephemeral));

        let server_static: [u8; 32] = self
            .decrypt(&field(&hello.static_key, "static key")?)?
            .try_into()
            .map_err(|_| Error::Handshake("server static key has the wrong size".into()))?;
        self.mix_key(&ephemeral.shared_secret(&server_static));

        let certificate = self.decrypt(&field(&hello.payload, "certificate")?)?;
        verify_certificate(&certificate, &server_static, root)?;

        let encrypted_static = self.encrypt(&noise_key.public)?;
        self.mix_key(&noise_key.shared_secret(&server_ephemeral));
        Ok(encrypted_static)
    }

    pub(crate) fn finish(self) -> (NoiseCipher, NoiseCipher) {
        let derived: [u8; 64] = crypto::hkdf_array(b"", Some(&self.salt), b"");
        (NoiseCipher::new(&derived[..32]), NoiseCipher::new(&derived[32..]))
    }
}

fn verify_certificate(raw: &[u8], server_static: &[u8; 32], root: &[u8; 32]) -> Result<()> {
    let invalid = |why: &str| Error::Handshake(format!("noise certificate rejected: {why}"));
    let chain = CertChain::decode(raw)?;
    let (Some(leaf), Some(intermediate)) = (chain.leaf, chain.intermediate) else {
        return Err(invalid("incomplete chain"));
    };
    let signed = |cert: &crate::proto::NoiseCertificate| -> Result<(Vec<u8>, [u8; 64])> {
        let details = cert.details.clone().ok_or_else(|| invalid("missing details"))?;
        let signature = cert
            .signature
            .clone()
            .and_then(|s| <[u8; 64]>::try_from(s).ok())
            .ok_or_else(|| invalid("missing signature"))?;
        Ok((details, signature))
    };

    let (intermediate_details, intermediate_signature) = signed(&intermediate)?;
    if !crypto::verify(root, &intermediate_details, &intermediate_signature) {
        return Err(invalid("intermediate is not signed by the whatsapp root"));
    }
    let intermediate = CertificateDetails::decode(intermediate_details.as_slice())?;
    if intermediate.issuer_serial.unwrap_or_default() != WA_ROOT_SERIAL {
        return Err(invalid("unexpected issuer serial"));
    }
    let now = crate::message::now_secs().max(0) as u64;
    let in_window = |details: &CertificateDetails| {
        details.not_before.is_none_or(|from| now + CERTIFICATE_SKEW_SECS >= from)
            && details.not_after.is_none_or(|to| now <= to + CERTIFICATE_SKEW_SECS)
    };
    if !in_window(&intermediate) {
        return Err(invalid("the intermediate is outside its validity window"));
    }
    let intermediate_serial = intermediate.serial;
    let intermediate_key: [u8; 32] =
        intermediate.key.and_then(|k| k.try_into().ok()).ok_or_else(|| invalid("intermediate key is malformed"))?;

    let (leaf_details, leaf_signature) = signed(&leaf)?;
    if !crypto::verify(&intermediate_key, &leaf_details, &leaf_signature) {
        return Err(invalid("leaf is not signed by the intermediate"));
    }
    let leaf = CertificateDetails::decode(leaf_details.as_slice())?;
    if !in_window(&leaf) {
        return Err(invalid("the leaf is outside its validity window"));
    }
    if intermediate_serial.is_some() && leaf.issuer_serial != intermediate_serial {
        return Err(invalid("the leaf was not issued by this intermediate"));
    }
    if leaf.key.as_deref() != Some(server_static.as_slice()) {
        return Err(invalid("leaf key does not match the server static key"));
    }
    Ok(())
}

#[cfg(any(test, feature = "testing"))]
pub(crate) struct ServerHandshake {
    inner: Handshake,
    ephemeral: KeyPair,
}

#[cfg(any(test, feature = "testing"))]
impl ServerHandshake {
    pub(crate) fn respond(
        client_ephemeral: &[u8; 32],
        server_static: &KeyPair,
        certificate: &[u8],
    ) -> Result<(Self, ServerHello)> {
        let mut inner = Handshake::new(client_ephemeral);
        let ephemeral = KeyPair::generate();
        inner.authenticate(&ephemeral.public);
        inner.mix_key(&ephemeral.shared_secret(client_ephemeral));
        let static_key = inner.encrypt(&server_static.public)?;
        inner.mix_key(&server_static.shared_secret(client_ephemeral));
        let payload = inner.encrypt(certificate)?;
        let hello = ServerHello {
            ephemeral: Some(ephemeral.public.to_vec()),
            static_key: Some(static_key),
            payload: Some(payload),
        };
        Ok((ServerHandshake { inner, ephemeral }, hello))
    }

    pub(crate) fn complete(mut self, finish: &ClientFinish) -> Result<([u8; 32], Vec<u8>, NoiseCipher, NoiseCipher)> {
        let missing = |what: &str| Error::Handshake(format!("client finish is missing {what}"));
        let client_static: [u8; 32] = self
            .inner
            .decrypt(finish.static_key.as_deref().ok_or_else(|| missing("the static key"))?)?
            .try_into()
            .map_err(|_| Error::Handshake("client static key has the wrong size".into()))?;
        self.inner.mix_key(&self.ephemeral.shared_secret(&client_static));
        let payload = self.inner.decrypt(finish.payload.as_deref().ok_or_else(|| missing("the payload"))?)?;
        let (client_writer, client_reader) = self.inner.finish();
        Ok((client_static, payload, client_reader, client_writer))
    }
}

#[cfg(any(test, feature = "testing"))]
pub(crate) fn issue_certificate_chain(root: &KeyPair, server_static: &[u8; 32]) -> Vec<u8> {
    let intermediate_key = KeyPair::generate();
    let now = crate::message::now_secs().max(0) as u64;
    let (not_before, not_after) = (now.saturating_sub(24 * 60 * 60), now + 10 * 365 * 24 * 60 * 60);
    let certificate = |signer: &KeyPair, details: CertificateDetails| {
        let details = details.encode_to_vec();
        NoiseCertificate { signature: Some(signer.sign(&details).to_vec()), details: Some(details) }
    };
    let intermediate = certificate(
        root,
        CertificateDetails {
            serial: Some(1),
            issuer_serial: Some(WA_ROOT_SERIAL),
            key: Some(intermediate_key.public.to_vec()),
            not_before: Some(not_before),
            not_after: Some(not_after),
        },
    );
    let leaf = certificate(
        &intermediate_key,
        CertificateDetails {
            serial: Some(2),
            issuer_serial: Some(1),
            key: Some(server_static.to_vec()),
            not_before: Some(not_before),
            not_after: Some(not_after),
        },
    );
    CertChain { leaf: Some(leaf), intermediate: Some(intermediate) }.encode_to_vec()
}
pub(crate) struct NoiseCipher {
    cipher: Aes256Gcm,
    counter: u32,
}

impl NoiseCipher {
    fn new(key: &[u8]) -> Self {
        NoiseCipher { cipher: crypto::gcm(key), counter: 0 }
    }

    pub(crate) fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
        let iv = crypto::gcm_iv(self.counter);
        self.counter = self.counter.wrapping_add(1);
        self.cipher.encrypt(Nonce::from_slice(&iv), plaintext).map_err(|_| Error::Crypto("transport encryption failed"))
    }

    pub(crate) fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>> {
        let iv = crypto::gcm_iv(self.counter);
        self.counter = self.counter.wrapping_add(1);
        self.cipher
            .decrypt(Nonce::from_slice(&iv), ciphertext)
            .map_err(|_| Error::Crypto("transport frame failed authentication"))
    }
}

pub(crate) fn frame(intro: Option<&[u8]>, payload: &[u8]) -> Result<Vec<u8>> {
    if payload.len() > MAX_FRAME {
        return Err(Error::other("frame is larger than 16 MiB"));
    }
    let intro = intro.unwrap_or_default();
    let mut out = Vec::with_capacity(intro.len() + 3 + payload.len());
    out.extend_from_slice(intro);
    let len = payload.len();
    out.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
    out.extend_from_slice(payload);
    Ok(out)
}

#[derive(Default)]
pub(crate) struct FrameDecoder {
    buf: Vec<u8>,
    start: usize,
}

impl FrameDecoder {
    pub(crate) fn push(&mut self, data: &[u8]) {
        if self.start > 0 && self.start == self.buf.len() {
            self.buf.clear();
            self.start = 0;
        } else if self.start > 64 * 1024 && self.start * 2 > self.buf.len() {
            self.buf.drain(..self.start);
            self.start = 0;
        }
        self.buf.extend_from_slice(data);
    }

    pub(crate) fn next_frame(&mut self) -> Option<Vec<u8>> {
        let pending = &self.buf[self.start..];
        if pending.len() < 3 {
            return None;
        }
        let len = (usize::from(pending[0]) << 16) | (usize::from(pending[1]) << 8) | usize::from(pending[2]);
        if pending.len() < 3 + len {
            return None;
        }
        let frame = pending[3..3 + len].to_vec();
        self.start += 3 + len;
        Some(frame)
    }
}

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

    #[test]
    fn frames_split_across_reads() {
        let a = frame(None, b"first").unwrap();
        let b = frame(None, &[7u8; 70_000]).unwrap();
        let mut stream = a.clone();
        stream.extend_from_slice(&b);
        let mut decoder = FrameDecoder::default();
        let mut frames = Vec::new();
        for chunk in stream.chunks(1000) {
            decoder.push(chunk);
            while let Some(frame) = decoder.next_frame() {
                frames.push(frame);
            }
        }
        assert_eq!(frames.len(), 2);
        assert_eq!(frames[0], b"first");
        assert_eq!(frames[1].len(), 70_000);
    }

    #[test]
    fn intro_with_routing_info() {
        let header = intro_header(Some(&[1, 2, 3]));
        assert_eq!(&header[..4], b"ED\x00\x01");
        assert_eq!(&header[4..7], &[0, 0, 3]);
        assert_eq!(&header[header.len() - 4..], &WA_HEADER);
    }
}