diff --git a/Cargo.lock b/Cargo.lock index 8abbb58..37d2046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,15 +99,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "block-padding" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" -dependencies = [ - "hybrid-array", -] - [[package]] name = "blowfish" version = "0.10.0" @@ -130,15 +121,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "cbc" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" -dependencies = [ - "cipher", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -442,7 +424,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "block-padding", "hybrid-array", ] @@ -768,10 +749,8 @@ dependencies = [ "aead", "aes", "aes-gcm", - "cbc", "chacha20", "cipher", - "ctr", "ctutils", "des", "hex-literal", diff --git a/ssh-cipher/Cargo.toml b/ssh-cipher/Cargo.toml index c6f1993..93d44b2 100644 --- a/ssh-cipher/Cargo.toml +++ b/ssh-cipher/Cargo.toml @@ -25,8 +25,6 @@ encoding = { package = "ssh-encoding", version = "0.3.0-rc.9" } aead = { version = "0.6.0-rc.10", optional = true, default-features = false } aes = { version = "0.9", optional = true, default-features = false } aes-gcm = { version = "0.11.0-rc.3", optional = true, default-features = false, features = ["aes"] } -cbc = { version = "0.2.1", optional = true } -ctr = { version = "0.10", optional = true, default-features = false } ctutils = { version = "0.4", optional = true, default-features = false } chacha20 = { version = "0.10", optional = true, default-features = false, features = ["cipher", "legacy"] } des = { version = "0.9", optional = true, default-features = false } @@ -37,9 +35,9 @@ zeroize = { version = "1", optional = true, default-features = false } hex-literal = "1" [features] -aes = ["dep:aead", "dep:aes", "dep:aes-gcm", "dep:cbc", "dep:ctr"] +aes = ["dep:aead", "dep:aes", "dep:aes-gcm"] chacha20poly1305 = ["dep:aead", "dep:chacha20", "dep:poly1305", "dep:ctutils"] -tdes = ["dep:des", "dep:cbc"] +tdes = ["dep:des"] zeroize = [ "dep:zeroize", "aes?/zeroize", diff --git a/ssh-cipher/src/block_cipher.rs b/ssh-cipher/src/block_cipher.rs index d402e30..02c5692 100644 --- a/ssh-cipher/src/block_cipher.rs +++ b/ssh-cipher/src/block_cipher.rs @@ -7,8 +7,77 @@ mod aes; mod decryptor; mod encryptor; +mod state; +#[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}, +}; + +/// Seal the `BlockCipher` trait so others cannot implement it. +pub(crate) mod sealed { + use crate::Cipher; + use ::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, common::InvalidLength}; + + /// Trait for block ciphers supported by this crate. + /// + /// This trait is deliberately sealed so it cannot be implemented by downstream crates. + /// Notably new ciphers added to SSH should be authenticated, and we shouldn't support a + /// proliferation of unauthenticated ciphers. + pub trait BlockCipher: BlockCipherDecrypt + BlockCipherEncrypt { + /// Initialize cipher from a byte slice. + /// + /// This is defined separate from the [`KeyInit`] trait so it can support variable-sized keys. + /// + /// # Errors + /// Returns [`InvalidLength`] if `slice` is not equal in length to the key size. + fn new_from_slice(slice: &[u8]) -> Result; + + /// Is this the correct block cipher implementation for the given cipher? + fn is_supported(cipher: Cipher) -> bool; + } +} +/// Supported block cipher modes of operation. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum BlockMode { + /// Cipher block chaining. + Cbc, + + /// Counter mode. + Ctr, +} + +/// Encryptor for the Advanced Encryption Standard (AES). +#[cfg(feature = "aes")] +pub type AesEncryptor = Encryptor; +/// Decryptor for the Advanced Encryption Standard (AES). #[cfg(feature = "aes")] -pub(crate) use self::aes::Aes; +pub type AesDecryptor = Decryptor; + +/// Encryptor for 3DES. +#[cfg(feature = "tdes")] +pub type TdesEncryptor = Encryptor; +/// Decryptor for 3DES. +#[cfg(feature = "tdes")] +pub type TdesDecryptor = Decryptor; + +#[cfg(feature = "tdes")] +impl sealed::BlockCipher for Tdes { + fn new_from_slice(slice: &[u8]) -> Result { + KeyInit::new_from_slice(slice) + } + + fn is_supported(cipher: Cipher) -> bool { + cipher == Cipher::TdesCbc + } +} diff --git a/ssh-cipher/src/block_cipher/aes.rs b/ssh-cipher/src/block_cipher/aes.rs index 383541f..98c83b9 100644 --- a/ssh-cipher/src/block_cipher/aes.rs +++ b/ssh-cipher/src/block_cipher/aes.rs @@ -1,15 +1,24 @@ //! AES block cipher. +use super::sealed::BlockCipher; +use crate::Cipher; use ::aes::{Aes128, Aes192, Aes256, Block}; use ::cipher::{ BlockCipherDecClosure, BlockCipherDecrypt, BlockCipherEncClosure, BlockCipherEncrypt, BlockSizeUser, InvalidLength, KeyInit, array::sizes::U16, }; +use core::fmt; +use core::fmt::Debug; /// Advanced Encryption Standard (AES) low-level block cipher. /// -/// Supports 128-bit, 192-bit, and 256-bit key sizes. -pub(crate) enum Aes { +/// Supports 128-bit, 192-bit, and 256-bit keys. +pub struct Aes { + inner: Inner, +} + +/// Inner enum over supported key sizes. +enum Inner { Aes128(Aes128), Aes192(Aes192), Aes256(Aes256), @@ -23,55 +32,79 @@ impl Aes { /// /// # Errors /// Returns [`InvalidLength`] if the length of `key` is not any of the above. - pub(crate) fn new(key: &[u8]) -> Result { + pub fn new_from_slice(key: &[u8]) -> Result { if let Ok(cipher) = Aes128::new_from_slice(key) { - return Ok(Aes::Aes128(cipher)); + return Ok(Self { + inner: Inner::Aes128(cipher), + }); } if let Ok(cipher) = Aes192::new_from_slice(key) { - return Ok(Aes::Aes192(cipher)); + return Ok(Self { + inner: Inner::Aes192(cipher), + }); } if let Ok(cipher) = Aes256::new_from_slice(key) { - return Ok(Aes::Aes256(cipher)); + return Ok(Self { + inner: Inner::Aes256(cipher), + }); } Err(InvalidLength) } } +impl BlockCipher for Aes { + fn new_from_slice(slice: &[u8]) -> Result { + Aes::new_from_slice(slice) + } + + fn is_supported(cipher: Cipher) -> bool { + matches!( + cipher, + Cipher::Aes128Cbc + | Cipher::Aes192Cbc + | Cipher::Aes256Cbc + | Cipher::Aes128Ctr + | Cipher::Aes192Ctr + | Cipher::Aes256Ctr + ) + } +} + impl BlockCipherDecrypt for Aes { fn decrypt_blocks(&self, blocks: &mut [Block]) { - match self { - Aes::Aes128(aes) => aes.decrypt_blocks(blocks), - Aes::Aes192(aes) => aes.decrypt_blocks(blocks), - Aes::Aes256(aes) => aes.decrypt_blocks(blocks), + match &self.inner { + Inner::Aes128(aes) => aes.decrypt_blocks(blocks), + Inner::Aes192(aes) => aes.decrypt_blocks(blocks), + Inner::Aes256(aes) => aes.decrypt_blocks(blocks), } } fn decrypt_with_backend(&self, f: impl BlockCipherDecClosure) { - match self { - Aes::Aes128(aes) => aes.decrypt_with_backend(f), - Aes::Aes192(aes) => aes.decrypt_with_backend(f), - Aes::Aes256(aes) => aes.decrypt_with_backend(f), + match &self.inner { + Inner::Aes128(aes) => aes.decrypt_with_backend(f), + Inner::Aes192(aes) => aes.decrypt_with_backend(f), + Inner::Aes256(aes) => aes.decrypt_with_backend(f), } } } impl BlockCipherEncrypt for Aes { fn encrypt_blocks(&self, blocks: &mut [Block]) { - match self { - Aes::Aes128(aes) => aes.encrypt_blocks(blocks), - Aes::Aes192(aes) => aes.encrypt_blocks(blocks), - Aes::Aes256(aes) => aes.encrypt_blocks(blocks), + match &self.inner { + Inner::Aes128(aes) => aes.encrypt_blocks(blocks), + Inner::Aes192(aes) => aes.encrypt_blocks(blocks), + Inner::Aes256(aes) => aes.encrypt_blocks(blocks), } } fn encrypt_with_backend(&self, f: impl BlockCipherEncClosure) { - match self { - Aes::Aes128(aes) => aes.encrypt_with_backend(f), - Aes::Aes192(aes) => aes.encrypt_with_backend(f), - Aes::Aes256(aes) => aes.encrypt_with_backend(f), + match &self.inner { + Inner::Aes128(aes) => aes.encrypt_with_backend(f), + Inner::Aes192(aes) => aes.encrypt_with_backend(f), + Inner::Aes256(aes) => aes.encrypt_with_backend(f), } } } @@ -79,3 +112,9 @@ impl BlockCipherEncrypt for Aes { impl BlockSizeUser for Aes { type BlockSize = U16; } + +impl Debug for Aes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Aes").finish_non_exhaustive() + } +} diff --git a/ssh-cipher/src/block_cipher/decryptor.rs b/ssh-cipher/src/block_cipher/decryptor.rs index 772aa5d..384e676 100644 --- a/ssh-cipher/src/block_cipher/decryptor.rs +++ b/ssh-cipher/src/block_cipher/decryptor.rs @@ -1,86 +1,41 @@ //! Stateful decryptor object. +use super::{BlockMode, State, sealed::BlockCipher}; use crate::{Cipher, Error, Result}; -use cipher::{ - Block, IvState, SetIvState, - block::{BlockCipherDecrypt, BlockModeDecrypt}, -}; +use ::cipher::{Block, typenum::Unsigned}; use core::fmt::{self, Debug}; -#[cfg(feature = "aes")] -use { - super::Aes, - cipher::{InnerIvInit, StreamCipher, StreamCipherSeek}, - ctr::{Ctr128BE, CtrCore}, -}; -#[cfg(feature = "tdes")] -use {cipher::KeyIvInit, des::TdesEde3}; -/// Stateful decryptor object for unauthenticated SSH symmetric ciphers. +/// Stateful decryptor object for unauthenticated symmetric ciphers used in the SSH packet +/// encryption protocol. /// -/// Note that this deliberately does not support AEAD modes such as AES-GCM and ChaCha20Poly1305, -/// which are one-shot by design. -pub struct Decryptor { - /// Inner enum over possible decryption ciphers. - inner: Inner, - - /// Cipher in use by this `Encryptor`. - cipher: Cipher, -} - -/// Inner decryptor enum which is deliberately kept out of the public API. -enum Inner { - #[cfg(feature = "aes")] - AesCbc(cbc::Decryptor), - #[cfg(feature = "aes")] - AesCtr(Ctr128BE), - #[cfg(feature = "tdes")] - TDesCbc(cbc::Decryptor), +/// Note we need encryption support for decryption in order to support AES-CTR, where encryption +/// and decryption are the same operation. +pub struct Decryptor { + /// Inner block cipher. + cipher: C, + + /// State of the block cipher's mode of operation. + state: State, } -/// Current IV state or position within the cipher. -enum State { - #[cfg(feature = "aes")] - AesCbc(aes::Block), - #[cfg(feature = "aes")] - AesCtr(u64), - #[cfg(feature = "tdes")] - TDesCbc(Block), -} - -impl Decryptor { +impl Decryptor { /// Create a new decryptor object with the given [`Cipher`], `key`, and `iv` (i.e. /// initialization vector). /// /// # Errors + /// - Returns [`Error::Crypto`] if the given `cipher` cannot be used with `Decryptor`. /// - Returns [`Error::Length`] if `key` or `iv` are the wrong length for the given `cipher`. /// - Returns [`Error::UnsupportedCipher`] if support for the given `cipher` is not enabled /// in the crate features. pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { - cipher.check_key_and_iv(key, iv)?; - - let inner = match cipher { - #[cfg(feature = "aes")] - Cipher::Aes128Cbc | Cipher::Aes192Cbc | Cipher::Aes256Cbc => { - cbc::Decryptor::inner_iv_slice_init(Aes::new(key)?, iv).map(Inner::AesCbc) - } - #[cfg(feature = "aes")] - Cipher::Aes128Ctr | Cipher::Aes192Ctr | Cipher::Aes256Ctr => { - let core = CtrCore::inner_iv_slice_init(Aes::new(key)?, iv)?; - Ok(Inner::AesCtr(Ctr128BE::from_core(core))) - } - #[cfg(feature = "tdes")] - Cipher::TDesCbc => cbc::Decryptor::new_from_slices(key, iv).map(Inner::TDesCbc), - _ => return Err(cipher.unsupported()), + if !C::is_supported(cipher) { + return Err(Error::UnsupportedCipher(cipher)); } - .map_err(|_| Error::Length)?; - - Ok(Self { inner, cipher }) - } - /// Get the cipher for this decryptor. - #[must_use] - pub fn cipher(&self) -> Cipher { - self.cipher + let mode = cipher.block_mode().ok_or(Error::Crypto)?; + let cipher = C::new_from_slice(key)?; + let state = State::new_from_slice(mode, iv)?; + Ok(Self { cipher, state }) } /// Decrypt the given buffer in place. @@ -89,15 +44,13 @@ impl Decryptor { /// 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<()> { - match &mut self.inner { - #[cfg(feature = "aes")] - Inner::AesCbc(cipher) => cbc_decrypt(cipher, buffer)?, - #[cfg(feature = "aes")] - Inner::AesCtr(cipher) => cipher - .try_apply_keystream(buffer) - .map_err(|_| Error::Crypto)?, - #[cfg(feature = "tdes")] - Inner::TDesCbc(cipher) => cbc_decrypt(cipher, buffer)?, + #[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(()) @@ -105,75 +58,40 @@ impl Decryptor { /// Call the provided function with an ephemeral [`Decryptor`] state which will be reset upon /// completion, returning the result of the function. - /// - /// # Errors - /// Returns errors propagated from `F`, or if an internal cryptographic error occurs. - pub fn peek(&mut self, mut f: F) -> Result + pub fn peek(&mut self, mut f: F) -> T where - F: FnMut(&mut Self) -> Result, + F: FnMut(&mut Self) -> T, { - let state = self.state(); + let state = self.state.clone(); let ret = f(self); - self.set_state(state)?; + self.state = state; ret } - /// Get the current cipher state, i.e. IV or position within the stream cipher. - fn state(&self) -> State { - match &self.inner { - #[cfg(feature = "aes")] - Inner::AesCbc(cipher) => State::AesCbc(cipher.iv_state()), - #[cfg(feature = "aes")] - Inner::AesCtr(cipher) => State::AesCtr(cipher.current_pos()), - #[cfg(feature = "tdes")] - Inner::TDesCbc(cipher) => State::TDesCbc(cipher.iv_state()), - } - } - - /// Set the current cipher state. - fn set_state(&mut self, state: State) -> Result<()> { - match (&mut self.inner, state) { - #[cfg(feature = "aes")] - (Inner::AesCbc(cipher), State::AesCbc(iv)) => { - cipher.set_iv(&iv); - Ok(()) - } - #[cfg(feature = "aes")] - (Inner::AesCtr(cipher), State::AesCtr(pos)) => { - cipher.try_seek(pos).map_err(|_| Error::Crypto) + /// Decrypt a single block. + /// + /// # Panics + /// If `block` is not the correct block size for this cipher. + fn decrypt_block(&mut self, block: &mut Block) { + match self.state.mode() { + BlockMode::Cbc => { + let pad = self.state.clone(); + self.state.as_mut().copy_from_slice(block); + self.cipher.decrypt_block(block); + pad.xor_into(block); } - #[cfg(feature = "tdes")] - (Inner::TDesCbc(cipher), State::TDesCbc(iv)) => { - cipher.set_iv(&iv); - Ok(()) + BlockMode::Ctr => { + let mut pad = self.state.clone(); + self.cipher.encrypt_block(pad.as_mut()); + pad.xor_into(block); + self.state.increment_counter(); } - #[allow(unreachable_patterns)] - _ => Err(Error::Crypto), // should be unreachable } } } -impl Debug for Decryptor { +impl Debug for Decryptor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Decryptor") - .field("cipher", &self.cipher()) - .finish_non_exhaustive() - } -} - -/// CBC mode decryption helper which assumes the input is unpadded and block-aligned. -#[cfg(any(feature = "aes", feature = "tdes"))] -fn cbc_decrypt(decryptor: &mut cbc::Decryptor, buffer: &mut [u8]) -> Result<()> -where - C: BlockCipherDecrypt, -{ - let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); - - // Ensure input is block-aligned. - if !remaining.is_empty() { - return Err(Error::Length); + f.debug_struct("Decryptor").finish_non_exhaustive() } - - decryptor.decrypt_blocks(blocks); - Ok(()) } diff --git a/ssh-cipher/src/block_cipher/encryptor.rs b/ssh-cipher/src/block_cipher/encryptor.rs index ac51eea..2a45c91 100644 --- a/ssh-cipher/src/block_cipher/encryptor.rs +++ b/ssh-cipher/src/block_cipher/encryptor.rs @@ -1,118 +1,81 @@ //! Stateful encryptor object. +use super::{BlockMode, State, sealed::BlockCipher}; use crate::{Cipher, Error, Result}; -use cipher::{Block, BlockCipherEncrypt, BlockModeEncrypt}; +use ::cipher::{Block, typenum::Unsigned}; use core::fmt::{self, Debug}; -#[cfg(feature = "aes")] -use { - super::Aes, - cipher::{InnerIvInit, StreamCipher}, - ctr::{Ctr128BE, CtrCore}, -}; -#[cfg(feature = "tdes")] -use {cipher::KeyIvInit, des::TdesEde3}; +/// Stateful encryptor object for unauthenticated symmetric ciphers used in the SSH packet +/// encryption protocol. +pub struct Encryptor { + /// Inner block cipher. + cipher: C, -/// Stateful encryptor object for unauthenticated SSH symmetric ciphers. -/// -/// Note that this deliberately does not support AEAD modes such as AES-GCM and ChaCha20Poly1305, -/// which are one-shot by design. -pub struct Encryptor { - /// Inner enum over possible encryption ciphers. - inner: Inner, - - /// Cipher in use by this `Encryptor`. - cipher: Cipher, -} - -/// Inner encryptor enum which is deliberately kept out of the public API. -enum Inner { - #[cfg(feature = "aes")] - AesCbc(cbc::Encryptor), - #[cfg(feature = "aes")] - AesCtr(Ctr128BE), - #[cfg(feature = "tdes")] - TDesCbc(cbc::Encryptor), + /// State of the block cipher's mode of operation. + state: State, } -impl Encryptor { +impl Encryptor { /// Create a new encryptor object with the given [`Cipher`], `key`, and `iv` (i.e. /// initialization vector). /// /// # Errors + /// - Returns [`Error::Crypto`] if the given `cipher` cannot be used with `Encryptor`. /// - Returns [`Error::Length`] if `key` or `iv` are the wrong length for the given `cipher`. /// - Returns [`Error::UnsupportedCipher`] if support for the given `cipher` is not enabled /// in the crate features. pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { - cipher.check_key_and_iv(key, iv)?; - - let inner = match cipher { - #[cfg(feature = "aes")] - Cipher::Aes128Cbc | Cipher::Aes192Cbc | Cipher::Aes256Cbc => { - cbc::Encryptor::inner_iv_slice_init(Aes::new(key)?, iv).map(Inner::AesCbc) - } - #[cfg(feature = "aes")] - Cipher::Aes128Ctr | Cipher::Aes192Ctr | Cipher::Aes256Ctr => { - let core = CtrCore::inner_iv_slice_init(Aes::new(key)?, iv)?; - Ok(Inner::AesCtr(Ctr128BE::from_core(core))) - } - #[cfg(feature = "tdes")] - Cipher::TDesCbc => cbc::Encryptor::new_from_slices(key, iv).map(Inner::TDesCbc), - _ => return Err(cipher.unsupported()), + if !C::is_supported(cipher) { + return Err(Error::UnsupportedCipher(cipher)); } - .map_err(|_| Error::Length)?; - - Ok(Self { inner, cipher }) - } - /// Get the cipher for this encryptor. - #[must_use] - pub fn cipher(&self) -> Cipher { - self.cipher + let mode = cipher.block_mode().ok_or(Error::Crypto)?; + let cipher = C::new_from_slice(key)?; + let state = State::new_from_slice(mode, iv)?; + Ok(Self { cipher, state }) } - /// Encrypt the given buffer in place. + /// 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<()> { - match &mut self.inner { - #[cfg(feature = "aes")] - Inner::AesCbc(cipher) => cbc_encrypt(cipher, buffer)?, - #[cfg(feature = "aes")] - Inner::AesCtr(cipher) => cipher - .try_apply_keystream(buffer) - .map_err(|_| Error::Crypto)?, - #[cfg(feature = "tdes")] - Inner::TDesCbc(cipher) => cbc_encrypt(cipher, buffer)?, + #[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.encrypt_block(block); } Ok(()) } -} -impl Debug for Encryptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Encryptor") - .field("cipher", &self.cipher()) - .finish_non_exhaustive() + /// 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); + } + BlockMode::Ctr => { + let mut pad = self.state.clone(); + self.cipher.encrypt_block(pad.as_mut()); + pad.xor_into(block); + self.state.increment_counter(); + } + } } } -/// CBC mode encryption helper which assumes the input is unpadded and block-aligned. -#[cfg(any(feature = "aes", feature = "tdes"))] -fn cbc_encrypt(encryptor: &mut cbc::Encryptor, buffer: &mut [u8]) -> Result<()> -where - C: BlockCipherEncrypt, -{ - let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); - - // Ensure input is block-aligned. - if !remaining.is_empty() { - return Err(Error::Length); +impl Debug for Encryptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Encryptor").finish_non_exhaustive() } - - encryptor.encrypt_blocks(blocks); - Ok(()) } diff --git a/ssh-cipher/src/block_cipher/state.rs b/ssh-cipher/src/block_cipher/state.rs new file mode 100644 index 0000000..562415f --- /dev/null +++ b/ssh-cipher/src/block_cipher/state.rs @@ -0,0 +1,75 @@ +//! Block cipher state. + +use super::BlockMode; +use crate::Result; +use cipher::common::array::{Array, ArraySize}; + +/// Block cipher state. Intended for reuse between CBC and CTR modes. +#[derive(Clone, Debug)] +pub(crate) struct State { + mode: BlockMode, + bytes: Array, +} + +impl State { + /// Initialize state for a particular block cipher mode of operation. + pub(crate) fn new(mode: BlockMode, iv: &Array) -> Self { + debug_assert!( + mode != BlockMode::Ctr || Size::USIZE == 16, + "we only support CTR for 128-bit block sizes" + ); + + Self { + bytes: iv.clone(), + mode, + } + } + + /// Initialize state for a particular block cipher mode of operation, with the initialization + /// vector provided as a slice. + /// + /// # Errors + /// Returns [`Error::Length`] in the event the IV is the wrong size. + pub(crate) fn new_from_slice(mode: BlockMode, iv: &[u8]) -> Result { + Ok(Self::new(mode, iv.try_into()?)) + } + + /// Get the block mode the state was initialized with. + pub(crate) fn mode(&self) -> BlockMode { + self.mode + } + + /// Increment the counter for counter mode. + pub(crate) fn increment_counter(&mut self) { + debug_assert_eq!(self.mode, BlockMode::Ctr); + debug_assert!(self.bytes.len() <= 16); + let offset = 16 - Size::USIZE; + + // Zero padding is needed to make the generic implementation work, + let mut bytes = [0u8; 16]; + bytes[offset..].copy_from_slice(&self.bytes); + + let n = u128::from_be_bytes(bytes); + let bytes = n.wrapping_add(1).to_be_bytes(); + self.bytes.copy_from_slice(&bytes[offset..]); + } + + /// XOR the current state into the given block. This is used to implement CBC mode. + pub(crate) fn xor_into(&self, block: &mut Array) { + for i in 0..Size::USIZE { + block[i] ^= self.bytes[i]; + } + } +} + +impl AsRef> for State { + fn as_ref(&self) -> &Array { + &self.bytes + } +} + +impl AsMut> for State { + fn as_mut(&mut self) -> &mut Array { + &mut self.bytes + } +} diff --git a/ssh-cipher/src/error.rs b/ssh-cipher/src/error.rs index ecfcb77..420cb17 100644 --- a/ssh-cipher/src/error.rs +++ b/ssh-cipher/src/error.rs @@ -1,7 +1,7 @@ //! Error types. use crate::Cipher; -use core::fmt; +use core::{array::TryFromSliceError, fmt}; /// Result type with `ssh-cipher` crate's [`Error`] as the error type. pub type Result = core::result::Result; @@ -44,6 +44,12 @@ impl fmt::Display for Error { impl core::error::Error for Error {} +impl From for Error { + fn from(_: TryFromSliceError) -> Error { + Error::Length + } +} + impl From for Error { fn from(_: cipher::InvalidLength) -> Error { Error::Length diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 3f34570..86b87ab 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -23,8 +23,14 @@ use cipher::array::{Array, typenum::U16}; use core::{fmt, str}; use encoding::{Label, LabelError}; +#[cfg(feature = "aes")] +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}; +use ::aead::{AeadInOut, KeyInit}; #[cfg(feature = "aes")] use { aead::array::typenum::U12, @@ -134,7 +140,7 @@ pub enum Cipher { ChaCha20Poly1305, /// `3des-cbc`: TripleDES in block chaining (CBC) mode - TDesCbc, + TdesCbc, } impl Cipher { @@ -172,7 +178,7 @@ impl Cipher { Self::Aes128Gcm => AES128_GCM, Self::Aes256Gcm => AES256_GCM, Self::ChaCha20Poly1305 => CHACHA20_POLY1305, - Self::TDesCbc => TDES_CBC, + Self::TdesCbc => TDES_CBC, } } @@ -190,7 +196,7 @@ impl Cipher { Self::Aes128Gcm => Some((16, 12)), Self::Aes256Gcm => Some((32, 12)), Self::ChaCha20Poly1305 => Some((32, 8)), - Self::TDesCbc => Some((24, 8)), + Self::TdesCbc => Some((24, 8)), } } @@ -198,7 +204,7 @@ impl Cipher { #[must_use] pub fn block_size(self) -> usize { match self { - Self::None | Self::ChaCha20Poly1305 | Self::TDesCbc => 8, + Self::None | Self::ChaCha20Poly1305 | Self::TdesCbc => 8, Self::Aes128Cbc | Self::Aes192Cbc | Self::Aes256Cbc @@ -285,18 +291,30 @@ impl Cipher { .decrypt_inout_detached(nonce, &[], buffer.into(), &tag) .map_err(|_| Error::Crypto) } - // Use `Decryptor` for non-AEAD modes - #[cfg(any(feature = "aes", feature = "tdes"))] - _ => { + #[cfg(feature = "aes")] + Self::Aes128Cbc + | Self::Aes192Cbc + | Self::Aes256Cbc + | Self::Aes128Ctr + | Self::Aes192Ctr + | Self::Aes256Ctr => { + // Non-AEAD modes don't take a tag. + if tag.is_some() { + return Err(Error::Crypto); + } + + self.decryptor::(key, iv)?.decrypt(buffer) + } + #[cfg(feature = "tdes")] + Self::TdesCbc => { // Non-AEAD modes don't take a tag. if tag.is_some() { return Err(Error::Crypto); } - self.decryptor(key, iv)?.decrypt(buffer) + self.decryptor::(key, iv)?.decrypt(buffer) } - #[cfg(not(any(feature = "aes", feature = "tdes")))] - _ => Err(self.unsupported()), + _ => Err(Error::UnsupportedCipher(self)), } } @@ -308,7 +326,10 @@ impl Cipher { /// # Errors /// Propagates errors from [`block_cipher::Decryptor::new`]. #[cfg(any(feature = "aes", feature = "tdes"))] - pub fn decryptor(self, key: &[u8], iv: &[u8]) -> Result { + pub fn decryptor(self, key: &[u8], iv: &[u8]) -> Result> + where + C: BlockCipher, + { block_cipher::Decryptor::new(self, key, iv) } @@ -349,14 +370,22 @@ impl Cipher { .map_err(|_| Error::Crypto)?; Ok(Some(tag)) } - // Use `Encryptor` for non-AEAD modes - #[cfg(any(feature = "aes", feature = "tdes"))] - _ => { - self.encryptor(key, iv)?.encrypt(buffer)?; + #[cfg(feature = "aes")] + Self::Aes128Cbc + | Self::Aes192Cbc + | Self::Aes256Cbc + | Self::Aes128Ctr + | Self::Aes192Ctr + | Self::Aes256Ctr => { + self.encryptor::(key, iv)?.encrypt(buffer)?; Ok(None) } - #[cfg(not(any(feature = "aes", feature = "tdes")))] - _ => Err(self.unsupported()), + #[cfg(feature = "tdes")] + Self::TdesCbc => { + self.encryptor::(key, iv)?.encrypt(buffer)?; + Ok(None) + } + _ => Err(Error::UnsupportedCipher(self)), } } @@ -368,31 +397,25 @@ impl Cipher { /// # Errors /// Propagates errors from [`block_cipher::Encryptor::new`]. #[cfg(any(feature = "aes", feature = "tdes"))] - pub fn encryptor(self, key: &[u8], iv: &[u8]) -> Result { + pub fn encryptor(self, key: &[u8], iv: &[u8]) -> Result> + where + C: BlockCipher, + { block_cipher::Encryptor::new(self, key, iv) } - /// Check that the key and IV are the expected length for this cipher. + /// Get the block cipher mode of operation for this `Cipher`, if applicable. #[cfg(any(feature = "aes", feature = "tdes"))] - fn check_key_and_iv(self, key: &[u8], iv: &[u8]) -> Result<()> { - let (key_size, iv_size) = self - .key_and_iv_size() - .ok_or(Error::UnsupportedCipher(self))?; - - if key.len() != key_size { - return Err(Error::KeySize); - } - - if iv.len() != iv_size { - return Err(Error::IvSize); + pub(crate) fn block_mode(self) -> Option { + match self { + #[cfg(feature = "aes")] + Self::Aes128Cbc | Self::Aes192Cbc | Self::Aes256Cbc => Some(BlockMode::Cbc), + #[cfg(feature = "aes")] + Self::Aes128Ctr | Self::Aes192Ctr | Self::Aes256Ctr => Some(BlockMode::Ctr), + #[cfg(feature = "tdes")] + Self::TdesCbc => Some(BlockMode::Cbc), + _ => None, } - - Ok(()) - } - - /// Create an unsupported cipher error. - fn unsupported(self) -> Error { - Error::UnsupportedCipher(self) } } @@ -425,7 +448,7 @@ impl str::FromStr for Cipher { AES128_GCM => Ok(Self::Aes128Gcm), AES256_GCM => Ok(Self::Aes256Gcm), CHACHA20_POLY1305 => Ok(Self::ChaCha20Poly1305), - TDES_CBC => Ok(Self::TDesCbc), + TDES_CBC => Ok(Self::TdesCbc), _ => Err(LabelError::new(ciphername)), } } diff --git a/ssh-cipher/tests/lib.rs b/ssh-cipher/tests/lib.rs index b8a14a4..bfa16b4 100644 --- a/ssh-cipher/tests/lib.rs +++ b/ssh-cipher/tests/lib.rs @@ -26,7 +26,7 @@ fn round_trip() { #[cfg(feature = "chacha20poly1305")] Cipher::ChaCha20Poly1305, #[cfg(feature = "tdes")] - Cipher::TDesCbc, + Cipher::TdesCbc, ]; for &cipher in CIPHERS { diff --git a/ssh-key/tests/encrypted_private_key.rs b/ssh-key/tests/encrypted_private_key.rs index a01806d..3e61508 100644 --- a/ssh-key/tests/encrypted_private_key.rs +++ b/ssh-key/tests/encrypted_private_key.rs @@ -139,7 +139,7 @@ fn decode_openssh_chacha20_poly1305() { fn decode_openssh_3des_cbc() { let key = PrivateKey::from_openssh(OPENSSH_3DES_CBC_ED25519_EXAMPLE).unwrap(); assert_eq!(Algorithm::Ed25519, key.algorithm()); - assert_eq!(Cipher::TDesCbc, key.cipher()); + assert_eq!(Cipher::TdesCbc, key.cipher()); assert_eq!(KdfAlg::Bcrypt, key.kdf().algorithm()); match key.kdf() { @@ -268,7 +268,7 @@ fn decrypt_openssh_chacha20_poly1305() { #[test] fn decrypt_openssh_3des() { let key_enc = PrivateKey::from_openssh(OPENSSH_3DES_CBC_ED25519_EXAMPLE).unwrap(); - assert_eq!(Cipher::TDesCbc, key_enc.cipher()); + assert_eq!(Cipher::TdesCbc, key_enc.cipher()); let key_dec = key_enc.decrypt(PASSWORD).unwrap(); assert_eq!( PrivateKey::from_openssh(OPENSSH_ED25519_EXAMPLE).unwrap(), @@ -483,7 +483,7 @@ fn encrypt_openssh_3des() { let key_dec = PrivateKey::from_openssh(OPENSSH_ED25519_EXAMPLE).unwrap(); let key_enc = key_dec - .encrypt_with_cipher(&mut OsRng, Cipher::TDesCbc, PASSWORD) + .encrypt_with_cipher(&mut OsRng, Cipher::TdesCbc, PASSWORD) .unwrap(); // Ensure encrypted key round trips through encoder/decoder