Skip to content

Commit

Permalink
Merge pull request #1733 from wiktor-k/relax-cipher-update
Browse files Browse the repository at this point in the history
Make `CipherCtx::cipher_update` more flexible
  • Loading branch information
sfackler committed Nov 26, 2022
2 parents 7db5cc7 + 5ecff30 commit 4edda63
Show file tree
Hide file tree
Showing 3 changed files with 310 additions and 18 deletions.
5 changes: 5 additions & 0 deletions openssl-sys/src/evp.rs
Expand Up @@ -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))
Expand Down
3 changes: 3 additions & 0 deletions openssl-sys/src/handwritten/evp.rs
Expand Up @@ -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" {
Expand All @@ -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;
}
}
}
Expand Down
320 changes: 302 additions & 18 deletions openssl/src/cipher_ctx.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -501,33 +560,61 @@ 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;
}
assert!(output.len() >= input.len() + block_size);
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
);
}

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()),
&mut outlen,
input.as_ptr(),
inlen,
))?;
}

cvt(ffi::EVP_CipherUpdate(
self.as_ptr(),
output.map_or(ptr::null_mut(), |b| b.as_mut_ptr()),
&mut outlen,
input.as_ptr(),
inlen,
))?;

Ok(outlen as usize)
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}

0 comments on commit 4edda63

Please sign in to comment.