Skip to content

Commit e3d63e3

Browse files
committed
Refactor block-cipher packet-length probing
1 parent dc8f5b4 commit e3d63e3

3 files changed

Lines changed: 134 additions & 11 deletions

File tree

russh/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ bytes.workspace = true
4444
cbc = { version = "0.1" }
4545
cbc_0_2 = { package = "cbc", version = "0.2.0" }
4646
cipher = "0.5.1" # only pinned due to a cargo-minimal-versions failure in 0.5.0
47-
ctr = "0.9"
47+
ctr = "0.9.2"
4848
ctr_0_10 = { package = "ctr", version = "0.10.0" }
4949
curve25519-dalek = "=5.0.0-pre.6"
5050
crypto-bigint = { version = "=0.7.0-rc.28", features = ["alloc"] }

russh/src/cipher/block.rs

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ fn new_cipher_from_slices<C: KeyIvInit>(k: &[u8], n: &[u8]) -> C {
3434
)
3535
}
3636

37-
pub struct SshBlockCipher<C: BlockStreamCipher + KeySizeUser + IvSizeUser>(pub PhantomData<C>);
37+
pub struct SshBlockCipher<C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser>(
38+
pub PhantomData<C>,
39+
);
3840

39-
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Clone + Send + 'static>
41+
impl<C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static>
4042
super::Cipher for SshBlockCipher<C>
4143
{
4244
fn key_len(&self) -> usize {
@@ -78,7 +80,7 @@ impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + KeyIvInit + Clone + Send
7880
}
7981
}
8082

81-
pub struct OpeningKey<C: BlockStreamCipher> {
83+
pub struct OpeningKey<C: BlockStreamCipher + PacketLengthProbe> {
8284
pub(crate) cipher: C,
8385
pub(crate) mac: Box<dyn Mac + Send>,
8486
}
@@ -88,7 +90,9 @@ pub struct SealingKey<C: BlockStreamCipher> {
8890
pub(crate) mac: Box<dyn Mac + Send>,
8991
}
9092

91-
impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + Clone> super::OpeningKey for OpeningKey<C> {
93+
impl<C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser> super::OpeningKey
94+
for OpeningKey<C>
95+
{
9296
fn packet_length_to_read_for_block_length(&self) -> usize {
9397
16
9498
}
@@ -108,9 +112,7 @@ impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser + Clone> super::OpeningKey
108112
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
109113
encrypted_packet_length[..4].try_into().unwrap()
110114
} else {
111-
let mut cipher = self.cipher.clone();
112-
113-
cipher.decrypt_data(&mut first_block);
115+
self.cipher.decrypt_packet_length_block(&mut first_block);
114116

115117
// Fine because of self.packet_length_to_read_for_block_length()
116118
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
@@ -212,6 +214,10 @@ pub trait BlockStreamCipher {
212214
fn decrypt_data(&mut self, data: &mut [u8]);
213215
}
214216

217+
pub(crate) trait PacketLengthProbe {
218+
fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]);
219+
}
220+
215221
impl<T: StreamCipher> BlockStreamCipher for T {
216222
fn encrypt_data(&mut self, data: &mut [u8]) {
217223
self.apply_keystream(data);
@@ -222,16 +228,48 @@ impl<T: StreamCipher> BlockStreamCipher for T {
222228
}
223229
}
224230

231+
impl<T: StreamCipher + Clone> PacketLengthProbe for T {
232+
fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) {
233+
let mut cipher = self.clone();
234+
cipher.apply_keystream(first_block);
235+
}
236+
}
237+
225238
#[cfg(test)]
226239
mod tests {
240+
use aes::cipher::KeyIvInit;
241+
use aes::cipher::StreamCipher;
242+
use aes::Aes128;
227243
use aes::cipher::{IvSizeUser, KeySizeUser};
244+
use ctr::Ctr128BE;
228245
use digest::typenum::U16;
229246
use tokio::io::AsyncWriteExt;
230247

231-
use super::{BlockStreamCipher, OpeningKey};
248+
use super::{BlockStreamCipher, OpeningKey, PacketLengthProbe};
232249
use crate::mac::MacAlgorithm;
233250
use crate::sshbuffer::SSHBuffer;
234251

252+
#[test]
253+
fn stream_cipher_probe_does_not_advance_cipher_state() {
254+
let plaintext = *b"0123456789ABCDEF";
255+
let key = fixture_bytes::<16>(7);
256+
let iv = fixture_bytes::<16>(3);
257+
258+
let mut encryptor = Ctr128BE::<Aes128>::new(&key.into(), &iv.into());
259+
let mut ciphertext = plaintext;
260+
encryptor.apply_keystream(&mut ciphertext);
261+
262+
let cipher = Ctr128BE::<Aes128>::new(&key.into(), &iv.into());
263+
let mut probed_block = ciphertext;
264+
cipher.decrypt_packet_length_block(&mut probed_block);
265+
assert_eq!(probed_block, plaintext);
266+
267+
let mut decrypted = ciphertext;
268+
let mut cipher_after_probe = cipher;
269+
cipher_after_probe.decrypt_data(&mut decrypted);
270+
assert_eq!(decrypted, plaintext);
271+
}
272+
235273
#[test]
236274
fn decrypt_packet_length_uses_independent_cipher_state() -> std::io::Result<()> {
237275
let runtime = tokio::runtime::Builder::new_current_thread()
@@ -292,4 +330,20 @@ mod tests {
292330
}
293331
}
294332
}
333+
334+
impl PacketLengthProbe for OwnedStateCipher {
335+
fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) {
336+
if let Some(prefix) = first_block.get_mut(..4) {
337+
prefix.copy_from_slice(&[0, 0, 0, 12]);
338+
}
339+
}
340+
}
341+
342+
fn fixture_bytes<const N: usize>(seed: u8) -> [u8; N] {
343+
let mut bytes = [0; N];
344+
for (i, byte) in bytes.iter_mut().enumerate() {
345+
*byte = seed.wrapping_add(i as u8);
346+
}
347+
bytes
348+
}
295349
}

russh/src/cipher/cbc.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use digest::crypto_common::InnerUser;
77
#[allow(deprecated)]
88
use digest::generic_array::GenericArray;
99

10-
use super::block::BlockStreamCipher;
10+
use super::block::{BlockStreamCipher, PacketLengthProbe};
1111

1212
// Allow deprecated generic-array 0.14 usage until RustCrypto crates (cipher, cbc, etc.)
1313
// upgrade to generic-array 1.x. Remove this when dependencies no longer use 0.14.
@@ -19,7 +19,6 @@ where
1919
GenericArray::from_slice(chunk).clone()
2020
}
2121

22-
#[derive(Clone)]
2322
pub struct CbcWrapper<C: BlockEncrypt + BlockCipher + BlockDecrypt> {
2423
encryptor: Encryptor<C>,
2524
decryptor: Decryptor<C>,
@@ -51,6 +50,20 @@ impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> BlockStreamCipher for CbcWrap
5150
}
5251
}
5352

53+
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> PacketLengthProbe for CbcWrapper<C>
54+
where
55+
C: BlockDecryptMut,
56+
{
57+
fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) {
58+
let mut decryptor = self.decryptor.clone();
59+
for chunk in first_block.chunks_exact_mut(C::block_size()) {
60+
let mut block = generic_array_from_slice(chunk);
61+
decryptor.decrypt_block_mut(&mut block);
62+
chunk.copy_from_slice(&block);
63+
}
64+
}
65+
}
66+
5467
impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> InnerIvInit for CbcWrapper<C>
5568
where
5669
C: BlockEncryptMut + BlockCipher,
@@ -63,3 +76,59 @@ where
6376
}
6477
}
6578
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use aes::cipher::KeyIvInit;
83+
use aes::Aes128;
84+
#[cfg(feature = "des")]
85+
use des::TdesEde3;
86+
87+
use super::{BlockStreamCipher, CbcWrapper, PacketLengthProbe};
88+
89+
#[test]
90+
fn packet_length_probe_does_not_advance_cbc_decryptor_state() {
91+
let plaintext = *b"0123456789ABCDEF";
92+
let key = fixture_bytes::<16>(11);
93+
let iv = fixture_bytes::<16>(5);
94+
95+
let mut encryptor = CbcWrapper::<Aes128>::new(&key.into(), &iv.into());
96+
let mut ciphertext = plaintext;
97+
encryptor.encrypt_data(&mut ciphertext);
98+
99+
let cipher = CbcWrapper::<Aes128>::new(&key.into(), &iv.into());
100+
let mut probed_block = ciphertext;
101+
cipher.decrypt_packet_length_block(&mut probed_block);
102+
assert_eq!(probed_block, plaintext);
103+
104+
let mut decrypted = ciphertext;
105+
let mut cipher_after_probe = cipher;
106+
cipher_after_probe.decrypt_data(&mut decrypted);
107+
assert_eq!(decrypted, plaintext);
108+
}
109+
110+
#[cfg(feature = "des")]
111+
#[test]
112+
fn packet_length_probe_respects_3des_block_size() {
113+
let plaintext = *b"0123456789ABCDEF";
114+
let key = fixture_bytes::<24>(11);
115+
let iv = fixture_bytes::<8>(5);
116+
117+
let mut encryptor = CbcWrapper::<TdesEde3>::new(&key.into(), &iv.into());
118+
let mut ciphertext = plaintext;
119+
encryptor.encrypt_data(&mut ciphertext);
120+
121+
let cipher = CbcWrapper::<TdesEde3>::new(&key.into(), &iv.into());
122+
let mut probed_block = ciphertext;
123+
cipher.decrypt_packet_length_block(&mut probed_block);
124+
assert_eq!(probed_block, plaintext);
125+
}
126+
127+
fn fixture_bytes<const N: usize>(seed: u8) -> [u8; N] {
128+
let mut bytes = [0; N];
129+
for (i, byte) in bytes.iter_mut().enumerate() {
130+
*byte = seed.wrapping_add(i as u8);
131+
}
132+
bytes
133+
}
134+
}

0 commit comments

Comments
 (0)