Skip to main content

zlicenser_protocol/crypto/
aead.rs

1use chacha20poly1305::{
2    aead::{Aead, KeyInit, Payload},
3    XChaCha20Poly1305, XNonce,
4};
5use rand::rngs::OsRng;
6use rand::RngCore;
7use subtle::ConstantTimeEq;
8use zeroize::{Zeroize, ZeroizeOnDrop};
9
10use crate::error::Error;
11
12#[derive(Zeroize, ZeroizeOnDrop)]
13pub struct AeadKey(chacha20poly1305::Key);
14
15impl AeadKey {
16    pub fn generate() -> Self {
17        let mut bytes = chacha20poly1305::Key::default();
18        OsRng.fill_bytes(&mut bytes);
19        Self(bytes)
20    }
21
22    pub fn from_bytes(b: &[u8; 32]) -> Self {
23        Self(*chacha20poly1305::Key::from_slice(b))
24    }
25}
26
27impl PartialEq for AeadKey {
28    fn eq(&self, other: &Self) -> bool {
29        self.0.ct_eq(&other.0).into()
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Nonce([u8; 24]);
35
36impl Nonce {
37    pub fn random() -> Self {
38        let mut b = [0u8; 24];
39        OsRng.fill_bytes(&mut b);
40        Self(b)
41    }
42
43    pub fn from_bytes(b: [u8; 24]) -> Self {
44        Self(b)
45    }
46
47    pub fn as_bytes(&self) -> &[u8; 24] {
48        &self.0
49    }
50}
51
52/// Encrypts with XChaCha20-Poly1305. Nonce is not prepended, callers track it in their wire structs.
53pub fn encrypt(key: &AeadKey, nonce: &Nonce, plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
54    let cipher = XChaCha20Poly1305::new(&key.0);
55    let xnonce = XNonce::from_slice(&nonce.0);
56    cipher
57        .encrypt(
58            xnonce,
59            Payload {
60                msg: plaintext,
61                aad,
62            },
63        )
64        .expect("XChaCha20Poly1305 encrypt: key or nonce size mismatch, this is a bug")
65}
66
67pub fn decrypt(
68    key: &AeadKey,
69    nonce: &Nonce,
70    ciphertext: &[u8],
71    aad: &[u8],
72) -> Result<Vec<u8>, Error> {
73    let cipher = XChaCha20Poly1305::new(&key.0);
74    let xnonce = XNonce::from_slice(&nonce.0);
75    cipher
76        .decrypt(
77            xnonce,
78            Payload {
79                msg: ciphertext,
80                aad,
81            },
82        )
83        .map_err(|_| Error::Decrypt)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    // XChaCha20-Poly1305 test vector from draft-irtf-cfrg-xchacha-03 chapter A.3
91    // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#appendix-A.3
92    #[test]
93    fn xchacha20poly1305_test_vector() {
94        let key_bytes: [u8; 32] = [
95            0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
96            0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
97            0x9c, 0x9d, 0x9e, 0x9f,
98        ];
99        let nonce_bytes: [u8; 24] = [
100            0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d,
101            0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
102        ];
103        let plaintext: &[u8] = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
104        let aad: &[u8] = &[
105            0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
106        ];
107        // Expected ciphertext+tag (130 bytes = 114 plaintext + 16 tag) from the draft
108        let expected_ct: &[u8] = &[
109            0xbd, 0x6d, 0x17, 0x9d, 0x3e, 0x83, 0xd4, 0x3b, 0x95, 0x76, 0x57, 0x94, 0x93, 0xc0,
110            0xe9, 0x39, 0x57, 0x2a, 0x17, 0x00, 0x25, 0x2b, 0xfa, 0xcc, 0xbe, 0xd2, 0x90, 0x2c,
111            0x21, 0x39, 0x6c, 0xbb, 0x73, 0x1c, 0x7f, 0x1b, 0x0b, 0x4a, 0xa6, 0x44, 0x0b, 0xf3,
112            0xa8, 0x2f, 0x4e, 0xda, 0x7e, 0x39, 0xae, 0x64, 0xc6, 0x70, 0x8c, 0x54, 0xc2, 0x16,
113            0xcb, 0x96, 0xb7, 0x2e, 0x12, 0x13, 0xb4, 0x52, 0x2f, 0x8c, 0x9b, 0xa4, 0x0d, 0xb5,
114            0xd9, 0x45, 0xb1, 0x1b, 0x69, 0xb9, 0x82, 0xc1, 0xbb, 0x9e, 0x3f, 0x3f, 0xac, 0x2b,
115            0xc3, 0x69, 0x48, 0x8f, 0x76, 0xb2, 0x38, 0x35, 0x65, 0xd3, 0xff, 0xf9, 0x21, 0xf9,
116            0x66, 0x4c, 0x97, 0x63, 0x7d, 0xa9, 0x76, 0x88, 0x12, 0xf6, 0x15, 0xc6, 0x8b, 0x13,
117            0xb5, 0x2e, 0xc0, 0x87, 0x59, 0x24, 0xc1, 0xc7, 0x98, 0x79, 0x47, 0xde, 0xaf, 0xd8,
118            0x78, 0x0a, 0xcf, 0x49,
119        ];
120
121        let key = AeadKey::from_bytes(&key_bytes);
122        let nonce = Nonce::from_bytes(nonce_bytes);
123        let ct = encrypt(&key, &nonce, plaintext, aad);
124        assert_eq!(ct.as_slice(), expected_ct);
125
126        let pt = decrypt(&key, &nonce, &ct, aad).unwrap();
127        assert_eq!(pt, plaintext);
128    }
129
130    #[test]
131    fn tampered_tag_returns_decrypt_error() {
132        let key = AeadKey::generate();
133        let nonce = Nonce::random();
134        let mut ct = encrypt(&key, &nonce, b"secret", b"");
135        // Flip last byte of the Poly1305 tag
136        let last = ct.len() - 1;
137        ct[last] ^= 0xff;
138        assert!(matches!(
139            decrypt(&key, &nonce, &ct, b""),
140            Err(Error::Decrypt)
141        ));
142    }
143
144    #[test]
145    fn wrong_aad_returns_decrypt_error() {
146        let key = AeadKey::generate();
147        let nonce = Nonce::random();
148        let ct = encrypt(&key, &nonce, b"msg", b"right-aad");
149        assert!(matches!(
150            decrypt(&key, &nonce, &ct, b"wrong-aad"),
151            Err(Error::Decrypt)
152        ));
153    }
154}