Skip to main content

russh/cipher/
mod.rs

1// Copyright 2016 Pierre-Étienne Meunier
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//!
16//! This module exports cipher names for use with [Preferred].
17use std::borrow::Borrow;
18use std::collections::HashMap;
19use std::convert::TryFrom;
20use std::fmt::Debug;
21use std::marker::PhantomData;
22use std::num::Wrapping;
23use std::sync::LazyLock;
24
25use aes::{Aes128, Aes192, Aes256};
26#[cfg(feature = "aws-lc-rs")]
27use aws_lc_rs::aead::{AES_128_GCM as ALGORITHM_AES_128_GCM, AES_256_GCM as ALGORITHM_AES_256_GCM};
28use byteorder::{BigEndian, ByteOrder};
29use block::CtrWrapper;
30use ctr::Ctr128BE;
31use delegate::delegate;
32use log::trace;
33#[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))]
34use ring::aead::{AES_128_GCM as ALGORITHM_AES_128_GCM, AES_256_GCM as ALGORITHM_AES_256_GCM};
35use ssh_encoding::Encode;
36use tokio::io::{AsyncRead, AsyncReadExt};
37
38use self::cbc::CbcWrapper;
39use crate::Error;
40use crate::mac::MacAlgorithm;
41use crate::sshbuffer::SSHBuffer;
42
43pub(crate) mod block;
44pub(crate) mod cbc;
45pub(crate) mod chacha20poly1305;
46pub(crate) mod clear;
47pub(crate) mod gcm;
48
49use block::SshBlockCipher;
50use chacha20poly1305::SshChacha20Poly1305Cipher;
51use clear::Clear;
52use gcm::GcmCipher;
53
54pub(crate) trait Cipher {
55    fn needs_mac(&self) -> bool {
56        false
57    }
58    fn key_len(&self) -> usize;
59    fn nonce_len(&self) -> usize {
60        0
61    }
62    fn make_opening_key(
63        &self,
64        key: &[u8],
65        nonce: &[u8],
66        mac_key: &[u8],
67        mac: &dyn MacAlgorithm,
68    ) -> Box<dyn OpeningKey + Send>;
69    fn make_sealing_key(
70        &self,
71        key: &[u8],
72        nonce: &[u8],
73        mac_key: &[u8],
74        mac: &dyn MacAlgorithm,
75    ) -> Box<dyn SealingKey + Send>;
76}
77
78/// `clear`
79pub const CLEAR: Name = Name("clear");
80/// `3des-cbc`
81#[cfg(feature = "des")]
82pub const TRIPLE_DES_CBC: Name = Name("3des-cbc");
83/// `aes128-ctr`
84pub const AES_128_CTR: Name = Name("aes128-ctr");
85/// `aes192-ctr`
86pub const AES_192_CTR: Name = Name("aes192-ctr");
87/// `aes128-cbc`
88pub const AES_128_CBC: Name = Name("aes128-cbc");
89/// `aes192-cbc`
90pub const AES_192_CBC: Name = Name("aes192-cbc");
91/// `aes256-cbc`
92pub const AES_256_CBC: Name = Name("aes256-cbc");
93/// `aes256-ctr`
94pub const AES_256_CTR: Name = Name("aes256-ctr");
95/// `aes128-gcm@openssh.com`
96pub const AES_128_GCM: Name = Name("aes128-gcm@openssh.com");
97/// `aes256-gcm@openssh.com`
98pub const AES_256_GCM: Name = Name("aes256-gcm@openssh.com");
99/// `chacha20-poly1305@openssh.com`
100pub const CHACHA20_POLY1305: Name = Name("chacha20-poly1305@openssh.com");
101/// `none`
102pub const NONE: Name = Name("none");
103
104pub(crate) static _CLEAR: Clear = Clear {};
105#[cfg(feature = "des")]
106static _3DES_CBC: SshBlockCipher<CbcWrapper<des::TdesEde3>> = SshBlockCipher(PhantomData);
107static _AES_128_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes128>>> = SshBlockCipher(PhantomData);
108static _AES_192_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes192>>> = SshBlockCipher(PhantomData);
109static _AES_256_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes256>>> = SshBlockCipher(PhantomData);
110static _AES_128_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_128_GCM);
111static _AES_256_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_256_GCM);
112static _AES_128_CBC: SshBlockCipher<CbcWrapper<Aes128>> = SshBlockCipher(PhantomData);
113static _AES_192_CBC: SshBlockCipher<CbcWrapper<Aes192>> = SshBlockCipher(PhantomData);
114static _AES_256_CBC: SshBlockCipher<CbcWrapper<Aes256>> = SshBlockCipher(PhantomData);
115static _CHACHA20_POLY1305: SshChacha20Poly1305Cipher = SshChacha20Poly1305Cipher {};
116
117pub static ALL_CIPHERS: &[&Name] = &[
118    &CLEAR,
119    &NONE,
120    #[cfg(feature = "des")]
121    &TRIPLE_DES_CBC,
122    &AES_128_CTR,
123    &AES_192_CTR,
124    &AES_256_CTR,
125    &AES_128_GCM,
126    &AES_256_GCM,
127    &AES_128_CBC,
128    &AES_192_CBC,
129    &AES_256_CBC,
130    &CHACHA20_POLY1305,
131];
132
133pub(crate) static CIPHERS: LazyLock<HashMap<&'static Name, &(dyn Cipher + Send + Sync)>> =
134    LazyLock::new(|| {
135        let mut h: HashMap<&'static Name, &(dyn Cipher + Send + Sync)> = HashMap::new();
136        h.insert(&CLEAR, &_CLEAR);
137        h.insert(&NONE, &_CLEAR);
138        #[cfg(feature = "des")]
139        h.insert(&TRIPLE_DES_CBC, &_3DES_CBC);
140        h.insert(&AES_128_CTR, &_AES_128_CTR);
141        h.insert(&AES_192_CTR, &_AES_192_CTR);
142        h.insert(&AES_256_CTR, &_AES_256_CTR);
143        h.insert(&AES_128_GCM, &_AES_128_GCM);
144        h.insert(&AES_256_GCM, &_AES_256_GCM);
145        h.insert(&AES_128_CBC, &_AES_128_CBC);
146        h.insert(&AES_192_CBC, &_AES_192_CBC);
147        h.insert(&AES_256_CBC, &_AES_256_CBC);
148        h.insert(&CHACHA20_POLY1305, &_CHACHA20_POLY1305);
149        assert_eq!(h.len(), ALL_CIPHERS.len());
150        h
151    });
152
153#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
154pub struct Name(&'static str);
155impl AsRef<str> for Name {
156    fn as_ref(&self) -> &str {
157        self.0
158    }
159}
160
161impl Encode for Name {
162    delegate! { to self.as_ref() {
163        fn encoded_len(&self) -> Result<usize, ssh_encoding::Error>;
164        fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error>;
165    }}
166}
167
168impl Borrow<str> for &Name {
169    fn borrow(&self) -> &str {
170        self.0
171    }
172}
173
174impl TryFrom<&str> for Name {
175    type Error = ();
176    fn try_from(s: &str) -> Result<Name, ()> {
177        CIPHERS.keys().find(|x| x.0 == s).map(|x| **x).ok_or(())
178    }
179}
180
181pub(crate) struct CipherPair {
182    pub local_to_remote: Box<dyn SealingKey + Send>,
183    pub remote_to_local: Box<dyn OpeningKey + Send>,
184}
185
186impl Debug for CipherPair {
187    fn fmt(&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
188        Ok(())
189    }
190}
191
192pub(crate) trait OpeningKey {
193    fn packet_length_to_read_for_block_length(&self) -> usize {
194        4
195    }
196
197    fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4];
198
199    fn tag_len(&self) -> usize;
200
201    fn open<'a>(&mut self, seqn: u32, ciphertext_and_tag: &'a mut [u8]) -> Result<&'a [u8], Error>;
202}
203
204pub(crate) trait SealingKey {
205    fn padding_length(&self, plaintext: &[u8]) -> usize;
206
207    fn fill_padding(&self, padding_out: &mut [u8]);
208
209    fn tag_len(&self) -> usize;
210
211    fn seal(&mut self, seqn: u32, plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]);
212
213    #[allow(clippy::indexing_slicing)] // PacketWriter reserves and sizes the packet buffer first
214    fn finish_packet(&mut self, offset: usize, payload_len: usize, buffer: &mut SSHBuffer) {
215        let payload_start = offset + PACKET_LENGTH_LEN + PADDING_LENGTH_LEN;
216        let payload_end = payload_start + payload_len;
217
218        trace!("writing, seqn = {:?}", buffer.seqn.0);
219        let padding_length = self.padding_length(&buffer.buffer[payload_start..payload_end]);
220        trace!("padding length {padding_length:?}");
221        let packet_length = PADDING_LENGTH_LEN + payload_len + padding_length;
222        trace!("packet_length {packet_length:?}");
223
224        // Maximum packet length:
225        // https://tools.ietf.org/html/rfc4253#section-6.1
226        assert!(packet_length <= u32::MAX as usize);
227        BigEndian::write_u32(
228            &mut buffer.buffer[offset..offset + PACKET_LENGTH_LEN],
229            packet_length as u32,
230        );
231
232        assert!(padding_length <= u8::MAX as usize);
233        buffer.buffer[offset + PACKET_LENGTH_LEN] = padding_length as u8;
234        buffer.buffer.resize(payload_end + padding_length, 0);
235        #[allow(clippy::indexing_slicing)] // length checked
236        self.fill_padding(&mut buffer.buffer[payload_end..]);
237        let tag_offset = buffer.buffer.len();
238        buffer.buffer.resize(tag_offset + self.tag_len(), 0);
239
240        #[allow(clippy::indexing_slicing)] // length checked
241        let (plaintext, tag) =
242            buffer.buffer[offset..].split_at_mut(PACKET_LENGTH_LEN + packet_length);
243
244        self.seal(buffer.seqn.0, plaintext, tag);
245
246        buffer.bytes += payload_len;
247        // Sequence numbers are on 32 bits and wrap.
248        // https://tools.ietf.org/html/rfc4253#section-6.4
249        buffer.seqn += Wrapping(1);
250    }
251
252    fn write(&mut self, payload: &[u8], buffer: &mut SSHBuffer) {
253        // https://tools.ietf.org/html/rfc4253#section-6
254        //
255        // The variables `payload`, `packet_length` and `padding_length` refer
256        // to the protocol fields of the same names.
257        trace!("writing, seqn = {:?}", buffer.seqn.0);
258
259        let padding_length = self.padding_length(payload);
260        trace!("padding length {padding_length:?}");
261        let packet_length = PADDING_LENGTH_LEN + payload.len() + padding_length;
262        trace!("packet_length {packet_length:?}");
263        let offset = buffer.buffer.len();
264
265        // Maximum packet length:
266        // https://tools.ietf.org/html/rfc4253#section-6.1
267        assert!(packet_length <= u32::MAX as usize);
268        buffer
269            .buffer
270            .extend_from_slice(&(packet_length as u32).to_be_bytes());
271
272        assert!(padding_length <= u8::MAX as usize);
273        buffer.buffer.push(padding_length as u8);
274        buffer.buffer.extend_from_slice(payload);
275        let pad_offset = buffer.buffer.len();
276        buffer.buffer.resize(pad_offset + padding_length, 0);
277        #[allow(clippy::indexing_slicing)] // length checked
278        self.fill_padding(&mut buffer.buffer[pad_offset..]);
279        let tag_offset = buffer.buffer.len();
280        buffer.buffer.resize(tag_offset + self.tag_len(), 0);
281
282        #[allow(clippy::indexing_slicing)] // length checked
283        let (plaintext, tag) =
284            buffer.buffer[offset..].split_at_mut(PACKET_LENGTH_LEN + packet_length);
285
286        self.seal(buffer.seqn.0, plaintext, tag);
287
288        buffer.bytes += payload.len();
289        // Sequence numbers are on 32 bits and wrap.
290        // https://tools.ietf.org/html/rfc4253#section-6.4
291        buffer.seqn += Wrapping(1);
292    }
293}
294
295pub(crate) async fn read<R: AsyncRead + Unpin>(
296    stream: &mut R,
297    buffer: &mut SSHBuffer,
298    cipher: &mut (dyn OpeningKey + Send),
299) -> Result<usize, Error> {
300    if buffer.len == 0 {
301        let mut len = vec![0; cipher.packet_length_to_read_for_block_length()];
302
303        stream.read_exact(&mut len).await?;
304        trace!("reading, len = {len:?}");
305        {
306            let seqn = buffer.seqn.0;
307            buffer.buffer.clear();
308            buffer.buffer.extend_from_slice(&len);
309            trace!("reading, seqn = {seqn:?}");
310            let len = cipher.decrypt_packet_length(seqn, &len);
311            let len = BigEndian::read_u32(&len) as usize;
312
313            if len > MAXIMUM_PACKET_LEN {
314                return Err(Error::PacketSize(len));
315            }
316
317            buffer.len = len + cipher.tag_len();
318            trace!("reading, clear len = {:?}", buffer.len);
319        }
320    }
321
322    buffer.buffer.resize(buffer.len + 4, 0);
323    trace!("read_exact {:?}", buffer.len + 4);
324
325    let l = cipher.packet_length_to_read_for_block_length();
326
327    #[allow(clippy::indexing_slicing)] // length checked
328    stream.read_exact(&mut buffer.buffer[l..]).await?;
329
330    trace!("read_exact done");
331    let seqn = buffer.seqn.0;
332    let plaintext = cipher.open(seqn, &mut buffer.buffer)?;
333
334    let padding_length = *plaintext.first().to_owned().unwrap_or(&0) as usize;
335    trace!("reading, padding_length {padding_length:?}");
336    let plaintext_end = plaintext
337        .len()
338        .checked_sub(padding_length)
339        .ok_or(Error::IndexOutOfBounds)?;
340
341    // Sequence numbers are on 32 bits and wrap.
342    // https://tools.ietf.org/html/rfc4253#section-6.4
343    buffer.seqn += Wrapping(1);
344    buffer.len = 0;
345
346    // Remove the padding
347    buffer.buffer.resize(plaintext_end + 4, 0);
348
349    Ok(plaintext_end + 4)
350}
351
352pub(crate) const PACKET_LENGTH_LEN: usize = 4;
353
354const MINIMUM_PACKET_LEN: usize = 16;
355// Keep the transport limit aligned with the 256 KiB channel packet baseline.
356const MAXIMUM_PACKET_LEN_BASELINE: usize = 256 * 1024;
357const CHANNEL_DATA_PACKET_OVERHEAD: usize = 1 + 4 + 4;
358const CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD: usize = CHANNEL_DATA_PACKET_OVERHEAD + 4;
359const PADDING_LENGTH_LEN: usize = 1;
360// SSH requires at least four bytes of padding; with 16-byte blocks, that means
361// a full-size channel packet can need up to 19 bytes of transport padding.
362const MAXIMUM_PADDING_LEN: usize = 19;
363const MAXIMUM_PACKET_LEN_HEADROOM: usize =
364    PADDING_LENGTH_LEN + CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD + MAXIMUM_PADDING_LEN;
365const MAXIMUM_PACKET_LEN: usize = MAXIMUM_PACKET_LEN_BASELINE + MAXIMUM_PACKET_LEN_HEADROOM;
366// Keep post-decompression growth within the same packet-acceptance model as
367// the transport read path.
368pub(crate) const MAXIMUM_DECOMPRESSED_PACKET_LEN: usize = MAXIMUM_PACKET_LEN;
369
370#[cfg(feature = "_bench")]
371pub mod benchmark;