Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make CipherCtx::cipher_update more flexible #1733

Merged
merged 2 commits into from Nov 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
wiktor-k marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}