Skip to content

Commit

Permalink
Implement -cbc ciphers. (#297)
Browse files Browse the repository at this point in the history
This PR addresses issues related to connecting to legacy Cisco devices
with no upgrade path (similar to issue #277).

Changes Introduced

• Refactored cipher/mod.rs: Make room to be able to implement CBC crypto
support.
• Updated cipher/block.rs: To provide an interface compatible with both
streaming ciphers and CBC.
• General Cipher Updates: Light modifications to other ciphers for
compatibility with the new interface.

Context

I had trouble connecting to older Cisco devices which posed challenges
due to their outdated cryptographic support.

---------

Co-authored-by: Eugene <x@null.page>
  • Loading branch information
Barre and Eugeny authored Jun 8, 2024
1 parent 97294d8 commit 800969b
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 29 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Russh

[![Rust](https://github.com/warp-tech/russh/actions/workflows/rust.yml/badge.svg)](https://github.com/warp-tech/russh/actions/workflows/rust.yml) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Expand All @@ -22,6 +23,9 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
* `aes256-ctr`
* `aes192-ctr`
* `aes128-ctr`
* `aes256-cbc`
* `aes192-cbc`
* `aes128-cbc`
* Key exchanges:
* `curve25519-sha256@libssh.org`
* `diffie-hellman-group1-sha1`
Expand Down
1 change: 1 addition & 0 deletions russh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ vendored-openssl = ["openssl/vendored", "russh-keys/vendored-openssl"]
[dependencies]
aes = { workspace = true }
aes-gcm = "0.10"
cbc = { version = "0.1" }
async-trait = { workspace = true }
bitflags = "2.0"
byteorder = { workspace = true }
Expand Down
70 changes: 50 additions & 20 deletions russh/src/cipher/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@
// limitations under the License.
//

use std::marker::PhantomData;

use aes::cipher::{IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher};
use generic_array::GenericArray;
use rand::RngCore;
use std::convert::TryInto;
use std::marker::PhantomData;

use super::super::Error;
use super::PACKET_LENGTH_LEN;
use crate::mac::{Mac, MacAlgorithm};

pub struct SshBlockCipher<C: StreamCipher + KeySizeUser + IvSizeUser>(pub PhantomData<C>);
pub struct SshBlockCipher<C: BlockStreamCipher + KeySizeUser + IvSizeUser>(pub PhantomData<C>);

impl<C: StreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> super::Cipher
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> super::Cipher
for SshBlockCipher<C>
{
fn key_len(&self) -> usize {
Expand Down Expand Up @@ -73,29 +73,44 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> su
}
}

pub struct OpeningKey<C: StreamCipher + KeySizeUser + IvSizeUser> {
cipher: C,
mac: Box<dyn Mac + Send>,
pub struct OpeningKey<C: BlockStreamCipher> {
pub(crate) cipher: C,
pub(crate) mac: Box<dyn Mac + Send>,
}

pub struct SealingKey<C: StreamCipher + KeySizeUser + IvSizeUser> {
cipher: C,
mac: Box<dyn Mac + Send>,
pub struct SealingKey<C: BlockStreamCipher> {
pub(crate) cipher: C,
pub(crate) mac: Box<dyn Mac + Send>,
}

impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKey<C> {
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKey<C> {
fn packet_length_to_read_for_block_length(&self) -> usize {
16
}

fn decrypt_packet_length(
&self,
_sequence_number: u32,
mut encrypted_packet_length: [u8; 4],
encrypted_packet_length: &[u8],
) -> [u8; 4] {
let mut first_block = [0u8; 16];
// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::indexing_slicing)]
first_block.copy_from_slice(&encrypted_packet_length[..16]);

if self.mac.is_etm() {
encrypted_packet_length
// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
encrypted_packet_length[..4].try_into().unwrap()
} else {
// Work around uncloneable Aes<>
let mut cipher: C = unsafe { std::ptr::read(&self.cipher as *const C) };
cipher.apply_keystream(&mut encrypted_packet_length);
encrypted_packet_length

cipher.decrypt_data(&mut first_block);

// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
first_block[..4].try_into().unwrap()
}
}

Expand All @@ -118,9 +133,9 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKe
}
#[allow(clippy::indexing_slicing)]
self.cipher
.apply_keystream(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]);
.decrypt_data(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]);
} else {
self.cipher.apply_keystream(ciphertext_in_plaintext_out);
self.cipher.decrypt_data(ciphertext_in_plaintext_out);

if !self
.mac
Expand All @@ -135,7 +150,7 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKe
}
}

impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::SealingKey for SealingKey<C> {
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser> super::SealingKey for SealingKey<C> {
fn padding_length(&self, payload: &[u8]) -> usize {
let block_size = 16;

Expand Down Expand Up @@ -176,13 +191,28 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::SealingKey for SealingKe
if self.mac.is_etm() {
#[allow(clippy::indexing_slicing)]
self.cipher
.apply_keystream(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]);
.encrypt_data(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]);
self.mac
.compute(sequence_number, plaintext_in_ciphertext_out, tag_out);
} else {
self.mac
.compute(sequence_number, plaintext_in_ciphertext_out, tag_out);
self.cipher.apply_keystream(plaintext_in_ciphertext_out);
self.cipher.encrypt_data(plaintext_in_ciphertext_out);
}
}
}

pub trait BlockStreamCipher {
fn encrypt_data(&mut self, data: &mut [u8]);
fn decrypt_data(&mut self, data: &mut [u8]);
}

impl<T: StreamCipher> BlockStreamCipher for T {
fn encrypt_data(&mut self, data: &mut [u8]) {
self.apply_keystream(data);
}

fn decrypt_data(&mut self, data: &mut [u8]) {
self.apply_keystream(data);
}
}
53 changes: 53 additions & 0 deletions russh/src/cipher/cbc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use aes::cipher::{
BlockCipher, BlockDecrypt, BlockDecryptMut, BlockEncrypt, BlockEncryptMut, InnerIvInit, Iv,
IvSizeUser,
};
use cbc::{Decryptor, Encryptor};
use digest::crypto_common::InnerUser;
use generic_array::GenericArray;

use super::block::BlockStreamCipher;

pub struct CbcWrapper<C: BlockEncrypt + BlockCipher + BlockDecrypt> {
encryptor: Encryptor<C>,
decryptor: Decryptor<C>,
}

impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> InnerUser for CbcWrapper<C> {
type Inner = C;
}

impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> IvSizeUser for CbcWrapper<C> {
type IvSize = C::BlockSize;
}

impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> BlockStreamCipher for CbcWrapper<C> {
fn encrypt_data(&mut self, data: &mut [u8]) {
for chunk in data.chunks_exact_mut(C::block_size()) {
let mut block: GenericArray<u8, _> = GenericArray::clone_from_slice(chunk);
self.encryptor.encrypt_block_mut(&mut block);
chunk.clone_from_slice(&block);
}
}

fn decrypt_data(&mut self, data: &mut [u8]) {
for chunk in data.chunks_exact_mut(C::block_size()) {
let mut block = GenericArray::clone_from_slice(chunk);
self.decryptor.decrypt_block_mut(&mut block);
chunk.clone_from_slice(&block);
}
}
}

impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> InnerIvInit for CbcWrapper<C>
where
C: BlockEncryptMut + BlockCipher,
{
#[inline]
fn inner_iv_init(cipher: C, iv: &Iv<Self>) -> Self {
Self {
encryptor: Encryptor::inner_iv_init(cipher.clone(), iv),
decryptor: Decryptor::inner_iv_init(cipher, iv),
}
}
}
8 changes: 7 additions & 1 deletion russh/src/cipher/chacha20poly1305.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use chacha20::{ChaCha20Legacy, ChaCha20LegacyCore};
use generic_array::typenum::{Unsigned, U16, U32, U8};
use generic_array::GenericArray;
use poly1305::Poly1305;
use std::convert::TryInto;
use subtle::ConstantTimeEq;

use super::super::Error;
Expand Down Expand Up @@ -94,11 +95,16 @@ impl super::OpeningKey for OpeningKey {
fn decrypt_packet_length(
&self,
sequence_number: u32,
mut encrypted_packet_length: [u8; 4],
encrypted_packet_length: &[u8],
) -> [u8; 4] {
// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
let mut encrypted_packet_length: [u8; 4] = encrypted_packet_length.try_into().unwrap();

let nonce = make_counter(sequence_number);
let mut cipher = ChaCha20Legacy::new(&self.k1, &nonce);
cipher.apply_keystream(&mut encrypted_packet_length);

encrypted_packet_length
}

Expand Down
8 changes: 6 additions & 2 deletions russh/src/cipher/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.
//

use std::convert::TryInto;

use crate::mac::MacAlgorithm;
use crate::Error;

Expand Down Expand Up @@ -48,8 +50,10 @@ impl super::Cipher for Clear {
}

impl super::OpeningKey for Key {
fn decrypt_packet_length(&self, _seqn: u32, packet_length: [u8; 4]) -> [u8; 4] {
packet_length
fn decrypt_packet_length(&self, _seqn: u32, packet_length: &[u8]) -> [u8; 4] {
// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
packet_length.try_into().unwrap()
}

fn tag_len(&self) -> usize {
Expand Down
8 changes: 6 additions & 2 deletions russh/src/cipher/gcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD

use std::convert::TryInto;

use aes_gcm::{AeadCore, AeadInPlace, Aes256Gcm, KeyInit, KeySizeUser};
use digest::typenum::Unsigned;
use generic_array::GenericArray;
Expand Down Expand Up @@ -97,9 +99,11 @@ impl super::OpeningKey for OpeningKey {
fn decrypt_packet_length(
&self,
_sequence_number: u32,
encrypted_packet_length: [u8; 4],
encrypted_packet_length: &[u8],
) -> [u8; 4] {
encrypted_packet_length
// Fine because of self.packet_length_to_read_for_block_length()
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
encrypted_packet_length.try_into().unwrap()
}

fn tag_len(&self) -> usize {
Expand Down
30 changes: 26 additions & 4 deletions russh/src/cipher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::num::Wrapping;

use aes::{Aes128, Aes192, Aes256};
use byteorder::{BigEndian, ByteOrder};
use cbc::CbcWrapper;
use ctr::Ctr128BE;
use log::debug;
use once_cell::sync::Lazy;
Expand All @@ -31,9 +32,11 @@ use crate::sshbuffer::SSHBuffer;
use crate::Error;

pub(crate) mod block;
pub(crate) mod cbc;
pub(crate) mod chacha20poly1305;
pub(crate) mod clear;
pub(crate) mod gcm;

use block::SshBlockCipher;
use chacha20poly1305::SshChacha20Poly1305Cipher;
use clear::Clear;
Expand Down Expand Up @@ -69,6 +72,12 @@ pub const CLEAR: Name = Name("clear");
pub const AES_128_CTR: Name = Name("aes128-ctr");
/// `aes192-ctr`
pub const AES_192_CTR: Name = Name("aes192-ctr");
/// `aes128-cbc`
pub const AES_128_CBC: Name = Name("aes128-cbc");
/// `aes192-cbc`
pub const AES_192_CBC: Name = Name("aes192-cbc");
/// `aes256-cbc`
pub const AES_256_CBC: Name = Name("aes256-cbc");
/// `aes256-ctr`
pub const AES_256_CTR: Name = Name("aes256-ctr");
/// `aes256-gcm@openssh.com`
Expand All @@ -83,6 +92,9 @@ static _AES_128_CTR: SshBlockCipher<Ctr128BE<Aes128>> = SshBlockCipher(PhantomDa
static _AES_192_CTR: SshBlockCipher<Ctr128BE<Aes192>> = SshBlockCipher(PhantomData);
static _AES_256_CTR: SshBlockCipher<Ctr128BE<Aes256>> = SshBlockCipher(PhantomData);
static _AES_256_GCM: GcmCipher = GcmCipher {};
static _AES_128_CBC: SshBlockCipher<CbcWrapper<Aes128>> = SshBlockCipher(PhantomData);
static _AES_192_CBC: SshBlockCipher<CbcWrapper<Aes192>> = SshBlockCipher(PhantomData);
static _AES_256_CBC: SshBlockCipher<CbcWrapper<Aes256>> = SshBlockCipher(PhantomData);
static _CHACHA20_POLY1305: SshChacha20Poly1305Cipher = SshChacha20Poly1305Cipher {};

pub(crate) static CIPHERS: Lazy<HashMap<&'static Name, &(dyn Cipher + Send + Sync)>> =
Expand All @@ -94,6 +106,9 @@ pub(crate) static CIPHERS: Lazy<HashMap<&'static Name, &(dyn Cipher + Send + Syn
h.insert(&AES_192_CTR, &_AES_192_CTR);
h.insert(&AES_256_CTR, &_AES_256_CTR);
h.insert(&AES_256_GCM, &_AES_256_GCM);
h.insert(&AES_128_CBC, &_AES_128_CBC);
h.insert(&AES_192_CBC, &_AES_192_CBC);
h.insert(&AES_256_CBC, &_AES_256_CBC);
h.insert(&CHACHA20_POLY1305, &_CHACHA20_POLY1305);
h
});
Expand All @@ -118,7 +133,11 @@ impl Debug for CipherPair {
}

pub(crate) trait OpeningKey {
fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: [u8; 4]) -> [u8; 4];
fn packet_length_to_read_for_block_length(&self) -> usize {
4
}

fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4];

fn tag_len(&self) -> usize;

Expand Down Expand Up @@ -182,15 +201,16 @@ pub(crate) async fn read<'a, R: AsyncRead + Unpin>(
cipher: &'a mut (dyn OpeningKey + Send),
) -> Result<usize, Error> {
if buffer.len == 0 {
let mut len = [0; 4];
let mut len = vec![0; cipher.packet_length_to_read_for_block_length()];

stream.read_exact(&mut len).await?;
debug!("reading, len = {:?}", len);
{
let seqn = buffer.seqn.0;
buffer.buffer.clear();
buffer.buffer.extend(&len);
debug!("reading, seqn = {:?}", seqn);
let len = cipher.decrypt_packet_length(seqn, len);
let len = cipher.decrypt_packet_length(seqn, &len);
buffer.len = BigEndian::read_u32(&len) as usize + cipher.tag_len();
debug!("reading, clear len = {:?}", buffer.len);
}
Expand All @@ -199,7 +219,9 @@ pub(crate) async fn read<'a, R: AsyncRead + Unpin>(
buffer.buffer.resize(buffer.len + 4);
debug!("read_exact {:?}", buffer.len + 4);
#[allow(clippy::indexing_slicing)] // length checked
stream.read_exact(&mut buffer.buffer[4..]).await?;
stream
.read_exact(&mut buffer.buffer[cipher.packet_length_to_read_for_block_length()..])
.await?;
debug!("read_exact done");
let seqn = buffer.seqn.0;
let ciphertext_len = buffer.buffer.len() - cipher.tag_len();
Expand Down

0 comments on commit 800969b

Please sign in to comment.