Skip to main content

ssh_cipher/
decryptor.rs

1//! Stateful decryptor object.
2
3use crate::{Cipher, Error, Result};
4use cipher::KeyIvInit;
5use core::fmt::{self, Debug};
6
7#[cfg(feature = "aes-ctr")]
8use crate::{Ctr128BE, encryptor::ctr_encrypt as ctr_decrypt};
9#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))]
10use aes::{Aes128, Aes192, Aes256};
11#[cfg(any(feature = "aes-cbc", feature = "tdes"))]
12use cipher::{
13    Block,
14    block::{BlockCipherDecrypt, BlockModeDecrypt},
15};
16#[cfg(feature = "tdes")]
17use des::TdesEde3;
18
19/// Stateful decryptor object for unauthenticated SSH symmetric ciphers.
20///
21/// Note that this deliberately does not support AEAD modes such as AES-GCM and ChaCha20Poly1305,
22/// which are one-shot by design.
23pub struct Decryptor {
24    /// Inner enum over possible decryption ciphers.
25    inner: Inner,
26}
27
28/// Inner decryptor enum which is deliberately kept out of the public API.
29enum Inner {
30    #[cfg(feature = "aes-cbc")]
31    Aes128Cbc(cbc::Decryptor<Aes128>),
32    #[cfg(feature = "aes-cbc")]
33    Aes192Cbc(cbc::Decryptor<Aes192>),
34    #[cfg(feature = "aes-cbc")]
35    Aes256Cbc(cbc::Decryptor<Aes256>),
36    #[cfg(feature = "aes-ctr")]
37    Aes128Ctr(Ctr128BE<Aes128>),
38    #[cfg(feature = "aes-ctr")]
39    Aes192Ctr(Ctr128BE<Aes192>),
40    #[cfg(feature = "aes-ctr")]
41    Aes256Ctr(Ctr128BE<Aes256>),
42    #[cfg(feature = "tdes")]
43    TDesCbc(cbc::Decryptor<TdesEde3>),
44}
45
46impl Decryptor {
47    /// Create a new decryptor object with the given [`Cipher`], `key`, and `iv` (i.e.
48    /// initialization vector).
49    ///
50    /// # Errors
51    /// - Returns [`Error::Length`] if `key` or `iv` are the wrong length for the given `cipher`.
52    /// - Returns [`Error::UnsupportedCipher`] if support for the given `cipher` is not enabled
53    ///   in the crate features.
54    pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result<Self> {
55        cipher.check_key_and_iv(key, iv)?;
56
57        let inner = match cipher {
58            #[cfg(feature = "aes-cbc")]
59            Cipher::Aes128Cbc => cbc::Decryptor::new_from_slices(key, iv).map(Inner::Aes128Cbc),
60            #[cfg(feature = "aes-cbc")]
61            Cipher::Aes192Cbc => cbc::Decryptor::new_from_slices(key, iv).map(Inner::Aes192Cbc),
62            #[cfg(feature = "aes-cbc")]
63            Cipher::Aes256Cbc => cbc::Decryptor::new_from_slices(key, iv).map(Inner::Aes256Cbc),
64            #[cfg(feature = "aes-ctr")]
65            Cipher::Aes128Ctr => Ctr128BE::new_from_slices(key, iv).map(Inner::Aes128Ctr),
66            #[cfg(feature = "aes-ctr")]
67            Cipher::Aes192Ctr => Ctr128BE::new_from_slices(key, iv).map(Inner::Aes192Ctr),
68            #[cfg(feature = "aes-ctr")]
69            Cipher::Aes256Ctr => Ctr128BE::new_from_slices(key, iv).map(Inner::Aes256Ctr),
70            #[cfg(feature = "tdes")]
71            Cipher::TDesCbc => cbc::Decryptor::new_from_slices(key, iv).map(Inner::TDesCbc),
72            _ => return Err(cipher.unsupported()),
73        }
74        .map_err(|_| Error::Length)?;
75
76        Ok(Self { inner })
77    }
78
79    /// Get the cipher for this decryptor.
80    #[must_use]
81    pub fn cipher(&self) -> Cipher {
82        match &self.inner {
83            #[cfg(feature = "aes-cbc")]
84            Inner::Aes128Cbc(_) => Cipher::Aes128Cbc,
85            #[cfg(feature = "aes-cbc")]
86            Inner::Aes192Cbc(_) => Cipher::Aes192Cbc,
87            #[cfg(feature = "aes-cbc")]
88            Inner::Aes256Cbc(_) => Cipher::Aes256Cbc,
89            #[cfg(feature = "aes-ctr")]
90            Inner::Aes128Ctr(_) => Cipher::Aes128Ctr,
91            #[cfg(feature = "aes-ctr")]
92            Inner::Aes192Ctr(_) => Cipher::Aes192Ctr,
93            #[cfg(feature = "aes-ctr")]
94            Inner::Aes256Ctr(_) => Cipher::Aes256Ctr,
95            #[cfg(feature = "tdes")]
96            Inner::TDesCbc(_) => Cipher::TDesCbc,
97        }
98    }
99
100    /// Decrypt the given buffer in place.
101    ///
102    /// # Errors
103    /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's
104    /// block size.
105    pub fn decrypt(&mut self, buffer: &mut [u8]) -> Result<()> {
106        #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))]
107        match &mut self.inner {
108            #[cfg(feature = "aes-cbc")]
109            Inner::Aes128Cbc(cipher) => cbc_decrypt(cipher, buffer),
110            #[cfg(feature = "aes-cbc")]
111            Inner::Aes192Cbc(cipher) => cbc_decrypt(cipher, buffer),
112            #[cfg(feature = "aes-cbc")]
113            Inner::Aes256Cbc(cipher) => cbc_decrypt(cipher, buffer),
114            #[cfg(feature = "aes-ctr")]
115            Inner::Aes128Ctr(cipher) => ctr_decrypt(cipher, buffer),
116            #[cfg(feature = "aes-ctr")]
117            Inner::Aes192Ctr(cipher) => ctr_decrypt(cipher, buffer),
118            #[cfg(feature = "aes-ctr")]
119            Inner::Aes256Ctr(cipher) => ctr_decrypt(cipher, buffer),
120            #[cfg(feature = "tdes")]
121            Inner::TDesCbc(cipher) => cbc_decrypt(cipher, buffer),
122        }
123        .map_err(|_| Error::Length)?;
124
125        Ok(())
126    }
127}
128
129impl Debug for Decryptor {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.debug_struct("Decryptor")
132            .field("cipher", &self.cipher())
133            .finish_non_exhaustive()
134    }
135}
136
137/// CBC mode decryption helper which assumes the input is unpadded and block-aligned.
138#[cfg(any(feature = "aes-cbc", feature = "tdes"))]
139fn cbc_decrypt<C>(decryptor: &mut cbc::Decryptor<C>, buffer: &mut [u8]) -> Result<()>
140where
141    C: BlockCipherDecrypt,
142{
143    let (blocks, remaining) = Block::<C>::slice_as_chunks_mut(buffer);
144
145    // Ensure input is block-aligned.
146    if !remaining.is_empty() {
147        return Err(Error::Length);
148    }
149
150    decryptor.decrypt_blocks(blocks);
151    Ok(())
152}