polycrypt 0.1.1

Simple symmetric authenticated encryption in memory. Nothing fancy here.
Documentation
use chacha20poly1305::{
    aead::{Aead, KeyInit},
    XChaCha20Poly1305,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;

pub type Key = chacha20poly1305::Key;
pub type XNonce = chacha20poly1305::XNonce;

pub const XNONCE_LEN: usize = 24;

/// Truncated HMAC-SHA256(key, plaintext)
pub fn derive_xnonce(plaintext: &[u8], key: &Key) -> XNonce {
    let mac: [u8; 32] = <Hmac<Sha256> as Mac>::new_from_slice(key.as_ref())
        .expect("HMAC construction never fails")
        .chain_update(plaintext)
        .finalize()
        .into_bytes()
        .into();

    let mut nonce = XNonce::default();
    nonce.copy_from_slice(&mac[..XNONCE_LEN]);
    nonce
}

/// Encrypt a piece of plaintext data using a deterministic pseudorandom nonce.
pub fn encrypt(plaintext: &[u8], key: &Key) -> Result<Vec<u8>, chacha20poly1305::Error> {
    // OK if pseudorandom, as long as we use extended ChaCha (XChaCha)
    let nonce = derive_xnonce(plaintext, key);

    let c = XChaCha20Poly1305::new(key);
    let mut ciphertext = c.encrypt(&nonce, plaintext)?;
    ciphertext.extend(nonce);
    Ok(ciphertext)
}

/// Decrypt a ciphertext. Note that we accept any nonce, not just deterministic ones.
pub fn decrypt(ciphertext: &[u8], key: &Key) -> Result<Vec<u8>, chacha20poly1305::Error> {
    if ciphertext.len() < XNONCE_LEN {
        return Err(chacha20poly1305::Error);
    }
    let nonce_ptr = ciphertext.len() - XNONCE_LEN;
    let nonce = XNonce::from_slice(&ciphertext[nonce_ptr..]);

    let c = XChaCha20Poly1305::new(key);
    let plaintext = c.decrypt(nonce, &ciphertext[..nonce_ptr])?;
    Ok(plaintext)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn chacha_encryption() {
        let key = Key::from([0u8; 32]);

        let ciphertext = encrypt(b"hello world", &key).unwrap();
        let plaintext = decrypt(&ciphertext, &key).unwrap();

        assert_eq!(
            hex::encode(&ciphertext),
            concat!(
                "eb3ed51c1a77277d8cac42",                           // actual message data
                "c866e16e75184b30110bbc22cfd77030",                 // poly1305 tag
                "c2ea634c993f050482b4e6243224087f7c23bdd3c07ab1a4", // xnonce
            ),
        );

        assert_eq!(&plaintext, b"hello world");

        for i in 0..ciphertext.len() {
            let mut manipulated = ciphertext.clone();
            manipulated[i] = ciphertext[i].wrapping_add(1);
            decrypt(&manipulated, &key).expect_err("authentication tag detects manipulation");
        }
    }
}