Skip to content

Commit 800969b

Browse files
BarreEugeny
andauthored
Implement -cbc ciphers. (#297)
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>
1 parent 97294d8 commit 800969b

File tree

8 files changed

+153
-29
lines changed

8 files changed

+153
-29
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Russh
2+
23
[![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 -->
34
[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-)
45
<!-- ALL-CONTRIBUTORS-BADGE:END -->
@@ -22,6 +23,9 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
2223
* `aes256-ctr`
2324
* `aes192-ctr`
2425
* `aes128-ctr`
26+
* `aes256-cbc`
27+
* `aes192-cbc`
28+
* `aes128-cbc`
2529
* Key exchanges:
2630
* `curve25519-sha256@libssh.org`
2731
* `diffie-hellman-group1-sha1`

russh/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ vendored-openssl = ["openssl/vendored", "russh-keys/vendored-openssl"]
2020
[dependencies]
2121
aes = { workspace = true }
2222
aes-gcm = "0.10"
23+
cbc = { version = "0.1" }
2324
async-trait = { workspace = true }
2425
bitflags = "2.0"
2526
byteorder = { workspace = true }

russh/src/cipher/block.rs

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@
1111
// limitations under the License.
1212
//
1313

14-
use std::marker::PhantomData;
15-
1614
use aes::cipher::{IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher};
1715
use generic_array::GenericArray;
1816
use rand::RngCore;
17+
use std::convert::TryInto;
18+
use std::marker::PhantomData;
1919

2020
use super::super::Error;
2121
use super::PACKET_LENGTH_LEN;
2222
use crate::mac::{Mac, MacAlgorithm};
2323

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

26-
impl<C: StreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> super::Cipher
26+
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> super::Cipher
2727
for SshBlockCipher<C>
2828
{
2929
fn key_len(&self) -> usize {
@@ -73,29 +73,44 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static> su
7373
}
7474
}
7575

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

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

86-
impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKey<C> {
86+
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKey<C> {
87+
fn packet_length_to_read_for_block_length(&self) -> usize {
88+
16
89+
}
90+
8791
fn decrypt_packet_length(
8892
&self,
8993
_sequence_number: u32,
90-
mut encrypted_packet_length: [u8; 4],
94+
encrypted_packet_length: &[u8],
9195
) -> [u8; 4] {
96+
let mut first_block = [0u8; 16];
97+
// Fine because of self.packet_length_to_read_for_block_length()
98+
#[allow(clippy::indexing_slicing)]
99+
first_block.copy_from_slice(&encrypted_packet_length[..16]);
100+
92101
if self.mac.is_etm() {
93-
encrypted_packet_length
102+
// Fine because of self.packet_length_to_read_for_block_length()
103+
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
104+
encrypted_packet_length[..4].try_into().unwrap()
94105
} else {
95106
// Work around uncloneable Aes<>
96107
let mut cipher: C = unsafe { std::ptr::read(&self.cipher as *const C) };
97-
cipher.apply_keystream(&mut encrypted_packet_length);
98-
encrypted_packet_length
108+
109+
cipher.decrypt_data(&mut first_block);
110+
111+
// Fine because of self.packet_length_to_read_for_block_length()
112+
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
113+
first_block[..4].try_into().unwrap()
99114
}
100115
}
101116

@@ -118,9 +133,9 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKe
118133
}
119134
#[allow(clippy::indexing_slicing)]
120135
self.cipher
121-
.apply_keystream(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]);
136+
.decrypt_data(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]);
122137
} else {
123-
self.cipher.apply_keystream(ciphertext_in_plaintext_out);
138+
self.cipher.decrypt_data(ciphertext_in_plaintext_out);
124139

125140
if !self
126141
.mac
@@ -135,7 +150,7 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for OpeningKe
135150
}
136151
}
137152

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

@@ -176,13 +191,28 @@ impl<C: StreamCipher + KeySizeUser + IvSizeUser> super::SealingKey for SealingKe
176191
if self.mac.is_etm() {
177192
#[allow(clippy::indexing_slicing)]
178193
self.cipher
179-
.apply_keystream(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]);
194+
.encrypt_data(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]);
180195
self.mac
181196
.compute(sequence_number, plaintext_in_ciphertext_out, tag_out);
182197
} else {
183198
self.mac
184199
.compute(sequence_number, plaintext_in_ciphertext_out, tag_out);
185-
self.cipher.apply_keystream(plaintext_in_ciphertext_out);
200+
self.cipher.encrypt_data(plaintext_in_ciphertext_out);
186201
}
187202
}
188203
}
204+
205+
pub trait BlockStreamCipher {
206+
fn encrypt_data(&mut self, data: &mut [u8]);
207+
fn decrypt_data(&mut self, data: &mut [u8]);
208+
}
209+
210+
impl<T: StreamCipher> BlockStreamCipher for T {
211+
fn encrypt_data(&mut self, data: &mut [u8]) {
212+
self.apply_keystream(data);
213+
}
214+
215+
fn decrypt_data(&mut self, data: &mut [u8]) {
216+
self.apply_keystream(data);
217+
}
218+
}

russh/src/cipher/cbc.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use aes::cipher::{
2+
BlockCipher, BlockDecrypt, BlockDecryptMut, BlockEncrypt, BlockEncryptMut, InnerIvInit, Iv,
3+
IvSizeUser,
4+
};
5+
use cbc::{Decryptor, Encryptor};
6+
use digest::crypto_common::InnerUser;
7+
use generic_array::GenericArray;
8+
9+
use super::block::BlockStreamCipher;
10+
11+
pub struct CbcWrapper<C: BlockEncrypt + BlockCipher + BlockDecrypt> {
12+
encryptor: Encryptor<C>,
13+
decryptor: Decryptor<C>,
14+
}
15+
16+
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> InnerUser for CbcWrapper<C> {
17+
type Inner = C;
18+
}
19+
20+
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> IvSizeUser for CbcWrapper<C> {
21+
type IvSize = C::BlockSize;
22+
}
23+
24+
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> BlockStreamCipher for CbcWrapper<C> {
25+
fn encrypt_data(&mut self, data: &mut [u8]) {
26+
for chunk in data.chunks_exact_mut(C::block_size()) {
27+
let mut block: GenericArray<u8, _> = GenericArray::clone_from_slice(chunk);
28+
self.encryptor.encrypt_block_mut(&mut block);
29+
chunk.clone_from_slice(&block);
30+
}
31+
}
32+
33+
fn decrypt_data(&mut self, data: &mut [u8]) {
34+
for chunk in data.chunks_exact_mut(C::block_size()) {
35+
let mut block = GenericArray::clone_from_slice(chunk);
36+
self.decryptor.decrypt_block_mut(&mut block);
37+
chunk.clone_from_slice(&block);
38+
}
39+
}
40+
}
41+
42+
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> InnerIvInit for CbcWrapper<C>
43+
where
44+
C: BlockEncryptMut + BlockCipher,
45+
{
46+
#[inline]
47+
fn inner_iv_init(cipher: C, iv: &Iv<Self>) -> Self {
48+
Self {
49+
encryptor: Encryptor::inner_iv_init(cipher.clone(), iv),
50+
decryptor: Decryptor::inner_iv_init(cipher, iv),
51+
}
52+
}
53+
}

russh/src/cipher/chacha20poly1305.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use chacha20::{ChaCha20Legacy, ChaCha20LegacyCore};
2222
use generic_array::typenum::{Unsigned, U16, U32, U8};
2323
use generic_array::GenericArray;
2424
use poly1305::Poly1305;
25+
use std::convert::TryInto;
2526
use subtle::ConstantTimeEq;
2627

2728
use super::super::Error;
@@ -94,11 +95,16 @@ impl super::OpeningKey for OpeningKey {
9495
fn decrypt_packet_length(
9596
&self,
9697
sequence_number: u32,
97-
mut encrypted_packet_length: [u8; 4],
98+
encrypted_packet_length: &[u8],
9899
) -> [u8; 4] {
100+
// Fine because of self.packet_length_to_read_for_block_length()
101+
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
102+
let mut encrypted_packet_length: [u8; 4] = encrypted_packet_length.try_into().unwrap();
103+
99104
let nonce = make_counter(sequence_number);
100105
let mut cipher = ChaCha20Legacy::new(&self.k1, &nonce);
101106
cipher.apply_keystream(&mut encrypted_packet_length);
107+
102108
encrypted_packet_length
103109
}
104110

russh/src/cipher/clear.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414
//
1515

16+
use std::convert::TryInto;
17+
1618
use crate::mac::MacAlgorithm;
1719
use crate::Error;
1820

@@ -48,8 +50,10 @@ impl super::Cipher for Clear {
4850
}
4951

5052
impl super::OpeningKey for Key {
51-
fn decrypt_packet_length(&self, _seqn: u32, packet_length: [u8; 4]) -> [u8; 4] {
52-
packet_length
53+
fn decrypt_packet_length(&self, _seqn: u32, packet_length: &[u8]) -> [u8; 4] {
54+
// Fine because of self.packet_length_to_read_for_block_length()
55+
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
56+
packet_length.try_into().unwrap()
5357
}
5458

5559
fn tag_len(&self) -> usize {

russh/src/cipher/gcm.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

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

18+
use std::convert::TryInto;
19+
1820
use aes_gcm::{AeadCore, AeadInPlace, Aes256Gcm, KeyInit, KeySizeUser};
1921
use digest::typenum::Unsigned;
2022
use generic_array::GenericArray;
@@ -97,9 +99,11 @@ impl super::OpeningKey for OpeningKey {
9799
fn decrypt_packet_length(
98100
&self,
99101
_sequence_number: u32,
100-
encrypted_packet_length: [u8; 4],
102+
encrypted_packet_length: &[u8],
101103
) -> [u8; 4] {
102-
encrypted_packet_length
104+
// Fine because of self.packet_length_to_read_for_block_length()
105+
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
106+
encrypted_packet_length.try_into().unwrap()
103107
}
104108

105109
fn tag_len(&self) -> usize {

russh/src/cipher/mod.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::num::Wrapping;
2121

2222
use aes::{Aes128, Aes192, Aes256};
2323
use byteorder::{BigEndian, ByteOrder};
24+
use cbc::CbcWrapper;
2425
use ctr::Ctr128BE;
2526
use log::debug;
2627
use once_cell::sync::Lazy;
@@ -31,9 +32,11 @@ use crate::sshbuffer::SSHBuffer;
3132
use crate::Error;
3233

3334
pub(crate) mod block;
35+
pub(crate) mod cbc;
3436
pub(crate) mod chacha20poly1305;
3537
pub(crate) mod clear;
3638
pub(crate) mod gcm;
39+
3740
use block::SshBlockCipher;
3841
use chacha20poly1305::SshChacha20Poly1305Cipher;
3942
use clear::Clear;
@@ -69,6 +72,12 @@ pub const CLEAR: Name = Name("clear");
6972
pub const AES_128_CTR: Name = Name("aes128-ctr");
7073
/// `aes192-ctr`
7174
pub const AES_192_CTR: Name = Name("aes192-ctr");
75+
/// `aes128-cbc`
76+
pub const AES_128_CBC: Name = Name("aes128-cbc");
77+
/// `aes192-cbc`
78+
pub const AES_192_CBC: Name = Name("aes192-cbc");
79+
/// `aes256-cbc`
80+
pub const AES_256_CBC: Name = Name("aes256-cbc");
7281
/// `aes256-ctr`
7382
pub const AES_256_CTR: Name = Name("aes256-ctr");
7483
/// `aes256-gcm@openssh.com`
@@ -83,6 +92,9 @@ static _AES_128_CTR: SshBlockCipher<Ctr128BE<Aes128>> = SshBlockCipher(PhantomDa
8392
static _AES_192_CTR: SshBlockCipher<Ctr128BE<Aes192>> = SshBlockCipher(PhantomData);
8493
static _AES_256_CTR: SshBlockCipher<Ctr128BE<Aes256>> = SshBlockCipher(PhantomData);
8594
static _AES_256_GCM: GcmCipher = GcmCipher {};
95+
static _AES_128_CBC: SshBlockCipher<CbcWrapper<Aes128>> = SshBlockCipher(PhantomData);
96+
static _AES_192_CBC: SshBlockCipher<CbcWrapper<Aes192>> = SshBlockCipher(PhantomData);
97+
static _AES_256_CBC: SshBlockCipher<CbcWrapper<Aes256>> = SshBlockCipher(PhantomData);
8698
static _CHACHA20_POLY1305: SshChacha20Poly1305Cipher = SshChacha20Poly1305Cipher {};
8799

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

120135
pub(crate) trait OpeningKey {
121-
fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: [u8; 4]) -> [u8; 4];
136+
fn packet_length_to_read_for_block_length(&self) -> usize {
137+
4
138+
}
139+
140+
fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4];
122141

123142
fn tag_len(&self) -> usize;
124143

@@ -182,15 +201,16 @@ pub(crate) async fn read<'a, R: AsyncRead + Unpin>(
182201
cipher: &'a mut (dyn OpeningKey + Send),
183202
) -> Result<usize, Error> {
184203
if buffer.len == 0 {
185-
let mut len = [0; 4];
204+
let mut len = vec![0; cipher.packet_length_to_read_for_block_length()];
205+
186206
stream.read_exact(&mut len).await?;
187207
debug!("reading, len = {:?}", len);
188208
{
189209
let seqn = buffer.seqn.0;
190210
buffer.buffer.clear();
191211
buffer.buffer.extend(&len);
192212
debug!("reading, seqn = {:?}", seqn);
193-
let len = cipher.decrypt_packet_length(seqn, len);
213+
let len = cipher.decrypt_packet_length(seqn, &len);
194214
buffer.len = BigEndian::read_u32(&len) as usize + cipher.tag_len();
195215
debug!("reading, clear len = {:?}", buffer.len);
196216
}
@@ -199,7 +219,9 @@ pub(crate) async fn read<'a, R: AsyncRead + Unpin>(
199219
buffer.buffer.resize(buffer.len + 4);
200220
debug!("read_exact {:?}", buffer.len + 4);
201221
#[allow(clippy::indexing_slicing)] // length checked
202-
stream.read_exact(&mut buffer.buffer[4..]).await?;
222+
stream
223+
.read_exact(&mut buffer.buffer[cipher.packet_length_to_read_for_block_length()..])
224+
.await?;
203225
debug!("read_exact done");
204226
let seqn = buffer.seqn.0;
205227
let ciphertext_len = buffer.buffer.len() - cipher.tag_len();

0 commit comments

Comments
 (0)