Skip to main content

vyn_core/
crypto.rs

1use ring::aead::{AES_256_GCM, Aad, LessSafeKey, NONCE_LEN, Nonce, UnboundKey};
2use ring::rand::{SecureRandom, SystemRandom};
3use secrecy::{ExposeSecret, SecretBox};
4use thiserror::Error;
5
6const PROJECT_KEY_LEN: usize = 32;
7
8pub type SecretBytes = SecretBox<[u8]>;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EncryptedData {
12    pub nonce: [u8; NONCE_LEN],
13    pub ciphertext: Vec<u8>,
14}
15
16#[derive(Debug, Error)]
17pub enum CryptoError {
18    #[error("project key must be 32 bytes")]
19    InvalidKeyLength,
20    #[error("failed to generate secure random bytes")]
21    RandomFailure,
22    #[error("failed to initialize AES-256-GCM key")]
23    KeyInitFailure,
24    #[error("encryption failed")]
25    EncryptionFailure,
26    #[error("decryption failed")]
27    DecryptionFailure,
28}
29
30pub fn secret_bytes(bytes: Vec<u8>) -> SecretBytes {
31    SecretBox::new(bytes.into_boxed_slice())
32}
33
34pub fn generate_project_key() -> Result<SecretBytes, CryptoError> {
35    let mut key = [0u8; PROJECT_KEY_LEN];
36    SystemRandom::new()
37        .fill(&mut key)
38        .map_err(|_| CryptoError::RandomFailure)?;
39    Ok(secret_bytes(key.to_vec()))
40}
41
42pub fn encrypt(key: &SecretBytes, plaintext: &SecretBytes) -> Result<EncryptedData, CryptoError> {
43    let aead_key = build_key(key.expose_secret())?;
44
45    let mut nonce_bytes = [0u8; NONCE_LEN];
46    SystemRandom::new()
47        .fill(&mut nonce_bytes)
48        .map_err(|_| CryptoError::RandomFailure)?;
49
50    let nonce = Nonce::assume_unique_for_key(nonce_bytes);
51    let mut buffer = plaintext.expose_secret().to_vec();
52
53    aead_key
54        .seal_in_place_append_tag(nonce, Aad::empty(), &mut buffer)
55        .map_err(|_| CryptoError::EncryptionFailure)?;
56
57    Ok(EncryptedData {
58        nonce: nonce_bytes,
59        ciphertext: buffer,
60    })
61}
62
63pub fn decrypt(key: &SecretBytes, encrypted: &EncryptedData) -> Result<SecretBytes, CryptoError> {
64    let aead_key = build_key(key.expose_secret())?;
65    let nonce = Nonce::assume_unique_for_key(encrypted.nonce);
66
67    let mut buffer = encrypted.ciphertext.clone();
68    let plaintext = aead_key
69        .open_in_place(nonce, Aad::empty(), &mut buffer)
70        .map_err(|_| CryptoError::DecryptionFailure)?;
71
72    Ok(secret_bytes(plaintext.to_vec()))
73}
74
75fn build_key(key: &[u8]) -> Result<LessSafeKey, CryptoError> {
76    if key.len() != PROJECT_KEY_LEN {
77        return Err(CryptoError::InvalidKeyLength);
78    }
79
80    let unbound = UnboundKey::new(&AES_256_GCM, key).map_err(|_| CryptoError::KeyInitFailure)?;
81    Ok(LessSafeKey::new(unbound))
82}
83
84#[cfg(test)]
85mod tests {
86    use super::{decrypt, encrypt, generate_project_key, secret_bytes};
87    use ring::rand::{SecureRandom, SystemRandom};
88    use secrecy::ExposeSecret;
89
90    #[test]
91    fn aes_gcm_roundtrip() {
92        let key = generate_project_key().expect("project key generation should succeed");
93
94        let mut plaintext = vec![0u8; 1024 * 1024];
95        SystemRandom::new()
96            .fill(&mut plaintext)
97            .expect("random data generation should succeed");
98
99        let encrypted =
100            encrypt(&key, &secret_bytes(plaintext.clone())).expect("encryption should succeed");
101        let decrypted = decrypt(&key, &encrypted).expect("decryption should succeed");
102
103        assert_eq!(decrypted.expose_secret(), plaintext.as_slice());
104    }
105}