Skip to content
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
9 changes: 7 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: install nasm
run: |
choco install nasm
echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build (no features enabled)
run: cargo build --verbose

Expand All @@ -45,8 +50,8 @@ jobs:
rustup toolchain add 1.81.0
rustup target add --toolchain 1.81.0 wasm32-wasip1
- name: Build (all features enabled)
run: cargo +1.81.0 build --verbose --target wasm32-wasip1 -p russh
- name: Build (WASM-compatible features)
run: cargo +1.81.0 build --verbose --target wasm32-wasip1 --no-default-features --features flate2,ring -p russh

Formatting:
runs-on: ubuntu-24.04
Expand Down
9 changes: 5 additions & 4 deletions russh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,26 @@ version = "0.52.1"
rust-version = "1.75"

[features]
default = ["flate2"]
default = ["flate2", "aws-lc-rs"]
aws-lc-rs = ["dep:aws-lc-rs"]
async-trait = ["dep:async-trait"]
legacy-ed25519-pkcs8-parser = ["yasna"]
# Danger: 3DES cipher is insecure.
des = ["dep:des"]
# Danger: DSA algorithm is insecure.
dsa = ["ssh-key/dsa"]
ring = ["dep:ring"]
_bench = ["dep:criterion"]

[dependencies]
aes-gcm = "0.10"
aes.workspace = true
async-trait = { workspace = true, optional = true }
aws-lc-rs = { version = "1.13.1", optional = true }
bitflags = "2.0"
block-padding = { version = "0.3", features = ["std"] }
byteorder.workspace = true
bytes.workspace = true
cbc = { version = "0.1" }
chacha20 = "0.9"
ctr = "0.9"
curve25519-dalek = "4.1.3"
data-encoding = "2.3"
Expand Down Expand Up @@ -62,9 +63,9 @@ pbkdf2 = "0.12"
pkcs1 = "0.7"
pkcs5 = "0.7"
pkcs8 = { version = "0.10", features = ["pkcs5", "encryption"] }
poly1305 = "0.8"
rand_core = { version = "0.6.4", features = ["getrandom", "std"] }
rand.workspace = true
ring = { version = "0.17.14", optional = true }
rsa.workspace = true
russh-cryptovec = { version = "0.52.0", path = "../cryptovec", features = [
"ssh-encoding",
Expand Down
7 changes: 4 additions & 3 deletions russh/src/cipher/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ pub fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group(format!("Cipher: {}", cipher_name.0));
for size in [100usize, 1000, 10000] {
let iterations = 10000 / size;
let mut tag = black_box(vec![0u8; sk.tag_len()]);

group.throughput(Throughput::Bytes(size as u64));
group.bench_function(format!("Block size: {size}"), |b| {
Expand All @@ -34,8 +33,10 @@ pub fn bench(c: &mut Criterion) {
},
|mut in_out| {
for _ in 0..iterations {
sk.seal(0, &mut in_out, &mut tag);
ok.open(0, &mut in_out, &tag).unwrap();
let len = in_out.len();
let (data, tag) = in_out.split_at_mut(len - sk.tag_len());
sk.seal(0, data, tag);
ok.open(0, &mut in_out).unwrap();
}
},
);
Expand Down
5 changes: 3 additions & 2 deletions russh/src/cipher/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ impl<C: BlockStreamCipher + KeySizeUser + IvSizeUser> super::OpeningKey for Open
fn open<'a>(
&mut self,
sequence_number: u32,
ciphertext_in_plaintext_out: &'a mut [u8],
tag: &[u8],
ciphertext_and_tag: &'a mut [u8],
) -> Result<&'a [u8], Error> {
let ciphertext_len = ciphertext_and_tag.len() - self.tag_len();
let (ciphertext_in_plaintext_out, tag) = ciphertext_and_tag.split_at_mut(ciphertext_len);
if self.mac.is_etm() {
if !self
.mac
Expand Down
137 changes: 39 additions & 98 deletions russh/src/cipher/chacha20poly1305.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,124 +15,85 @@

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

use std::convert::TryInto;

use aes::cipher::{BlockSizeUser, StreamCipherSeek};
use byteorder::{BigEndian, ByteOrder};
use chacha20::cipher::{KeyInit, KeyIvInit, StreamCipher};
use chacha20::{ChaCha20Legacy, ChaCha20LegacyCore};
use generic_array::typenum::{Unsigned, U16, U32, U8};
use generic_array::GenericArray;
use poly1305::Poly1305;
use subtle::ConstantTimeEq;
#[cfg(feature = "aws-lc-rs")]
use aws_lc_rs::aead::chacha20_poly1305_openssh;
#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))]
use ring::aead::chacha20_poly1305_openssh;

use super::super::Error;
use crate::cipher::PACKET_LENGTH_LEN;
use crate::mac::MacAlgorithm;

pub struct SshChacha20Poly1305Cipher {}

type KeyLength = U32;
type NonceLength = U8;
type TagLength = U16;
type Key = GenericArray<u8, KeyLength>;
type Nonce = GenericArray<u8, NonceLength>;

impl super::Cipher for SshChacha20Poly1305Cipher {
fn key_len(&self) -> usize {
KeyLength::to_usize() * 2
chacha20_poly1305_openssh::KEY_LEN
}

#[allow(clippy::indexing_slicing)] // length checked
fn make_opening_key(
&self,
k: &[u8],
_: &[u8],
_: &[u8],
_: &dyn MacAlgorithm,
) -> Box<dyn super::OpeningKey + Send> {
let mut k1 = Key::default();
let mut k2 = Key::default();
k1.clone_from_slice(&k[KeyLength::to_usize()..]);
k2.clone_from_slice(&k[..KeyLength::to_usize()]);
Box::new(OpeningKey { k1, k2 })
Box::new(OpeningKey(chacha20_poly1305_openssh::OpeningKey::new(
#[allow(clippy::unwrap_used)]
k.try_into().unwrap(),
)))
}

#[allow(clippy::indexing_slicing)] // length checked
fn make_sealing_key(
&self,
k: &[u8],
_: &[u8],
_: &[u8],
_: &dyn MacAlgorithm,
) -> Box<dyn super::SealingKey + Send> {
let mut k1 = Key::default();
let mut k2 = Key::default();
k1.clone_from_slice(&k[KeyLength::to_usize()..]);
k2.clone_from_slice(&k[..KeyLength::to_usize()]);
Box::new(SealingKey { k1, k2 })
Box::new(SealingKey(chacha20_poly1305_openssh::SealingKey::new(
#[allow(clippy::unwrap_used)]
k.try_into().unwrap(),
)))
}
}

pub struct OpeningKey {
k1: Key,
k2: Key,
}

pub struct SealingKey {
k1: Key,
k2: Key,
}
pub struct OpeningKey(chacha20_poly1305_openssh::OpeningKey);

#[allow(clippy::indexing_slicing)] // length checked
fn make_counter(sequence_number: u32) -> Nonce {
let mut nonce = Nonce::default();
let i0 = NonceLength::to_usize() - 4;
BigEndian::write_u32(&mut nonce[i0..], sequence_number);
nonce
}
pub struct SealingKey(chacha20_poly1305_openssh::SealingKey);

impl super::OpeningKey for OpeningKey {
fn decrypt_packet_length(
&self,
sequence_number: u32,
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
self.0.decrypt_packet_length(
sequence_number,
#[allow(clippy::unwrap_used)]
encrypted_packet_length.try_into().unwrap(),
)
}

fn tag_len(&self) -> usize {
TagLength::to_usize()
chacha20_poly1305_openssh::TAG_LEN
}

#[allow(clippy::indexing_slicing)] // lengths checked
fn open<'a>(
&mut self,
sequence_number: u32,
ciphertext_in_plaintext_out: &'a mut [u8],
tag: &[u8],
ciphertext_and_tag: &'a mut [u8],
) -> Result<&'a [u8], Error> {
let nonce = make_counter(sequence_number);
let expected_tag = compute_poly1305(&nonce, &self.k2, ciphertext_in_plaintext_out);

if !bool::from(expected_tag.ct_eq(tag)) {
return Err(Error::DecryptionError);
}

let mut cipher = ChaCha20Legacy::new(&self.k2, &nonce);

cipher.seek(<ChaCha20LegacyCore as BlockSizeUser>::BlockSize::to_usize());
cipher.apply_keystream(&mut ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..]);

Ok(&ciphertext_in_plaintext_out[PACKET_LENGTH_LEN..])
let ciphertext_len = ciphertext_and_tag.len() - self.tag_len();
let (ciphertext_in_plaintext_out, tag) = ciphertext_and_tag.split_at_mut(ciphertext_len);

self.0
.open_in_place(
sequence_number,
ciphertext_in_plaintext_out,
#[allow(clippy::unwrap_used)]
&tag.try_into().unwrap(),
)
.map_err(|_| Error::DecryptionError)
}
}

Expand Down Expand Up @@ -163,7 +124,7 @@ impl super::SealingKey for SealingKey {
}

fn tag_len(&self) -> usize {
TagLength::to_usize()
chacha20_poly1305_openssh::TAG_LEN
}

fn seal(
Expand All @@ -172,31 +133,11 @@ impl super::SealingKey for SealingKey {
plaintext_in_ciphertext_out: &mut [u8],
tag: &mut [u8],
) {
let nonce = make_counter(sequence_number);

let mut cipher = ChaCha20Legacy::new(&self.k1, &nonce);
#[allow(clippy::indexing_slicing)] // length checked
cipher.apply_keystream(&mut plaintext_in_ciphertext_out[..PACKET_LENGTH_LEN]);

// --
let mut cipher = ChaCha20Legacy::new(&self.k2, &nonce);

cipher.seek(<ChaCha20LegacyCore as BlockSizeUser>::BlockSize::to_usize());
#[allow(clippy::indexing_slicing, clippy::unwrap_used)]
cipher.apply_keystream(&mut plaintext_in_ciphertext_out[PACKET_LENGTH_LEN..]);

// --

tag.copy_from_slice(
compute_poly1305(&nonce, &self.k2, plaintext_in_ciphertext_out).as_slice(),
self.0.seal_in_place(
sequence_number,
plaintext_in_ciphertext_out,
#[allow(clippy::unwrap_used)]
tag.try_into().unwrap(),
);
}
}

fn compute_poly1305(nonce: &Nonce, key: &Key, data: &[u8]) -> poly1305::Tag {
let mut cipher = ChaCha20Legacy::new(key, nonce);
let mut poly_key = GenericArray::<u8, U32>::default();
cipher.apply_keystream(&mut poly_key);

Poly1305::new(&poly_key).compute_unpadded(data)
}
6 changes: 2 additions & 4 deletions russh/src/cipher/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,10 @@ impl super::OpeningKey for Key {
fn open<'a>(
&mut self,
_seqn: u32,
ciphertext_in_plaintext_out: &'a mut [u8],
tag: &[u8],
ciphertext_and_tag: &'a mut [u8],
) -> Result<&'a [u8], Error> {
debug_assert_eq!(tag.len(), 0); // self.tag_len());
#[allow(clippy::indexing_slicing)] // length known
Ok(&ciphertext_in_plaintext_out[4..])
Ok(&ciphertext_and_tag[4..])
}
}

Expand Down
Loading
Loading