Skip to content

Commit

Permalink
ssh-cipher: add Decryptor and Encryptor (#253)
Browse files Browse the repository at this point in the history
Adds stateful types which can incrementally decrypt/encrypt data in CBC
or CTR modes.

Closes #164
  • Loading branch information
tarcieri authored Jul 28, 2024
1 parent e0ec4ea commit e64c44f
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 144 deletions.
142 changes: 142 additions & 0 deletions ssh-cipher/src/decryptor.rs
Original file line number Diff line number Diff line change
@@ -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<Aes128>),
#[cfg(feature = "aes-cbc")]
Aes192Cbc(cbc::Decryptor<Aes192>),
#[cfg(feature = "aes-cbc")]
Aes256Cbc(cbc::Decryptor<Aes256>),
#[cfg(feature = "aes-ctr")]
Aes128Ctr(Ctr128BE<Aes128>),
#[cfg(feature = "aes-ctr")]
Aes192Ctr(Ctr128BE<Aes192>),
#[cfg(feature = "aes-ctr")]
Aes256Ctr(Ctr128BE<Aes256>),
#[cfg(feature = "tdes")]
TDesCbc(cbc::Decryptor<TdesEde3>),
}

impl Decryptor {
/// Create a new decryptor object with the given [`Cipher`], key, and IV.
pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result<Self> {
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<C>(decryptor: &mut cbc::Decryptor<C>, buffer: &mut [u8]) -> Result<()>
where
C: BlockCipher + BlockCipherDecrypt,
{
let (blocks, remaining) = Block::<C>::slice_as_chunks_mut(buffer);

// Ensure input is block-aligned.
if !remaining.is_empty() {
return Err(Error::Crypto);
}

decryptor.decrypt_blocks(blocks);
Ok(())
}
161 changes: 161 additions & 0 deletions ssh-cipher/src/encryptor.rs
Original file line number Diff line number Diff line change
@@ -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<Aes128>),
#[cfg(feature = "aes-cbc")]
Aes192Cbc(cbc::Encryptor<Aes192>),
#[cfg(feature = "aes-cbc")]
Aes256Cbc(cbc::Encryptor<Aes256>),
#[cfg(feature = "aes-ctr")]
Aes128Ctr(Ctr128BE<Aes128>),
#[cfg(feature = "aes-ctr")]
Aes192Ctr(Ctr128BE<Aes192>),
#[cfg(feature = "aes-ctr")]
Aes256Ctr(Ctr128BE<Aes256>),
#[cfg(feature = "tdes")]
TDesCbc(cbc::Encryptor<TdesEde3>),
}

impl Encryptor {
/// Create a new encryptor object with the given [`Cipher`], key, and IV.
pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result<Self> {
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<C>(encryptor: &mut cbc::Encryptor<C>, buffer: &mut [u8]) -> Result<()>
where
C: BlockCipher + BlockCipherEncrypt,
{
let (blocks, remaining) = Block::<C>::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<C>(encryptor: &mut Ctr128BE<C>, buffer: &mut [u8]) -> Result<()>
where
C: BlockCipher<BlockSize = U16> + BlockCipherEncrypt,
{
let (blocks, remaining) = Block::<C>::slice_as_chunks_mut(buffer);

// Ensure input is block-aligned.
if !remaining.is_empty() {
return Err(Error::Crypto);
}

encryptor.apply_keystream_blocks(blocks);
Ok(())
}
Loading

0 comments on commit e64c44f

Please sign in to comment.