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