From e3c97d5a9fcf4c9766f72ef3ca2c7ea5b42b5baf Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 14 Aug 2024 16:52:54 -0600 Subject: [PATCH] ssh-cipher: refactor `ChaCha20Poly1305` to use `aead` API Changes the `ChaCha20Poly1305` type to impl the standard AEAD APIs exported by the `aead` crate. --- Cargo.lock | 1 + ssh-cipher/Cargo.toml | 7 ++- ssh-cipher/src/chacha20poly1305.rs | 93 +++++++++++++++++++++++++----- ssh-cipher/src/lib.rs | 48 +++++++++------ ssh-key/src/kdf.rs | 2 +- 5 files changed, 112 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 484bda7..6e77c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,6 +785,7 @@ dependencies = [ name = "ssh-cipher" version = "0.3.0-pre.0" dependencies = [ + "aead", "aes", "aes-gcm", "cbc", diff --git a/ssh-cipher/Cargo.toml b/ssh-cipher/Cargo.toml index a5b53f5..8c1d319 100644 --- a/ssh-cipher/Cargo.toml +++ b/ssh-cipher/Cargo.toml @@ -23,11 +23,12 @@ cipher = "=0.5.0-pre.6" encoding = { package = "ssh-encoding", version = "=0.3.0-pre.0", path = "../ssh-encoding" } # optional dependencies +aead = { version = "0.6.0-rc.0", optional = true, default-features = false } aes = { version = "=0.9.0-pre.1", optional = true, default-features = false } aes-gcm = { version = "=0.11.0-pre.1", optional = true, default-features = false, features = ["aes"] } cbc = { version = "=0.2.0-pre.1", optional = true } ctr = { version = "=0.10.0-pre.1", optional = true, default-features = false } -chacha20 = { version = "=0.10.0-pre.1", optional = true, default-features = false, features = ["cipher"] } +chacha20 = { version = "=0.10.0-pre.1", optional = true, default-features = false, features = ["cipher", "legacy"] } des = { version = "=0.9.0-pre.1", optional = true, default-features = false } poly1305 = { version = "0.9.0-rc.0", optional = true, default-features = false } subtle = { version = "2", optional = true, default-features = false } @@ -37,8 +38,8 @@ std = [] aes-cbc = ["dep:aes", "dep:cbc"] aes-ctr = ["dep:aes", "dep:ctr"] -aes-gcm = ["dep:aes", "dep:aes-gcm"] -chacha20poly1305 = ["dep:chacha20", "dep:poly1305", "dep:subtle"] +aes-gcm = ["dep:aead", "dep:aes", "dep:aes-gcm"] +chacha20poly1305 = ["dep:aead", "dep:chacha20", "dep:poly1305", "dep:subtle"] tdes = ["dep:des", "dep:cbc"] [package.metadata.docs.rs] diff --git a/ssh-cipher/src/chacha20poly1305.rs b/ssh-cipher/src/chacha20poly1305.rs index 3688860..eb76d00 100644 --- a/ssh-cipher/src/chacha20poly1305.rs +++ b/ssh-cipher/src/chacha20poly1305.rs @@ -1,11 +1,21 @@ //! OpenSSH variant of ChaCha20Poly1305. -use crate::{Error, Nonce, Result, Tag}; -use chacha20::{ChaCha20, Key}; -use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek}; +use crate::Tag; +use aead::{ + array::typenum::{U0, U16, U32, U8}, + AeadCore, AeadInPlace, Error, KeyInit, KeySizeUser, Result, +}; +use chacha20::ChaCha20Legacy as ChaCha20; +use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; use poly1305::Poly1305; use subtle::ConstantTimeEq; +/// Key for `chacha20-poly1305@openssh.com`. +pub type ChaChaKey = chacha20::Key; + +/// Nonce for `chacha20-poly1305@openssh.com`. +pub type ChaChaNonce = chacha20::LegacyNonce; + /// OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com` /// as described in [PROTOCOL.chacha20poly1305]. /// @@ -16,19 +26,60 @@ use subtle::ConstantTimeEq; /// /// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD /// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439 +#[derive(Clone)] pub struct ChaCha20Poly1305 { + // TODO(tarcieri): zeroize on drop + key: ChaChaKey, +} + +impl KeySizeUser for ChaCha20Poly1305 { + type KeySize = U32; +} + +impl KeyInit for ChaCha20Poly1305 { + #[inline] + fn new(key: &ChaChaKey) -> Self { + Self { key: *key } + } +} + +impl AeadCore for ChaCha20Poly1305 { + type NonceSize = U8; + type TagSize = U16; + type CiphertextOverhead = U0; +} + +impl AeadInPlace for ChaCha20Poly1305 { + fn encrypt_in_place_detached( + &self, + nonce: &ChaChaNonce, + associated_data: &[u8], + buffer: &mut [u8], + ) -> Result { + Cipher::new(&self.key, nonce).encrypt(associated_data, buffer) + } + + fn decrypt_in_place_detached( + &self, + nonce: &ChaChaNonce, + associated_data: &[u8], + buffer: &mut [u8], + tag: &Tag, + ) -> Result<()> { + Cipher::new(&self.key, nonce).decrypt(associated_data, buffer, *tag) + } +} + +/// Internal type representing a cipher instance. +struct Cipher { cipher: ChaCha20, mac: Poly1305, } -impl ChaCha20Poly1305 { - /// 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 { - 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()); +impl Cipher { + /// Create a new cipher instance. + pub fn new(key: &ChaChaKey, nonce: &ChaChaNonce) -> Self { + let mut cipher = ChaCha20::new(&key, nonce.into()); let mut poly1305_key = poly1305::Key::default(); cipher.apply_keystream(&mut poly1305_key); @@ -37,14 +88,19 @@ impl ChaCha20Poly1305 { // Seek to block 1 cipher.seek(64); - Ok(Self { cipher, mac }) + Self { cipher, mac } } /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag. #[inline] - pub fn encrypt(mut self, buffer: &mut [u8]) -> Tag { + pub fn encrypt(mut self, associated_data: &[u8], buffer: &mut [u8]) -> Result { + // TODO(tarcieri): support associated data (RustCrypto/SSH#279) + if !associated_data.is_empty() { + return Err(Error); + } + self.cipher.apply_keystream(buffer); - self.mac.compute_unpadded(buffer).into() + Ok(self.mac.compute_unpadded(buffer).into()) } /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305 @@ -55,14 +111,19 @@ impl ChaCha20Poly1305 { /// /// Upon success, `Ok(())` is returned and `buffer` is rewritten with the decrypted plaintext. #[inline] - pub fn decrypt(mut self, buffer: &mut [u8], tag: Tag) -> Result<()> { + pub fn decrypt(mut self, associated_data: &[u8], buffer: &mut [u8], tag: Tag) -> Result<()> { + // TODO(tarcieri): support associated data (RustCrypto/SSH#279) + if !associated_data.is_empty() { + return Err(Error); + } + let expected_tag = self.mac.compute_unpadded(buffer); if expected_tag.ct_eq(&tag).into() { self.cipher.apply_keystream(buffer); Ok(()) } else { - Err(Error::Crypto) + Err(Error) } } } diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 7772376..ba78c7b 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -35,20 +35,24 @@ mod encryptor; pub use crate::error::{Error, Result}; -#[cfg(feature = "chacha20poly1305")] -pub use crate::chacha20poly1305::ChaCha20Poly1305; - #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] pub use crate::{decryptor::Decryptor, encryptor::Encryptor}; +#[cfg(feature = "chacha20poly1305")] +pub use crate::chacha20poly1305::{ChaCha20Poly1305, ChaChaKey, ChaChaNonce}; + +use cipher::array::{typenum::U16, Array}; use core::{fmt, str}; use encoding::{Label, LabelError}; #[cfg(feature = "aes-gcm")] -use aes_gcm::{aead::AeadInPlace, Aes128Gcm, Aes256Gcm}; +use { + aead::array::typenum::U12, + aes_gcm::{Aes128Gcm, Aes256Gcm}, +}; -#[cfg(feature = "aes-gcm")] -use cipher::KeyInit; +#[cfg(any(feature = "aes-gcm", feature = "chacha20poly1305"))] +use aead::{AeadInPlace, KeyInit}; /// AES-128 in block chaining (CBC) mode const AES128_CBC: &str = "aes128-cbc"; @@ -80,17 +84,15 @@ const CHACHA20_POLY1305: &str = "chacha20-poly1305@openssh.com"; /// Triple-DES in block chaining (CBC) mode const TDES_CBC: &str = "3des-cbc"; -/// Nonce for AEAD modes. -/// -/// This is used by e.g. `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com` and -/// `chacha20-poly1305@openssh.com`. -pub type Nonce = [u8; 12]; +/// Nonce for `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com`. +#[cfg(feature = "aes-gcm")] +pub type AesGcmNonce = Array; /// Authentication tag for ciphertext data. /// /// This is used by e.g. `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com` and /// `chacha20-poly1305@openssh.com`. -pub type Tag = [u8; 16]; +pub type Tag = Array; /// Counter mode with a 128-bit big endian counter. #[cfg(feature = "aes-ctr")] @@ -172,7 +174,7 @@ impl Cipher { Self::Aes256Ctr => Some((32, 16)), Self::Aes128Gcm => Some((16, 12)), Self::Aes256Gcm => Some((32, 12)), - Self::ChaCha20Poly1305 => Some((32, 12)), + Self::ChaCha20Poly1305 => Some((32, 8)), Self::TDesCbc => Some((24, 8)), } } @@ -233,7 +235,7 @@ impl Cipher { #[cfg(feature = "aes-gcm")] Self::Aes128Gcm => { let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; - let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?; + let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?; let tag = tag.ok_or(Error::TagSize)?; cipher .decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into()) @@ -244,7 +246,7 @@ impl Cipher { #[cfg(feature = "aes-gcm")] Self::Aes256Gcm => { let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; - let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?; + let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?; let tag = tag.ok_or(Error::TagSize)?; cipher .decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into()) @@ -254,8 +256,12 @@ impl Cipher { } #[cfg(feature = "chacha20poly1305")] Self::ChaCha20Poly1305 => { + let key = key.try_into().map_err(|_| Error::KeySize)?; + let nonce = iv.try_into().map_err(|_| Error::IvSize)?; let tag = tag.ok_or(Error::TagSize)?; - ChaCha20Poly1305::new(key, iv)?.decrypt(buffer, tag) + ChaCha20Poly1305::new(key) + .decrypt_in_place_detached(nonce, b"", buffer, &tag) + .map_err(|_| Error::Crypto) } // Use `Decryptor` for non-AEAD modes #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] @@ -294,7 +300,7 @@ impl Cipher { #[cfg(feature = "aes-gcm")] Self::Aes128Gcm => { let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; - let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?; + let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?; let tag = cipher .encrypt_in_place_detached(&nonce.into(), &[], buffer) .map_err(|_| Error::Crypto)?; @@ -304,7 +310,7 @@ impl Cipher { #[cfg(feature = "aes-gcm")] Self::Aes256Gcm => { let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; - let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?; + let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?; let tag = cipher .encrypt_in_place_detached(&nonce.into(), &[], buffer) .map_err(|_| Error::Crypto)?; @@ -313,7 +319,11 @@ impl Cipher { } #[cfg(feature = "chacha20poly1305")] Self::ChaCha20Poly1305 => { - let tag = ChaCha20Poly1305::new(key, iv)?.encrypt(buffer); + let key = key.try_into().map_err(|_| Error::KeySize)?; + let nonce = iv.try_into().map_err(|_| Error::IvSize)?; + let tag = ChaCha20Poly1305::new(key) + .encrypt_in_place_detached(nonce, b"", buffer) + .map_err(|_| Error::Crypto)?; Ok(Some(tag)) } // Use `Encryptor` for non-AEAD modes diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 454b084..9eb47e2 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -100,7 +100,7 @@ impl Kdf { // key encryption, relying on a unique salt used in the password-based encryption key // derivation to ensure that each encryption key is only used once. if cipher == Cipher::ChaCha20Poly1305 { - iv.copy_from_slice(&cipher::Nonce::default()); + iv.copy_from_slice(&cipher::ChaChaNonce::default()); } Ok((okm, iv))