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

Implement -cbc ciphers. #297

Merged
merged 6 commits into from
Jun 8, 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
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:
* `[email protected]`
* `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");
/// `[email protected]`
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
Loading