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