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`
26/// as described in [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/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
39/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
40#[derive(Clone)]
41pub struct ChaCha20Poly1305 {
42    key: ChaChaKey,
43}
44
45impl KeySizeUser for ChaCha20Poly1305 {
46    type KeySize = U32;
47}
48
49impl KeyInit for ChaCha20Poly1305 {
50    #[inline]
51    fn new(key: &ChaChaKey) -> Self {
52        Self { key: *key }
53    }
54}
55
56impl AeadCore for ChaCha20Poly1305 {
57    type NonceSize = U8;
58    type TagSize = U16;
59    const TAG_POSITION: TagPosition = TagPosition::Postfix;
60}
61
62impl AeadInOut for ChaCha20Poly1305 {
63    fn encrypt_inout_detached(
64        &self,
65        nonce: &ChaChaNonce,
66        associated_data: &[u8],
67        buffer: InOutBuf<'_, '_, u8>,
68    ) -> Result<Tag> {
69        Cipher::new(&self.key, *nonce).encrypt(associated_data, buffer)
70    }
71
72    fn decrypt_inout_detached(
73        &self,
74        nonce: &ChaChaNonce,
75        associated_data: &[u8],
76        buffer: InOutBuf<'_, '_, u8>,
77        tag: &Tag,
78    ) -> Result<()> {
79        Cipher::new(&self.key, *nonce).decrypt(associated_data, buffer, tag)
80    }
81}
82
83impl Debug for ChaCha20Poly1305 {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.debug_struct("ChaCha20Poly1305").finish_non_exhaustive()
86    }
87}
88
89impl Drop for ChaCha20Poly1305 {
90    fn drop(&mut self) {
91        #[cfg(feature = "zeroize")]
92        self.key.zeroize();
93    }
94}
95
96#[cfg(feature = "zeroize")]
97impl ZeroizeOnDrop for ChaCha20Poly1305 {}
98
99/// Internal type representing a cipher instance.
100struct Cipher {
101    cipher: ChaCha20,
102    mac: Poly1305,
103}
104
105impl Cipher {
106    /// Create a new cipher instance.
107    fn new(key: &ChaChaKey, nonce: ChaChaNonce) -> Self {
108        let mut cipher = ChaCha20::new(key, &nonce);
109        let mut poly1305_key = poly1305::Key::default();
110        cipher.apply_keystream(&mut poly1305_key);
111
112        let mac = Poly1305::new(&poly1305_key);
113
114        // Seek to block 1
115        cipher.seek(64);
116
117        Self { cipher, mac }
118    }
119
120    /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
121    #[inline]
122    fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result<Tag> {
123        self.cipher.apply_keystream_inout(buffer.reborrow());
124        compute_mac(self.mac, aad, buffer.get_out())
125    }
126
127    /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
128    /// authentication `tag`.
129    #[inline]
130    fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> {
131        let expected_tag = compute_mac(self.mac, aad, buffer.get_in())?;
132
133        if expected_tag.ct_eq(tag).into() {
134            self.cipher.apply_keystream_inout(buffer);
135            Ok(())
136        } else {
137            Err(Error)
138        }
139    }
140}
141
142/// Compute the MAC for a given input buffer (containing ciphertext).
143fn compute_mac(mut mac: Poly1305, aad: &[u8], buffer: &[u8]) -> Result<Tag> {
144    // We only support up to one block (16-bytes) of AAD.
145    // In practice the sizes that matter are `0` and `4` (i.e. length prefix size).
146    if aad.len() > poly1305::BLOCK_SIZE {
147        return Err(Error);
148    }
149
150    // Compute the first Poly1305 block which incorporates any AAD.
151    let mut block = poly1305::Block::default();
152    block[..aad.len()].copy_from_slice(aad);
153
154    let block_remaining = poly1305::BLOCK_SIZE.checked_sub(aad.len()).ok_or(Error)?;
155    let remaining = if buffer.len() <= block_remaining {
156        // If total AAD + buffer length is less than or equal to a block, compute a partial block
157        let msg_len = aad.len().checked_add(buffer.len()).ok_or(Error)?;
158        block[aad.len()..msg_len].copy_from_slice(buffer);
159        &block[..msg_len]
160    } else {
161        // Compute the first block and return any remaining data
162        let (head, tail) = buffer.split_at(block_remaining);
163        block[aad.len()..].copy_from_slice(head);
164        mac.update(&[block]);
165        tail
166    };
167
168    // Compute Poly1305 over the remaining message data.
169    Ok(mac.compute_unpadded(remaining))
170}
171
172#[cfg(test)]
173mod tests {
174    use super::{AeadInOut, ChaCha20Poly1305, KeyInit, Poly1305, compute_mac};
175    use hex_literal::hex;
176
177    #[test]
178    fn test_vector() {
179        const KEY: [u8; 32] =
180            hex!("379a8ca9e7e705763633213511e8d92eb148a46f1dd0045ec8164e5d23e456eb");
181        const NONCE: [u8; 8] = hex!("0000000000000003");
182        const AAD: [u8; 4] = hex!("5709db2d");
183        const PT: [u8; 24] = hex!("06050000000c7373682d7573657261757468de5949ab061f");
184        const CT: [u8; 24] = hex!("6dcfb03be8a55e7f0220465672edd921489ea0171198e8a7");
185        const TAG: [u8; 16] = hex!("3e82fe0a2db7128d58ef8d9047963ca3");
186
187        let cipher = ChaCha20Poly1305::new(&KEY.into());
188        let mut buffer = PT;
189        let actual_tag = cipher
190            .encrypt_inout_detached(&NONCE.into(), &AAD, buffer.as_mut_slice().into())
191            .unwrap();
192
193        assert_eq!(buffer, CT);
194        assert_eq!(actual_tag, TAG);
195
196        cipher
197            .decrypt_inout_detached(
198                &NONCE.into(),
199                &AAD,
200                buffer.as_mut_slice().into(),
201                &actual_tag,
202            )
203            .unwrap();
204
205        assert_eq!(buffer, PT);
206    }
207
208    #[test]
209    fn mac_computation_with_aad() {
210        const KEY: &[u8; poly1305::KEY_SIZE] = b"11112222333344445555666677778888";
211        const AAD: &[u8; poly1305::BLOCK_SIZE] = b"0123456789ABCDEF";
212        const PT: &[u8; poly1305::BLOCK_SIZE] = b"abcdefghijklmnop";
213
214        for aad_len in 0..=poly1305::BLOCK_SIZE {
215            for pt_len in 0..=poly1305::BLOCK_SIZE {
216                let mut buffer = [0; poly1305::BLOCK_SIZE * 2];
217                let aad = &AAD[..aad_len];
218                let pt = &PT[..pt_len];
219
220                let eob = aad_len + pt_len;
221                buffer[..aad_len].copy_from_slice(aad);
222                buffer[aad_len..eob].copy_from_slice(pt);
223
224                let poly = Poly1305::new(KEY.into());
225                let expected_mac = poly.clone().compute_unpadded(&buffer[..eob]);
226                let actual_mac = compute_mac(poly, aad, pt).unwrap();
227
228                assert_eq!(expected_mac, actual_mac);
229            }
230        }
231    }
232}