use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("{0} decrypt failed")]
pub struct AeadError(&'static str);
impl AeadError {
pub const CODE: &'static str = "aead_verification_failed";
#[must_use]
pub const fn code(&self) -> &'static str {
Self::CODE
}
}
#[must_use]
pub fn chacha20_poly1305_encrypt(
key: &[u8],
nonce: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> Vec<u8> {
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
cipher
.encrypt(
Nonce::from_slice(nonce),
Payload {
msg: plaintext,
aad,
},
)
.expect("chacha20-poly1305 encryption cannot fail for an in-memory buffer")
}
pub fn chacha20_poly1305_decrypt(
key: &[u8],
nonce: &[u8],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, AeadError> {
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
cipher
.decrypt(
Nonce::from_slice(nonce),
Payload {
msg: ciphertext,
aad,
},
)
.map_err(|_| AeadError("chacha20-poly1305"))
}
#[must_use]
pub fn xchacha20_poly1305_encrypt(
key: &[u8],
nonce: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> Vec<u8> {
let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
cipher
.encrypt(
XNonce::from_slice(nonce),
Payload {
msg: plaintext,
aad,
},
)
.expect("xchacha20-poly1305 encryption cannot fail for an in-memory buffer")
}
pub fn xchacha20_poly1305_decrypt(
key: &[u8],
nonce: &[u8],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, AeadError> {
let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
cipher
.decrypt(
XNonce::from_slice(nonce),
Payload {
msg: ciphertext,
aad,
},
)
.map_err(|_| AeadError("xchacha20-poly1305"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chacha20_roundtrips_and_appends_a_16_byte_tag() {
let key = [7u8; 32];
let nonce = [3u8; 12];
let aad = b"cardano-poe-kek-v1";
let plaintext = b"wrap me";
let sealed = chacha20_poly1305_encrypt(&key, &nonce, aad, plaintext);
assert_eq!(sealed.len(), plaintext.len() + 16);
let opened = chacha20_poly1305_decrypt(&key, &nonce, aad, &sealed).unwrap();
assert_eq!(opened, plaintext);
}
#[test]
fn xchacha20_roundtrips_and_appends_a_16_byte_tag() {
let key = [9u8; 32];
let nonce = [4u8; 24];
let aad = b"content-aad";
let plaintext = b"seal the content body";
let sealed = xchacha20_poly1305_encrypt(&key, &nonce, aad, plaintext);
assert_eq!(sealed.len(), plaintext.len() + 16);
let opened = xchacha20_poly1305_decrypt(&key, &nonce, aad, &sealed).unwrap();
assert_eq!(opened, plaintext);
}
#[test]
fn open_rejects_a_tampered_tag() {
let key = [1u8; 32];
let nonce = [2u8; 12];
let mut sealed = chacha20_poly1305_encrypt(&key, &nonce, b"", b"x");
let last = sealed.len() - 1;
sealed[last] ^= 0x01;
assert_eq!(
chacha20_poly1305_decrypt(&key, &nonce, b"", &sealed),
Err(AeadError("chacha20-poly1305")),
);
}
#[test]
fn open_rejects_mismatched_aad() {
let key = [5u8; 32];
let nonce = [6u8; 24];
let sealed = xchacha20_poly1305_encrypt(&key, &nonce, b"aad-a", b"x");
assert!(xchacha20_poly1305_decrypt(&key, &nonce, b"aad-b", &sealed).is_err());
}
#[test]
fn error_carries_the_shared_code() {
let err = chacha20_poly1305_decrypt(&[0u8; 32], &[0u8; 12], b"", &[0u8; 16]).unwrap_err();
assert_eq!(err.code(), "aead_verification_failed");
}
}