vyn-core 0.1.3

Core library for vyn. Crypto, keychain, manifest, storage, and diff engine
Documentation
use ring::aead::{AES_256_GCM, Aad, LessSafeKey, NONCE_LEN, Nonce, UnboundKey};
use ring::rand::{SecureRandom, SystemRandom};
use secrecy::{ExposeSecret, SecretBox};
use thiserror::Error;

const PROJECT_KEY_LEN: usize = 32;

pub type SecretBytes = SecretBox<[u8]>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedData {
    pub nonce: [u8; NONCE_LEN],
    pub ciphertext: Vec<u8>,
}

#[derive(Debug, Error)]
pub enum CryptoError {
    #[error("project key must be 32 bytes")]
    InvalidKeyLength,
    #[error("failed to generate secure random bytes")]
    RandomFailure,
    #[error("failed to initialize AES-256-GCM key")]
    KeyInitFailure,
    #[error("encryption failed")]
    EncryptionFailure,
    #[error("decryption failed")]
    DecryptionFailure,
}

pub fn secret_bytes(bytes: Vec<u8>) -> SecretBytes {
    SecretBox::new(bytes.into_boxed_slice())
}

pub fn generate_project_key() -> Result<SecretBytes, CryptoError> {
    let mut key = [0u8; PROJECT_KEY_LEN];
    SystemRandom::new()
        .fill(&mut key)
        .map_err(|_| CryptoError::RandomFailure)?;
    Ok(secret_bytes(key.to_vec()))
}

pub fn encrypt(key: &SecretBytes, plaintext: &SecretBytes) -> Result<EncryptedData, CryptoError> {
    let aead_key = build_key(key.expose_secret())?;

    let mut nonce_bytes = [0u8; NONCE_LEN];
    SystemRandom::new()
        .fill(&mut nonce_bytes)
        .map_err(|_| CryptoError::RandomFailure)?;

    let nonce = Nonce::assume_unique_for_key(nonce_bytes);
    let mut buffer = plaintext.expose_secret().to_vec();

    aead_key
        .seal_in_place_append_tag(nonce, Aad::empty(), &mut buffer)
        .map_err(|_| CryptoError::EncryptionFailure)?;

    Ok(EncryptedData {
        nonce: nonce_bytes,
        ciphertext: buffer,
    })
}

pub fn decrypt(key: &SecretBytes, encrypted: &EncryptedData) -> Result<SecretBytes, CryptoError> {
    let aead_key = build_key(key.expose_secret())?;
    let nonce = Nonce::assume_unique_for_key(encrypted.nonce);

    let mut buffer = encrypted.ciphertext.clone();
    let plaintext = aead_key
        .open_in_place(nonce, Aad::empty(), &mut buffer)
        .map_err(|_| CryptoError::DecryptionFailure)?;

    Ok(secret_bytes(plaintext.to_vec()))
}

fn build_key(key: &[u8]) -> Result<LessSafeKey, CryptoError> {
    if key.len() != PROJECT_KEY_LEN {
        return Err(CryptoError::InvalidKeyLength);
    }

    let unbound = UnboundKey::new(&AES_256_GCM, key).map_err(|_| CryptoError::KeyInitFailure)?;
    Ok(LessSafeKey::new(unbound))
}

#[cfg(test)]
mod tests {
    use super::{decrypt, encrypt, generate_project_key, secret_bytes};
    use ring::rand::{SecureRandom, SystemRandom};
    use secrecy::ExposeSecret;

    #[test]
    fn aes_gcm_roundtrip() {
        let key = generate_project_key().expect("project key generation should succeed");

        let mut plaintext = vec![0u8; 1024 * 1024];
        SystemRandom::new()
            .fill(&mut plaintext)
            .expect("random data generation should succeed");

        let encrypted =
            encrypt(&key, &secret_bytes(plaintext.clone())).expect("encryption should succeed");
        let decrypted = decrypt(&key, &encrypted).expect("decryption should succeed");

        assert_eq!(decrypted.expose_secret(), plaintext.as_slice());
    }
}