From 257246704f074a17445567d20175723da8c2ead7 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 24 May 2026 17:30:42 -0600 Subject: [PATCH] ssh-cipher: impl `BlockMode*` for `Decryptor`/`Encryptor` Impls the `BlockMode*` traits from the `cipher` crate, bringing the implementation closer in line with our other block mode implementations. This does have one weird quirk: we can't impl `BlockModeDecrypt::decrypt_with_backend` for `Decryptor` because that only gives us access to `BlockCipherDecBackend`, whereas to implement CTR mode we need access to `BlockCipherEncBackend` too as encryption and decryption are the same operation in CTR mode. So this foregoes such an impl with an `unimplemented!` and directly implements the `decrypt_block` and `decrypt_blocks` methods instead. This doesn't currently support any form of padding, although with the `BlockMode*` traits implemented a downstream user can provide whatever padding they wish with `encrypt_padded`/`decrypt_padded`. --- ssh-cipher/src/block_cipher.rs | 11 +- ssh-cipher/src/block_cipher/decryptor.rs | 49 ++++----- ssh-cipher/src/block_cipher/encryptor.rs | 130 ++++++++++++++++++----- ssh-cipher/src/lib.rs | 63 +++++++++-- 4 files changed, 188 insertions(+), 65 deletions(-) diff --git a/ssh-cipher/src/block_cipher.rs b/ssh-cipher/src/block_cipher.rs index 02c5692..aef584c 100644 --- a/ssh-cipher/src/block_cipher.rs +++ b/ssh-cipher/src/block_cipher.rs @@ -9,19 +9,20 @@ mod decryptor; mod encryptor; mod state; +pub use self::{decryptor::Decryptor, encryptor::Encryptor}; +pub use ::cipher::{ + Block, BlockModeDecrypt, BlockModeEncrypt, common::BlockSizeUser, common::InvalidLength, +}; + #[cfg(feature = "aes")] pub use self::aes::Aes; -pub use self::{decryptor::Decryptor, encryptor::Encryptor}; #[cfg(feature = "tdes")] pub use ::des::TdesEde3 as Tdes; use self::state::State; #[cfg(feature = "tdes")] -use { - crate::Cipher, - ::cipher::common::{InvalidLength, KeyInit}, -}; +use {crate::Cipher, ::cipher::common::KeyInit}; /// Seal the `BlockCipher` trait so others cannot implement it. pub(crate) mod sealed { diff --git a/ssh-cipher/src/block_cipher/decryptor.rs b/ssh-cipher/src/block_cipher/decryptor.rs index 384e676..c9cfee6 100644 --- a/ssh-cipher/src/block_cipher/decryptor.rs +++ b/ssh-cipher/src/block_cipher/decryptor.rs @@ -2,7 +2,10 @@ use super::{BlockMode, State, sealed::BlockCipher}; use crate::{Cipher, Error, Result}; -use ::cipher::{Block, typenum::Unsigned}; +use ::cipher::{ + Block, BlockModeDecClosure, BlockModeDecrypt, + common::{BlockSizeUser, InnerUser}, +}; use core::fmt::{self, Debug}; /// Stateful decryptor object for unauthenticated symmetric ciphers used in the SSH packet @@ -38,24 +41,6 @@ impl Decryptor { Ok(Self { cipher, state }) } - /// Decrypt the given buffer in place. - /// - /// # Errors - /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's - /// block size. - pub fn decrypt(&mut self, buffer: &mut [u8]) -> Result<()> { - #[allow(clippy::integer_division_remainder_used, reason = "non-secret length")] - if buffer.len() % C::BlockSize::USIZE != 0 { - return Err(Error::Length); - } - - for block in Block::::slice_as_chunks_mut(buffer).0 { - self.decrypt_block(block); - } - - Ok(()) - } - /// Call the provided function with an ephemeral [`Decryptor`] state which will be reset upon /// completion, returning the result of the function. pub fn peek(&mut self, mut f: F) -> T @@ -67,12 +52,14 @@ impl Decryptor { self.state = state; ret } +} + +impl BlockModeDecrypt for Decryptor { + fn decrypt_with_backend(&mut self, _f: impl BlockModeDecClosure) { + unimplemented!("CTR mode support is incompatible with BlockModeDecrypt") + } - /// Decrypt a single block. - /// - /// # Panics - /// If `block` is not the correct block size for this cipher. - fn decrypt_block(&mut self, block: &mut Block) { + fn decrypt_block(&mut self, block: &mut Block) { match self.state.mode() { BlockMode::Cbc => { let pad = self.state.clone(); @@ -88,6 +75,16 @@ impl Decryptor { } } } + + fn decrypt_blocks(&mut self, blocks: &mut [Block]) { + // TODO(tarcieri): parallel decryption support + for block in blocks { + self.decrypt_block(block); + } + } +} +impl BlockSizeUser for Decryptor { + type BlockSize = C::BlockSize; } impl Debug for Decryptor { @@ -95,3 +92,7 @@ impl Debug for Decryptor { f.debug_struct("Decryptor").finish_non_exhaustive() } } + +impl InnerUser for Decryptor { + type Inner = C; +} diff --git a/ssh-cipher/src/block_cipher/encryptor.rs b/ssh-cipher/src/block_cipher/encryptor.rs index 2a45c91..74ed7ca 100644 --- a/ssh-cipher/src/block_cipher/encryptor.rs +++ b/ssh-cipher/src/block_cipher/encryptor.rs @@ -2,7 +2,15 @@ use super::{BlockMode, State, sealed::BlockCipher}; use crate::{Cipher, Error, Result}; -use ::cipher::{Block, typenum::Unsigned}; +use ::cipher::{ + Block, BlockCipherEncBackend, BlockCipherEncClosure, BlockModeEncBackend, BlockModeEncClosure, + BlockModeEncrypt, + common::{ + BlockSizeUser, InnerUser, ParBlocksSizeUser, + array::{ArraySize, sizes::U1}, + }, + inout::InOut, +}; use core::fmt::{self, Debug}; /// Stateful encryptor object for unauthenticated symmetric ciphers used in the SSH packet @@ -34,48 +42,114 @@ impl Encryptor { let state = State::new_from_slice(mode, iv)?; Ok(Self { cipher, state }) } +} - /// Encrypt the given buffer in-place. - /// - /// # Errors - /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's - /// block size. - pub fn encrypt(&mut self, buffer: &mut [u8]) -> Result<()> { - #[allow(clippy::integer_division_remainder_used, reason = "non-secret length")] - if buffer.len() % C::BlockSize::USIZE != 0 { - return Err(Error::Length); +impl BlockModeEncrypt for Encryptor { + fn encrypt_with_backend(&mut self, f: impl BlockModeEncClosure) { + struct Closure<'a, BS, BC> + where + BS: ArraySize, + BC: BlockModeEncClosure, + { + state: &'a mut State, + f: BC, + } + + impl BlockSizeUser for Closure<'_, BS, BC> + where + BS: ArraySize, + BC: BlockModeEncClosure, + { + type BlockSize = BS; } - for block in Block::::slice_as_chunks_mut(buffer).0 { - self.encrypt_block(block); + impl BlockCipherEncClosure for Closure<'_, BS, BC> + where + BS: ArraySize, + BC: BlockModeEncClosure, + { + #[inline(always)] + fn call>( + self, + cipher_backend: &B, + ) { + let Self { state, f } = self; + f.call(&mut Backend { + state, + cipher_backend, + }); + } } - Ok(()) + let Self { cipher, state } = self; + cipher.encrypt_with_backend(Closure { state, f }); } +} + +impl BlockSizeUser for Encryptor { + type BlockSize = C::BlockSize; +} + +impl Debug for Encryptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Encryptor").finish_non_exhaustive() + } +} + +impl InnerUser for Encryptor { + type Inner = C; +} + +struct Backend<'a, BS, BK> +where + BS: ArraySize, + BK: BlockCipherEncBackend, +{ + state: &'a mut State, + cipher_backend: &'a BK, +} + +impl BlockSizeUser for Backend<'_, BS, BK> +where + BS: ArraySize, + BK: BlockCipherEncBackend, +{ + type BlockSize = BS; +} + +impl ParBlocksSizeUser for Backend<'_, BS, BK> +where + BS: ArraySize, + BK: BlockCipherEncBackend, +{ + // CBC encryption cannot be performed in parallel + // TODO(tarcieri): parallel encryption support for CTR mode, serial for CBC + type ParBlocksSize = U1; +} + +impl BlockModeEncBackend for Backend<'_, BS, BK> +where + BS: ArraySize, + BK: BlockCipherEncBackend, +{ + #[inline(always)] + fn encrypt_block(&mut self, mut block: InOut<'_, '_, Block>) { + let mut t = block.clone_in(); - /// Encrypt a single block. - /// - /// # Panics - /// If `block` is not the correct block size for this cipher. - fn encrypt_block(&mut self, block: &mut Block) { match self.state.mode() { BlockMode::Cbc => { - self.state.xor_into(block); - self.cipher.encrypt_block(block); - self.state.as_mut().copy_from_slice(block); + self.state.xor_into(&mut t); + self.cipher_backend.encrypt_block(InOut::from(&mut t)); + self.state.as_mut().copy_from_slice(&t); } BlockMode::Ctr => { let mut pad = self.state.clone(); - self.cipher.encrypt_block(pad.as_mut()); - pad.xor_into(block); + self.cipher_backend.encrypt_block(pad.as_mut().into()); + pad.xor_into(&mut t); self.state.increment_counter(); } } - } -} -impl Debug for Encryptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Encryptor").finish_non_exhaustive() + *block.get_out() = t; } } diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 86b87ab..08bd283 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -27,10 +27,13 @@ use encoding::{Label, LabelError}; use self::block_cipher::Aes; #[cfg(feature = "tdes")] use self::block_cipher::Tdes; -#[cfg(any(feature = "aes", feature = "tdes"))] -use self::block_cipher::{BlockMode, sealed::BlockCipher}; #[cfg(any(feature = "aes", feature = "chacha20poly1305"))] use ::aead::{AeadInOut, KeyInit}; +#[cfg(any(feature = "aes", feature = "tdes"))] +use { + self::block_cipher::{BlockMode, sealed::BlockCipher}, + ::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt}, +}; #[cfg(feature = "aes")] use { aead::array::typenum::U12, @@ -302,8 +305,7 @@ impl Cipher { if tag.is_some() { return Err(Error::Crypto); } - - self.decryptor::(key, iv)?.decrypt(buffer) + self.decrypt_with_block_cipher::(key, iv, buffer) } #[cfg(feature = "tdes")] Self::TdesCbc => { @@ -311,13 +313,35 @@ impl Cipher { if tag.is_some() { return Err(Error::Crypto); } - - self.decryptor::(key, iv)?.decrypt(buffer) + self.decrypt_with_block_cipher::(key, iv, buffer) } _ => Err(Error::UnsupportedCipher(self)), } } + /// Perform decryption using a dynamically selected block cipher mode of operation. + /// + /// Note that this does not support any form of padding currently. + /// + /// # Errors + /// Returns [`Error::Length`] unless the length of `buffer` is a multiple of the block size. + #[cfg(any(feature = "aes", feature = "tdes"))] + fn decrypt_with_block_cipher( + self, + key: &[u8], + iv: &[u8], + buffer: &mut [u8], + ) -> Result<()> { + let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); + + if !remaining.is_empty() { + return Err(Error::Length); + } + + self.decryptor::(key, iv)?.decrypt_blocks(blocks); + Ok(()) + } + /// Get a stateful [`block_cipher::Decryptor`] for the given key and IV. /// /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with @@ -377,18 +401,41 @@ impl Cipher { | Self::Aes128Ctr | Self::Aes192Ctr | Self::Aes256Ctr => { - self.encryptor::(key, iv)?.encrypt(buffer)?; + self.encrypt_with_block_cipher::(key, iv, buffer)?; Ok(None) } #[cfg(feature = "tdes")] Self::TdesCbc => { - self.encryptor::(key, iv)?.encrypt(buffer)?; + self.encrypt_with_block_cipher::(key, iv, buffer)?; Ok(None) } _ => Err(Error::UnsupportedCipher(self)), } } + /// Perform decryption using a dynamically selected block cipher mode of operation. + /// + /// Note that this does not support any form of padding currently. + /// + /// # Errors + /// Returns [`Error::Length`] unless the length of `buffer` is a multiple of the block size. + #[cfg(any(feature = "aes", feature = "tdes"))] + fn encrypt_with_block_cipher( + self, + key: &[u8], + iv: &[u8], + buffer: &mut [u8], + ) -> Result<()> { + let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); + + if !remaining.is_empty() { + return Err(Error::Length); + } + + self.encryptor::(key, iv)?.encrypt_blocks(blocks); + Ok(()) + } + /// Get a stateful [`block_cipher::Encryptor`] for the given key and IV. /// /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with