Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssh-cipher: add Decryptor and Encryptor #253

Merged
merged 1 commit into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading