Skip to main content

netcode/
crypto.rs

1//! AEAD encryption primitives.
2//!
3//! Packets and challenge tokens are encrypted with ChaCha20-Poly1305 (IETF, 96-bit nonce).
4//! Private connect token data is encrypted with XChaCha20-Poly1305 (IETF, 192-bit nonce).
5//!
6//! In both cases the buffer layout is the message followed by a 16-byte authentication
7//! tag, matching the libsodium combined mode used by the reference implementation.
8
9use chacha20poly1305::aead::AeadInOut;
10use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Tag, XChaCha20Poly1305};
11
12use crate::{Error, Key, MAC_BYTES};
13
14pub(crate) const NONCE_BYTES: usize = 12;
15pub(crate) const XNONCE_BYTES: usize = 24;
16
17/// The 96-bit nonce for packet and challenge token encryption: four zero bytes
18/// followed by the sequence number as a 64-bit little-endian value.
19pub(crate) fn sequence_nonce(sequence: u64) -> [u8; NONCE_BYTES] {
20    let mut nonce = [0u8; NONCE_BYTES];
21    nonce[4..].copy_from_slice(&sequence.to_le_bytes());
22    nonce
23}
24
25/// Encrypts `buffer[..len-16]` in place and writes the authentication tag to the last
26/// 16 bytes of the buffer.
27pub(crate) fn encrypt_aead(
28    buffer: &mut [u8],
29    additional_data: &[u8],
30    nonce: &[u8; NONCE_BYTES],
31    key: &Key,
32) -> Result<(), Error> {
33    let (message, mac) = buffer.split_last_chunk_mut::<MAC_BYTES>().ok_or(Error::EncryptFailed)?;
34    let tag = ChaCha20Poly1305::new(key.into())
35        .encrypt_inout_detached(nonce.into(), additional_data, message.into())
36        .map_err(|_| Error::EncryptFailed)?;
37    mac.copy_from_slice(&tag);
38    Ok(())
39}
40
41/// Decrypts `buffer[..len-16]` in place, authenticating against the tag in the last
42/// 16 bytes of the buffer. The tag bytes are left untouched.
43pub(crate) fn decrypt_aead(
44    buffer: &mut [u8],
45    additional_data: &[u8],
46    nonce: &[u8; NONCE_BYTES],
47    key: &Key,
48) -> Result<(), Error> {
49    let (message, mac) = buffer.split_last_chunk_mut::<MAC_BYTES>().ok_or(Error::DecryptFailed)?;
50    ChaCha20Poly1305::new(key.into())
51        .decrypt_inout_detached(nonce.into(), additional_data, message.into(), &Tag::from(*mac))
52        .map_err(|_| Error::DecryptFailed)
53}
54
55/// XChaCha20-Poly1305 variant of [`encrypt_aead`], used for private connect token data.
56pub(crate) fn encrypt_aead_big_nonce(
57    buffer: &mut [u8],
58    additional_data: &[u8],
59    nonce: &[u8; XNONCE_BYTES],
60    key: &Key,
61) -> Result<(), Error> {
62    let (message, mac) = buffer.split_last_chunk_mut::<MAC_BYTES>().ok_or(Error::EncryptFailed)?;
63    let tag = XChaCha20Poly1305::new(key.into())
64        .encrypt_inout_detached(nonce.into(), additional_data, message.into())
65        .map_err(|_| Error::EncryptFailed)?;
66    mac.copy_from_slice(&tag);
67    Ok(())
68}
69
70/// XChaCha20-Poly1305 variant of [`decrypt_aead`], used for private connect token data.
71pub(crate) fn decrypt_aead_big_nonce(
72    buffer: &mut [u8],
73    additional_data: &[u8],
74    nonce: &[u8; XNONCE_BYTES],
75    key: &Key,
76) -> Result<(), Error> {
77    let (message, mac) = buffer.split_last_chunk_mut::<MAC_BYTES>().ok_or(Error::DecryptFailed)?;
78    XChaCha20Poly1305::new(key.into())
79        .decrypt_inout_detached(nonce.into(), additional_data, message.into(), &Tag::from(*mac))
80        .map_err(|_| Error::DecryptFailed)
81}
82
83pub(crate) fn random_bytes(buffer: &mut [u8]) {
84    getrandom::fill(buffer).expect("the operating system random number generator failed");
85}
86
87/// Generates a cryptographically secure random 256-bit key.
88pub fn generate_key() -> Key {
89    let mut key = [0u8; crate::KEY_BYTES];
90    random_bytes(&mut key);
91    key
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn aead_round_trip() {
100        let key = generate_key();
101        let nonce = sequence_nonce(1000);
102        let additional_data = b"additional data";
103
104        let mut buffer = [0u8; 32 + MAC_BYTES];
105        buffer[..32].copy_from_slice(&[0x42; 32]);
106
107        encrypt_aead(&mut buffer, additional_data, &nonce, &key).unwrap();
108        assert_ne!(&buffer[..32], &[0x42; 32]);
109
110        decrypt_aead(&mut buffer, additional_data, &nonce, &key).unwrap();
111        assert_eq!(&buffer[..32], &[0x42; 32]);
112    }
113
114    #[test]
115    fn aead_rejects_tampering() {
116        let key = generate_key();
117        let nonce = sequence_nonce(1);
118
119        let mut buffer = [0u8; 32 + MAC_BYTES];
120        encrypt_aead(&mut buffer, &[], &nonce, &key).unwrap();
121
122        buffer[0] ^= 1;
123        assert!(matches!(decrypt_aead(&mut buffer, &[], &nonce, &key), Err(Error::DecryptFailed)));
124    }
125
126    #[test]
127    fn aead_rejects_wrong_key() {
128        let key = generate_key();
129        let nonce = sequence_nonce(1);
130
131        let mut buffer = [0u8; 32 + MAC_BYTES];
132        encrypt_aead(&mut buffer, &[], &nonce, &key).unwrap();
133
134        let wrong_key = generate_key();
135        assert!(matches!(
136            decrypt_aead(&mut buffer, &[], &nonce, &wrong_key),
137            Err(Error::DecryptFailed)
138        ));
139    }
140
141    #[test]
142    fn big_nonce_round_trip() {
143        let key = generate_key();
144        let mut nonce = [0u8; XNONCE_BYTES];
145        random_bytes(&mut nonce);
146
147        let mut buffer = [0u8; 64 + MAC_BYTES];
148        buffer[..64].copy_from_slice(&[0x37; 64]);
149
150        encrypt_aead_big_nonce(&mut buffer, b"aad", &nonce, &key).unwrap();
151        decrypt_aead_big_nonce(&mut buffer, b"aad", &nonce, &key).unwrap();
152        assert_eq!(&buffer[..64], &[0x37; 64]);
153    }
154
155    #[test]
156    fn nonce_is_little_endian_sequence_in_high_bytes() {
157        // [0 0 0 0][seq as 64-bit little-endian] -- getting this backwards produces
158        // ciphertext incompatible with every other netcode implementation
159        let nonce = sequence_nonce(0x1122334455667788);
160        assert_eq!(nonce, [0, 0, 0, 0, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]);
161    }
162}