enigma-storage 0.0.1

Encrypted local storage for Enigma with mandatory at-rest encryption and cross-platform key vault providers.
Documentation
use blake3::Hasher;
use chacha20poly1305::aead::{Aead, Payload};
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
use rand::RngCore;

use crate::codec::{decode_envelope, encode_envelope, ENVELOPE_VERSION};
use crate::error::{EnigmaStorageError, Result};
use crate::key_provider::MasterKey;

pub fn encrypt_value(master: &MasterKey, namespace: &str, key: &str, value: &[u8]) -> Result<Vec<u8>> {
    let mut nonce = [0u8; 24];
    rand::thread_rng().fill_bytes(&mut nonce);
    let aad = compute_aad(namespace, key);
    let cipher = XChaCha20Poly1305::new(Key::from_slice(master.as_bytes()));
    let ciphertext = cipher
        .encrypt(XNonce::from_slice(&nonce), Payload { msg: value, aad: aad.as_slice() })
        .map_err(|_| EnigmaStorageError::AeadError)?;
    Ok(encode_envelope(&nonce, &ciphertext))
}

pub fn decrypt_value(master: &MasterKey, namespace: &str, key: &str, data: &[u8]) -> Result<Vec<u8>> {
    let (version, nonce, ciphertext) = decode_envelope(data)?;
    if version != ENVELOPE_VERSION {
        return Err(EnigmaStorageError::UnsupportedVersion);
    }
    let aad = compute_aad(namespace, key);
    let cipher = XChaCha20Poly1305::new(Key::from_slice(master.as_bytes()));
    let plaintext = cipher
        .decrypt(XNonce::from_slice(&nonce), Payload { msg: ciphertext.as_slice(), aad: aad.as_slice() })
        .map_err(|_| EnigmaStorageError::AeadError)?;
    Ok(plaintext)
}

fn compute_aad(namespace: &str, key: &str) -> [u8; 32] {
    let mut hasher = Hasher::new();
    hasher.update(b"enigma:storage:aad:v1");
    hasher.update(namespace.as_bytes());
    hasher.update(key.as_bytes());
    let result = hasher.finalize();
    *result.as_bytes()
}