use anyhow::{Result, anyhow};
use chacha20poly1305::{
ChaCha20Poly1305,
aead::{Aead, KeyInit, Nonce, Payload},
};
use rand::{TryRng, rngs::SysRng};
use secrecy::{ExposeSecret, SecretSlice};
pub struct ChaCha20Poly1305Crypto {
key: SecretSlice<u8>,
}
impl super::Crypto for ChaCha20Poly1305Crypto {
fn new(key: SecretSlice<u8>) -> Self {
Self { key }
}
fn encrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>, anyhow::Error> {
let cipher = ChaCha20Poly1305::new_from_slice(self.key.expose_secret())
.map_err(|err| anyhow!("Invalid key length: {err}"))?;
let mut nonce_bytes = [0u8; 12];
SysRng
.try_fill_bytes(&mut nonce_bytes)
.map_err(|err| anyhow!("Error generating nonce: {err}"))?;
let nonce = Nonce::<ChaCha20Poly1305>::from(nonce_bytes);
let payload = Payload {
msg: data,
aad: fingerprint,
};
cipher.encrypt(&nonce, payload).map_or_else(
|_| Err(anyhow!("Failed to encrypt data")),
|ciphertext| {
let mut encrypted_data = nonce_bytes.to_vec();
encrypted_data.extend_from_slice(&ciphertext);
Ok(encrypted_data)
},
)
}
fn decrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>, anyhow::Error> {
if data.len() < 12 {
return Err(anyhow!(
"Invalid encrypted data: too short (expected at least 12 bytes, got {})",
data.len()
));
}
let cipher = ChaCha20Poly1305::new_from_slice(self.key.expose_secret())
.map_err(|err| anyhow!("Invalid key length: {err}"))?;
let (nonce, ciphertext) = data.split_at(12);
let nonce = <&Nonce<ChaCha20Poly1305>>::try_from(nonce)
.map_err(|err| anyhow!("Invalid nonce: {err}"))?;
let decrypted_data = cipher
.decrypt(
nonce,
Payload {
msg: ciphertext,
aad: fingerprint,
},
)
.map_err(|err| anyhow!("Error decrypting password: {err}"))?;
Ok(decrypted_data)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::unwrap_in_result)]
mod tests {
use super::*;
use crate::vault::crypto::Crypto;
use rand::{TryRng, rngs::SysRng};
use std::collections::HashSet;
const TEST_DATA: &str = "The quick brown fox jumps over the lazy dog";
const FINGERPRINT: &str = "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM";
#[test]
fn test_chacha20poly1305() {
let mut password = [0_u8; 32];
SysRng.try_fill_bytes(&mut password).unwrap();
let key = SecretSlice::new(password.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto
.decrypt(&encrypted_data, FINGERPRINT.as_bytes())
.unwrap();
assert_eq!(TEST_DATA.as_bytes(), decrypted_data);
}
#[test]
fn test_chacha20poly1305_wrong_fingerprint() {
let mut password = [0_u8; 32];
SysRng.try_fill_bytes(&mut password).unwrap();
let key = SecretSlice::new(password.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
let encrypted_data = crypto
.encrypt(TEST_DATA.as_bytes(), FINGERPRINT.as_bytes())
.unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, b"SHA256:invalid_fingerprint");
assert!(decrypted_data.is_err());
}
#[test]
fn test_chacha20poly1305_rand() {
let mut unique_keys = HashSet::new();
for _ in 0..1000 {
let mut rng = SysRng;
let mut key_bytes = [0u8; 32];
rng.try_fill_bytes(&mut key_bytes).unwrap();
assert!(unique_keys.insert(key_bytes), "Duplicate key found");
let key = SecretSlice::new(key_bytes.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
let mut data = vec![0u8; 300];
rng.try_fill_bytes(&mut data).unwrap();
let mut fingerprint = vec![0u8; 100];
rng.try_fill_bytes(&mut fingerprint).unwrap();
let encrypted_data = crypto.encrypt(&data, &fingerprint).unwrap();
let decrypted_data = crypto.decrypt(&encrypted_data, &fingerprint).unwrap();
assert_eq!(data, decrypted_data);
}
}
#[test]
fn test_chacha20poly1305_decrypt_empty_data() {
let mut password = [0_u8; 32];
SysRng.try_fill_bytes(&mut password).unwrap();
let key = SecretSlice::new(password.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
let result = crypto.decrypt(&[], FINGERPRINT.as_bytes());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too short"));
}
#[test]
fn test_chacha20poly1305_decrypt_short_data() {
let mut password = [0_u8; 32];
SysRng.try_fill_bytes(&mut password).unwrap();
let key = SecretSlice::new(password.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
for len in 1..12 {
let short_data = vec![0u8; len];
let result = crypto.decrypt(&short_data, FINGERPRINT.as_bytes());
assert!(result.is_err(), "Should fail with {len} bytes");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("too short"),
"Error message should mention 'too short', got: {err_msg}",
);
assert!(
err_msg.contains(&len.to_string()),
"Error message should mention length {len}",
);
}
}
#[test]
fn test_chacha20poly1305_decrypt_exact_minimum() {
let mut password = [0_u8; 32];
SysRng.try_fill_bytes(&mut password).unwrap();
let key = SecretSlice::new(password.into());
let crypto = ChaCha20Poly1305Crypto::new(key);
let data = vec![0u8; 12];
let result = crypto.decrypt(&data, FINGERPRINT.as_bytes());
assert!(result.is_err());
}
}