ssh_cipher/
chacha20poly1305.rs

1//! OpenSSH variant of ChaCha20Poly1305.
2
3pub use chacha20::ChaCha20Legacy as ChaCha20;
4
5use crate::Tag;
6use aead::{
7    AeadCore, Error, KeyInit, KeySizeUser, Result, TagPosition,
8    array::typenum::{U8, U16, U32},
9};
10use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
11use poly1305::Poly1305;
12use subtle::ConstantTimeEq;
13
14#[cfg(feature = "zeroize")]
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17/// Key for `chacha20-poly1305@openssh.com`.
18pub type ChaChaKey = chacha20::Key;
19
20/// Nonce for `chacha20-poly1305@openssh.com`.
21pub type ChaChaNonce = chacha20::LegacyNonce;
22
23/// OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com`
24/// as described in [PROTOCOL.chacha20poly1305].
25///
26/// Differences from ChaCha20Poly1305-IETF as described in [RFC8439]:
27/// - Nonce is 64-bit instead of 96-bit (i.e. uses legacy "djb" ChaCha20 variant).
28/// - The AAD and ciphertext inputs of Poly1305 are not padded.
29/// - The lengths of ciphertext and AAD are not authenticated using Poly1305.
30///
31/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
32/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
33#[derive(Clone)]
34pub struct ChaCha20Poly1305 {
35    key: ChaChaKey,
36}
37
38impl KeySizeUser for ChaCha20Poly1305 {
39    type KeySize = U32;
40}
41
42impl KeyInit for ChaCha20Poly1305 {
43    #[inline]
44    fn new(key: &ChaChaKey) -> Self {
45        Self { key: *key }
46    }
47}
48
49impl AeadCore for ChaCha20Poly1305 {
50    type NonceSize = U8;
51    type TagSize = U16;
52    const TAG_POSITION: TagPosition = TagPosition::Postfix;
53}
54
55impl ChaCha20Poly1305 {
56    /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
57    ///
58    /// The input `buffer` should contain the concatenation of any additional associated data (AAD)
59    /// and the plaintext to be encrypted, where in the context of the SSH packet encryption
60    /// protocol the AAD represents an encrypted packet length, which is itself 4-bytes / 64-bits.
61    ///
62    /// `aad_len` is the length of the AAD in bytes:
63    /// - In the context of SSH packet encryption, this should be `4`.
64    /// - In the context of SSH key encryption, `aad_len` should be `0`.
65    ///
66    /// The first `aad_len` bytes of `buffer` will be unmodified after encryption is completed.
67    /// Only the data after `aad_len` will be encrypted.
68    ///
69    /// The resulting `Tag` authenticates both the AAD and the ciphertext in the buffer.
70    pub fn encrypt(&self, nonce: &ChaChaNonce, buffer: &mut [u8], aad_len: usize) -> Result<Tag> {
71        Cipher::new(&self.key, nonce).encrypt(buffer, aad_len)
72    }
73
74    /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
75    /// authentication `tag`.
76    ///
77    /// The input `buffer` should contain the concatenation of any additional associated data (AAD)
78    /// and the ciphertext to be authenticated, where in the context of the SSH packet encryption
79    /// protocol the AAD represents an encrypted packet length, which is itself 4-bytes / 64-bits.
80    ///
81    /// `aad_len` is the length of the AAD in bytes:
82    /// - In the context of SSH packet encryption, this should be `4`.
83    /// - In the context of SSH key encryption, `aad_len` should be `0`.
84    ///
85    /// The first `aad_len` bytes of `buffer` will be unmodified after decryption completes
86    /// successfully. Only data after `aad_len` will be decrypted.
87    pub fn decrypt(
88        &self,
89        nonce: &ChaChaNonce,
90        buffer: &mut [u8],
91        tag: Tag,
92        aad_len: usize,
93    ) -> Result<()> {
94        Cipher::new(&self.key, nonce).decrypt(buffer, tag, aad_len)
95    }
96}
97
98impl Drop for ChaCha20Poly1305 {
99    fn drop(&mut self) {
100        #[cfg(feature = "zeroize")]
101        self.key.zeroize();
102    }
103}
104
105#[cfg(feature = "zeroize")]
106impl ZeroizeOnDrop for ChaCha20Poly1305 {}
107
108/// Internal type representing a cipher instance.
109struct Cipher {
110    cipher: ChaCha20,
111    mac: Poly1305,
112}
113
114impl Cipher {
115    /// Create a new cipher instance.
116    pub fn new(key: &ChaChaKey, nonce: &ChaChaNonce) -> Self {
117        let mut cipher = ChaCha20::new(key, nonce);
118        let mut poly1305_key = poly1305::Key::default();
119        cipher.apply_keystream(&mut poly1305_key);
120
121        let mac = Poly1305::new(&poly1305_key);
122
123        // Seek to block 1
124        cipher.seek(64);
125
126        Self { cipher, mac }
127    }
128
129    /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
130    #[inline]
131    pub fn encrypt(mut self, buffer: &mut [u8], aad_len: usize) -> Result<Tag> {
132        if buffer.len() < aad_len {
133            return Err(Error);
134        }
135
136        self.cipher.apply_keystream(&mut buffer[aad_len..]);
137        Ok(self.mac.compute_unpadded(buffer))
138    }
139
140    /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
141    /// authentication `tag`.
142    #[inline]
143    pub fn decrypt(mut self, buffer: &mut [u8], tag: Tag, aad_len: usize) -> Result<()> {
144        if buffer.len() < aad_len {
145            return Err(Error);
146        }
147
148        let expected_tag = self.mac.compute_unpadded(buffer);
149
150        if expected_tag.ct_eq(&tag).into() {
151            self.cipher.apply_keystream(&mut buffer[aad_len..]);
152            Ok(())
153        } else {
154            Err(Error)
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{ChaCha20Poly1305, KeyInit};
162    use hex_literal::hex;
163
164    #[test]
165    fn test_vector() {
166        let key = hex!("379a8ca9e7e705763633213511e8d92eb148a46f1dd0045ec8164e5d23e456eb");
167        let nonce = hex!("0000000000000003");
168        let aad = hex!("5709db2d");
169        let plaintext = hex!("06050000000c7373682d7573657261757468de5949ab061f");
170        let ciphertext = hex!("6dcfb03be8a55e7f0220465672edd921489ea0171198e8a7");
171        let tag = hex!("3e82fe0a2db7128d58ef8d9047963ca3");
172
173        const AAD_LEN: usize = 4;
174        const PT_LEN: usize = 24;
175        assert_eq!(aad.len(), AAD_LEN);
176        assert_eq!(plaintext.len(), PT_LEN);
177
178        let cipher = ChaCha20Poly1305::new(key.as_ref());
179        let mut buffer = [0u8; AAD_LEN + PT_LEN];
180        let (a, p) = buffer.split_at_mut(AAD_LEN);
181        a.copy_from_slice(&aad);
182        p.copy_from_slice(&plaintext);
183
184        let actual_tag = cipher
185            .encrypt(nonce.as_ref(), &mut buffer, AAD_LEN)
186            .unwrap();
187
188        assert_eq!(&buffer[..AAD_LEN], aad);
189        assert_eq!(&buffer[AAD_LEN..], ciphertext);
190        assert_eq!(actual_tag, tag);
191
192        cipher
193            .decrypt(nonce.as_ref(), &mut buffer, actual_tag, AAD_LEN)
194            .unwrap();
195
196        assert_eq!(&buffer[..AAD_LEN], aad);
197        assert_eq!(&buffer[AAD_LEN..], plaintext);
198    }
199}