use crate::keystore::{Cipher, Error, Result};
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use rand::RngCore;
use zeroize::Zeroizing;
const NONCE_LEN: usize = 24;
pub struct XChaCha20Poly1305Cipher {
cipher: XChaCha20Poly1305,
}
impl XChaCha20Poly1305Cipher {
pub fn new(key: [u8; 32]) -> Self {
let key = Zeroizing::new(key);
let cipher = XChaCha20Poly1305::new_from_slice(key.as_slice()).expect("32-byte key");
Self { cipher }
}
}
impl Cipher for XChaCha20Poly1305Cipher {
fn encrypt(&self, aad: Option<&[u8]>, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut nonce = [0u8; NONCE_LEN];
rand::thread_rng().fill_bytes(&mut nonce);
let ciphertext = self
.cipher
.encrypt(
XNonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad: aad.unwrap_or(&[]),
},
)
.map_err(|_| Error::EncryptFailed)?;
let mut blob = Vec::with_capacity(NONCE_LEN + ciphertext.len());
blob.extend_from_slice(&nonce);
blob.extend_from_slice(&ciphertext);
Ok(blob)
}
fn decrypt(&self, aad: Option<&[u8]>, ciphertext: &[u8]) -> Result<Vec<u8>> {
if ciphertext.len() < NONCE_LEN {
return Err(Error::DecryptFailed);
}
let (nonce, ct) = ciphertext.split_at(NONCE_LEN);
self.cipher
.decrypt(
XNonce::from_slice(nonce),
Payload {
msg: ct,
aad: aad.unwrap_or(&[]),
},
)
.map_err(|_| Error::DecryptFailed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cipher() -> XChaCha20Poly1305Cipher {
XChaCha20Poly1305Cipher::new([7u8; 32])
}
#[test]
fn round_trip_with_aad() {
let c = cipher();
let blob = c.encrypt(Some(b"label"), b"secret").unwrap();
assert_eq!(c.decrypt(Some(b"label"), &blob).unwrap(), b"secret");
}
#[test]
fn tampered_ciphertext_is_rejected() {
let c = cipher();
let mut blob = c.encrypt(Some(b"label"), b"secret").unwrap();
*blob.last_mut().unwrap() ^= 0xff;
assert!(matches!(
c.decrypt(Some(b"label"), &blob),
Err(Error::DecryptFailed)
));
}
#[test]
fn truncated_blob_below_nonce_len_is_rejected() {
let c = cipher();
assert!(matches!(
c.decrypt(Some(b"label"), &[0u8; NONCE_LEN - 1]),
Err(Error::DecryptFailed)
));
}
#[test]
fn mismatched_aad_fails() {
let c = cipher();
let blob = c.encrypt(Some(b"label-a"), b"secret").unwrap();
assert!(matches!(
c.decrypt(Some(b"label-b"), &blob),
Err(Error::DecryptFailed)
));
}
}