diff --git a/ssh-cipher/src/decryptor.rs b/ssh-cipher/src/decryptor.rs new file mode 100644 index 0000000..3880832 --- /dev/null +++ b/ssh-cipher/src/decryptor.rs @@ -0,0 +1,142 @@ +//! Stateful decryptor object. + +use crate::{Cipher, Error, Result}; +use cipher::KeyIvInit; + +#[cfg(feature = "aes-ctr")] +use crate::{encryptor::ctr_encrypt as ctr_decrypt, Ctr128BE}; + +#[cfg(feature = "tdes")] +use des::TdesEde3; + +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] +use aes::{Aes128, Aes192, Aes256}; + +#[cfg(any(feature = "aes-cbc", feature = "tdes"))] +use cipher::{Block, BlockCipher, BlockCipherDecrypt, BlockModeDecrypt}; + +/// Stateful decryptor 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 Decryptor { + /// Inner enum over possible decryption ciphers. + inner: Inner, +} + +/// Inner decryptor enum which is deliberately kept out of the public API. +enum Inner { + #[cfg(feature = "aes-cbc")] + Aes128Cbc(cbc::Decryptor), + #[cfg(feature = "aes-cbc")] + Aes192Cbc(cbc::Decryptor), + #[cfg(feature = "aes-cbc")] + Aes256Cbc(cbc::Decryptor), + #[cfg(feature = "aes-ctr")] + Aes128Ctr(Ctr128BE), + #[cfg(feature = "aes-ctr")] + Aes192Ctr(Ctr128BE), + #[cfg(feature = "aes-ctr")] + Aes256Ctr(Ctr128BE), + #[cfg(feature = "tdes")] + TDesCbc(cbc::Decryptor), +} + +impl Decryptor { + /// Create a new decryptor object with the given [`Cipher`], key, and IV. + pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { + let inner = match cipher { + #[cfg(feature = "aes-cbc")] + Cipher::Aes128Cbc => Inner::Aes128Cbc( + cbc::Decryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-cbc")] + Cipher::Aes192Cbc => Inner::Aes192Cbc( + cbc::Decryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-cbc")] + Cipher::Aes256Cbc => Inner::Aes256Cbc( + cbc::Decryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-ctr")] + Cipher::Aes128Ctr => { + Inner::Aes128Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "aes-ctr")] + Cipher::Aes192Ctr => { + Inner::Aes192Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "aes-ctr")] + Cipher::Aes256Ctr => { + Inner::Aes256Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "tdes")] + Cipher::TDesCbc => Inner::TDesCbc( + cbc::Decryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + _ => return Err(cipher.unsupported()), + }; + + Ok(Self { inner }) + } + + /// Get the cipher for this decryptor. + pub fn cipher(&self) -> Cipher { + match &self.inner { + #[cfg(feature = "aes-cbc")] + Inner::Aes128Cbc(_) => Cipher::Aes128Cbc, + #[cfg(feature = "aes-cbc")] + Inner::Aes192Cbc(_) => Cipher::Aes192Cbc, + #[cfg(feature = "aes-cbc")] + Inner::Aes256Cbc(_) => Cipher::Aes256Cbc, + #[cfg(feature = "aes-ctr")] + Inner::Aes128Ctr(_) => Cipher::Aes128Ctr, + #[cfg(feature = "aes-ctr")] + Inner::Aes192Ctr(_) => Cipher::Aes192Ctr, + #[cfg(feature = "aes-ctr")] + Inner::Aes256Ctr(_) => Cipher::Aes256Ctr, + #[cfg(feature = "tdes")] + Inner::TDesCbc(_) => Cipher::TDesCbc, + } + } + + /// Decrypt the given buffer in place, returning [`Error::Crypto`] on padding failure. + pub fn decrypt(&mut self, buffer: &mut [u8]) -> Result<()> { + #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] + match &mut self.inner { + #[cfg(feature = "aes-cbc")] + Inner::Aes128Cbc(cipher) => cbc_decrypt(cipher, buffer)?, + #[cfg(feature = "aes-cbc")] + Inner::Aes192Cbc(cipher) => cbc_decrypt(cipher, buffer)?, + #[cfg(feature = "aes-cbc")] + Inner::Aes256Cbc(cipher) => cbc_decrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes128Ctr(cipher) => ctr_decrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes192Ctr(cipher) => ctr_decrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes256Ctr(cipher) => ctr_decrypt(cipher, buffer)?, + #[cfg(feature = "tdes")] + Inner::TDesCbc(cipher) => cbc_decrypt(cipher, buffer)?, + } + + Ok(()) + } +} + +/// CBC mode decryption helper which assumes the input is unpadded and block-aligned. +#[cfg(any(feature = "aes-cbc", feature = "tdes"))] +fn cbc_decrypt(decryptor: &mut cbc::Decryptor, buffer: &mut [u8]) -> Result<()> +where + C: BlockCipher + BlockCipherDecrypt, +{ + let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); + + // Ensure input is block-aligned. + if !remaining.is_empty() { + return Err(Error::Crypto); + } + + decryptor.decrypt_blocks(blocks); + Ok(()) +} diff --git a/ssh-cipher/src/encryptor.rs b/ssh-cipher/src/encryptor.rs new file mode 100644 index 0000000..d71f6cb --- /dev/null +++ b/ssh-cipher/src/encryptor.rs @@ -0,0 +1,161 @@ +//! Stateful encryptor object. + +use crate::{Cipher, Error, Result}; +use cipher::{Block, BlockCipher, BlockCipherEncrypt, KeyIvInit}; + +#[cfg(feature = "aes-ctr")] +use { + crate::Ctr128BE, + cipher::{array::sizes::U16, StreamCipherCore}, +}; + +#[cfg(feature = "tdes")] +use des::TdesEde3; + +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] +use aes::{Aes128, Aes192, Aes256}; + +#[cfg(any(feature = "aes-cbc", feature = "tdes"))] +use cipher::BlockModeEncrypt; + +/// 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, +} + +/// Inner encryptor enum which is deliberately kept out of the public API. +enum Inner { + #[cfg(feature = "aes-cbc")] + Aes128Cbc(cbc::Encryptor), + #[cfg(feature = "aes-cbc")] + Aes192Cbc(cbc::Encryptor), + #[cfg(feature = "aes-cbc")] + Aes256Cbc(cbc::Encryptor), + #[cfg(feature = "aes-ctr")] + Aes128Ctr(Ctr128BE), + #[cfg(feature = "aes-ctr")] + Aes192Ctr(Ctr128BE), + #[cfg(feature = "aes-ctr")] + Aes256Ctr(Ctr128BE), + #[cfg(feature = "tdes")] + TDesCbc(cbc::Encryptor), +} + +impl Encryptor { + /// Create a new encryptor object with the given [`Cipher`], key, and IV. + pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { + let inner = match cipher { + #[cfg(feature = "aes-cbc")] + Cipher::Aes128Cbc => Inner::Aes128Cbc( + cbc::Encryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-cbc")] + Cipher::Aes192Cbc => Inner::Aes192Cbc( + cbc::Encryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-cbc")] + Cipher::Aes256Cbc => Inner::Aes256Cbc( + cbc::Encryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + #[cfg(feature = "aes-ctr")] + Cipher::Aes128Ctr => { + Inner::Aes128Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "aes-ctr")] + Cipher::Aes192Ctr => { + Inner::Aes192Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "aes-ctr")] + Cipher::Aes256Ctr => { + Inner::Aes256Ctr(Ctr128BE::new_from_slices(key, iv).map_err(|_| Error::KeySize)?) + } + #[cfg(feature = "tdes")] + Cipher::TDesCbc => Inner::TDesCbc( + cbc::Encryptor::new_from_slices(key, iv).map_err(|_| Error::KeySize)?, + ), + _ => return Err(cipher.unsupported()), + }; + + Ok(Self { inner }) + } + + /// Get the cipher for this encryptor. + pub fn cipher(&self) -> Cipher { + match &self.inner { + #[cfg(feature = "aes-cbc")] + Inner::Aes128Cbc(_) => Cipher::Aes128Cbc, + #[cfg(feature = "aes-cbc")] + Inner::Aes192Cbc(_) => Cipher::Aes192Cbc, + #[cfg(feature = "aes-cbc")] + Inner::Aes256Cbc(_) => Cipher::Aes256Cbc, + #[cfg(feature = "aes-ctr")] + Inner::Aes128Ctr(_) => Cipher::Aes128Ctr, + #[cfg(feature = "aes-ctr")] + Inner::Aes192Ctr(_) => Cipher::Aes192Ctr, + #[cfg(feature = "aes-ctr")] + Inner::Aes256Ctr(_) => Cipher::Aes256Ctr, + #[cfg(feature = "tdes")] + Inner::TDesCbc(_) => Cipher::TDesCbc, + } + } + + /// Encrypt the given buffer in place, returning [`Error::Crypto`] on padding failure. + pub fn encrypt(&mut self, buffer: &mut [u8]) -> Result<()> { + match &mut self.inner { + #[cfg(feature = "aes-cbc")] + Inner::Aes128Cbc(cipher) => cbc_encrypt(cipher, buffer)?, + #[cfg(feature = "aes-cbc")] + Inner::Aes192Cbc(cipher) => cbc_encrypt(cipher, buffer)?, + #[cfg(feature = "aes-cbc")] + Inner::Aes256Cbc(cipher) => cbc_encrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes128Ctr(cipher) => ctr_encrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes192Ctr(cipher) => ctr_encrypt(cipher, buffer)?, + #[cfg(feature = "aes-ctr")] + Inner::Aes256Ctr(cipher) => ctr_encrypt(cipher, buffer)?, + #[cfg(feature = "tdes")] + Inner::TDesCbc(cipher) => cbc_encrypt(cipher, buffer)?, + } + + Ok(()) + } +} + +/// CBC mode encryption helper which assumes the input is unpadded and block-aligned. +#[cfg(any(feature = "aes-cbc", feature = "tdes"))] +fn cbc_encrypt(encryptor: &mut cbc::Encryptor, buffer: &mut [u8]) -> Result<()> +where + C: BlockCipher + BlockCipherEncrypt, +{ + let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); + + // Ensure input is block-aligned. + if !remaining.is_empty() { + return Err(Error::Crypto); + } + + encryptor.encrypt_blocks(blocks); + Ok(()) +} + +/// CTR mode encryption helper which assumes the input is unpadded and block-aligned. +#[cfg(feature = "aes-ctr")] +pub(crate) fn ctr_encrypt(encryptor: &mut Ctr128BE, buffer: &mut [u8]) -> Result<()> +where + C: BlockCipher + BlockCipherEncrypt, +{ + let (blocks, remaining) = Block::::slice_as_chunks_mut(buffer); + + // Ensure input is block-aligned. + if !remaining.is_empty() { + return Err(Error::Crypto); + } + + encryptor.apply_keystream_blocks(blocks); + Ok(()) +} diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index f07c543..57d2df5 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -28,42 +28,28 @@ mod error; #[cfg(feature = "chacha20poly1305")] mod chacha20poly1305; +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] +mod decryptor; +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] +mod encryptor; pub use crate::error::{Error, Result}; +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] +pub use crate::{decryptor::Decryptor, encryptor::Encryptor}; + use core::{fmt, str}; use encoding::{Label, LabelError}; -#[cfg(feature = "aes-ctr")] -use cipher::StreamCipherCore; - #[cfg(feature = "aes-gcm")] use aes_gcm::{aead::AeadInPlace, Aes128Gcm, Aes256Gcm}; #[cfg(feature = "chacha20poly1305")] use crate::chacha20poly1305::ChaCha20Poly1305; -#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] -use aes::{Aes128, Aes192, Aes256}; - -#[cfg(any(feature = "aes-cbc", feature = "tdes"))] -use { - cbc::{Decryptor, Encryptor}, - cipher::{ - block_padding::NoPadding, BlockCipher, BlockCipherDecrypt, BlockCipherEncrypt, - BlockModeDecrypt, BlockModeEncrypt, - }, -}; - -#[cfg(any(feature = "aes-cbc", feature = "aes-gcm", feature = "tdes"))] +#[cfg(feature = "aes-gcm")] use cipher::KeyInit; -#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] -use cipher::KeyIvInit; - -#[cfg(feature = "tdes")] -use des::TdesEde3; - /// AES-128 in block chaining (CBC) mode const AES128_CBC: &str = "aes128-cbc"; @@ -235,39 +221,12 @@ impl Cipher { } /// Decrypt the ciphertext in the `buffer` in-place using this cipher. + #[cfg_attr( + not(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes")), + allow(unused_variables) + )] pub fn decrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8], tag: Option) -> Result<()> { match self { - #[cfg(feature = "aes-cbc")] - Self::Aes128Cbc => { - if tag.is_some() { - return Err(Error::TagSize); - } - cbc_decrypt::(key, iv, buffer) - } - #[cfg(feature = "aes-cbc")] - Self::Aes192Cbc => { - if tag.is_some() { - return Err(Error::TagSize); - } - cbc_decrypt::(key, iv, buffer) - } - #[cfg(feature = "aes-cbc")] - Self::Aes256Cbc => { - if tag.is_some() { - return Err(Error::TagSize); - } - cbc_decrypt::(key, iv, buffer) - } - #[cfg(feature = "aes-ctr")] - Self::Aes128Ctr | Self::Aes192Ctr | Self::Aes256Ctr => { - if tag.is_some() { - return Err(Error::TagSize); - } - - // Counter mode encryption and decryption are the same operation - self.encrypt(key, iv, buffer)?; - Ok(()) - } #[cfg(feature = "aes-gcm")] Self::Aes128Gcm => { let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; @@ -295,54 +254,37 @@ impl Cipher { let tag = tag.ok_or(Error::TagSize)?; ChaCha20Poly1305::new(key, iv)?.decrypt(buffer, tag) } - #[cfg(feature = "tdes")] - Self::TDesCbc => { + // Use `Decryptor` for non-AEAD modes + #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] + _ => { + // Non-AEAD modes don't take a tag. if tag.is_some() { - return Err(Error::TagSize); + return Err(Error::Crypto); } - cbc_decrypt::(key, iv, buffer) - } - _ => { - // Suppress unused variable warnings. - let (_, _, _, _) = (key, iv, buffer, tag); - Err(self.unsupported()) + + self.decryptor(key, iv)?.decrypt(buffer) } + #[cfg(not(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes")))] + _ => return Err(self.unsupported()), } } + /// Get a stateful [`Decryptor`] for the given key and IV. + /// + /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with + /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305). + #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] + pub fn decryptor(self, key: &[u8], iv: &[u8]) -> Result { + Decryptor::new(self, key, iv) + } + /// Encrypt the ciphertext in the `buffer` in-place using this cipher. + #[cfg_attr( + not(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes")), + allow(unused_variables) + )] pub fn encrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result> { match self { - #[cfg(feature = "aes-cbc")] - Self::Aes128Cbc => { - cbc_encrypt::(key, iv, buffer)?; - Ok(None) - } - #[cfg(feature = "aes-cbc")] - Self::Aes192Cbc => { - cbc_encrypt::(key, iv, buffer)?; - Ok(None) - } - #[cfg(feature = "aes-cbc")] - Self::Aes256Cbc => { - cbc_encrypt::(key, iv, buffer)?; - Ok(None) - } - #[cfg(feature = "aes-ctr")] - Self::Aes128Ctr => { - ctr_encrypt::>(key, iv, buffer)?; - Ok(None) - } - #[cfg(feature = "aes-ctr")] - Self::Aes192Ctr => { - ctr_encrypt::>(key, iv, buffer)?; - Ok(None) - } - #[cfg(feature = "aes-ctr")] - Self::Aes256Ctr => { - ctr_encrypt::>(key, iv, buffer)?; - Ok(None) - } #[cfg(feature = "aes-gcm")] Self::Aes128Gcm => { let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?; @@ -368,19 +310,26 @@ impl Cipher { let tag = ChaCha20Poly1305::new(key, iv)?.encrypt(buffer); Ok(Some(tag)) } - #[cfg(feature = "tdes")] - Self::TDesCbc => { - cbc_encrypt::(key, iv, buffer)?; - Ok(None) - } + // Use `Encryptor` for non-AEAD modes + #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] _ => { - // Suppress unused variable warnings. - let (_, _, _) = (key, iv, buffer); - Err(self.unsupported()) + self.encryptor(key, iv)?.encrypt(buffer)?; + Ok(None) } + #[cfg(not(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes")))] + _ => return Err(self.unsupported()), } } + /// Get a stateful [`Encryptor`] for the given key and IV. + /// + /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with + /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305). + #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] + pub fn encryptor(self, key: &[u8], iv: &[u8]) -> Result { + Encryptor::new(self, key, iv) + } + /// Create an unsupported cipher error. fn unsupported(self) -> Error { Error::UnsupportedCipher(self) @@ -421,47 +370,3 @@ impl str::FromStr for Cipher { } } } - -#[cfg(any(feature = "aes-cbc", feature = "tdes"))] -fn cbc_encrypt(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()> -where - C: BlockCipherEncrypt + BlockCipher + KeyInit, -{ - let cipher = Encryptor::::new_from_slices(key, iv).map_err(|_| Error::KeySize)?; - - // Since the passed in buffer is already padded, using NoPadding here - cipher - .encrypt_padded::(buffer, buffer.len()) - .map_err(|_| Error::Crypto)?; - - Ok(()) -} - -#[cfg(any(feature = "aes-cbc", feature = "tdes"))] -fn cbc_decrypt(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()> -where - C: BlockCipherDecrypt + BlockCipher + KeyInit, -{ - let cipher = Decryptor::::new_from_slices(key, iv).map_err(|_| Error::KeySize)?; - - // Since the passed in buffer is already padded, using NoPadding here - cipher - .decrypt_padded::(buffer) - .map_err(|_| Error::Crypto)?; - - Ok(()) -} - -#[cfg(feature = "aes-ctr")] -fn ctr_encrypt(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()> -where - C: StreamCipherCore + KeyIvInit, -{ - let cipher = C::new_from_slices(key, iv).map_err(|_| Error::KeySize)?; - - cipher - .try_apply_keystream_partial(buffer.into()) - .map_err(|_| Error::Crypto)?; - - Ok(()) -}