kaniop-data-mover 0.16.2

S3-compatible data mover for Kanidm backup payload and manifest transfer
use aes_gcm::aead::{Aead, KeyInit, Nonce};
use aes_gcm::{Aes256Gcm, Key};
use rand::Rng;
use sha2::{Digest, Sha256};

use kaniop_backup_core::auth::ENCRYPTION_KEY_ENV;
use kaniop_backup_core::manifest::ClientSideEncryptionMeta;

pub const AES_256_GCM_ALGORITHM: &str = "AES-256-GCM";
pub const DEK_SIZE: usize = 32;
pub const KEK_SIZE: usize = 32;
pub const NONCE_SIZE: usize = 12;
pub const SALT_SIZE: usize = 4;
pub const TAG_SIZE: usize = 16;

#[derive(Debug, thiserror::Error)]
pub enum CryptoError {
    #[error("encryption key not found: set {0} environment variable")]
    MissingKek(String),
    #[error("encryption key must be exactly {expected} bytes (got {actual})")]
    InvalidKekLength { expected: usize, actual: usize },
    #[error("encryption key decode failed: {0}")]
    KekDecode(String),
    #[error("encryption operation failed: {0}")]
    Encrypt(String),
    #[error("decryption operation failed: {0}")]
    Decrypt(String),
    #[error("wrapped DEK decode failed: {0}")]
    WrappedDekDecode(String),
    #[error("nonce salt decode failed: {0}")]
    NonceSaltDecode(String),
}

pub struct EnvelopeKeys {
    pub dek: [u8; DEK_SIZE],
    pub nonce_salt: [u8; SALT_SIZE],
}

pub fn load_kek_from_env() -> Result<[u8; KEK_SIZE], CryptoError> {
    let raw = std::env::var(ENCRYPTION_KEY_ENV)
        .map_err(|_| CryptoError::MissingKek(ENCRYPTION_KEY_ENV.to_string()))?;
    parse_kek(&raw)
}

pub fn parse_kek(raw: &str) -> Result<[u8; KEK_SIZE], CryptoError> {
    let trimmed = raw.trim();
    if trimmed.len() == KEK_SIZE {
        let mut kek = [0u8; KEK_SIZE];
        kek.copy_from_slice(trimmed.as_bytes());
        return Ok(kek);
    }
    let decoded = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, trimmed)
        .map_err(|e| CryptoError::KekDecode(format!("base64 decode failed: {e}")))?;
    if decoded.len() != KEK_SIZE {
        return Err(CryptoError::InvalidKekLength {
            expected: KEK_SIZE,
            actual: decoded.len(),
        });
    }
    let mut kek = [0u8; KEK_SIZE];
    kek.copy_from_slice(&decoded);
    Ok(kek)
}

pub fn generate_dek() -> [u8; DEK_SIZE] {
    let mut dek = [0u8; DEK_SIZE];
    rand::rng().fill_bytes(&mut dek);
    dek
}

pub fn generate_nonce_salt() -> [u8; SALT_SIZE] {
    let mut salt = [0u8; SALT_SIZE];
    rand::rng().fill_bytes(&mut salt);
    salt
}

pub fn wrap_dek(dek: &[u8; DEK_SIZE], kek: &[u8; KEK_SIZE]) -> Result<Vec<u8>, CryptoError> {
    let key: Key<Aes256Gcm> = kek.as_slice().try_into().unwrap();
    let cipher = Aes256Gcm::new(&key);
    let mut wrap_nonce_bytes = [0u8; NONCE_SIZE];
    rand::rng().fill_bytes(&mut wrap_nonce_bytes);
    let nonce: Nonce<Aes256Gcm> = wrap_nonce_bytes.as_slice().try_into().unwrap();
    let ciphertext = cipher
        .encrypt(&nonce, dek.as_ref())
        .map_err(|e| CryptoError::Encrypt(format!("DEK wrap failed: {e}")))?;
    let mut wrapped = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
    wrapped.extend_from_slice(&wrap_nonce_bytes);
    wrapped.extend_from_slice(&ciphertext);
    Ok(wrapped)
}

pub fn unwrap_dek(wrapped: &[u8], kek: &[u8; KEK_SIZE]) -> Result<[u8; DEK_SIZE], CryptoError> {
    if wrapped.len() < NONCE_SIZE + TAG_SIZE {
        return Err(CryptoError::WrappedDekDecode(format!(
            "wrapped DEK too short: {} bytes",
            wrapped.len()
        )));
    }
    let key: Key<Aes256Gcm> = kek.as_slice().try_into().unwrap();
    let cipher = Aes256Gcm::new(&key);
    let nonce: Nonce<Aes256Gcm> = wrapped[..NONCE_SIZE].try_into().unwrap();
    let plaintext = cipher
        .decrypt(&nonce, &wrapped[NONCE_SIZE..])
        .map_err(|e| CryptoError::Decrypt(format!("DEK unwrap failed: {e}")))?;
    if plaintext.len() != DEK_SIZE {
        return Err(CryptoError::Decrypt(format!(
            "unwrapped DEK has wrong length: {} (expected {DEK_SIZE})",
            plaintext.len()
        )));
    }
    let mut dek = [0u8; DEK_SIZE];
    dek.copy_from_slice(&plaintext);
    Ok(dek)
}

pub fn derive_nonce(salt: &[u8; SALT_SIZE], part_index: u64) -> [u8; NONCE_SIZE] {
    let mut nonce = [0u8; NONCE_SIZE];
    nonce[..SALT_SIZE].copy_from_slice(salt);
    nonce[SALT_SIZE..].copy_from_slice(&part_index.to_le_bytes());
    nonce
}

pub fn seal_chunk(
    dek: &[u8; DEK_SIZE],
    nonce: &[u8; NONCE_SIZE],
    plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    let key: Key<Aes256Gcm> = dek.as_slice().try_into().unwrap();
    let cipher = Aes256Gcm::new(&key);
    let nonce: Nonce<Aes256Gcm> = nonce.as_slice().try_into().unwrap();
    cipher
        .encrypt(&nonce, plaintext)
        .map_err(|e| CryptoError::Encrypt(format!("chunk seal failed: {e}")))
}

pub fn open_chunk(
    dek: &[u8; DEK_SIZE],
    nonce: &[u8; NONCE_SIZE],
    ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    let key: Key<Aes256Gcm> = dek.as_slice().try_into().unwrap();
    let cipher = Aes256Gcm::new(&key);
    let nonce: Nonce<Aes256Gcm> = nonce.as_slice().try_into().unwrap();
    cipher
        .decrypt(&nonce, ciphertext)
        .map_err(|e| CryptoError::Decrypt(format!("chunk open failed: {e}")))
}

pub fn kek_fingerprint(kek: &[u8; KEK_SIZE]) -> String {
    let hash = Sha256::digest(kek);
    hex::encode(&hash[..8])
}

pub fn prepare_envelope(
    kek: &[u8; KEK_SIZE],
    chunk_size: u64,
) -> Result<(EnvelopeKeys, ClientSideEncryptionMeta), CryptoError> {
    let dek = generate_dek();
    let nonce_salt = generate_nonce_salt();
    let wrapped_dek = wrap_dek(&dek, kek)?;
    let meta = ClientSideEncryptionMeta {
        algorithm: AES_256_GCM_ALGORITHM.to_string(),
        wrapped_dek: base64::Engine::encode(
            &base64::engine::general_purpose::STANDARD,
            &wrapped_dek,
        ),
        nonce_salt: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, nonce_salt),
        chunk_size_bytes: chunk_size,
        kek_fingerprint: kek_fingerprint(kek),
    };
    let keys = EnvelopeKeys { dek, nonce_salt };
    Ok((keys, meta))
}

pub fn decode_nonce_salt(meta: &ClientSideEncryptionMeta) -> Result<[u8; SALT_SIZE], CryptoError> {
    let decoded =
        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &meta.nonce_salt)
            .map_err(|e| CryptoError::NonceSaltDecode(format!("base64 decode failed: {e}")))?;
    if decoded.len() != SALT_SIZE {
        return Err(CryptoError::NonceSaltDecode(format!(
            "nonce salt has wrong length: {} (expected {SALT_SIZE})",
            decoded.len()
        )));
    }
    let mut salt = [0u8; SALT_SIZE];
    salt.copy_from_slice(&decoded);
    Ok(salt)
}

pub fn decode_wrapped_dek(meta: &ClientSideEncryptionMeta) -> Result<Vec<u8>, CryptoError> {
    base64::Engine::decode(
        &base64::engine::general_purpose::STANDARD,
        &meta.wrapped_dek,
    )
    .map_err(|e| CryptoError::WrappedDekDecode(format!("base64 decode failed: {e}")))
}

pub fn load_envelope_for_upload(
    chunk_size: u64,
) -> Result<(EnvelopeKeys, ClientSideEncryptionMeta), CryptoError> {
    let kek = load_kek_from_env()?;
    prepare_envelope(&kek, chunk_size)
}

pub fn load_envelope_for_download(
    meta: &ClientSideEncryptionMeta,
) -> Result<(EnvelopeKeys, ClientSideEncryptionMeta), CryptoError> {
    let kek = load_kek_from_env()?;
    let wrapped_dek_bytes = decode_wrapped_dek(meta)?;
    let dek = unwrap_dek(&wrapped_dek_bytes, &kek)?;
    let nonce_salt = decode_nonce_salt(meta)?;
    let fingerprint = kek_fingerprint(&kek);
    if fingerprint != meta.kek_fingerprint {
        return Err(CryptoError::Decrypt(format!(
            "KEK fingerprint mismatch: manifest expects {}, derived {} (wrong KEK?)",
            meta.kek_fingerprint, fingerprint
        )));
    }
    let keys = EnvelopeKeys { dek, nonce_salt };
    Ok((keys, meta.clone()))
}

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

    fn test_kek() -> [u8; KEK_SIZE] {
        [0x42u8; KEK_SIZE]
    }

    #[test]
    fn encrypt_decrypt_single_chunk_roundtrip() {
        let kek = test_kek();
        let (keys, _meta) = prepare_envelope(&kek, 1024).unwrap();
        let nonce = derive_nonce(&keys.nonce_salt, 0);
        let plaintext = b"hello world backup data";
        let ciphertext = seal_chunk(&keys.dek, &nonce, plaintext).unwrap();
        let decrypted = open_chunk(&keys.dek, &nonce, &ciphertext).unwrap();
        assert_eq!(decrypted, plaintext);
    }

    #[test]
    fn encrypt_decrypt_multi_chunk_roundtrip() {
        let kek = test_kek();
        let (keys, _meta) = prepare_envelope(&kek, 16).unwrap();
        let chunks: Vec<&[u8]> = vec![b"chunk-zero-data!", b"chunk-one-data!", b"last"];
        for (i, plain) in chunks.iter().enumerate() {
            let nonce = derive_nonce(&keys.nonce_salt, i as u64);
            let ct = seal_chunk(&keys.dek, &nonce, plain).unwrap();
            let pt = open_chunk(&keys.dek, &nonce, &ct).unwrap();
            assert_eq!(pt, *plain);
        }
    }

    #[test]
    fn chunk_tamper_detected() {
        let kek = test_kek();
        let (keys, _meta) = prepare_envelope(&kek, 1024).unwrap();
        let nonce = derive_nonce(&keys.nonce_salt, 0);
        let plaintext = b"important data";
        let mut ciphertext = seal_chunk(&keys.dek, &nonce, plaintext).unwrap();
        let last = ciphertext.len() - 1;
        ciphertext[last] ^= 0xFF;
        let result = open_chunk(&keys.dek, &nonce, &ciphertext);
        assert!(result.is_err());
    }

    #[test]
    fn wrong_kek_fails_unwrap() {
        let kek = test_kek();
        let (_keys, meta) = prepare_envelope(&kek, 1024).unwrap();
        let wrapped = decode_wrapped_dek(&meta).unwrap();
        let wrong_kek = [0x99u8; KEK_SIZE];
        let result = unwrap_dek(&wrapped, &wrong_kek);
        assert!(result.is_err());
    }

    #[test]
    fn nonce_uniqueness_across_parts() {
        let salt = [0xABu8; SALT_SIZE];
        let n0 = derive_nonce(&salt, 0);
        let n1 = derive_nonce(&salt, 1);
        let n2 = derive_nonce(&salt, 2);
        assert_ne!(n0, n1);
        assert_ne!(n1, n2);
        assert_ne!(n0, n2);
        assert_eq!(&n0[..SALT_SIZE], &salt);
        assert_eq!(&n1[..SALT_SIZE], &salt);
    }

    #[test]
    fn kek_length_validation_raw() {
        let short = "tooshort";
        assert!(parse_kek(short).is_err());
    }

    #[test]
    fn kek_length_validation_base64() {
        let kek = test_kek();
        let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, kek);
        let parsed = parse_kek(&encoded).unwrap();
        assert_eq!(parsed, kek);
    }

    #[test]
    fn kek_raw_32_bytes_accepted() {
        let raw = "A".repeat(KEK_SIZE);
        let parsed = parse_kek(&raw).unwrap();
        assert_eq!(parsed, [0x41u8; KEK_SIZE]);
    }

    #[test]
    fn wrapped_dek_unwrap_roundtrip() {
        let kek = test_kek();
        let dek = generate_dek();
        let wrapped = wrap_dek(&dek, &kek).unwrap();
        let unwrapped = unwrap_dek(&wrapped, &kek).unwrap();
        assert_eq!(unwrapped, dek);
    }

    #[test]
    fn kek_fingerprint_is_deterministic() {
        let kek = test_kek();
        let fp1 = kek_fingerprint(&kek);
        let fp2 = kek_fingerprint(&kek);
        assert_eq!(fp1, fp2);
        assert_eq!(fp1.len(), 16);
    }

    #[test]
    fn metadata_roundtrip() {
        let kek = test_kek();
        let (_keys, meta) = prepare_envelope(&kek, 8 * 1024 * 1024).unwrap();
        let json = serde_json::to_string(&meta).unwrap();
        let parsed: ClientSideEncryptionMeta = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, meta);
        assert_eq!(parsed.algorithm, AES_256_GCM_ALGORITHM);
        assert_eq!(parsed.chunk_size_bytes, 8 * 1024 * 1024);
    }
}