ssh_cipher/chacha20poly1305.rs
1//! OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com`
2//!
3//! Differences from ChaCha20Poly1305 as described in RFC8439:
4//!
5//! - Construction uses two separately keyed instances of ChaCha20: one for data, one for lengths
6//! - The input of Poly1305 is not padded
7//! - The lengths of ciphertext and AAD are not authenticated using Poly1305
8//!
9//! [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
10
11use crate::{Error, Nonce, Result, Tag};
12use chacha20::{ChaCha20, Key};
13use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek};
14use poly1305::Poly1305;
15use subtle::ConstantTimeEq;
16
17const KEY_SIZE: usize = 32;
18
19pub(crate) struct ChaCha20Poly1305 {
20 cipher: ChaCha20,
21 mac: Poly1305,
22}
23
24impl ChaCha20Poly1305 {
25 /// Create a new [`ChaCha20Poly1305`] instance with a 64-byte key.
26 /// From [PROTOCOL.chacha20poly1305]:
27 ///
28 /// > The chacha20-poly1305@openssh.com cipher requires 512 bits of key
29 /// > material as output from the SSH key exchange. This forms two 256 bit
30 /// > keys (K_1 and K_2), used by two separate instances of chacha20.
31 /// > The first 256 bits constitute K_2 and the second 256 bits become
32 /// > K_1.
33 /// >
34 /// > The instance keyed by K_1 is a stream cipher that is used only
35 /// > to encrypt the 4 byte packet length field. The second instance,
36 /// > keyed by K_2, is used in conjunction with poly1305 to build an AEAD
37 /// > (Authenticated Encryption with Associated Data) that is used to encrypt
38 /// > and authenticate the entire packet.
39 ///
40 /// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
41 pub fn new(key: &[u8], nonce: &[u8]) -> Result<Self> {
42 #[allow(clippy::arithmetic_side_effects)]
43 if key.len() != KEY_SIZE * 2 {
44 return Err(Error::KeySize);
45 }
46
47 // TODO(tarcieri): support for using both keys
48 let (k_2, _k_1) = key.split_at(KEY_SIZE);
49 let key = Key::from_slice(k_2);
50
51 let nonce = if nonce.is_empty() {
52 // For key encryption
53 Nonce::default()
54 } else {
55 Nonce::try_from(nonce).map_err(|_| Error::IvSize)?
56 };
57
58 let mut cipher = ChaCha20::new(key, &nonce.into());
59 let mut poly1305_key = poly1305::Key::default();
60 cipher.apply_keystream(&mut poly1305_key);
61
62 let mac = Poly1305::new(&poly1305_key);
63
64 // Seek to block 1
65 cipher.seek(64);
66
67 Ok(Self { cipher, mac })
68 }
69
70 #[inline]
71 pub fn encrypt(mut self, buffer: &mut [u8]) -> Tag {
72 self.cipher.apply_keystream(buffer);
73 self.mac.compute_unpadded(buffer).into()
74 }
75
76 #[inline]
77 pub fn decrypt(mut self, buffer: &mut [u8], tag: Tag) -> Result<()> {
78 let expected_tag = self.mac.compute_unpadded(buffer);
79
80 if expected_tag.ct_eq(&tag).into() {
81 self.cipher.apply_keystream(buffer);
82 Ok(())
83 } else {
84 Err(Error::Crypto)
85 }
86 }
87}