Unverified Commit 4edda635 authored by Steven Fackler's avatar Steven Fackler Committed by GitHub
Browse files

Merge pull request #1733 from wiktor-k/relax-cipher-update

Make `CipherCtx::cipher_update` more flexible
parents 7db5cc72 5ecff30d
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -97,6 +97,11 @@ cfg_if! {
        pub unsafe fn EVP_CIPHER_CTX_iv_length(ctx: *const EVP_CIPHER_CTX) -> c_int {
            EVP_CIPHER_CTX_get_iv_length(ctx)
        }

        #[inline]
        pub unsafe fn EVP_CIPHER_CTX_num(ctx: *const EVP_CIPHER_CTX) -> c_int {
            EVP_CIPHER_CTX_get_num(ctx)
        }
    } else {
        pub unsafe fn EVP_MD_CTX_size(ctx: *const EVP_MD_CTX) -> c_int {
            EVP_MD_size(EVP_MD_CTX_md(ctx))
+3 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ cfg_if! {
            pub fn EVP_CIPHER_CTX_get_key_length(ctx: *const EVP_CIPHER_CTX) -> c_int;
            pub fn EVP_CIPHER_CTX_get_iv_length(ctx: *const EVP_CIPHER_CTX) -> c_int;
            pub fn EVP_CIPHER_CTX_get_tag_length(ctx: *const EVP_CIPHER_CTX) -> c_int;
            pub fn EVP_CIPHER_CTX_get_num(ctx: *const EVP_CIPHER_CTX) -> c_int;
        }
    } else {
        extern "C" {
@@ -44,6 +45,8 @@ cfg_if! {
            pub fn EVP_CIPHER_CTX_block_size(ctx: *const EVP_CIPHER_CTX) -> c_int;
            pub fn EVP_CIPHER_CTX_key_length(ctx: *const EVP_CIPHER_CTX) -> c_int;
            pub fn EVP_CIPHER_CTX_iv_length(ctx: *const EVP_CIPHER_CTX) -> c_int;
            #[cfg(ossl110)]
            pub fn EVP_CIPHER_CTX_num(ctx: *const EVP_CIPHER_CTX) -> c_int;
        }
    }
}
+302 −18
Original line number Diff line number Diff line
@@ -363,6 +363,65 @@ impl CipherCtxRef {
        unsafe { ffi::EVP_CIPHER_CTX_iv_length(self.as_ptr()) as usize }
    }

    /// Returns the `num` parameter of the cipher.
    ///
    /// Built-in ciphers typically use this to track how much of the
    /// current underlying block has been "used" already.
    ///
    /// # Panics
    ///
    /// Panics if the context has not been initialized with a cipher.
    #[corresponds(EVP_CIPHER_CTX_num)]
    #[cfg(ossl110)]
    pub fn num(&self) -> usize {
        self.assert_cipher();

        unsafe { ffi::EVP_CIPHER_CTX_num(self.as_ptr()) as usize }
    }

    /// Returns number of bytes cached in partial block update.
    #[cfg(ossl110)]
    fn used_block_size(&self) -> usize {
        self.num()
    }

    /// Returns maximum number of bytes that could be cached.
    #[cfg(not(ossl110))]
    fn used_block_size(&self) -> usize {
        self.block_size()
    }

    /// Calculate the minimal size of the output buffer given the
    /// input buffer size.
    ///
    /// For streaming ciphers the minimal output size is the same as
    /// the input size. For block ciphers the minimal output size
    /// additionally depends on the partial blocks that might have
    /// been written in previous calls to [`Self::cipher_update`].
    ///
    /// This function takes into account the number of partially
    /// written blocks for block ciphers for supported targets
    /// (OpenSSL >= 1.1). For unsupported targets the number of
    /// partially written bytes is assumed to contain one full block
    /// (pessimistic case).
    ///
    /// # Panics
    ///
    /// Panics if the context has not been initialized with a cipher.
    pub fn minimal_output_size(&self, inlen: usize) -> usize {
        let block_size = self.block_size();
        if block_size > 1 {
            // block cipher
            let num = self.used_block_size();
            let total_size = inlen + num;
            let num_blocks = total_size / block_size;
            num_blocks * block_size
        } else {
            // streaming cipher
            inlen
        }
    }

    /// Sets the length of the IV expected by this context.
    ///
    /// Only some ciphers support configurable IV lengths.
@@ -501,25 +560,54 @@ impl CipherCtxRef {
    ///
    /// # Panics
    ///
    /// Panics if `output.len()` is less than `input.len()` plus the cipher's block size.
    /// Panics if `output` doesn't contain enough space for data to be
    /// written as specified by [`Self::minimal_output_size`].
    #[corresponds(EVP_CipherUpdate)]
    pub fn cipher_update(
        &mut self,
        input: &[u8],
        output: Option<&mut [u8]>,
    ) -> Result<usize, ErrorStack> {
        let inlen = c_int::try_from(input.len()).unwrap();

        if let Some(output) = &output {
            let mut block_size = self.block_size();
            if block_size == 1 {
                block_size = 0;
            let min_output_size = self.minimal_output_size(input.len());
            assert!(
                output.len() >= min_output_size,
                "Output buffer size should be at least {} bytes.",
                min_output_size
            );
        }
            assert!(output.len() >= input.len() + block_size);

        unsafe { self.cipher_update_unchecked(input, output) }
    }

    /// Writes data into the context.
    ///
    /// Providing no output buffer will cause the input to be considered additional authenticated data (AAD).
    ///
    /// Returns the number of bytes written to `output`.
    ///
    /// This function is the same as [`Self::cipher_update`] but with the
    /// output size check removed. It can be used when the exact
    /// buffer size control is maintained by the caller and the
    /// underlying cryptographic library doesn't expose exact block
    /// cache data (e.g. OpenSSL < 1.1, BoringSSL, LibreSSL).
    ///
    /// SAFETY: The caller is expected to provide `output` buffer
    /// large enough to contain correct number of bytes. For streaming
    /// ciphers the output buffer size should be at least as big as
    /// the input buffer. For block ciphers the size of the output
    /// buffer depends on the state of partially updated blocks (see
    /// [`Self::minimal_output_size`]).
    #[corresponds(EVP_CipherUpdate)]
    pub unsafe fn cipher_update_unchecked(
        &mut self,
        input: &[u8],
        output: Option<&mut [u8]>,
    ) -> Result<usize, ErrorStack> {
        let inlen = c_int::try_from(input.len()).unwrap();

        let mut outlen = 0;
        unsafe {

        cvt(ffi::EVP_CipherUpdate(
            self.as_ptr(),
            output.map_or(ptr::null_mut(), |b| b.as_mut_ptr()),
@@ -527,7 +615,6 @@ impl CipherCtxRef {
            input.as_ptr(),
            inlen,
        ))?;
        }

        Ok(outlen as usize)
    }
@@ -588,7 +675,7 @@ impl CipherCtxRef {
#[cfg(test)]
mod test {
    use super::*;
    use crate::cipher::Cipher;
    use crate::{cipher::Cipher, rand::rand_bytes};
    #[cfg(not(boringssl))]
    use std::slice;

@@ -669,4 +756,201 @@ mod test {
        let cipher = Cipher::aes_128_cbc();
        aes_128_cbc(cipher);
    }

    #[test]
    #[cfg(ossl110)]
    fn partial_block_updates() {
        test_block_cipher_for_partial_block_updates(Cipher::aes_128_cbc());
        test_block_cipher_for_partial_block_updates(Cipher::aes_256_cbc());
        test_block_cipher_for_partial_block_updates(Cipher::des_ede3_cbc());
    }

    #[cfg(ossl110)]
    fn test_block_cipher_for_partial_block_updates(cipher: &'static CipherRef) {
        let mut key = vec![0; cipher.key_length()];
        rand_bytes(&mut key).unwrap();
        let mut iv = vec![0; cipher.iv_length()];
        rand_bytes(&mut iv).unwrap();

        let mut ctx = CipherCtx::new().unwrap();

        ctx.encrypt_init(Some(cipher), Some(&key), Some(&iv))
            .unwrap();
        ctx.set_padding(false);

        let block_size = cipher.block_size();
        assert!(block_size > 1, "Need a block cipher, not a stream cipher");

        // update cipher with non-full block
        // expect no output until a block is complete
        let outlen = ctx
            .cipher_update(&vec![0; block_size - 1], Some(&mut [0; 0]))
            .unwrap();
        assert_eq!(0, outlen);

        // update cipher with missing bytes from the previous block
        // and one additional block, output should contain two blocks
        let mut two_blocks = vec![0; block_size * 2];
        let outlen = ctx
            .cipher_update(&vec![0; block_size + 1], Some(&mut two_blocks))
            .unwrap();
        assert_eq!(block_size * 2, outlen);

        ctx.cipher_final_vec(&mut vec![0; 0]).unwrap();

        // try to decrypt
        ctx.decrypt_init(Some(cipher), Some(&key), Some(&iv))
            .unwrap();
        ctx.set_padding(false);

        // update cipher with non-full block
        // expect no output until a block is complete
        let outlen = ctx
            .cipher_update(&two_blocks[0..block_size - 1], Some(&mut [0; 0]))
            .unwrap();
        assert_eq!(0, outlen);

        // update cipher with missing bytes from the previous block
        // and one additional block, output should contain two blocks
        let mut two_blocks_decrypted = vec![0; block_size * 2];
        let outlen = ctx
            .cipher_update(
                &two_blocks[block_size - 1..],
                Some(&mut two_blocks_decrypted),
            )
            .unwrap();
        assert_eq!(block_size * 2, outlen);

        ctx.cipher_final_vec(&mut vec![0; 0]).unwrap();
        // check if the decrypted blocks are the same as input (all zeros)
        assert_eq!(two_blocks_decrypted, vec![0; block_size * 2]);
    }

    #[test]
    fn test_stream_ciphers() {
        test_stream_cipher(Cipher::aes_192_ctr());
        test_stream_cipher(Cipher::aes_256_ctr());
    }

    fn test_stream_cipher(cipher: &'static CipherRef) {
        let mut key = vec![0; cipher.key_length()];
        rand_bytes(&mut key).unwrap();
        let mut iv = vec![0; cipher.iv_length()];
        rand_bytes(&mut iv).unwrap();

        let mut ctx = CipherCtx::new().unwrap();

        ctx.encrypt_init(Some(cipher), Some(&key), Some(&iv))
            .unwrap();
        ctx.set_padding(false);

        assert_eq!(
            1,
            cipher.block_size(),
            "Need a stream cipher, not a block cipher"
        );

        // update cipher with non-full block
        // this is a streaming cipher so the number of output bytes
        // will be the same as the number of input bytes
        let mut output = vec![0; 32];
        let outlen = ctx
            .cipher_update(&[1; 15], Some(&mut output[0..15]))
            .unwrap();
        assert_eq!(15, outlen);

        // update cipher with missing bytes from the previous block
        // as previously it will output the same number of bytes as
        // the input
        let outlen = ctx
            .cipher_update(&[1; 17], Some(&mut output[15..]))
            .unwrap();
        assert_eq!(17, outlen);

        ctx.cipher_final_vec(&mut vec![0; 0]).unwrap();

        // try to decrypt
        ctx.decrypt_init(Some(cipher), Some(&key), Some(&iv))
            .unwrap();
        ctx.set_padding(false);

        // update cipher with non-full block
        // expect that the output for stream cipher will contain
        // the same number of bytes as the input
        let mut output_decrypted = vec![0; 32];
        let outlen = ctx
            .cipher_update(&output[0..15], Some(&mut output_decrypted[0..15]))
            .unwrap();
        assert_eq!(15, outlen);

        let outlen = ctx
            .cipher_update(&output[15..], Some(&mut output_decrypted[15..]))
            .unwrap();
        assert_eq!(17, outlen);

        ctx.cipher_final_vec(&mut vec![0; 0]).unwrap();
        // check if the decrypted blocks are the same as input (all ones)
        assert_eq!(output_decrypted, vec![1; 32]);
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 16 bytes.")]
    #[cfg(ossl110)]
    fn full_block_updates_aes_128() {
        output_buffer_too_small(Cipher::aes_128_cbc());
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 16 bytes.")]
    #[cfg(ossl110)]
    fn full_block_updates_aes_256() {
        output_buffer_too_small(Cipher::aes_256_cbc());
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 8 bytes.")]
    #[cfg(ossl110)]
    fn full_block_updates_3des() {
        output_buffer_too_small(Cipher::des_ede3_cbc());
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 32 bytes.")]
    #[cfg(not(ossl110))]
    fn full_block_updates_aes_128() {
        output_buffer_too_small(Cipher::aes_128_cbc());
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 32 bytes.")]
    #[cfg(not(ossl110))]
    fn full_block_updates_aes_256() {
        output_buffer_too_small(Cipher::aes_256_cbc());
    }

    #[test]
    #[should_panic(expected = "Output buffer size should be at least 16 bytes.")]
    #[cfg(not(ossl110))]
    fn full_block_updates_3des() {
        output_buffer_too_small(Cipher::des_ede3_cbc());
    }

    fn output_buffer_too_small(cipher: &'static CipherRef) {
        let mut key = vec![0; cipher.key_length()];
        rand_bytes(&mut key).unwrap();
        let mut iv = vec![0; cipher.iv_length()];
        rand_bytes(&mut iv).unwrap();

        let mut ctx = CipherCtx::new().unwrap();

        ctx.encrypt_init(Some(cipher), Some(&key), Some(&iv))
            .unwrap();
        ctx.set_padding(false);

        let block_size = cipher.block_size();
        assert!(block_size > 1, "Need a block cipher, not a stream cipher");

        ctx.cipher_update(&vec![0; block_size + 1], Some(&mut vec![0; block_size - 1]))
            .unwrap();
    }
}