diff --git a/ssh-cipher/Cargo.toml b/ssh-cipher/Cargo.toml index ed4b4052..8ddec639 100644 --- a/ssh-cipher/Cargo.toml +++ b/ssh-cipher/Cargo.toml @@ -52,6 +52,8 @@ zeroize = [ "poly1305?/zeroize" ] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "docsrs"] diff --git a/ssh-cipher/README.md b/ssh-cipher/README.md index 6041910c..655cac77 100644 --- a/ssh-cipher/README.md +++ b/ssh-cipher/README.md @@ -19,13 +19,6 @@ ciphers. Built on the pure Rust cryptography implementations maintained by the [RustCrypto] organization. -## Minimum Supported Rust Version - -This crate requires **Rust 1.85** at a minimum. - -We may change the MSRV in the future, but it will be accompanied by a minor -version bump. - ## License Licensed under either of: diff --git a/ssh-cipher/src/chacha20poly1305.rs b/ssh-cipher/src/chacha20poly1305.rs index f9ea1e80..134297f5 100644 --- a/ssh-cipher/src/chacha20poly1305.rs +++ b/ssh-cipher/src/chacha20poly1305.rs @@ -9,6 +9,7 @@ use aead::{ inout::InOutBuf, }; use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; +use core::fmt::{self, Debug}; use ctutils::CtEq; use poly1305::{Poly1305, universal_hash::UniversalHash}; @@ -59,14 +60,13 @@ impl AeadCore for ChaCha20Poly1305 { } impl AeadInOut for ChaCha20Poly1305 { - // Required methods fn encrypt_inout_detached( &self, nonce: &ChaChaNonce, associated_data: &[u8], buffer: InOutBuf<'_, '_, u8>, ) -> Result { - Cipher::new(&self.key, nonce).encrypt(associated_data, buffer) + Cipher::new(&self.key, *nonce).encrypt(associated_data, buffer) } fn decrypt_inout_detached( @@ -76,7 +76,13 @@ impl AeadInOut for ChaCha20Poly1305 { buffer: InOutBuf<'_, '_, u8>, tag: &Tag, ) -> Result<()> { - Cipher::new(&self.key, nonce).decrypt(associated_data, buffer, tag) + Cipher::new(&self.key, *nonce).decrypt(associated_data, buffer, tag) + } +} + +impl Debug for ChaCha20Poly1305 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChaCha20Poly1305").finish_non_exhaustive() } } @@ -98,8 +104,8 @@ struct Cipher { impl Cipher { /// Create a new cipher instance. - pub fn new(key: &ChaChaKey, nonce: &ChaChaNonce) -> Self { - let mut cipher = ChaCha20::new(key, nonce); + fn new(key: &ChaChaKey, nonce: ChaChaNonce) -> Self { + let mut cipher = ChaCha20::new(key, &nonce); let mut poly1305_key = poly1305::Key::default(); cipher.apply_keystream(&mut poly1305_key); @@ -113,7 +119,7 @@ impl Cipher { /// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag. #[inline] - pub fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result { + fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result { self.cipher.apply_keystream_inout(buffer.reborrow()); compute_mac(self.mac, aad, buffer.get_out()) } @@ -121,7 +127,7 @@ impl Cipher { /// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305 /// authentication `tag`. #[inline] - pub fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> { + fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> { let expected_tag = compute_mac(self.mac, aad, buffer.get_in())?; if expected_tag.ct_eq(tag).into() { diff --git a/ssh-cipher/src/decryptor.rs b/ssh-cipher/src/decryptor.rs index b1423ebd..22917968 100644 --- a/ssh-cipher/src/decryptor.rs +++ b/ssh-cipher/src/decryptor.rs @@ -2,21 +2,19 @@ use crate::{Cipher, Error, Result}; use cipher::KeyIvInit; +use core::fmt::{self, Debug}; #[cfg(feature = "aes-ctr")] use crate::{Ctr128BE, encryptor::ctr_encrypt as ctr_decrypt}; - -#[cfg(feature = "tdes")] -use des::TdesEde3; - #[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] use aes::{Aes128, Aes192, Aes256}; - #[cfg(any(feature = "aes-cbc", feature = "tdes"))] use cipher::{ Block, block::{BlockCipherDecrypt, BlockModeDecrypt}, }; +#[cfg(feature = "tdes")] +use des::TdesEde3; /// Stateful decryptor object for unauthenticated SSH symmetric ciphers. /// @@ -46,7 +44,13 @@ enum Inner { } impl Decryptor { - /// Create a new decryptor object with the given [`Cipher`], key, and IV. + /// Create a new decryptor object with the given [`Cipher`], `key`, and `iv` (i.e. + /// initialization vector). + /// + /// # Errors + /// - Returns [`Error::Length`] if `key` or `iv` are the wrong length for the given `cipher`. + /// - Returns [`Error::UnsupportedCipher`] if support for the given `cipher` is not enabled + /// in the crate features. pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { cipher.check_key_and_iv(key, iv)?; @@ -73,6 +77,7 @@ impl Decryptor { } /// Get the cipher for this decryptor. + #[must_use] pub fn cipher(&self) -> Cipher { match &self.inner { #[cfg(feature = "aes-cbc")] @@ -94,6 +99,7 @@ impl Decryptor { /// Decrypt the given buffer in place. /// + /// # Errors /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's /// block size. pub fn decrypt(&mut self, buffer: &mut [u8]) -> Result<()> { @@ -120,6 +126,14 @@ impl Decryptor { } } +impl Debug for Decryptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Decryptor") + .field("cipher", &self.cipher()) + .finish_non_exhaustive() + } +} + /// CBC mode decryption helper which assumes the input is unpadded and block-aligned. #[cfg(any(feature = "aes-cbc", feature = "tdes"))] fn cbc_decrypt(decryptor: &mut cbc::Decryptor, buffer: &mut [u8]) -> Result<()> diff --git a/ssh-cipher/src/encryptor.rs b/ssh-cipher/src/encryptor.rs index 7082e276..07313cc6 100644 --- a/ssh-cipher/src/encryptor.rs +++ b/ssh-cipher/src/encryptor.rs @@ -2,22 +2,20 @@ use crate::{Cipher, Error, Result}; use cipher::{Block, BlockCipherEncrypt, KeyIvInit}; +use core::fmt::{self, Debug}; +#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] +use aes::{Aes128, Aes192, Aes256}; +#[cfg(any(feature = "aes-cbc", feature = "tdes"))] +use cipher::block::BlockModeEncrypt; +#[cfg(feature = "tdes")] +use des::TdesEde3; #[cfg(feature = "aes-ctr")] use { crate::Ctr128BE, cipher::{BlockSizeUser, StreamCipherCore, array::sizes::U16}, }; -#[cfg(feature = "tdes")] -use des::TdesEde3; - -#[cfg(any(feature = "aes-cbc", feature = "aes-ctr"))] -use aes::{Aes128, Aes192, Aes256}; - -#[cfg(any(feature = "aes-cbc", feature = "tdes"))] -use cipher::block::BlockModeEncrypt; - /// Stateful encryptor object for unauthenticated SSH symmetric ciphers. /// /// Note that this deliberately does not support AEAD modes such as AES-GCM and ChaCha20Poly1305, @@ -46,7 +44,13 @@ enum Inner { } impl Encryptor { - /// Create a new encryptor object with the given [`Cipher`], key, and IV. + /// Create a new encryptor object with the given [`Cipher`], `key`, and `iv` (i.e. + /// initialization vector). + /// + /// # Errors + /// - Returns [`Error::Length`] if `key` or `iv` are the wrong length for the given `cipher`. + /// - Returns [`Error::UnsupportedCipher`] if support for the given `cipher` is not enabled + /// in the crate features. pub fn new(cipher: Cipher, key: &[u8], iv: &[u8]) -> Result { cipher.check_key_and_iv(key, iv)?; @@ -73,6 +77,7 @@ impl Encryptor { } /// Get the cipher for this encryptor. + #[must_use] pub fn cipher(&self) -> Cipher { match &self.inner { #[cfg(feature = "aes-cbc")] @@ -94,6 +99,7 @@ impl Encryptor { /// Encrypt the given buffer in place. /// + /// # Errors /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's /// block size. pub fn encrypt(&mut self, buffer: &mut [u8]) -> Result<()> { @@ -118,6 +124,14 @@ impl Encryptor { } } +impl Debug for Encryptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Encryptor") + .field("cipher", &self.cipher()) + .finish_non_exhaustive() + } +} + /// CBC mode encryption helper which assumes the input is unpadded and block-aligned. #[cfg(any(feature = "aes-cbc", feature = "tdes"))] fn cbc_encrypt(encryptor: &mut cbc::Encryptor, buffer: &mut [u8]) -> Result<()> diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 69f41d43..f452a0df 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -1,25 +1,10 @@ #![no_std] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc( html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg" )] -#![forbid(unsafe_code)] -#![warn( - clippy::alloc_instead_of_core, - clippy::arithmetic_side_effects, - clippy::mod_module_files, - clippy::panic, - clippy::panic_in_result_fn, - clippy::std_instead_of_alloc, - clippy::std_instead_of_core, - clippy::unwrap_used, - missing_docs, - rust_2018_idioms, - unused_lifetimes, - unused_qualifications -)] mod error; @@ -138,12 +123,25 @@ impl Cipher { /// Decode cipher algorithm from the given `ciphername`. /// /// # Supported cipher names - /// - `aes256-ctr` + /// `aes128-cbc` + /// `aes192-cbc` + /// `aes256-cbc` + /// `aes128-ctr` + /// `aes192-ctr` + /// `aes256-ctr` + /// `aes128-gcm@openssh.com` + /// `aes256-gcm@openssh.com` + /// `chacha20-poly1305@openssh.com` + /// `3des-cbc` + /// + /// # Errors + /// Returns [`LabelError`] if the provided `ciphername` is unknown. pub fn new(ciphername: &str) -> core::result::Result { ciphername.parse() } /// Get the string identifier which corresponds to this algorithm. + #[must_use] pub fn as_str(self) -> &'static str { match self { Self::None => "none", @@ -161,6 +159,7 @@ impl Cipher { } /// Get the key and IV size for this cipher in bytes. + #[must_use] pub fn key_and_iv_size(self) -> Option<(usize, usize)> { match self { Self::None => None, @@ -178,6 +177,7 @@ impl Cipher { } /// Get the block size for this cipher in bytes. + #[must_use] pub fn block_size(self) -> usize { match self { Self::None | Self::ChaCha20Poly1305 | Self::TDesCbc => 8, @@ -195,7 +195,12 @@ impl Cipher { /// Compute the length of padding necessary to pad the given input to /// the block size. #[allow(clippy::arithmetic_side_effects)] + #[must_use] pub fn padding_len(self, input_size: usize) -> usize { + #[allow( + clippy::integer_division_remainder_used, + reason = "input_size is non-secret" + )] match input_size % self.block_size() { 0 => 0, input_rem => self.block_size() - input_rem, @@ -203,6 +208,7 @@ impl Cipher { } /// Does this cipher have an authentication tag? (i.e. is it an AEAD mode?) + #[must_use] pub fn has_tag(self) -> bool { matches!( self, @@ -211,17 +217,20 @@ impl Cipher { } /// Is this cipher `none`? + #[must_use] pub fn is_none(self) -> bool { self == Self::None } /// Is the cipher anything other than `none`? + #[must_use] pub fn is_some(self) -> bool { !self.is_none() } /// Decrypt the ciphertext in the `buffer` in-place using this cipher. /// + /// # Errors /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's /// block size. #[cfg_attr( @@ -280,6 +289,9 @@ impl Cipher { /// /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305). + /// + /// # Errors + /// Propagates errors from [`Decryptor::new`]. #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] pub fn decryptor(self, key: &[u8], iv: &[u8]) -> Result { Decryptor::new(self, key, iv) @@ -287,6 +299,7 @@ impl Cipher { /// Encrypt the ciphertext in the `buffer` in-place using this cipher. /// + /// # Errors /// Returns [`Error::Length`] in the event that `buffer` is not a multiple of the cipher's /// block size. #[cfg_attr( @@ -339,6 +352,9 @@ impl Cipher { /// /// Only applicable to unauthenticated modes (e.g. AES-CBC, AES-CTR). Not usable with /// authenticated modes which are inherently one-shot (AES-GCM, ChaCha20Poly1305). + /// + /// # Errors + /// Propagates errors from [`Encryptor::new`]. #[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))] pub fn encryptor(self, key: &[u8], iv: &[u8]) -> Result { Encryptor::new(self, key, iv) diff --git a/ssh-cipher/tests/lib.rs b/ssh-cipher/tests/lib.rs index c4cb861f..dc3b6207 100644 --- a/ssh-cipher/tests/lib.rs +++ b/ssh-cipher/tests/lib.rs @@ -1,5 +1,6 @@ -/// Integration tests for `ssh-cipher`. +//! Integration tests for `ssh-cipher`. // TODO(tarcieri): test vectors for each supported cipher + use ssh_cipher::Cipher; #[test]