connexa 0.5.3

High level abstraction of rust-libp2p
Documentation
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;

/// XChaCha20-Poly1305 [`Cipher`] backend.
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)
        ));
    }
}