diff --git a/ssh-cipher/src/chacha20poly1305.rs b/ssh-cipher/src/chacha20poly1305.rs index 2a4e8d7..7a75289 100644 --- a/ssh-cipher/src/chacha20poly1305.rs +++ b/ssh-cipher/src/chacha20poly1305.rs @@ -1,8 +1,6 @@ //! OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com` //! //! Differences from ChaCha20Poly1305 as described in RFC8439: -//! -//! - Construction uses two separately keyed instances of ChaCha20: one for data, one for lengths //! - The input of Poly1305 is not padded //! - The lengths of ciphertext and AAD are not authenticated using Poly1305 //! @@ -14,47 +12,18 @@ use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek}; use poly1305::Poly1305; use subtle::ConstantTimeEq; -const KEY_SIZE: usize = 32; - pub(crate) struct ChaCha20Poly1305 { cipher: ChaCha20, mac: Poly1305, } impl ChaCha20Poly1305 { - /// Create a new [`ChaCha20Poly1305`] instance with a 64-byte key. - /// From [PROTOCOL.chacha20poly1305]: - /// - /// > The chacha20-poly1305@openssh.com cipher requires 512 bits of key - /// > material as output from the SSH key exchange. This forms two 256 bit - /// > keys (K_1 and K_2), used by two separate instances of chacha20. - /// > The first 256 bits constitute K_2 and the second 256 bits become - /// > K_1. - /// > - /// > The instance keyed by K_1 is a stream cipher that is used only - /// > to encrypt the 4 byte packet length field. The second instance, - /// > keyed by K_2, is used in conjunction with poly1305 to build an AEAD - /// > (Authenticated Encryption with Associated Data) that is used to encrypt - /// > and authenticate the entire packet. + /// Create a new [`ChaCha20Poly1305`] instance with a 32-byte key. /// /// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD pub fn new(key: &[u8], nonce: &[u8]) -> Result { - #[allow(clippy::arithmetic_side_effects)] - if key.len() != KEY_SIZE * 2 { - return Err(Error::KeySize); - } - - // TODO(tarcieri): support for using both keys - let (k_2, _k_1) = key.split_at(KEY_SIZE); - let key = Key::try_from(k_2).map_err(|_| Error::KeySize)?; - - let nonce = if nonce.is_empty() { - // For key encryption - Nonce::default() - } else { - Nonce::try_from(nonce).map_err(|_| Error::IvSize)? - }; - + let key = Key::try_from(key).map_err(|_| Error::KeySize)?; + let nonce = Nonce::try_from(nonce).map_err(|_| Error::IvSize)?; let mut cipher = ChaCha20::new(&key, &nonce.into()); let mut poly1305_key = poly1305::Key::default(); cipher.apply_keystream(&mut poly1305_key); diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 57d2df5..2437c03 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -172,7 +172,7 @@ impl Cipher { Self::Aes256Ctr => Some((32, 16)), Self::Aes128Gcm => Some((16, 12)), Self::Aes256Gcm => Some((32, 12)), - Self::ChaCha20Poly1305 => Some((64, 0)), + Self::ChaCha20Poly1305 => Some((32, 12)), Self::TDesCbc => Some((24, 8)), } } diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 5396510..a7ddaa3 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -86,14 +86,43 @@ impl Kdf { cipher: Cipher, password: impl AsRef<[u8]>, ) -> Result<(Zeroizing>, Vec)> { - let (key_size, iv_size) = cipher.key_and_iv_size().ok_or(Error::Decrypted)?; + let (key_size, iv_size) = match cipher { + // Derive two ChaCha20Poly1305 keys, but only use the first. + // In the typical SSH protocol, the second key is used for length encryption. + // + // From `PROTOCOL.chacha20poly1305`: + // + // > The chacha20-poly1305@openssh.com cipher requires 512 bits of key + // > material as output from the SSH key exchange. This forms two 256 bit + // > keys (K_1 and K_2), used by two separate instances of chacha20. + // > The first 256 bits constitute K_2 and the second 256 bits become + // > K_1. + // > + // > The instance keyed by K_1 is a stream cipher that is used only + // > to encrypt the 4 byte packet length field. The second instance, + // > keyed by K_2, is used in conjunction with poly1305 to build an AEAD + // > (Authenticated Encryption with Associated Data) that is used to encrypt + // > and authenticate the entire packet. + Cipher::ChaCha20Poly1305 => (64, 0), + _ => cipher.key_and_iv_size().ok_or(Error::Decrypted)?, + }; + let okm_size = key_size .checked_add(iv_size) .ok_or(encoding::Error::Length)?; let mut okm = Zeroizing::new(vec![0u8; okm_size]); self.derive(password, &mut okm)?; - let iv = okm.split_off(key_size); + let mut iv = okm.split_off(key_size); + + if cipher == Cipher::ChaCha20Poly1305 { + // Only use the first ChaCha20 key. + okm.truncate(32); + + // Use an all-zero nonce (with a key derived from password + salt providing uniqueness) + iv.extend_from_slice(&cipher::Nonce::default()); + } + Ok((okm, iv)) }