diff --git a/cipher/src/stream.rs b/cipher/src/stream.rs index f6169595d..baa8facbc 100644 --- a/cipher/src/stream.rs +++ b/cipher/src/stream.rs @@ -1,6 +1,6 @@ //! Traits which define functionality of stream ciphers. //! -//! See [RustCrypto/stream-ciphers](https://github.com/RustCrypto/stream-ciphers) +//! See the [RustCrypto/stream-ciphers](https://github.com/RustCrypto/stream-ciphers) repository //! for ciphers implementation. use crate::block::{BlockModeDecrypt, BlockModeEncrypt}; @@ -20,7 +20,7 @@ pub use errors::{OverflowError, StreamCipherError}; #[cfg(feature = "stream-wrapper")] pub use wrapper::StreamCipherCoreWrapper; -/// Marker trait for block-level asynchronous stream ciphers +/// Asynchronous stream cipher trait. pub trait AsyncStreamCipher: Sized { /// Encrypt data using `InOutBuf`. fn encrypt_inout(mut self, data: InOutBuf<'_, '_, u8>) @@ -86,34 +86,127 @@ pub trait AsyncStreamCipher: Sized { } } -/// Synchronous stream cipher core trait. +/// Stream cipher trait. +/// +/// This trait applies only to synchronous stream ciphers, which generate a keystream and +/// XOR data with it during both encryption and decryption. Therefore, instead of separate methods +/// for encryption and decryption, this trait provides methods for keystream application. +/// +/// # Notes on Keystream Repetition +/// All stream ciphers have a finite state, so the generated keystream inevitably repeats itself, +/// making the cipher vulnerable to chosen plaintext attack. Typically, the repetition period is +/// astronomically large, rendering keystream repetition impossible to encounter in practice. +/// +/// However, counter-based stream ciphers allow seeking across the keystream, and some also use +/// small counters (e.g. 32 bits). This can result in triggering keystream repetition in practice. +/// +/// To guard against this, methods either panic (e.g. [`StreamCipher::apply_keystream`]) or +/// return [`StreamCipherError`] (e.g. [`StreamCipher::try_apply_keystream`]) when +/// keystream repetition occurs. We also provide a number of "unchecked" methods +/// (e.g. [`StreamCipher::unchecked_apply_keystream`]), but they should be used with extreme care. +/// +/// For efficiency reasons, the check for keystream repetition is typically implemented by +/// forbidding the generation of the last keystream block in both the keystream application methods +/// and the seeking methods defined in the [`StreamCipherSeek`] trait. pub trait StreamCipher { + /// Check that the cipher can generate a keystream with a length of `data_len` bytes. + fn check_remaining(&self, data_len: usize) -> Result<(), StreamCipherError>; + + /// Apply keystream to `inout` without checking for keystream repetition. + /// + /// # WARNING + /// This method should be used with extreme caution! Triggering keystream repetition can expose + /// the stream cipher to chosen plaintext attacks. + fn unchecked_apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>); + + /// Apply keystream to `buf` without checking for keystream repetition. + /// + /// # WARNING + /// This method should be used with extreme caution! Triggering keystream repetition can expose + /// the stream cipher to chosen plaintext attacks. + fn unchecked_write_keystream(&mut self, buf: &mut [u8]); + + /// Apply keystream to data behind `buf` without checking for keystream repetition. + /// + /// # WARNING + /// This method should be used with extreme caution! Triggering keystream repetition can expose + /// the stream cipher to chosen plaintext attacks. + #[inline] + fn unchecked_apply_keystream(&mut self, buf: &mut [u8]) { + self.unchecked_apply_keystream_inout(buf.into()) + } + + /// Apply keystream to data buffer-to-buffer without checking for keystream repetition. + /// + /// It will XOR generated keystream with data from the `input` buffer + /// and will write result to the `output` buffer. + /// + /// Returns [`NotEqualError`] if the `input` and `output` buffers have different lengths. + /// + /// # WARNING + /// This method should be used with extreme caution! Triggering keystream repetition can expose + /// the stream cipher to chosen plaintext attacks. + #[inline] + fn unchecked_apply_keystream_b2b( + &mut self, + input: &[u8], + output: &mut [u8], + ) -> Result<(), NotEqualError> { + let buf = InOutBuf::new(input, output)?; + self.unchecked_apply_keystream_inout(buf); + Ok(()) + } + /// Apply keystream to `inout` data. /// - /// If end of the keystream will be achieved with the given data length, - /// method will return [`StreamCipherError`] without modifying provided `data`. + /// If the end of the keystream is reached with the given buffer length, + /// the method will return [`StreamCipherError`] without modifying `buf`. fn try_apply_keystream_inout( &mut self, buf: InOutBuf<'_, '_, u8>, - ) -> Result<(), StreamCipherError>; + ) -> Result<(), StreamCipherError> { + self.check_remaining(buf.len())?; + self.unchecked_apply_keystream_inout(buf); + Ok(()) + } /// Apply keystream to data behind `buf`. /// - /// If end of the keystream will be achieved with the given data length, - /// method will return [`StreamCipherError`] without modifying provided `data`. + /// If the end of the keystream is reached with the given buffer length, + /// the method will return [`StreamCipherError`] without modifying `buf`. #[inline] fn try_apply_keystream(&mut self, buf: &mut [u8]) -> Result<(), StreamCipherError> { self.try_apply_keystream_inout(buf.into()) } + /// Apply keystream to data buffer-to-buffer. + /// + /// It will XOR generated keystream with data from the `input` buffer + /// and will write result to the `output` buffer. + /// + /// Returns [`StreamCipherError`] without modifying the buffers if the `input` and `output` + /// buffers have different lengths, or if the end of the keystream is reached with + /// the given data length. + #[inline] + fn try_apply_keystream_b2b( + &mut self, + input: &[u8], + output: &mut [u8], + ) -> Result<(), StreamCipherError> { + InOutBuf::new(input, output) + .map_err(|_| StreamCipherError) + .and_then(|buf| self.try_apply_keystream_inout(buf)) + } + /// Write keystream to `buf`. /// - /// If end of the keystream will be achieved with the given data length, - /// method will return [`StreamCipherError`] without modifying provided `data`. + /// If the end of the keystream is reached with the given buffer length, + /// the method will return [`StreamCipherError`] without modifying `buf`. #[inline] fn try_write_keystream(&mut self, buf: &mut [u8]) -> Result<(), StreamCipherError> { - buf.fill(0); - self.try_apply_keystream(buf) + self.check_remaining(buf.len())?; + self.unchecked_write_keystream(buf); + Ok(()) } /// Apply keystream to `inout` data. @@ -122,8 +215,7 @@ pub trait StreamCipher { /// and will write result to `out` pointer. /// /// # Panics - /// If end of the keystream will be reached with the given data length, - /// method will panic without modifying the provided `data`. + /// If the end of the keystream is reached with the given buffer length. #[inline] fn apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>) { self.try_apply_keystream_inout(buf).unwrap(); @@ -135,72 +227,65 @@ pub trait StreamCipher { /// to the same buffer. /// /// # Panics - /// If end of the keystream will be reached with the given data length, - /// method will panic without modifying the provided `data`. + /// If the end of the keystream is reached with the given buffer length. #[inline] fn apply_keystream(&mut self, buf: &mut [u8]) { self.try_apply_keystream(buf).unwrap(); } - /// Write keystream to `buf`. + /// Apply keystream to data buffer-to-buffer. + /// + /// It will XOR generated keystream with data from the `input` buffer + /// and will write result to the `output` buffer. /// /// # Panics - /// If end of the keystream will be reached with the given data length, - /// method will panic without modifying the provided `data`. + /// If the end of the keystream is reached with the given buffer length, + /// of if the `input` and `output` buffers have different lengths. #[inline] - fn write_keystream(&mut self, buf: &mut [u8]) { - self.try_write_keystream(buf).unwrap(); + fn apply_keystream_b2b(&mut self, input: &[u8], output: &mut [u8]) { + let Ok(buf) = InOutBuf::new(input, output) else { + panic!("Lengths of input and output buffers are not equal to each other!"); + }; + self.apply_keystream_inout(buf) } - /// Apply keystream to data buffer-to-buffer. - /// - /// It will XOR generated keystream with data from the `input` buffer - /// and will write result to the `output` buffer. + /// Write keystream to `buf`. /// - /// Returns [`StreamCipherError`] if provided `in_blocks` and `out_blocks` - /// have different lengths or if end of the keystream will be reached with - /// the given input data length. + /// # Panics + /// If the end of the keystream is reached with the given buffer length. #[inline] - fn apply_keystream_b2b( - &mut self, - input: &[u8], - output: &mut [u8], - ) -> Result<(), StreamCipherError> { - InOutBuf::new(input, output) - .map_err(|_| StreamCipherError) - .and_then(|buf| self.try_apply_keystream_inout(buf)) + fn write_keystream(&mut self, buf: &mut [u8]) { + self.try_write_keystream(buf).unwrap(); } } /// Trait for seekable stream ciphers. /// -/// Methods of this trait are generic over the [`SeekNum`] trait, which is -/// implemented for primitive numeric types, i.e.: `i32`, `u32`, `u64`, -/// `u128`, and `usize`. +/// Methods of this trait are generic over the [`SeekNum`] trait, +/// i.e. they can be used with `i32`, `u32`, `u64`, `u128`, and `usize`. pub trait StreamCipherSeek { - /// Try to get current keystream position + /// Try to get current keystream position in bytes. /// - /// Returns [`OverflowError`] if position can not be represented by type `T` + /// Returns [`OverflowError`] if the position value can not be represented by type `T`. fn try_current_pos(&self) -> Result; - /// Try to seek to the given position + /// Try to seek to the provided position in bytes. /// - /// Returns [`StreamCipherError`] if provided position value is bigger than - /// keystream length. + /// Returns [`StreamCipherError`] if the position value is bigger than keystream length. fn try_seek(&mut self, pos: T) -> Result<(), StreamCipherError>; - /// Get current keystream position + /// Get current keystream position in bytes. /// /// # Panics - /// If position can not be represented by type `T` + /// If the position value can not be represented by type `T`. fn current_pos(&self) -> T { self.try_current_pos().unwrap() } - /// Seek to the given position + /// Seek to the provided keystream position in bytes. /// /// # Panics - /// If provided position value is bigger than keystream length + /// If the position value is bigger than keystream length. fn seek(&mut self, pos: T) { self.try_seek(pos).unwrap() } @@ -208,11 +293,18 @@ pub trait StreamCipherSeek { impl StreamCipher for &mut C { #[inline] - fn try_apply_keystream_inout( - &mut self, - buf: InOutBuf<'_, '_, u8>, - ) -> Result<(), StreamCipherError> { - C::try_apply_keystream_inout(self, buf) + fn check_remaining(&self, data_len: usize) -> Result<(), StreamCipherError> { + C::check_remaining(self, data_len) + } + + #[inline] + fn unchecked_apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>) { + C::unchecked_apply_keystream_inout(self, buf) + } + + #[inline] + fn unchecked_write_keystream(&mut self, buf: &mut [u8]) { + C::unchecked_write_keystream(self, buf) } } @@ -249,8 +341,8 @@ macro_rules! impl_seek_num { } fn into_block_byte(self, block_size: u8) -> Result<(T, u8), OverflowError> { - let bs: Self = block_size.into(); - let byte = (self % bs) as u8; + let bs = Self::from(block_size); + let byte = u8::try_from(self % bs).expect("bs fits into u8"); let block = T::try_from(self / bs).map_err(|_| OverflowError)?; Ok((block, byte)) } diff --git a/cipher/src/stream/core_api.rs b/cipher/src/stream/core_api.rs index 200c1fee0..b1c68ec74 100644 --- a/cipher/src/stream/core_api.rs +++ b/cipher/src/stream/core_api.rs @@ -37,11 +37,10 @@ pub trait StreamCipherClosure: BlockSizeUser { /// Block-level synchronous stream ciphers. pub trait StreamCipherCore: BlockSizeUser + Sized { - /// Return number of remaining blocks before cipher wraps around. + /// Return number of remaining blocks before the cipher wraps around. /// /// Returns `None` if number of remaining blocks can not be computed - /// (e.g. in ciphers based on the sponge construction) or it's too big - /// to fit into `usize`. + /// (e.g. in the case of sponge-based stream ciphers) or it’s too big to fit into `usize`. fn remaining_blocks(&self) -> Option; /// Process data using backend provided to the rank-2 closure. @@ -91,23 +90,23 @@ pub trait StreamCipherCore: BlockSizeUser + Sized { /// Try to apply keystream to data not divided into blocks. /// - /// Consumes cipher since it may consume final keystream block only - /// partially. + /// Consumes cipher since it may consume the final keystream block only partially. /// - /// Returns an error if number of remaining blocks is not sufficient - /// for processing the input data. + /// Returns an error if the number of remaining blocks is not sufficient + /// for processing of the input data. #[inline] fn try_apply_keystream_partial( mut self, mut buf: InOutBuf<'_, '_, u8>, ) -> Result<(), StreamCipherError> { - if let Some(rem) = self.remaining_blocks() { - let blocks = if buf.len() % Self::BlockSize::USIZE == 0 { - buf.len() % Self::BlockSize::USIZE - } else { - buf.len() % Self::BlockSize::USIZE + 1 - }; - if blocks > rem { + if let Some(rem_blocks) = self.remaining_blocks() { + // Note that if `rem_blocks` is equal to zero, it means that + // the next generated block will be the last in the keystream and + // the cipher core will wrap to its initial state. + // Since we consume `self`, it's fine to generate the last keystream block, + // so we can use division instead of `div_ceil` to compute `req_blocks`. + let req_blocks = buf.len() / Self::BlockSize::USIZE; + if req_blocks > rem_blocks { return Err(StreamCipherError); } } @@ -164,7 +163,10 @@ pub trait StreamCipherCounter: + TryInto + TryInto + TryInto + + Copy { + /// Returns `true` if `self` is equal to the max counter value. + fn is_max(&self) -> bool; } /// Block-level seeking trait for stream ciphers. @@ -181,7 +183,13 @@ pub trait StreamCipherSeekCore: StreamCipherCore { macro_rules! impl_counter { {$($t:ty )*} => { - $( impl StreamCipherCounter for $t { } )* + $( + impl StreamCipherCounter for $t { + fn is_max(&self) -> bool { + *self == <$t>::MAX + } + } + )* }; } diff --git a/cipher/src/stream/wrapper.rs b/cipher/src/stream/wrapper.rs index 4529b87ec..cce2f10b3 100644 --- a/cipher/src/stream/wrapper.rs +++ b/cipher/src/stream/wrapper.rs @@ -1,3 +1,5 @@ +use crate::StreamCipherCounter; + use super::{ OverflowError, SeekNum, StreamCipher, StreamCipherCore, StreamCipherSeek, StreamCipherSeekCore, errors::StreamCipherError, @@ -49,37 +51,27 @@ impl StreamCipherCoreWrapper { pub fn get_core(&self) -> &T { &self.core } +} +impl StreamCipher for StreamCipherCoreWrapper { + #[inline] fn check_remaining(&self, data_len: usize) -> Result<(), StreamCipherError> { - let rem_blocks = match self.core.remaining_blocks() { - Some(v) => v, - None => return Ok(()), + let Some(rem_blocks) = self.core.remaining_blocks() else { + return Ok(()); }; - - let buf_rem = self.buffer.remaining(); - let data_len = match data_len.checked_sub(buf_rem) { - Some(0) | None => return Ok(()), - Some(res) => res, + let Some(data_len) = data_len.checked_sub(self.buffer.remaining()) else { + return Ok(()); }; - - let bs = T::BlockSize::USIZE; - let blocks = data_len.div_ceil(bs); - if blocks > rem_blocks { + let req_blocks = data_len.div_ceil(T::BlockSize::USIZE); + if req_blocks > rem_blocks { Err(StreamCipherError) } else { Ok(()) } } -} -impl StreamCipher for StreamCipherCoreWrapper { #[inline] - fn try_apply_keystream_inout( - &mut self, - data: InOutBuf<'_, '_, u8>, - ) -> Result<(), StreamCipherError> { - self.check_remaining(data.len())?; - + fn unchecked_apply_keystream_inout(&mut self, data: InOutBuf<'_, '_, u8>) { let head_ks = self.buffer.read_cached(data.len()); let (mut head, data) = data.split_at(head_ks.len()); @@ -95,14 +87,10 @@ impl StreamCipher for StreamCipherCoreWrapper { tail.xor_in2out(tail_ks); }, ); - - Ok(()) } #[inline] - fn try_write_keystream(&mut self, data: &mut [u8]) -> Result<(), StreamCipherError> { - self.check_remaining(data.len())?; - + fn unchecked_write_keystream(&mut self, data: &mut [u8]) { let head_ks = self.buffer.read_cached(data.len()); let (head, data) = data.split_at_mut(head_ks.len()); @@ -116,8 +104,6 @@ impl StreamCipher for StreamCipherCoreWrapper { |b| self.core.write_keystream_block(b), |tail_ks| tail.copy_from_slice(tail_ks), ); - - Ok(()) } } @@ -129,8 +115,11 @@ impl StreamCipherSeek for StreamCipherCoreWrapper { } fn try_seek(&mut self, new_pos: SN) -> Result<(), StreamCipherError> { - let (block_pos, byte_pos) = new_pos.into_block_byte(T::BlockSize::U8)?; - // For correct implementations of `SeekNum` compiler should be able to + let (block_pos, byte_pos) = new_pos.into_block_byte::(T::BlockSize::U8)?; + if byte_pos != 0 && block_pos.is_max() { + return Err(StreamCipherError); + } + // For correct implementations of `SeekNum` the compiler should be able to // eliminate this assert assert!(byte_pos < T::BlockSize::U8); diff --git a/cipher/tests/stream.rs b/cipher/tests/stream.rs index 359e0f7d4..7f9d8c547 100644 --- a/cipher/tests/stream.rs +++ b/cipher/tests/stream.rs @@ -101,6 +101,8 @@ fn dummy_stream_cipher_core() { #[cfg(feature = "stream-wrapper")] mod wrapper { + use core::panic; + use super::*; use cipher::{StreamCipher, StreamCipherCoreWrapper, StreamCipherSeek}; @@ -127,26 +129,37 @@ mod wrapper { #[test] fn dummy_stream_cipher_seek_limit() { let mut cipher = DummyStreamCipher::new(&KEY.into(), &IV.into()); + let mut buf = [0u8; 64]; + + let block_size = DummyStreamCipherCore::block_size(); + let block_size_u128 = u128::try_from(block_size).unwrap(); + let keystream_end = 1u128 << 68; + let last_block_pos = keystream_end - block_size_u128; + + // Seeking to the last block or past it should return error + for offset in 0..block_size_u128 { + let res = cipher.try_seek(keystream_end - offset); + assert!(res.is_err()); + let res = cipher.try_seek(keystream_end + offset); + assert!(res.is_err()); + } - let pos = ((u64::MAX as u128) << 4) - 20; - cipher.try_seek(pos).unwrap(); - - let mut buf = [0u8; 30]; - let res = cipher.try_apply_keystream(&mut buf); - assert!(res.is_err()); - let cur_pos: u128 = cipher.current_pos(); - assert_eq!(cur_pos, pos); - - let res = cipher.try_apply_keystream(&mut buf[..19]); - assert!(res.is_ok()); - let cur_pos: u128 = cipher.current_pos(); - assert_eq!(cur_pos, pos + 19); - - cipher.try_seek(pos).unwrap(); - - // TODO: fix as part of https://github.com/RustCrypto/traits/issues/1808 - // let res = cipher.try_apply_keystream(&mut buf[..20]); - // assert!(res.is_err()); + // Trying to apply the last keystream block should return error + for offset in block_size..buf.len() { + for len in 0..buf.len() { + let pos = keystream_end - u128::try_from(offset).unwrap(); + let res = cipher.try_seek(pos); + assert!(res.is_ok()); + let res = cipher.try_apply_keystream(&mut buf[..len]); + let expected_pos = pos + u128::try_from(len).unwrap(); + if expected_pos > last_block_pos { + assert!(res.is_err()); + } else { + assert!(res.is_ok()); + assert_eq!(cipher.current_pos::(), expected_pos); + } + } + } } #[cfg(feature = "dev")]