rgp 0.7.0

Enabling E2EE for a range of applications.
Documentation
/*
Copyright (c) 2026 Ordinary Labs, LLC

SPDX-License-Identifier: AGPL-3.0-only
*/
use anyhow::bail;
use chacha20poly1305::{XChaCha20Poly1305, aead::Aead};
use ed25519_dalek::Signer;
use getrandom::{SysRng, rand_core::UnwrapErr};

use classic_mceliece_rust::{
    CRYPTO_CIPHERTEXTBYTES as KEM_CIPHERTEXT_SIZE, CRYPTO_SECRETKEYBYTES as KEM_SECRET_KEY_SIZE,
};

mod modes;
pub use modes::{
    DH_MODE, DH_WITH_HMAC_MODE, HMAC_MODE, KEM_MODE, KEM_WITH_DH_HYBRID_MODE, KemKeyReader,
    SESSION_MODE, SESSION_WITH_KEYGEN_MODE, generate_dh_keys, generate_kem_keys,
};
use modes::{
    dh_decrypt, dh_encrypt, dh_extract, hmac_decrypt, hmac_encrypt, hmac_extract, kem_decrypt,
    kem_encrypt, kem_extract, session_decrypt, session_encrypt, session_extract,
};

mod decrypt;
pub use decrypt::*;

mod encrypt;
pub use encrypt::*;

mod extract;
use crate::ZEROED_KEY;
pub use extract::*;

const NONCE_SIZE: usize = 24;
const KEY_SIZE: usize = 32;
const SIGNATURE_SIZE: usize = 64;

/// generates fingerprints and verifying keys for signing.
///
/// ```rust
/// use rgp::generate_fingerprint;
///
/// let (fingerprint, verifier) = generate_fingerprint();
///
/// assert_eq!(fingerprint.len(), 32);
/// assert_eq!(verifier.len(), 32);
/// ```
#[must_use]
pub fn generate_fingerprint() -> ([u8; 32], [u8; 32]) {
    let mut rng = UnwrapErr(SysRng);
    let fingerprint = ed25519_dalek::SigningKey::generate(&mut rng);

    (
        fingerprint.to_bytes(),
        fingerprint.verifying_key().to_bytes(),
    )
}

fn usize_to_bytes(val: usize) -> anyhow::Result<(usize, [u8; 9])> {
    let mut out = [0u8; 9];

    if val < 128 {
        out[0] = u8::try_from((val << 1) | 1)?;

        Ok((1, out))
    } else {
        match val {
            128..=255 => {
                out[0] = 0 << 2;
                out[1] = u8::try_from(val)?;

                Ok((2, out))
            }
            256..=65_535 => {
                out[0] = 1 << 2;
                out[1..3].copy_from_slice(&u16::try_from(val)?.to_be_bytes());

                Ok((3, out))
            }
            65_536..=4_294_967_295 => {
                out[0] = 2 << 2;
                out[1..5].copy_from_slice(&u32::try_from(val)?.to_be_bytes());

                Ok((5, out))
            }
            _ => {
                out[0] = 3 << 2;
                out[1..9].copy_from_slice(&(val as u64).to_be_bytes());

                Ok((9, out))
            }
        }
    }
}

fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result<(usize, usize)> {
    let first_byte = bytes[0];

    if (first_byte & 0b0000_0001) != 0 {
        Ok((1, (first_byte >> 1) as usize))
    } else {
        Ok(match (first_byte >> 2) & 0b0000_0011 {
            0 => (2, bytes[1] as usize),
            1 => (3, u16::from_be_bytes(bytes[1..3].try_into()?) as usize),
            2 => (5, u32::from_be_bytes(bytes[1..5].try_into()?) as usize),
            3 => (
                9,
                usize::try_from(u64::from_be_bytes(bytes[1..9].try_into()?))?,
            ),
            _ => unreachable!(),
        })
    }
}

/// signs content.
fn sign(fingerprint: &[u8; 32], content: &[u8]) -> [u8; 64] {
    let fingerprint = ed25519_dalek::SigningKey::from_bytes(fingerprint);
    let signature = fingerprint.sign(content);

    signature.to_bytes()
}

/// signs and encrypts content.
fn base_encrypt(
    fingerprint: &[u8; 32],
    nonce: &[u8; 24],
    key: &[u8; 32],
    content: &mut Vec<u8>,
) -> anyhow::Result<Vec<u8>> {
    use chacha20poly1305::KeyInit;

    let signature = sign(fingerprint, content);
    content.extend(signature);

    let content_cipher = XChaCha20Poly1305::new(key.into());
    match content_cipher.encrypt(nonce.into(), content.as_ref()) {
        Ok(encrypted_content) => Ok(encrypted_content),
        Err(_) => bail!("failed to encrypt content"),
    }
}

/// verifies signatures.
fn verify(signature: &[u8; 64], verifier: &[u8; 32], content: &[u8]) -> anyhow::Result<()> {
    if verifier == &ZEROED_KEY {
        bail!("verifier cannot be all 0s");
    }

    let signature = ed25519_dalek::Signature::from_bytes(signature);
    let Ok(verifier) = ed25519_dalek::VerifyingKey::from_bytes(verifier) else {
        bail!("failed to convert verifying key");
    };

    match verifier.verify_strict(content, &signature) {
        Ok(()) => Ok(()),
        Err(_) => bail!("failed to verify signature"),
    }
}

/// decrypts and verifies content.
fn base_decrypt(
    verifier: Option<&[u8; 32]>,
    nonce: &[u8; 24],
    key: &[u8; 32],
    encrypted_content: &[u8],
) -> anyhow::Result<(Vec<u8>, [u8; KEY_SIZE])> {
    let content_cipher = {
        use chacha20poly1305::KeyInit;
        XChaCha20Poly1305::new(key.into())
    };

    match content_cipher.decrypt(nonce.into(), encrypted_content) {
        Ok(mut content) => {
            let signature = content.split_off(content.len() - SIGNATURE_SIZE);

            match verifier {
                Some(verifier) => {
                    let signature_as_bytes: [u8; SIGNATURE_SIZE] = match signature.try_into() {
                        Ok(v) => v,
                        Err(_) => bail!("failed to convert signature to bytes"),
                    };

                    verify(&signature_as_bytes, verifier, &content)?;
                    Ok((content, *key))
                }
                None => Ok((content, *key)),
            }
        }
        Err(_) => bail!("failed to decrypt content"),
    }
}