Skip to main content

ssh_cipher/
chacha20poly1305.rs

1//! OpenSSH variant of ChaCha20Poly1305.
2
3pub use chacha20::ChaCha20Legacy as ChaCha20;
4
5use crate::Tag;
6use aead::{
7    AeadCore, AeadInOut, Error, KeyInit, KeySizeUser, Result, TagPosition,
8    array::typenum::{U8, U16, U32},
9    inout::InOutBuf,
10};
11use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
12use core::fmt::{self, Debug};
13use ctutils::CtEq;
14use poly1305::{Poly1305, universal_hash::UniversalHash};
15
16#[cfg(feature = "zeroize")]
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19/// Key for `chacha20-poly1305@openssh.com`.
20pub type ChaChaKey = chacha20::Key;
21
22/// Nonce for `chacha20-poly1305@openssh.com`.
23pub type ChaChaNonce = chacha20::LegacyNonce;
24
25/// OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com` as described in
26/// [PROTOCOL.chacha20poly1305].
27///
28/// Differences from ChaCha20Poly1305-IETF as described in [RFC8439]:
29/// - Nonce is 64-bit instead of 96-bit (i.e. uses legacy "djb" ChaCha20 variant).
30/// - The AAD and ciphertext inputs of Poly1305 are not padded.
31/// - The lengths of ciphertext and AAD are not authenticated using Poly1305.
32/// - Maximum supported AAD size is 16.
33///
34/// ## Usage notes
35/// - In the context of SSH packet encryption, AAD will be 4 bytes and contain the encrypted length.
36/// - In the context of SSH key encryption, AAD will be empty.
37///
38/// ## Implementing packet encryption
39/// The [`ChaCha20Poly1305`] type implements *just* the AEAD primitive used by OpenSSH, and takes
40/// a 32-byte/256-bit key. However, the full construction used for packet encryption requires
41/// 64-byte/512-bit keys, which are split into two 32-byte/256-bit keys (`K_1` and `K_2`), where
42/// `K_1` is used for length encryption, and `K_2` for the packet body in addition to authenticating
43/// the ciphertext encrypted under `K_1`.
44///
45/// ## Packet encryption example
46///
47/// ```
48/// use ssh_cipher::{
49///     ChaCha20, ChaCha20Poly1305, ChaChaKey, ChaChaNonce, Tag,
50///     aead::{AeadInOut, KeyInit},
51///     cipher::{KeyIvInit, StreamCipher},
52/// };
53///
54/// fn encrypt_packet(
55///     key: &[u8; 64],
56///     seq_num: u32,
57///     packet: &mut [u8],
58/// ) -> Result<([u8; 4], Tag), aead::Error> {
59///     let (k1, k2) = split_key(key);
60///     let mut nonce = ChaChaNonce::default();
61///     nonce[4..].copy_from_slice(&seq_num.to_be_bytes());
62///
63///     // Encrypt 4-byte packet length under `K_1` using raw `ChaCha20`.
64///     let packet_len = u32::try_from(packet.len()).map_err(|_| aead::Error)?;
65///     let mut len_bytes = packet_len.to_be_bytes();
66///     let mut len_cipher = ChaCha20::new(&k1, &nonce);
67///     len_cipher.apply_keystream(&mut len_bytes);
68///
69///     // Encrypt packet body while authenticating encrypted `len_ct` as AAD.
70///     let body_cipher = ChaCha20Poly1305::new(&k2);
71///     let tag = body_cipher.encrypt_inout_detached(&nonce, &len_bytes, packet.into())?;
72///     Ok((len_bytes, tag))
73/// }
74///
75/// fn decrypt_packet(
76///     key: &[u8; 64],
77///     seq_num: u32,
78///     mut enc_len: [u8; 4],
79///     packet: &mut [u8],
80///     tag: &Tag,
81/// ) -> Result<u32, aead::Error> {
82///     let (k1, k2) = split_key(key);
83///     let mut nonce = ChaChaNonce::default();
84///     nonce[4..].copy_from_slice(&seq_num.to_be_bytes());
85///
86///     // Authenticate length and decrypt body
87///     let aead = ChaCha20Poly1305::new(&k2);
88///     aead.decrypt_inout_detached(&nonce, &enc_len, packet.into(), tag)?;
89///
90///     // If ciphertext is authentic, decrypt the length
91///     let mut len_cipher = ChaCha20::new(&k2, &nonce);
92///     len_cipher.apply_keystream(&mut enc_len);
93///     Ok(u32::from_be_bytes(enc_len))
94/// }
95///
96/// // Split 512-bit key into `K_1` and `K_2`
97/// fn split_key(key: &[u8; 64]) -> (ChaChaKey, ChaChaKey) {
98///     let (k1, k2) = key.split_at(32);
99///     (ChaChaKey::try_from(k1).unwrap(), ChaChaKey::try_from(k2).unwrap())
100/// }
101/// ```
102///
103/// [PROTOCOL.chacha20poly1305]: https://web.mit.edu/freebsd/head/crypto/openssh/PROTOCOL.chacha20poly1305
104/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
105#[derive(Clone)]
106pub struct ChaCha20Poly1305 {
107    key: ChaChaKey,
108}
109
110impl KeySizeUser for ChaCha20Poly1305 {
111    type KeySize = U32;
112}
113
114impl KeyInit for ChaCha20Poly1305 {
115    #[inline]
116    fn new(key: &ChaChaKey) -> Self {
117        Self { key: *key }
118    }
119}
120
121impl AeadCore for ChaCha20Poly1305 {
122    type NonceSize = U8;
123    type TagSize = U16;
124    const TAG_POSITION: TagPosition = TagPosition::Postfix;
125}
126
127impl AeadInOut for ChaCha20Poly1305 {
128    fn encrypt_inout_detached(
129        &self,
130        nonce: &ChaChaNonce,
131        associated_data: &[u8],
132        buffer: InOutBuf<'_, '_, u8>,
133    ) -> Result<Tag> {
134        Cipher::new(&self.key, *nonce).encrypt(associated_data, buffer)
135    }
136
137    fn decrypt_inout_detached(
138        &self,
139        nonce: &ChaChaNonce,
140        associated_data: &[u8],
141        buffer: InOutBuf<'_, '_, u8>,
142        tag: &Tag,
143    ) -> Result<()> {
144        Cipher::new(&self.key, *nonce).decrypt(associated_data, buffer, tag)
145    }
146}
147
148impl Debug for ChaCha20Poly1305 {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("ChaCha20Poly1305").finish_non_exhaustive()
151    }
152}
153
154impl Drop for ChaCha20Poly1305 {
155    fn drop(&mut self) {
156        #[cfg(feature = "zeroize")]
157        self.key.zeroize();
158    }
159}
160
161#[cfg(feature = "zeroize")]
162impl ZeroizeOnDrop for ChaCha20Poly1305 {}
163
164/// Internal type representing a cipher instance.
165struct Cipher {
166    cipher: ChaCha20,
167    mac: Poly1305,
168}
169
170impl Cipher {
171    /// Create a new cipher instance.
172    fn new(key: &ChaChaKey, nonce: ChaChaNonce) -> Self {
173        let mut cipher = ChaCha20::new(key, &nonce);
174        let mut poly1305_key = poly1305::Key::default();
175        cipher.apply_keystream(&mut poly1305_key);
176
177        let mac = Poly1305::new(&poly1305_key);
178
179        // Seek to block 1
180        cipher.seek(64);
181
182        Self { cipher, mac }
183    }
184
185    /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
186    #[inline]
187    fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result<Tag> {
188        self.cipher.apply_keystream_inout(buffer.reborrow());
189        compute_mac(self.mac, aad, buffer.get_out())
190    }
191
192    /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
193    /// authentication `tag`.
194    #[inline]
195    fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> {
196        let expected_tag = compute_mac(self.mac, aad, buffer.get_in())?;
197
198        if expected_tag.ct_eq(tag).into() {
199            self.cipher.apply_keystream_inout(buffer);
200            Ok(())
201        } else {
202            Err(Error)
203        }
204    }
205}
206
207/// Compute the MAC for a given input buffer (containing ciphertext).
208fn compute_mac(mut mac: Poly1305, aad: &[u8], buffer: &[u8]) -> Result<Tag> {
209    // We only support up to one block (16-bytes) of AAD.
210    // In practice the sizes that matter are `0` and `4` (i.e. length prefix size).
211    if aad.len() > poly1305::BLOCK_SIZE {
212        return Err(Error);
213    }
214
215    // Compute the first Poly1305 block which incorporates any AAD.
216    let mut block = poly1305::Block::default();
217    block[..aad.len()].copy_from_slice(aad);
218
219    let block_remaining = poly1305::BLOCK_SIZE.checked_sub(aad.len()).ok_or(Error)?;
220    let remaining = if buffer.len() <= block_remaining {
221        // If total AAD + buffer length is less than or equal to a block, compute a partial block
222        let msg_len = aad.len().checked_add(buffer.len()).ok_or(Error)?;
223        block[aad.len()..msg_len].copy_from_slice(buffer);
224        &block[..msg_len]
225    } else {
226        // Compute the first block and return any remaining data
227        let (head, tail) = buffer.split_at(block_remaining);
228        block[aad.len()..].copy_from_slice(head);
229        mac.update(&[block]);
230        tail
231    };
232
233    // Compute Poly1305 over the remaining message data.
234    Ok(mac.compute_unpadded(remaining))
235}
236
237#[cfg(test)]
238mod tests {
239    use super::{AeadInOut, ChaCha20Poly1305, KeyInit, Poly1305, compute_mac};
240    use hex_literal::hex;
241
242    #[test]
243    fn test_vector() {
244        const KEY: [u8; 32] =
245            hex!("379a8ca9e7e705763633213511e8d92eb148a46f1dd0045ec8164e5d23e456eb");
246        const NONCE: [u8; 8] = hex!("0000000000000003");
247        const AAD: [u8; 4] = hex!("5709db2d");
248        const PT: [u8; 24] = hex!("06050000000c7373682d7573657261757468de5949ab061f");
249        const CT: [u8; 24] = hex!("6dcfb03be8a55e7f0220465672edd921489ea0171198e8a7");
250        const TAG: [u8; 16] = hex!("3e82fe0a2db7128d58ef8d9047963ca3");
251
252        let cipher = ChaCha20Poly1305::new(&KEY.into());
253        let mut buffer = PT;
254        let actual_tag = cipher
255            .encrypt_inout_detached(&NONCE.into(), &AAD, buffer.as_mut_slice().into())
256            .unwrap();
257
258        assert_eq!(buffer, CT);
259        assert_eq!(actual_tag, TAG);
260
261        cipher
262            .decrypt_inout_detached(
263                &NONCE.into(),
264                &AAD,
265                buffer.as_mut_slice().into(),
266                &actual_tag,
267            )
268            .unwrap();
269
270        assert_eq!(buffer, PT);
271    }
272
273    #[test]
274    fn mac_computation_with_aad() {
275        const KEY: &[u8; poly1305::KEY_SIZE] = b"11112222333344445555666677778888";
276        const AAD: &[u8; poly1305::BLOCK_SIZE] = b"0123456789ABCDEF";
277        const PT: &[u8; poly1305::BLOCK_SIZE] = b"abcdefghijklmnop";
278
279        for aad_len in 0..=poly1305::BLOCK_SIZE {
280            for pt_len in 0..=poly1305::BLOCK_SIZE {
281                let mut buffer = [0; poly1305::BLOCK_SIZE * 2];
282                let aad = &AAD[..aad_len];
283                let pt = &PT[..pt_len];
284
285                let eob = aad_len + pt_len;
286                buffer[..aad_len].copy_from_slice(aad);
287                buffer[aad_len..eob].copy_from_slice(pt);
288
289                let poly = Poly1305::new(KEY.into());
290                let expected_mac = poly.clone().compute_unpadded(&buffer[..eob]);
291                let actual_mac = compute_mac(poly, aad, pt).unwrap();
292
293                assert_eq!(expected_mac, actual_mac);
294            }
295        }
296    }
297}