fcrypt 0.3.3

Cross-platform CLI for password-based file encryption and decryption
use crate::error::{AppError, Result};
use zeroize::Zeroize;

pub struct RecipientKeypair {
    pub mlkem1024_public: Vec<u8>,
    pub mlkem1024_secret: Vec<u8>,
    pub hqc256_public: Vec<u8>,
    pub hqc256_secret: Vec<u8>,
}

pub struct EncapsulatedSecrets {
    pub mlkem1024_ciphertext: Vec<u8>,
    pub mlkem1024_shared_secret: Vec<u8>,
    pub hqc256_ciphertext: Vec<u8>,
    pub hqc256_shared_secret: Vec<u8>,
}

pub struct SigningKeypair {
    pub mldsa87_public: Vec<u8>,
    pub mldsa87_secret: Vec<u8>,
}

impl Drop for RecipientKeypair {
    fn drop(&mut self) {
        self.mlkem1024_secret.zeroize();
        self.hqc256_secret.zeroize();
    }
}

impl Drop for EncapsulatedSecrets {
    fn drop(&mut self) {
        self.mlkem1024_shared_secret.zeroize();
        self.hqc256_shared_secret.zeroize();
    }
}

impl Drop for SigningKeypair {
    fn drop(&mut self) {
        self.mldsa87_secret.zeroize();
    }
}

#[cfg(feature = "pqc")]
pub fn ensure_enabled() -> Result<()> {
    oqs::init();
    let required_kems = [oqs::kem::Algorithm::MlKem1024, oqs::kem::Algorithm::Hqc256];
    for alg in required_kems {
        if !alg.is_enabled() {
            return Err(AppError::Pqc(format!("required KEM is disabled: {alg}")));
        }
    }
    let sig = oqs::sig::Algorithm::MlDsa87;
    if !sig.is_enabled() {
        return Err(AppError::Pqc(format!(
            "required signature is disabled: {sig}"
        )));
    }
    Ok(())
}

#[cfg(not(feature = "pqc"))]
pub fn ensure_enabled() -> Result<()> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn generate_recipient_keypair() -> Result<RecipientKeypair> {
    ensure_enabled()?;
    let mlkem = oqs::kem::Kem::new(oqs::kem::Algorithm::MlKem1024).map_err(oqs_error)?;
    let hqc = oqs::kem::Kem::new(oqs::kem::Algorithm::Hqc256).map_err(oqs_error)?;
    let (mlkem_pk, mlkem_sk) = mlkem.keypair().map_err(oqs_error)?;
    let (hqc_pk, hqc_sk) = hqc.keypair().map_err(oqs_error)?;
    Ok(RecipientKeypair {
        mlkem1024_public: mlkem_pk.into_vec(),
        mlkem1024_secret: mlkem_sk.into_vec(),
        hqc256_public: hqc_pk.into_vec(),
        hqc256_secret: hqc_sk.into_vec(),
    })
}

#[cfg(not(feature = "pqc"))]
pub fn generate_recipient_keypair() -> Result<RecipientKeypair> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn encapsulate_recipient(
    mlkem1024_public: &[u8],
    hqc256_public: &[u8],
) -> Result<EncapsulatedSecrets> {
    ensure_enabled()?;
    let mlkem = oqs::kem::Kem::new(oqs::kem::Algorithm::MlKem1024).map_err(oqs_error)?;
    let hqc = oqs::kem::Kem::new(oqs::kem::Algorithm::Hqc256).map_err(oqs_error)?;
    let mlkem_pk = mlkem
        .public_key_from_bytes(mlkem1024_public)
        .ok_or_else(|| {
            AppError::InvalidAsymmetricKeyFile("invalid ML-KEM-1024 public key length".to_string())
        })?;
    let hqc_pk = hqc.public_key_from_bytes(hqc256_public).ok_or_else(|| {
        AppError::InvalidAsymmetricKeyFile("invalid HQC-256 public key length".to_string())
    })?;

    let (mlkem_ct, mlkem_ss) = mlkem.encapsulate(mlkem_pk).map_err(oqs_error)?;
    let (hqc_ct, hqc_ss) = hqc.encapsulate(hqc_pk).map_err(oqs_error)?;
    Ok(EncapsulatedSecrets {
        mlkem1024_ciphertext: mlkem_ct.into_vec(),
        mlkem1024_shared_secret: mlkem_ss.into_vec(),
        hqc256_ciphertext: hqc_ct.into_vec(),
        hqc256_shared_secret: hqc_ss.into_vec(),
    })
}

#[cfg(not(feature = "pqc"))]
pub fn encapsulate_recipient(
    _mlkem1024_public: &[u8],
    _hqc256_public: &[u8],
) -> Result<EncapsulatedSecrets> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn decapsulate_recipient(
    mlkem1024_secret: &[u8],
    hqc256_secret: &[u8],
    mlkem1024_ciphertext: &[u8],
    hqc256_ciphertext: &[u8],
) -> Result<(Vec<u8>, Vec<u8>)> {
    ensure_enabled()?;
    let mlkem = oqs::kem::Kem::new(oqs::kem::Algorithm::MlKem1024).map_err(oqs_error)?;
    let hqc = oqs::kem::Kem::new(oqs::kem::Algorithm::Hqc256).map_err(oqs_error)?;
    let mlkem_sk = mlkem
        .secret_key_from_bytes(mlkem1024_secret)
        .ok_or_else(|| {
            AppError::InvalidAsymmetricKeyFile("invalid ML-KEM-1024 secret key length".to_string())
        })?;
    let hqc_sk = hqc.secret_key_from_bytes(hqc256_secret).ok_or_else(|| {
        AppError::InvalidAsymmetricKeyFile("invalid HQC-256 secret key length".to_string())
    })?;
    let mlkem_ct = mlkem
        .ciphertext_from_bytes(mlkem1024_ciphertext)
        .ok_or(AppError::AsymmetricAuthenticationFailed)?;
    let hqc_ct = hqc
        .ciphertext_from_bytes(hqc256_ciphertext)
        .ok_or(AppError::AsymmetricAuthenticationFailed)?;
    let mlkem_ss = mlkem
        .decapsulate(mlkem_sk, mlkem_ct)
        .map_err(|_| AppError::AsymmetricAuthenticationFailed)?;
    let hqc_ss = hqc
        .decapsulate(hqc_sk, hqc_ct)
        .map_err(|_| AppError::AsymmetricAuthenticationFailed)?;
    Ok((mlkem_ss.into_vec(), hqc_ss.into_vec()))
}

#[cfg(not(feature = "pqc"))]
pub fn decapsulate_recipient(
    _mlkem1024_secret: &[u8],
    _hqc256_secret: &[u8],
    _mlkem1024_ciphertext: &[u8],
    _hqc256_ciphertext: &[u8],
) -> Result<(Vec<u8>, Vec<u8>)> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn generate_signing_keypair() -> Result<SigningKeypair> {
    ensure_enabled()?;
    let sig = oqs::sig::Sig::new(oqs::sig::Algorithm::MlDsa87).map_err(oqs_error)?;
    let (pk, sk) = sig.keypair().map_err(oqs_error)?;
    Ok(SigningKeypair {
        mldsa87_public: pk.into_vec(),
        mldsa87_secret: sk.into_vec(),
    })
}

#[cfg(not(feature = "pqc"))]
pub fn generate_signing_keypair() -> Result<SigningKeypair> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn sign_mldsa87(secret_key: &[u8], message: &[u8]) -> Result<Vec<u8>> {
    ensure_enabled()?;
    let sig = oqs::sig::Sig::new(oqs::sig::Algorithm::MlDsa87).map_err(oqs_error)?;
    let sk = sig.secret_key_from_bytes(secret_key).ok_or_else(|| {
        AppError::InvalidAsymmetricKeyFile("invalid ML-DSA-87 secret key length".to_string())
    })?;
    let signature = sig.sign(message, sk).map_err(oqs_error)?;
    Ok(signature.into_vec())
}

#[cfg(not(feature = "pqc"))]
pub fn sign_mldsa87(_secret_key: &[u8], _message: &[u8]) -> Result<Vec<u8>> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
pub fn verify_mldsa87(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<()> {
    ensure_enabled()?;
    let sig = oqs::sig::Sig::new(oqs::sig::Algorithm::MlDsa87).map_err(oqs_error)?;
    let pk = sig.public_key_from_bytes(public_key).ok_or_else(|| {
        AppError::InvalidAsymmetricKeyFile("invalid ML-DSA-87 public key length".to_string())
    })?;
    let signature_ref = sig
        .signature_from_bytes(signature)
        .ok_or(AppError::SignatureVerificationFailed)?;
    sig.verify(message, signature_ref, pk)
        .map_err(|_| AppError::SignatureVerificationFailed)
}

#[cfg(not(feature = "pqc"))]
pub fn verify_mldsa87(_public_key: &[u8], _message: &[u8], _signature: &[u8]) -> Result<()> {
    Err(AppError::AsymmetricUnavailable)
}

#[cfg(feature = "pqc")]
fn oqs_error(error: oqs::Error) -> AppError {
    AppError::Pqc(error.to_string())
}