Skip to main content

ssh_cipher/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
7)]
8
9#[cfg(any(feature = "aes", feature = "tdes"))]
10pub mod block_cipher;
11
12#[cfg(feature = "chacha20poly1305")]
13mod chacha20poly1305;
14mod error;
15
16pub use crate::error::{Error, Result};
17pub use cipher;
18
19#[cfg(feature = "chacha20poly1305")]
20pub use crate::chacha20poly1305::{ChaCha20, ChaCha20Poly1305, ChaChaKey, ChaChaNonce};
21
22use cipher::array::{Array, typenum::U16};
23use core::{fmt, str};
24use encoding::{Label, LabelError};
25
26#[cfg(feature = "aes")]
27use self::block_cipher::Aes;
28#[cfg(feature = "tdes")]
29use self::block_cipher::Tdes;
30#[cfg(any(feature = "aes", feature = "chacha20poly1305"))]
31use ::aead::{AeadInOut, KeyInit};
32#[cfg(any(feature = "aes", feature = "tdes"))]
33use {
34    self::block_cipher::{BlockMode, sealed::BlockCipher},
35    ::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt},
36};
37#[cfg(feature = "aes")]
38use {
39    aead::array::typenum::U12,
40    aes_gcm::{Aes128Gcm, Aes256Gcm},
41};
42
43/// AES-128 in block chaining (CBC) mode
44const AES128_CBC: &str = "aes128-cbc";
45/// AES-192 in block chaining (CBC) mode
46const AES192_CBC: &str = "aes192-cbc";
47/// AES-256 in block chaining (CBC) mode
48const AES256_CBC: &str = "aes256-cbc";
49
50/// AES-128 in counter (CTR) mode
51const AES128_CTR: &str = "aes128-ctr";
52/// AES-192 in counter (CTR) mode
53const AES192_CTR: &str = "aes192-ctr";
54/// AES-256 in counter (CTR) mode
55const AES256_CTR: &str = "aes256-ctr";
56
57/// AES-128 in Galois/Counter Mode (GCM).
58const AES128_GCM: &str = "aes128-gcm@openssh.com";
59/// AES-256 in Galois/Counter Mode (GCM).
60const AES256_GCM: &str = "aes256-gcm@openssh.com";
61
62/// ChaCha20-Poly1305
63const CHACHA20_POLY1305: &str = "chacha20-poly1305@openssh.com";
64
65/// Triple-DES in block chaining (CBC) mode
66const TDES_CBC: &str = "3des-cbc";
67
68/// Nonce for `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com`.
69#[cfg(feature = "aes")]
70pub type AesGcmNonce = Array<u8, U12>;
71
72/// Authentication tag for ciphertext data.
73///
74/// This is used by e.g. `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com` and
75/// `chacha20-poly1305@openssh.com`.
76pub type Tag = Array<u8, U16>;
77
78/// Cipher algorithms.
79///
80/// A "cipher" within the scope of SSH was originally described in [RFC4253 § 6.3] as a part of
81/// of the packet encryption protocol, where it refers to the combination of a symmetric block
82/// cipher, such as AES or 3DES, with a particular mode of operation, such as CBC or CTR.
83///
84/// This has been subsequently expanded by other standards documents, and now includes modern
85/// authenticated or "AEAD" modes such as AES-GCM and ChaCha20Poly1305, which we recommend and are
86/// marked with a ✅ in the table below.
87///
88/// Below is a table of the ciphers we support and what standards document defines them, along with
89/// which crate feature needs to be enabled to perform encryption with a given algorithm:
90///
91/// | Cipher name                     | Feature | AEAD | Algorithm   | Standard
92/// |---------------------------------|---------|------|-------------|---------
93/// | `3des-cbc`                      | `tdes`  | ⛔   | 3DES-CBC    | [RFC4253 § 6.3]
94/// | `aes128‑cbc`                    | `aes`   | ⛔   | AES-128-CBC | [RFC4253 § 6.3]
95/// | `aes192‑cbc`                    | `aes`   | ⛔   | AES-192-CBC | [RFC4253 § 6.3]
96/// | `aes256‑cbc`                    | `aes`   | ⛔   | AES-256-CBC | [RFC4253 § 6.3]
97/// | `aes128‑ctr`                    | `aes`   | ⛔   | AES-128-CTR | [RFC4344]
98/// | `aes192‑ctr`                    | `aes`   | ⛔   | AES-192-CTR | [RFC4344]
99/// | `aes256‑ctr`                    | `aes`   | ⛔   | AES-256-CTR | [RFC4344]
100/// | `aes128‑gcm@openssh.com`        | `aes`   | ✅   | AES-128-GCM | [RFC5647]
101/// | `aes256‑gcm@openssh.com`        | `aes`   | ✅   | AES-256-GCM | [RFC5647]
102/// | `chacha20‑poly1305@openssh.com` | `chacha20poly1305` | ✅ | ChaCha20Poly1305† | [PROTOCOL.chacha20poly1305]
103///
104/// † The construction called "ChaCha20Poly1305" as used by OpenSSH is different from other
105/// constructions with that name including the one defined in RFC8439 and the one found in NaCl
106/// variants like libsodium. See [`ChaCha20Poly1305`] for more information.
107///
108/// [RFC4253 § 6.3]: https://datatracker.ietf.org/doc/html/rfc4253#section-6.3
109/// [RFC4344]: https://datatracker.ietf.org/doc/html/rfc4344
110/// [RFC5647]: https://datatracker.ietf.org/doc/html/rfc5647
111/// [PROTOCOL.chacha20poly1305]: https://web.mit.edu/freebsd/head/crypto/openssh/PROTOCOL.chacha20poly1305
112#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
113#[non_exhaustive]
114pub enum Cipher {
115    /// `none`: no cipher.
116    None,
117
118    /// `aes128-cbc`: AES-128 in cipher block chaining (CBC) mode.
119    Aes128Cbc,
120
121    /// `aes192-cbc`: AES-192 in cipher block chaining (CBC) mode.
122    Aes192Cbc,
123
124    /// `aes256-cbc`: AES-256 in cipher block chaining (CBC) mode.
125    Aes256Cbc,
126
127    /// `aes128-ctr`: AES-128 in counter (CTR) mode.
128    Aes128Ctr,
129
130    /// `aes192-ctr`: AES-192 in counter (CTR) mode.
131    Aes192Ctr,
132
133    /// `aes256-ctr`: AES-256 in counter (CTR) mode.
134    Aes256Ctr,
135
136    /// `aes128-gcm@openssh.com`: AES-128 in Galois/Counter Mode (GCM).
137    Aes128Gcm,
138
139    /// `aes256-gcm@openssh.com`: AES-256 in Galois/Counter Mode (GCM).
140    Aes256Gcm,
141
142    /// `chacha20-poly1305@openssh.com`: ChaCha20-Poly1305
143    ChaCha20Poly1305,
144
145    /// `3des-cbc`: TripleDES in block chaining (CBC) mode
146    TdesCbc,
147}
148
149impl Cipher {
150    /// Decode cipher algorithm from the given `ciphername`.
151    ///
152    /// # Supported cipher names
153    /// - `aes128-cbc`
154    /// - `aes192-cbc`
155    /// - `aes256-cbc`
156    /// - `aes128-ctr`
157    /// - `aes192-ctr`
158    /// - `aes256-ctr`
159    /// - `aes128-gcm@openssh.com`
160    /// - `aes256-gcm@openssh.com`
161    /// - `chacha20-poly1305@openssh.com`
162    /// - `3des-cbc`
163    ///
164    /// # Errors
165    /// Returns [`LabelError`] if the provided `ciphername` is unknown.
166    pub fn new(ciphername: &str) -> core::result::Result<Self, LabelError> {
167        ciphername.parse()
168    }
169
170    /// Get the string identifier which corresponds to this algorithm.
171    #[must_use]
172    pub fn as_str(self) -> &'static str {
173        match self {
174            Self::None => "none",
175            Self::Aes128Cbc => AES128_CBC,
176            Self::Aes192Cbc => AES192_CBC,
177            Self::Aes256Cbc => AES256_CBC,
178            Self::Aes128Ctr => AES128_CTR,
179            Self::Aes192Ctr => AES192_CTR,
180            Self::Aes256Ctr => AES256_CTR,
181            Self::Aes128Gcm => AES128_GCM,
182            Self::Aes256Gcm => AES256_GCM,
183            Self::ChaCha20Poly1305 => CHACHA20_POLY1305,
184            Self::TdesCbc => TDES_CBC,
185        }
186    }
187
188    /// Get the key and IV size for this cipher in bytes.
189    #[must_use]
190    pub fn key_and_iv_size(self) -> Option<(usize, usize)> {
191        match self {
192            Self::None => None,
193            Self::Aes128Cbc => Some((16, 16)),
194            Self::Aes192Cbc => Some((24, 16)),
195            Self::Aes256Cbc => Some((32, 16)),
196            Self::Aes128Ctr => Some((16, 16)),
197            Self::Aes192Ctr => Some((24, 16)),
198            Self::Aes256Ctr => Some((32, 16)),
199            Self::Aes128Gcm => Some((16, 12)),
200            Self::Aes256Gcm => Some((32, 12)),
201            Self::ChaCha20Poly1305 => Some((32, 8)),
202            Self::TdesCbc => Some((24, 8)),
203        }
204    }
205
206    /// Get the block size for this cipher in bytes.
207    #[must_use]
208    pub fn block_size(self) -> usize {
209        match self {
210            Self::None | Self::ChaCha20Poly1305 | Self::TdesCbc => 8,
211            Self::Aes128Cbc
212            | Self::Aes192Cbc
213            | Self::Aes256Cbc
214            | Self::Aes128Ctr
215            | Self::Aes192Ctr
216            | Self::Aes256Ctr
217            | Self::Aes128Gcm
218            | Self::Aes256Gcm => 16,
219        }
220    }
221
222    /// Compute the length of padding necessary to pad the given input to
223    /// the block size.
224    #[allow(clippy::arithmetic_side_effects)]
225    #[must_use]
226    pub fn padding_len(self, input_size: usize) -> usize {
227        #[allow(
228            clippy::integer_division_remainder_used,
229            reason = "input_size is non-secret"
230        )]
231        match input_size % self.block_size() {
232            0 => 0,
233            input_rem => self.block_size() - input_rem,
234        }
235    }
236
237    /// Does this cipher have an authentication tag? (i.e. is it an AEAD mode?)
238    #[must_use]
239    pub fn has_tag(self) -> bool {
240        matches!(
241            self,
242            Self::Aes128Gcm | Self::Aes256Gcm | Self::ChaCha20Poly1305
243        )
244    }
245
246    /// Is this cipher `none`?
247    #[must_use]
248    pub fn is_none(self) -> bool {
249        self == Self::None
250    }
251
252    /// Is the cipher anything other than `none`?
253    #[must_use]
254    pub fn is_some(self) -> bool {
255        !self.is_none()
256    }
257
258    /// Decrypt the ciphertext in the `buffer` in-place using this cipher.
259    ///
260    /// # Errors
261    /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's
262    /// block size.
263    #[cfg_attr(not(any(feature = "aes", feature = "tdes")), allow(unused_variables))]
264    pub fn decrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8], tag: Option<Tag>) -> Result<()> {
265        match self {
266            #[cfg(feature = "aes")]
267            Self::Aes128Gcm => {
268                let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
269                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
270                let tag = tag.ok_or(Error::TagSize)?;
271                cipher
272                    .decrypt_inout_detached(nonce, &[], buffer.into(), &tag)
273                    .map_err(|_| Error::Crypto)?;
274
275                Ok(())
276            }
277            #[cfg(feature = "aes")]
278            Self::Aes256Gcm => {
279                let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
280                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
281                let tag = tag.ok_or(Error::TagSize)?;
282                cipher
283                    .decrypt_inout_detached(nonce, &[], buffer.into(), &tag)
284                    .map_err(|_| Error::Crypto)?;
285
286                Ok(())
287            }
288            #[cfg(feature = "chacha20poly1305")]
289            Self::ChaCha20Poly1305 => {
290                let key = key.try_into().map_err(|_| Error::KeySize)?;
291                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
292                let tag = tag.ok_or(Error::TagSize)?;
293                ChaCha20Poly1305::new(key)
294                    .decrypt_inout_detached(nonce, &[], buffer.into(), &tag)
295                    .map_err(|_| Error::Crypto)
296            }
297            #[cfg(feature = "aes")]
298            Self::Aes128Cbc
299            | Self::Aes192Cbc
300            | Self::Aes256Cbc
301            | Self::Aes128Ctr
302            | Self::Aes192Ctr
303            | Self::Aes256Ctr => {
304                // Non-AEAD modes don't take a tag.
305                if tag.is_some() {
306                    return Err(Error::Crypto);
307                }
308                self.decrypt_with_block_cipher::<Aes>(key, iv, buffer)
309            }
310            #[cfg(feature = "tdes")]
311            Self::TdesCbc => {
312                // Non-AEAD modes don't take a tag.
313                if tag.is_some() {
314                    return Err(Error::Crypto);
315                }
316                self.decrypt_with_block_cipher::<Tdes>(key, iv, buffer)
317            }
318            _ => Err(Error::UnsupportedCipher(self)),
319        }
320    }
321
322    /// Perform decryption using a dynamically selected block cipher mode of operation.
323    ///
324    /// Note that this does not support any form of padding currently.
325    ///
326    /// # Errors
327    /// Returns [`Error::Length`] unless the length of `buffer` is a multiple of the block size.
328    #[cfg(any(feature = "aes", feature = "tdes"))]
329    fn decrypt_with_block_cipher<C: BlockCipher>(
330        self,
331        key: &[u8],
332        iv: &[u8],
333        buffer: &mut [u8],
334    ) -> Result<()> {
335        let (blocks, remaining) = Block::<C>::slice_as_chunks_mut(buffer);
336
337        if !remaining.is_empty() {
338            return Err(Error::Length);
339        }
340
341        self.decryptor::<C>(key, iv)?.decrypt_blocks(blocks);
342        Ok(())
343    }
344
345    /// Get a stateful [`block_cipher::Decryptor`] for the given key and IV.
346    ///
347    /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with
348    /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305).
349    ///
350    /// # Errors
351    /// Propagates errors from [`block_cipher::Decryptor::new`].
352    #[cfg(any(feature = "aes", feature = "tdes"))]
353    pub fn decryptor<C>(self, key: &[u8], iv: &[u8]) -> Result<block_cipher::Decryptor<C>>
354    where
355        C: BlockCipher,
356    {
357        block_cipher::Decryptor::new(self, key, iv)
358    }
359
360    /// Encrypt the ciphertext in the `buffer` in-place using this cipher.
361    ///
362    /// # Errors
363    /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's
364    /// block size.
365    #[cfg_attr(not(any(feature = "aes", feature = "tdes")), allow(unused_variables))]
366    pub fn encrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<Option<Tag>> {
367        match self {
368            #[cfg(feature = "aes")]
369            Self::Aes128Gcm => {
370                let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
371                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
372                let tag = cipher
373                    .encrypt_inout_detached(nonce, &[], buffer.into())
374                    .map_err(|_| Error::Crypto)?;
375
376                Ok(Some(tag))
377            }
378            #[cfg(feature = "aes")]
379            Self::Aes256Gcm => {
380                let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
381                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
382                let tag = cipher
383                    .encrypt_inout_detached(nonce, &[], buffer.into())
384                    .map_err(|_| Error::Crypto)?;
385
386                Ok(Some(tag))
387            }
388            #[cfg(feature = "chacha20poly1305")]
389            Self::ChaCha20Poly1305 => {
390                let key = key.try_into().map_err(|_| Error::KeySize)?;
391                let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
392                let tag = ChaCha20Poly1305::new(key)
393                    .encrypt_inout_detached(nonce, &[], buffer.into())
394                    .map_err(|_| Error::Crypto)?;
395                Ok(Some(tag))
396            }
397            #[cfg(feature = "aes")]
398            Self::Aes128Cbc
399            | Self::Aes192Cbc
400            | Self::Aes256Cbc
401            | Self::Aes128Ctr
402            | Self::Aes192Ctr
403            | Self::Aes256Ctr => {
404                self.encrypt_with_block_cipher::<Aes>(key, iv, buffer)?;
405                Ok(None)
406            }
407            #[cfg(feature = "tdes")]
408            Self::TdesCbc => {
409                self.encrypt_with_block_cipher::<Tdes>(key, iv, buffer)?;
410                Ok(None)
411            }
412            _ => Err(Error::UnsupportedCipher(self)),
413        }
414    }
415
416    /// Perform decryption using a dynamically selected block cipher mode of operation.
417    ///
418    /// Note that this does not support any form of padding currently.
419    ///
420    /// # Errors
421    /// Returns [`Error::Length`] unless the length of `buffer` is a multiple of the block size.
422    #[cfg(any(feature = "aes", feature = "tdes"))]
423    fn encrypt_with_block_cipher<C: BlockCipher>(
424        self,
425        key: &[u8],
426        iv: &[u8],
427        buffer: &mut [u8],
428    ) -> Result<()> {
429        let (blocks, remaining) = Block::<C>::slice_as_chunks_mut(buffer);
430
431        if !remaining.is_empty() {
432            return Err(Error::Length);
433        }
434
435        self.encryptor::<C>(key, iv)?.encrypt_blocks(blocks);
436        Ok(())
437    }
438
439    /// Get a stateful [`block_cipher::Encryptor`] for the given key and IV.
440    ///
441    /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with
442    /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305).
443    ///
444    /// # Errors
445    /// Propagates errors from [`block_cipher::Encryptor::new`].
446    #[cfg(any(feature = "aes", feature = "tdes"))]
447    pub fn encryptor<C>(self, key: &[u8], iv: &[u8]) -> Result<block_cipher::Encryptor<C>>
448    where
449        C: BlockCipher,
450    {
451        block_cipher::Encryptor::new(self, key, iv)
452    }
453
454    /// Get the block cipher mode of operation for this `Cipher`, if applicable.
455    #[cfg(any(feature = "aes", feature = "tdes"))]
456    pub(crate) fn block_mode(self) -> Option<BlockMode> {
457        match self {
458            #[cfg(feature = "aes")]
459            Self::Aes128Cbc | Self::Aes192Cbc | Self::Aes256Cbc => Some(BlockMode::Cbc),
460            #[cfg(feature = "aes")]
461            Self::Aes128Ctr | Self::Aes192Ctr | Self::Aes256Ctr => Some(BlockMode::Ctr),
462            #[cfg(feature = "tdes")]
463            Self::TdesCbc => Some(BlockMode::Cbc),
464            _ => None,
465        }
466    }
467}
468
469impl AsRef<str> for Cipher {
470    fn as_ref(&self) -> &str {
471        self.as_str()
472    }
473}
474
475impl Label for Cipher {}
476
477impl fmt::Display for Cipher {
478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479        f.write_str(self.as_str())
480    }
481}
482
483impl str::FromStr for Cipher {
484    type Err = LabelError;
485
486    fn from_str(ciphername: &str) -> core::result::Result<Self, LabelError> {
487        match ciphername {
488            "none" => Ok(Self::None),
489            AES128_CBC => Ok(Self::Aes128Cbc),
490            AES192_CBC => Ok(Self::Aes192Cbc),
491            AES256_CBC => Ok(Self::Aes256Cbc),
492            AES128_CTR => Ok(Self::Aes128Ctr),
493            AES192_CTR => Ok(Self::Aes192Ctr),
494            AES256_CTR => Ok(Self::Aes256Ctr),
495            AES128_GCM => Ok(Self::Aes128Gcm),
496            AES256_GCM => Ok(Self::Aes256Gcm),
497            CHACHA20_POLY1305 => Ok(Self::ChaCha20Poly1305),
498            TDES_CBC => Ok(Self::TdesCbc),
499            _ => Err(LabelError::new(ciphername)),
500        }
501    }
502}