use std::fmt;
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::XChaCha20Poly1305;
use zeroize::Zeroizing;
const STENOXIDE_AAD: &[u8] = b"STENOXIDE-v1";
const ZSTD_LEVEL: i32 = 19;
#[derive(Debug)]
pub enum AEADError {
AuthenticationFailed,
CipherError(String),
}
impl fmt::Display for AEADError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AEADError::AuthenticationFailed => {
write!(f, "authentication failed: wrong password or corrupted data")
}
AEADError::CipherError(message) => write!(f, "cipher error: {message}"),
}
}
}
impl std::error::Error for AEADError {}
#[derive(Debug)]
pub enum CryptoError {
CompressionError(String),
DecompressionError(String),
AEADError(AEADError),
}
impl fmt::Display for CryptoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CryptoError::CompressionError(message) => {
write!(f, "failed to compress the payload: {message}")
}
CryptoError::DecompressionError(message) => {
write!(f, "failed to decompress the payload: {message}")
}
CryptoError::AEADError(err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for CryptoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CryptoError::AEADError(err) => Some(err),
_ => None,
}
}
}
impl From<AEADError> for CryptoError {
fn from(err: AEADError) -> Self {
CryptoError::AEADError(err)
}
}
pub trait AEADCipher: Send + Sync {
fn encrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 24],
plaintext: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, AEADError>;
fn decrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 24],
ciphertext: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, AEADError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct XChaCha20Poly1305Cipher;
impl XChaCha20Poly1305Cipher {
pub fn new() -> Self {
Self
}
}
impl AEADCipher for XChaCha20Poly1305Cipher {
fn encrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 24],
plaintext: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, AEADError> {
let cipher = XChaCha20Poly1305::new(key.into());
let ciphertext = cipher
.encrypt(
nonce.into(),
Payload {
msg: plaintext,
aad,
},
)
.map_err(|err| AEADError::CipherError(err.to_string()))?;
Ok(Zeroizing::new(ciphertext))
}
fn decrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 24],
ciphertext: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, AEADError> {
let cipher = XChaCha20Poly1305::new(key.into());
let plaintext = cipher
.decrypt(
nonce.into(),
Payload {
msg: ciphertext,
aad,
},
)
.map_err(|_| AEADError::AuthenticationFailed)?;
Ok(Zeroizing::new(plaintext))
}
}
pub fn compress_and_encrypt(
plaintext: &[u8],
enc_key: &[u8; 32],
nonce: &[u8; 24],
cipher: &dyn AEADCipher,
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let compressed = Zeroizing::new(
zstd::encode_all(plaintext, ZSTD_LEVEL)
.map_err(|err| CryptoError::CompressionError(err.to_string()))?,
);
let ciphertext = cipher.encrypt(enc_key, nonce, &compressed, STENOXIDE_AAD)?;
drop(compressed);
Ok(ciphertext)
}
pub fn decrypt_and_decompress(
ciphertext: &[u8],
enc_key: &[u8; 32],
nonce: &[u8; 24],
cipher: &dyn AEADCipher,
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let compressed = cipher.decrypt(enc_key, nonce, ciphertext, STENOXIDE_AAD)?;
let plaintext = Zeroizing::new(
zstd::decode_all(compressed.as_slice())
.map_err(|err| CryptoError::DecompressionError(err.to_string()))?,
);
drop(compressed);
Ok(plaintext)
}