Skip to content

Commit bb1db47

Browse files
authored
ssh-cipher: make Encryptor and Decryptor generic around cipher (#533)
Adds a `BlockCipher` extension trait and defines it for the internally defined multi-key-size `Aes` type as well as `des::TdesEde3` which is now re-exported as `ssh_cipher::block_cipher::Tdes`. The `Decryptor` and `Encryptor` types are now generic around a sealed `BlockCipher` trait, and have `AesDecryptor`/`AesEncryptor` and `TdesDecryptor`/`TdesEncryptor` type aliases. They both support CBC mode, and AES ciphers support CTR too. The API is otherwise the same except callers need to use one of the typed `*Encryptor`/`*Decryptor` traits. The crate now implements both block cipher modes of operation internally and no longer relies on the `cbc` and `ctr` crates. This makes dynamically dispatching between the two modes easier while avoiding some monomorphization bloat, and actually doesn't add significant complexity. With `Decryptor`/`Encryptor` now well-typed and supporting typed block sizes, it should be possible to make them impelement the `BlockMode*` traits and therefore make them work like the other block modes, but that particular change is deferred for a followup PR.
1 parent 63114d5 commit bb1db47

11 files changed

Lines changed: 376 additions & 306 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ssh-cipher/Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ encoding = { package = "ssh-encoding", version = "0.3.0-rc.9" }
2525
aead = { version = "0.6.0-rc.10", optional = true, default-features = false }
2626
aes = { version = "0.9", optional = true, default-features = false }
2727
aes-gcm = { version = "0.11.0-rc.3", optional = true, default-features = false, features = ["aes"] }
28-
cbc = { version = "0.2.1", optional = true }
29-
ctr = { version = "0.10", optional = true, default-features = false }
3028
ctutils = { version = "0.4", optional = true, default-features = false }
3129
chacha20 = { version = "0.10", optional = true, default-features = false, features = ["cipher", "legacy"] }
3230
des = { version = "0.9", optional = true, default-features = false }
@@ -37,9 +35,9 @@ zeroize = { version = "1", optional = true, default-features = false }
3735
hex-literal = "1"
3836

3937
[features]
40-
aes = ["dep:aead", "dep:aes", "dep:aes-gcm", "dep:cbc", "dep:ctr"]
38+
aes = ["dep:aead", "dep:aes", "dep:aes-gcm"]
4139
chacha20poly1305 = ["dep:aead", "dep:chacha20", "dep:poly1305", "dep:ctutils"]
42-
tdes = ["dep:des", "dep:cbc"]
40+
tdes = ["dep:des"]
4341
zeroize = [
4442
"dep:zeroize",
4543
"aes?/zeroize",

ssh-cipher/src/block_cipher.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,77 @@
77
mod aes;
88
mod decryptor;
99
mod encryptor;
10+
mod state;
1011

12+
#[cfg(feature = "aes")]
13+
pub use self::aes::Aes;
1114
pub use self::{decryptor::Decryptor, encryptor::Encryptor};
15+
#[cfg(feature = "tdes")]
16+
pub use ::des::TdesEde3 as Tdes;
17+
18+
use self::state::State;
19+
20+
#[cfg(feature = "tdes")]
21+
use {
22+
crate::Cipher,
23+
::cipher::common::{InvalidLength, KeyInit},
24+
};
25+
26+
/// Seal the `BlockCipher` trait so others cannot implement it.
27+
pub(crate) mod sealed {
28+
use crate::Cipher;
29+
use ::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, common::InvalidLength};
30+
31+
/// Trait for block ciphers supported by this crate.
32+
///
33+
/// This trait is deliberately sealed so it cannot be implemented by downstream crates.
34+
/// Notably new ciphers added to SSH should be authenticated, and we shouldn't support a
35+
/// proliferation of unauthenticated ciphers.
36+
pub trait BlockCipher: BlockCipherDecrypt + BlockCipherEncrypt {
37+
/// Initialize cipher from a byte slice.
38+
///
39+
/// This is defined separate from the [`KeyInit`] trait so it can support variable-sized keys.
40+
///
41+
/// # Errors
42+
/// Returns [`InvalidLength`] if `slice` is not equal in length to the key size.
43+
fn new_from_slice(slice: &[u8]) -> Result<Self, InvalidLength>;
44+
45+
/// Is this the correct block cipher implementation for the given cipher?
46+
fn is_supported(cipher: Cipher) -> bool;
47+
}
48+
}
1249

50+
/// Supported block cipher modes of operation.
51+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
52+
pub(crate) enum BlockMode {
53+
/// Cipher block chaining.
54+
Cbc,
55+
56+
/// Counter mode.
57+
Ctr,
58+
}
59+
60+
/// Encryptor for the Advanced Encryption Standard (AES).
61+
#[cfg(feature = "aes")]
62+
pub type AesEncryptor = Encryptor<Aes>;
63+
/// Decryptor for the Advanced Encryption Standard (AES).
1364
#[cfg(feature = "aes")]
14-
pub(crate) use self::aes::Aes;
65+
pub type AesDecryptor = Decryptor<Aes>;
66+
67+
/// Encryptor for 3DES.
68+
#[cfg(feature = "tdes")]
69+
pub type TdesEncryptor = Encryptor<Tdes>;
70+
/// Decryptor for 3DES.
71+
#[cfg(feature = "tdes")]
72+
pub type TdesDecryptor = Decryptor<Tdes>;
73+
74+
#[cfg(feature = "tdes")]
75+
impl sealed::BlockCipher for Tdes {
76+
fn new_from_slice(slice: &[u8]) -> Result<Self, InvalidLength> {
77+
KeyInit::new_from_slice(slice)
78+
}
79+
80+
fn is_supported(cipher: Cipher) -> bool {
81+
cipher == Cipher::TdesCbc
82+
}
83+
}

ssh-cipher/src/block_cipher/aes.rs

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
//! AES block cipher.
22
3+
use super::sealed::BlockCipher;
4+
use crate::Cipher;
35
use ::aes::{Aes128, Aes192, Aes256, Block};
46
use ::cipher::{
57
BlockCipherDecClosure, BlockCipherDecrypt, BlockCipherEncClosure, BlockCipherEncrypt,
68
BlockSizeUser, InvalidLength, KeyInit, array::sizes::U16,
79
};
10+
use core::fmt;
11+
use core::fmt::Debug;
812

913
/// Advanced Encryption Standard (AES) low-level block cipher.
1014
///
11-
/// Supports 128-bit, 192-bit, and 256-bit key sizes.
12-
pub(crate) enum Aes {
15+
/// Supports 128-bit, 192-bit, and 256-bit keys.
16+
pub struct Aes {
17+
inner: Inner,
18+
}
19+
20+
/// Inner enum over supported key sizes.
21+
enum Inner {
1322
Aes128(Aes128),
1423
Aes192(Aes192),
1524
Aes256(Aes256),
@@ -23,59 +32,89 @@ impl Aes {
2332
///
2433
/// # Errors
2534
/// Returns [`InvalidLength`] if the length of `key` is not any of the above.
26-
pub(crate) fn new(key: &[u8]) -> Result<Self, InvalidLength> {
35+
pub fn new_from_slice(key: &[u8]) -> Result<Self, InvalidLength> {
2736
if let Ok(cipher) = Aes128::new_from_slice(key) {
28-
return Ok(Aes::Aes128(cipher));
37+
return Ok(Self {
38+
inner: Inner::Aes128(cipher),
39+
});
2940
}
3041

3142
if let Ok(cipher) = Aes192::new_from_slice(key) {
32-
return Ok(Aes::Aes192(cipher));
43+
return Ok(Self {
44+
inner: Inner::Aes192(cipher),
45+
});
3346
}
3447

3548
if let Ok(cipher) = Aes256::new_from_slice(key) {
36-
return Ok(Aes::Aes256(cipher));
49+
return Ok(Self {
50+
inner: Inner::Aes256(cipher),
51+
});
3752
}
3853

3954
Err(InvalidLength)
4055
}
4156
}
4257

58+
impl BlockCipher for Aes {
59+
fn new_from_slice(slice: &[u8]) -> Result<Self, InvalidLength> {
60+
Aes::new_from_slice(slice)
61+
}
62+
63+
fn is_supported(cipher: Cipher) -> bool {
64+
matches!(
65+
cipher,
66+
Cipher::Aes128Cbc
67+
| Cipher::Aes192Cbc
68+
| Cipher::Aes256Cbc
69+
| Cipher::Aes128Ctr
70+
| Cipher::Aes192Ctr
71+
| Cipher::Aes256Ctr
72+
)
73+
}
74+
}
75+
4376
impl BlockCipherDecrypt for Aes {
4477
fn decrypt_blocks(&self, blocks: &mut [Block]) {
45-
match self {
46-
Aes::Aes128(aes) => aes.decrypt_blocks(blocks),
47-
Aes::Aes192(aes) => aes.decrypt_blocks(blocks),
48-
Aes::Aes256(aes) => aes.decrypt_blocks(blocks),
78+
match &self.inner {
79+
Inner::Aes128(aes) => aes.decrypt_blocks(blocks),
80+
Inner::Aes192(aes) => aes.decrypt_blocks(blocks),
81+
Inner::Aes256(aes) => aes.decrypt_blocks(blocks),
4982
}
5083
}
5184

5285
fn decrypt_with_backend(&self, f: impl BlockCipherDecClosure<BlockSize = Self::BlockSize>) {
53-
match self {
54-
Aes::Aes128(aes) => aes.decrypt_with_backend(f),
55-
Aes::Aes192(aes) => aes.decrypt_with_backend(f),
56-
Aes::Aes256(aes) => aes.decrypt_with_backend(f),
86+
match &self.inner {
87+
Inner::Aes128(aes) => aes.decrypt_with_backend(f),
88+
Inner::Aes192(aes) => aes.decrypt_with_backend(f),
89+
Inner::Aes256(aes) => aes.decrypt_with_backend(f),
5790
}
5891
}
5992
}
6093

6194
impl BlockCipherEncrypt for Aes {
6295
fn encrypt_blocks(&self, blocks: &mut [Block]) {
63-
match self {
64-
Aes::Aes128(aes) => aes.encrypt_blocks(blocks),
65-
Aes::Aes192(aes) => aes.encrypt_blocks(blocks),
66-
Aes::Aes256(aes) => aes.encrypt_blocks(blocks),
96+
match &self.inner {
97+
Inner::Aes128(aes) => aes.encrypt_blocks(blocks),
98+
Inner::Aes192(aes) => aes.encrypt_blocks(blocks),
99+
Inner::Aes256(aes) => aes.encrypt_blocks(blocks),
67100
}
68101
}
69102

70103
fn encrypt_with_backend(&self, f: impl BlockCipherEncClosure<BlockSize = Self::BlockSize>) {
71-
match self {
72-
Aes::Aes128(aes) => aes.encrypt_with_backend(f),
73-
Aes::Aes192(aes) => aes.encrypt_with_backend(f),
74-
Aes::Aes256(aes) => aes.encrypt_with_backend(f),
104+
match &self.inner {
105+
Inner::Aes128(aes) => aes.encrypt_with_backend(f),
106+
Inner::Aes192(aes) => aes.encrypt_with_backend(f),
107+
Inner::Aes256(aes) => aes.encrypt_with_backend(f),
75108
}
76109
}
77110
}
78111

79112
impl BlockSizeUser for Aes {
80113
type BlockSize = U16;
81114
}
115+
116+
impl Debug for Aes {
117+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118+
f.debug_struct("Aes").finish_non_exhaustive()
119+
}
120+
}

0 commit comments

Comments
 (0)