quadra-a-core 0.1.0-beta.1

Core protocol primitives for the quadra-a agent network
Documentation
use anyhow::{anyhow, Context, Result};
use chacha20poly1305::{
    aead::{Aead, Payload},
    KeyInit, XChaCha20Poly1305, XNonce,
};
use hkdf::Hkdf;
use rand::{rngs::OsRng, RngCore};
use sha2::Sha256;
use x25519_dalek::{PublicKey, StaticSecret};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct X25519KeyPair {
    pub public_key: [u8; 32],
    pub private_key: [u8; 32],
}

pub fn array32(bytes: &[u8], label: &str) -> Result<[u8; 32]> {
    bytes
        .try_into()
        .map_err(|_| anyhow!("{} must be 32 bytes", label))
}

pub fn array24(bytes: &[u8], label: &str) -> Result<[u8; 24]> {
    bytes
        .try_into()
        .map_err(|_| anyhow!("{} must be 24 bytes", label))
}

pub fn generate_x25519_key_pair() -> X25519KeyPair {
    let secret = StaticSecret::random_from_rng(OsRng);
    let public = PublicKey::from(&secret);
    X25519KeyPair {
        public_key: public.to_bytes(),
        private_key: secret.to_bytes(),
    }
}

pub fn derive_x25519_public_key(private_key: &[u8]) -> Result<[u8; 32]> {
    let secret = StaticSecret::from(array32(private_key, "X25519 private key")?);
    Ok(PublicKey::from(&secret).to_bytes())
}

pub fn diffie_hellman_x25519(private_key: &[u8], public_key: &[u8]) -> Result<[u8; 32]> {
    let secret = StaticSecret::from(array32(private_key, "X25519 private key")?);
    let public = PublicKey::from(array32(public_key, "X25519 public key")?);
    Ok(secret.diffie_hellman(&public).to_bytes())
}

pub fn hkdf_sha256(
    input_key_material: &[u8],
    salt: &[u8],
    info: &[u8],
    length: usize,
) -> Result<Vec<u8>> {
    let hk = Hkdf::<Sha256>::new(Some(salt), input_key_material);
    let mut output = vec![0u8; length];
    hk.expand(info, &mut output)
        .map_err(|_| anyhow!("Failed to expand HKDF-SHA256 output"))?;
    Ok(output)
}

pub fn encrypt_xchacha20poly1305(
    key: &[u8],
    nonce: &[u8],
    plaintext: &[u8],
    associated_data: &[u8],
) -> Result<Vec<u8>> {
    let cipher =
        XChaCha20Poly1305::new_from_slice(key).context("Invalid XChaCha20-Poly1305 key")?;
    let nonce = array24(nonce, "XChaCha20-Poly1305 nonce")?;
    cipher
        .encrypt(
            XNonce::from_slice(&nonce),
            Payload {
                msg: plaintext,
                aad: associated_data,
            },
        )
        .map_err(|_| anyhow!("Failed to encrypt with XChaCha20-Poly1305"))
}

pub fn decrypt_xchacha20poly1305(
    key: &[u8],
    nonce: &[u8],
    ciphertext: &[u8],
    associated_data: &[u8],
) -> Result<Vec<u8>> {
    let cipher =
        XChaCha20Poly1305::new_from_slice(key).context("Invalid XChaCha20-Poly1305 key")?;
    let nonce = array24(nonce, "XChaCha20-Poly1305 nonce")?;
    cipher
        .decrypt(
            XNonce::from_slice(&nonce),
            Payload {
                msg: ciphertext,
                aad: associated_data,
            },
        )
        .map_err(|_| anyhow!("Failed to decrypt with XChaCha20-Poly1305"))
}

pub fn bytes_to_hex(bytes: &[u8]) -> String {
    hex::encode(bytes)
}

pub fn random_bytes(length: usize) -> Vec<u8> {
    let mut output = vec![0u8; length];
    OsRng.fill_bytes(&mut output);
    output
}