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 poly1305::{Poly1305, universal_hash::UniversalHash};
13use subtle::ConstantTimeEq;
14
15#[cfg(feature = "zeroize")]
16use zeroize::{Zeroize, ZeroizeOnDrop};
17
18/// Key for `chacha20-poly1305@openssh.com`.
19pub type ChaChaKey = chacha20::Key;
20
21/// Nonce for `chacha20-poly1305@openssh.com`.
22pub type ChaChaNonce = chacha20::LegacyNonce;
23
24/// OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com`
25/// as described in [PROTOCOL.chacha20poly1305].
26///
27/// Differences from ChaCha20Poly1305-IETF as described in [RFC8439]:
28/// - Nonce is 64-bit instead of 96-bit (i.e. uses legacy "djb" ChaCha20 variant).
29/// - The AAD and ciphertext inputs of Poly1305 are not padded.
30/// - The lengths of ciphertext and AAD are not authenticated using Poly1305.
31/// - Maximum supported AAD size is 16.
32///
33/// ## Usage notes
34/// - In the context of SSH packet encryption, AAD will be 4 bytes and contain the encrypted length.
35/// - In the context of SSH key encryption, AAD will be empty.
36///
37/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
38/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
39#[derive(Clone)]
40pub struct ChaCha20Poly1305 {
41    key: ChaChaKey,
42}
43
44impl KeySizeUser for ChaCha20Poly1305 {
45    type KeySize = U32;
46}
47
48impl KeyInit for ChaCha20Poly1305 {
49    #[inline]
50    fn new(key: &ChaChaKey) -> Self {
51        Self { key: *key }
52    }
53}
54
55impl AeadCore for ChaCha20Poly1305 {
56    type NonceSize = U8;
57    type TagSize = U16;
58    const TAG_POSITION: TagPosition = TagPosition::Postfix;
59}
60
61impl AeadInOut for ChaCha20Poly1305 {
62    // Required methods
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 Drop for ChaCha20Poly1305 {
84    fn drop(&mut self) {
85        #[cfg(feature = "zeroize")]
86        self.key.zeroize();
87    }
88}
89
90#[cfg(feature = "zeroize")]
91impl ZeroizeOnDrop for ChaCha20Poly1305 {}
92
93/// Internal type representing a cipher instance.
94struct Cipher {
95    cipher: ChaCha20,
96    mac: Poly1305,
97}
98
99impl Cipher {
100    /// Create a new cipher instance.
101    pub fn new(key: &ChaChaKey, nonce: &ChaChaNonce) -> Self {
102        let mut cipher = ChaCha20::new(key, nonce);
103        let mut poly1305_key = poly1305::Key::default();
104        cipher.apply_keystream(&mut poly1305_key);
105
106        let mac = Poly1305::new(&poly1305_key);
107
108        // Seek to block 1
109        cipher.seek(64);
110
111        Self { cipher, mac }
112    }
113
114    /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
115    #[inline]
116    pub fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result<Tag> {
117        self.cipher.apply_keystream_inout(buffer.reborrow());
118        compute_mac(self.mac, aad, buffer.get_out())
119    }
120
121    /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
122    /// authentication `tag`.
123    #[inline]
124    pub fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> {
125        let expected_tag = compute_mac(self.mac, aad, buffer.get_in())?;
126
127        if expected_tag.ct_eq(tag).into() {
128            self.cipher.apply_keystream_inout(buffer);
129            Ok(())
130        } else {
131            Err(Error)
132        }
133    }
134}
135
136/// Compute the MAC for a given input buffer (containing ciphertext).
137fn compute_mac(mut mac: Poly1305, aad: &[u8], buffer: &[u8]) -> Result<Tag> {
138    match aad.len() {
139        0 => Ok(mac.compute_unpadded(buffer)),
140        1..poly1305::BLOCK_SIZE => {
141            let mut block = poly1305::Block::default();
142            block[..aad.len()].copy_from_slice(aad);
143
144            let block_remaining = poly1305::BLOCK_SIZE.checked_sub(aad.len()).ok_or(Error)?;
145            if buffer.len() > block_remaining {
146                let (head, tail) = buffer.split_at(block_remaining);
147                block[aad.len()..].copy_from_slice(head);
148                mac.update(&[block]);
149                Ok(mac.compute_unpadded(tail))
150            } else {
151                let msg_len = aad.len().checked_add(buffer.len()).ok_or(Error)?;
152                block[aad.len()..msg_len].copy_from_slice(buffer);
153                Ok(mac.compute_unpadded(&block[..msg_len]))
154            }
155        }
156        _ => Err(Error),
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::{AeadInOut, ChaCha20Poly1305, KeyInit};
163    use hex_literal::hex;
164
165    #[test]
166    fn test_vector() {
167        let key = hex!("379a8ca9e7e705763633213511e8d92eb148a46f1dd0045ec8164e5d23e456eb");
168        let nonce = hex!("0000000000000003");
169        let aad = hex!("5709db2d");
170        let plaintext = hex!("06050000000c7373682d7573657261757468de5949ab061f");
171        let ciphertext = hex!("6dcfb03be8a55e7f0220465672edd921489ea0171198e8a7");
172        let tag = hex!("3e82fe0a2db7128d58ef8d9047963ca3");
173
174        let cipher = ChaCha20Poly1305::new(key.as_ref());
175        let mut buffer = plaintext.clone();
176        let actual_tag = cipher
177            .encrypt_inout_detached(nonce.as_ref(), &aad, buffer.as_mut_slice().into())
178            .unwrap();
179
180        assert_eq!(buffer, ciphertext);
181        assert_eq!(actual_tag, tag);
182
183        cipher
184            .decrypt_inout_detached(
185                nonce.as_ref(),
186                &aad,
187                buffer.as_mut_slice().into(),
188                &actual_tag,
189            )
190            .unwrap();
191
192        assert_eq!(buffer, plaintext);
193    }
194}