ssh_cipher/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_auto_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#![forbid(unsafe_code)]
9#![warn(
10    clippy::arithmetic_side_effects,
11    clippy::panic,
12    clippy::panic_in_result_fn,
13    clippy::unwrap_used,
14    missing_docs,
15    rust_2018_idioms,
16    unused_lifetimes,
17    unused_qualifications
18)]
19
20#[cfg(feature = "std")]
21extern crate std;
22
23mod error;
24
25#[cfg(feature = "chacha20poly1305")]
26mod chacha20poly1305;
27
28pub use crate::error::{Error, Result};
29
30use core::{fmt, str};
31use encoding::{Label, LabelError};
32
33#[cfg(feature = "aes-ctr")]
34use cipher::StreamCipherCore;
35
36#[cfg(feature = "aes-gcm")]
37use aes_gcm::{aead::AeadInPlace, Aes128Gcm, Aes256Gcm};
38
39#[cfg(feature = "chacha20poly1305")]
40use crate::chacha20poly1305::ChaCha20Poly1305;
41
42#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))]
43use aes::{Aes128, Aes192, Aes256};
44
45#[cfg(any(feature = "aes-cbc", feature = "tdes"))]
46use {
47    cbc::{Decryptor, Encryptor},
48    cipher::{block_padding::NoPadding, BlockCipher, BlockDecryptMut, BlockEncryptMut},
49};
50
51#[cfg(any(feature = "aes-cbc", feature = "aes-gcm", feature = "tdes"))]
52use cipher::KeyInit;
53
54#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))]
55use cipher::KeyIvInit;
56
57#[cfg(feature = "tdes")]
58use des::TdesEde3;
59
60/// AES-128 in block chaining (CBC) mode
61const AES128_CBC: &str = "aes128-cbc";
62
63/// AES-192 in block chaining (CBC) mode
64const AES192_CBC: &str = "aes192-cbc";
65
66/// AES-256 in block chaining (CBC) mode
67const AES256_CBC: &str = "aes256-cbc";
68
69/// AES-128 in counter (CTR) mode
70const AES128_CTR: &str = "aes128-ctr";
71
72/// AES-192 in counter (CTR) mode
73const AES192_CTR: &str = "aes192-ctr";
74
75/// AES-256 in counter (CTR) mode
76const AES256_CTR: &str = "aes256-ctr";
77
78/// AES-128 in Galois/Counter Mode (GCM).
79const AES128_GCM: &str = "aes128-gcm@openssh.com";
80
81/// AES-256 in Galois/Counter Mode (GCM).
82const AES256_GCM: &str = "aes256-gcm@openssh.com";
83
84/// ChaCha20-Poly1305
85const CHACHA20_POLY1305: &str = "chacha20-poly1305@openssh.com";
86
87/// Triple-DES in block chaining (CBC) mode
88const TDES_CBC: &str = "3des-cbc";
89
90/// Nonce for AEAD modes.
91///
92/// This is used by e.g. `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com` and
93/// `chacha20-poly1305@openssh.com`.
94pub type Nonce = [u8; 12];
95
96/// Authentication tag for ciphertext data.
97///
98/// This is used by e.g. `aes128-gcm@openssh.com`/`aes256-gcm@openssh.com` and
99/// `chacha20-poly1305@openssh.com`.
100pub type Tag = [u8; 16];
101
102/// Counter mode with a 32-bit big endian counter.
103#[cfg(feature = "aes-ctr")]
104type Ctr128BE<Cipher> = ctr::CtrCore<Cipher, ctr::flavors::Ctr128BE>;
105
106/// Cipher algorithms.
107#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
108#[non_exhaustive]
109pub enum Cipher {
110    /// No cipher.
111    None,
112
113    /// AES-128 in cipher block chaining (CBC) mode.
114    Aes128Cbc,
115
116    /// AES-192 in cipher block chaining (CBC) mode.
117    Aes192Cbc,
118
119    /// AES-256 in cipher block chaining (CBC) mode.
120    Aes256Cbc,
121
122    /// AES-128 in counter (CTR) mode.
123    Aes128Ctr,
124
125    /// AES-192 in counter (CTR) mode.
126    Aes192Ctr,
127
128    /// AES-256 in counter (CTR) mode.
129    Aes256Ctr,
130
131    /// AES-128 in Galois/Counter Mode (GCM).
132    Aes128Gcm,
133
134    /// AES-256 in Galois/Counter Mode (GCM).
135    Aes256Gcm,
136
137    /// ChaCha20-Poly1305
138    ChaCha20Poly1305,
139
140    /// TripleDES in block chaining (CBC) mode
141    TDesCbc,
142}
143
144impl Cipher {
145    /// Decode cipher algorithm from the given `ciphername`.
146    ///
147    /// # Supported cipher names
148    /// - `aes256-ctr`
149    pub fn new(ciphername: &str) -> core::result::Result<Self, LabelError> {
150        ciphername.parse()
151    }
152
153    /// Get the string identifier which corresponds to this algorithm.
154    pub fn as_str(self) -> &'static str {
155        match self {
156            Self::None => "none",
157            Self::Aes128Cbc => AES128_CBC,
158            Self::Aes192Cbc => AES192_CBC,
159            Self::Aes256Cbc => AES256_CBC,
160            Self::Aes128Ctr => AES128_CTR,
161            Self::Aes192Ctr => AES192_CTR,
162            Self::Aes256Ctr => AES256_CTR,
163            Self::Aes128Gcm => AES128_GCM,
164            Self::Aes256Gcm => AES256_GCM,
165            Self::ChaCha20Poly1305 => CHACHA20_POLY1305,
166            Self::TDesCbc => TDES_CBC,
167        }
168    }
169
170    /// Get the key and IV size for this cipher in bytes.
171    pub fn key_and_iv_size(self) -> Option<(usize, usize)> {
172        match self {
173            Self::None => None,
174            Self::Aes128Cbc => Some((16, 16)),
175            Self::Aes192Cbc => Some((24, 16)),
176            Self::Aes256Cbc => Some((32, 16)),
177            Self::Aes128Ctr => Some((16, 16)),
178            Self::Aes192Ctr => Some((24, 16)),
179            Self::Aes256Ctr => Some((32, 16)),
180            Self::Aes128Gcm => Some((16, 12)),
181            Self::Aes256Gcm => Some((32, 12)),
182            Self::ChaCha20Poly1305 => Some((64, 0)),
183            Self::TDesCbc => Some((24, 8)),
184        }
185    }
186
187    /// Get the block size for this cipher in bytes.
188    pub fn block_size(self) -> usize {
189        match self {
190            Self::None | Self::ChaCha20Poly1305 | Self::TDesCbc => 8,
191            Self::Aes128Cbc
192            | Self::Aes192Cbc
193            | Self::Aes256Cbc
194            | Self::Aes128Ctr
195            | Self::Aes192Ctr
196            | Self::Aes256Ctr
197            | Self::Aes128Gcm
198            | Self::Aes256Gcm => 16,
199        }
200    }
201
202    /// Compute the length of padding necessary to pad the given input to
203    /// the block size.
204    #[allow(clippy::arithmetic_side_effects)]
205    pub fn padding_len(self, input_size: usize) -> usize {
206        match input_size % self.block_size() {
207            0 => 0,
208            input_rem => self.block_size() - input_rem,
209        }
210    }
211
212    /// Does this cipher have an authentication tag? (i.e. is it an AEAD mode?)
213    pub fn has_tag(self) -> bool {
214        matches!(
215            self,
216            Self::Aes128Gcm | Self::Aes256Gcm | Self::ChaCha20Poly1305
217        )
218    }
219
220    /// Is this cipher `none`?
221    pub fn is_none(self) -> bool {
222        self == Self::None
223    }
224
225    /// Is the cipher anything other than `none`?
226    pub fn is_some(self) -> bool {
227        !self.is_none()
228    }
229
230    /// Decrypt the ciphertext in the `buffer` in-place using this cipher.
231    pub fn decrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8], tag: Option<Tag>) -> Result<()> {
232        match self {
233            #[cfg(feature = "aes-cbc")]
234            Self::Aes128Cbc => {
235                if tag.is_some() {
236                    return Err(Error::TagSize);
237                }
238                cbc_decrypt::<Aes128>(key, iv, buffer)
239            }
240            #[cfg(feature = "aes-cbc")]
241            Self::Aes192Cbc => {
242                if tag.is_some() {
243                    return Err(Error::TagSize);
244                }
245                cbc_decrypt::<Aes192>(key, iv, buffer)
246            }
247            #[cfg(feature = "aes-cbc")]
248            Self::Aes256Cbc => {
249                if tag.is_some() {
250                    return Err(Error::TagSize);
251                }
252                cbc_decrypt::<Aes256>(key, iv, buffer)
253            }
254            #[cfg(feature = "aes-ctr")]
255            Self::Aes128Ctr | Self::Aes192Ctr | Self::Aes256Ctr => {
256                if tag.is_some() {
257                    return Err(Error::TagSize);
258                }
259
260                // Counter mode encryption and decryption are the same operation
261                self.encrypt(key, iv, buffer)?;
262                Ok(())
263            }
264            #[cfg(feature = "aes-gcm")]
265            Self::Aes128Gcm => {
266                let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
267                let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
268                let tag = tag.ok_or(Error::TagSize)?;
269                cipher
270                    .decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into())
271                    .map_err(|_| Error::Crypto)?;
272
273                Ok(())
274            }
275            #[cfg(feature = "aes-gcm")]
276            Self::Aes256Gcm => {
277                let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
278                let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
279                let tag = tag.ok_or(Error::TagSize)?;
280                cipher
281                    .decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into())
282                    .map_err(|_| Error::Crypto)?;
283
284                Ok(())
285            }
286            #[cfg(feature = "chacha20poly1305")]
287            Self::ChaCha20Poly1305 => {
288                let tag = tag.ok_or(Error::TagSize)?;
289                ChaCha20Poly1305::new(key, iv)?.decrypt(buffer, tag)
290            }
291            #[cfg(feature = "tdes")]
292            Self::TDesCbc => {
293                if tag.is_some() {
294                    return Err(Error::TagSize);
295                }
296                cbc_decrypt::<TdesEde3>(key, iv, buffer)
297            }
298            _ => {
299                // Suppress unused variable warnings.
300                let (_, _, _, _) = (key, iv, buffer, tag);
301                Err(self.unsupported())
302            }
303        }
304    }
305
306    /// Encrypt the ciphertext in the `buffer` in-place using this cipher.
307    pub fn encrypt(self, key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<Option<Tag>> {
308        match self {
309            #[cfg(feature = "aes-cbc")]
310            Self::Aes128Cbc => {
311                cbc_encrypt::<Aes128>(key, iv, buffer)?;
312                Ok(None)
313            }
314            #[cfg(feature = "aes-cbc")]
315            Self::Aes192Cbc => {
316                cbc_encrypt::<Aes192>(key, iv, buffer)?;
317                Ok(None)
318            }
319            #[cfg(feature = "aes-cbc")]
320            Self::Aes256Cbc => {
321                cbc_encrypt::<Aes256>(key, iv, buffer)?;
322                Ok(None)
323            }
324            #[cfg(feature = "aes-ctr")]
325            Self::Aes128Ctr => {
326                ctr_encrypt::<Ctr128BE<Aes128>>(key, iv, buffer)?;
327                Ok(None)
328            }
329            #[cfg(feature = "aes-ctr")]
330            Self::Aes192Ctr => {
331                ctr_encrypt::<Ctr128BE<Aes192>>(key, iv, buffer)?;
332                Ok(None)
333            }
334            #[cfg(feature = "aes-ctr")]
335            Self::Aes256Ctr => {
336                ctr_encrypt::<Ctr128BE<Aes256>>(key, iv, buffer)?;
337                Ok(None)
338            }
339            #[cfg(feature = "aes-gcm")]
340            Self::Aes128Gcm => {
341                let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
342                let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
343                let tag = cipher
344                    .encrypt_in_place_detached(&nonce.into(), &[], buffer)
345                    .map_err(|_| Error::Crypto)?;
346
347                Ok(Some(tag.into()))
348            }
349            #[cfg(feature = "aes-gcm")]
350            Self::Aes256Gcm => {
351                let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
352                let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
353                let tag = cipher
354                    .encrypt_in_place_detached(&nonce.into(), &[], buffer)
355                    .map_err(|_| Error::Crypto)?;
356
357                Ok(Some(tag.into()))
358            }
359            #[cfg(feature = "chacha20poly1305")]
360            Self::ChaCha20Poly1305 => {
361                let tag = ChaCha20Poly1305::new(key, iv)?.encrypt(buffer);
362                Ok(Some(tag))
363            }
364            #[cfg(feature = "tdes")]
365            Self::TDesCbc => {
366                cbc_encrypt::<TdesEde3>(key, iv, buffer)?;
367                Ok(None)
368            }
369            _ => {
370                // Suppress unused variable warnings.
371                let (_, _, _) = (key, iv, buffer);
372                Err(self.unsupported())
373            }
374        }
375    }
376
377    /// Create an unsupported cipher error.
378    fn unsupported(self) -> Error {
379        Error::UnsupportedCipher(self)
380    }
381}
382
383impl AsRef<str> for Cipher {
384    fn as_ref(&self) -> &str {
385        self.as_str()
386    }
387}
388
389impl Label for Cipher {}
390
391impl fmt::Display for Cipher {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        f.write_str(self.as_str())
394    }
395}
396
397impl str::FromStr for Cipher {
398    type Err = LabelError;
399
400    fn from_str(ciphername: &str) -> core::result::Result<Self, LabelError> {
401        match ciphername {
402            "none" => Ok(Self::None),
403            AES128_CBC => Ok(Self::Aes128Cbc),
404            AES192_CBC => Ok(Self::Aes192Cbc),
405            AES256_CBC => Ok(Self::Aes256Cbc),
406            AES128_CTR => Ok(Self::Aes128Ctr),
407            AES192_CTR => Ok(Self::Aes192Ctr),
408            AES256_CTR => Ok(Self::Aes256Ctr),
409            AES128_GCM => Ok(Self::Aes128Gcm),
410            AES256_GCM => Ok(Self::Aes256Gcm),
411            CHACHA20_POLY1305 => Ok(Self::ChaCha20Poly1305),
412            TDES_CBC => Ok(Self::TDesCbc),
413            _ => Err(LabelError::new(ciphername)),
414        }
415    }
416}
417
418#[cfg(any(feature = "aes-cbc", feature = "tdes"))]
419fn cbc_encrypt<C>(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()>
420where
421    C: BlockEncryptMut + BlockCipher + KeyInit,
422{
423    let cipher = Encryptor::<C>::new_from_slices(key, iv).map_err(|_| Error::KeySize)?;
424
425    // Since the passed in buffer is already padded, using NoPadding here
426    cipher
427        .encrypt_padded_mut::<NoPadding>(buffer, buffer.len())
428        .map_err(|_| Error::Crypto)?;
429
430    Ok(())
431}
432
433#[cfg(any(feature = "aes-cbc", feature = "tdes"))]
434fn cbc_decrypt<C>(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()>
435where
436    C: BlockDecryptMut + BlockCipher + KeyInit,
437{
438    let cipher = Decryptor::<C>::new_from_slices(key, iv).map_err(|_| Error::KeySize)?;
439
440    // Since the passed in buffer is already padded, using NoPadding here
441    cipher
442        .decrypt_padded_mut::<NoPadding>(buffer)
443        .map_err(|_| Error::Crypto)?;
444
445    Ok(())
446}
447
448#[cfg(feature = "aes-ctr")]
449fn ctr_encrypt<C>(key: &[u8], iv: &[u8], buffer: &mut [u8]) -> Result<()>
450where
451    C: StreamCipherCore + KeyIvInit,
452{
453    let cipher = C::new_from_slices(key, iv).map_err(|_| Error::KeySize)?;
454
455    cipher
456        .try_apply_keystream_partial(buffer.into())
457        .map_err(|_| Error::Crypto)?;
458
459    Ok(())
460}