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