Repository: aws/aws-lc-rs (language: rust)

Issue:
Decrypting AES ECB payload
### Security issue notifications

If you discover a potential security issue in AWS-LC for Rust we ask that you notify AWS Security via our
[vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a
public github issue, if in doubt contact AWS security first.

### Problem:

I have incoming payloads that for historical purposes are encrypted using AES in ECB mode (this is the default AES implementation for Java). From what I've seen, the crate uses ecb internally through the aws-lc library, but the only externally exposed implementations are CTR and CBC. Is there any way to expose that algorithm or use a workaround that

### Solution:

A description of the possible solution in terms of AWS-LC for Rust architecture. Highlight and explain any potentially
controversial design decisions taken.

* **Does this change any public APIs?** If yes, explain.
Yes, this would effect the public api for AES. I assume the reason this implementation was left as internal only is due to the lower security compared to AES with CBC.

* **Which algorithm(s) will this impact?**
AES

### Requirements / Acceptance Criteria:

What must a solution address in order to solve the problem? How do we know the solution is complete?

Either a support for ECB mode, or a workaround for using ECB mode with the existing API.

* **RFC links:** Links to relevant RFC(s)
* **Related Issues:** Link any relevant issues
* **Will the Usage Guide or other documentation need to be updated?**
If the API were to change, it should include strong warnings against using this algorithm.
* **Testing:** How will this change be tested? Call out new integration tests, functional tests, or particularly
  interesting/important unit tests.

### Out of scope:

Is there anything the solution will intentionally NOT address?



Discussion / hints:
Hello @rmullin-apptio,

Can you confirm whether you are using `AES/ECB/NoPadding` or `AES/ECB/PKCS5Padding` in your Java application? Just trying to figure out if you need ECB with with or without padding.

Thanks!

Fix patch:
diff --git a/aws-lc-rs/src/aead/quic.rs b/aws-lc-rs/src/aead/quic.rs
index b5ae045eaa6..5e71dee22e3 100644
--- a/aws-lc-rs/src/aead/quic.rs
+++ b/aws-lc-rs/src/aead/quic.rs
@@ -7,7 +7,7 @@
 //!
 //! See draft-ietf-quic-tls.
 
-use crate::cipher::aes::encrypt_block_aes;
+use crate::cipher::aes::encrypt_block;
 use crate::cipher::block;
 use crate::cipher::chacha::encrypt_block_chacha20;
 use crate::cipher::key::SymmetricCipherKey;
@@ -149,7 +149,7 @@ fn cipher_new_mask(
 
     let encrypted_block = match cipher_key {
         SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
-            encrypt_block_aes(enc_key, block)
+            encrypt_block(enc_key, block)
         }
         SymmetricCipherKey::ChaCha20 { raw_key } => {
             let plaintext = block.as_ref();
diff --git a/aws-lc-rs/src/cipher.rs b/aws-lc-rs/src/cipher.rs
index e5c8608dc42..01b47e984a3 100644
--- a/aws-lc-rs/src/cipher.rs
+++ b/aws-lc-rs/src/cipher.rs
@@ -230,38 +230,35 @@ pub use streaming::{BufferUpdate, StreamingDecryptingKey, StreamingEncryptingKey
 
 use crate::buffer::Buffer;
 use crate::error::Unspecified;
-use crate::fips::indicator_check;
 use crate::hkdf;
 use crate::hkdf::KeyType;
 use crate::iv::{FixedLength, IV_LEN_128_BIT};
 use crate::ptr::ConstPointer;
 use aws_lc::{
-    AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt, EVP_aes_128_cbc, EVP_aes_128_cfb128,
-    EVP_aes_128_ctr, EVP_aes_256_cbc, EVP_aes_256_cfb128, EVP_aes_256_ctr, AES_DECRYPT,
-    AES_ENCRYPT, AES_KEY, EVP_CIPHER,
+    EVP_aes_128_cbc, EVP_aes_128_cfb128, EVP_aes_128_ctr, EVP_aes_128_ecb, EVP_aes_256_cbc,
+    EVP_aes_256_cfb128, EVP_aes_256_ctr, EVP_aes_256_ecb, EVP_CIPHER,
 };
 use core::fmt::Debug;
 use key::SymmetricCipherKey;
-use zeroize::Zeroize;
 
 /// The number of bytes in an AES 128-bit key
-pub const AES_128_KEY_LEN: usize = 16;
+pub use crate::cipher::aes::AES_128_KEY_LEN;
 
 /// The number of bytes in an AES 256-bit key
-pub const AES_256_KEY_LEN: usize = 32;
+pub use crate::cipher::aes::AES_256_KEY_LEN;
 
 const MAX_CIPHER_KEY_LEN: usize = AES_256_KEY_LEN;
 
 /// The number of bytes for an AES-CBC initialization vector (IV)
-pub const AES_CBC_IV_LEN: usize = 16;
+pub use crate::cipher::aes::AES_CBC_IV_LEN;
 
 /// The number of bytes for an AES-CTR initialization vector (IV)
-pub const AES_CTR_IV_LEN: usize = 16;
+pub use crate::cipher::aes::AES_CTR_IV_LEN;
 
 /// The number of bytes for an AES-CFB initialization vector (IV)
-pub const AES_CFB_IV_LEN: usize = 16;
+pub use crate::cipher::aes::AES_CFB_IV_LEN;
 
-const AES_BLOCK_LEN: usize = 16;
+use crate::cipher::aes::AES_BLOCK_LEN;
 
 const MAX_CIPHER_BLOCK_LEN: usize = AES_BLOCK_LEN;
 
@@ -277,6 +274,9 @@ pub enum OperatingMode {
 
     /// CFB 128-bit mode.
     CFB128,
+
+    /// Electronic Code Book (ECB) mode.
+    ECB,
 }
 
 impl OperatingMode {
@@ -286,9 +286,11 @@ impl OperatingMode {
             (OperatingMode::CBC, AlgorithmId::Aes128) => unsafe { EVP_aes_128_cbc() },
             (OperatingMode::CTR, AlgorithmId::Aes128) => unsafe { EVP_aes_128_ctr() },
             (OperatingMode::CFB128, AlgorithmId::Aes128) => unsafe { EVP_aes_128_cfb128() },
+            (OperatingMode::ECB, AlgorithmId::Aes128) => unsafe { EVP_aes_128_ecb() },
             (OperatingMode::CBC, AlgorithmId::Aes256) => unsafe { EVP_aes_256_cbc() },
             (OperatingMode::CTR, AlgorithmId::Aes256) => unsafe { EVP_aes_256_ctr() },
             (OperatingMode::CFB128, AlgorithmId::Aes256) => unsafe { EVP_aes_256_cfb128() },
+            (OperatingMode::ECB, AlgorithmId::Aes256) => unsafe { EVP_aes_256_ecb() },
         })
         .unwrap()
     }
@@ -301,6 +303,9 @@ macro_rules! define_cipher_context {
         pub enum $name {
             /// A 128-bit Initialization Vector.
             Iv128(FixedLength<IV_LEN_128_BIT>),
+
+            /// No Cipher Context
+            None,
         }
 
         impl<'a> TryFrom<&'a $name> for &'a [u8] {
@@ -309,6 +314,7 @@ macro_rules! define_cipher_context {
             fn try_from(value: &'a $name) -> Result<Self, Unspecified> {
                 match value {
                     $name::Iv128(iv) => Ok(iv.as_ref()),
+                    _ => Err(Unspecified),
                 }
             }
         }
@@ -317,6 +323,7 @@ macro_rules! define_cipher_context {
             fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
                 match self {
                     Self::Iv128(_) => write!(f, "Iv128"),
+                    Self::None => write!(f, "None"),
                 }
             }
         }
@@ -325,6 +332,7 @@ macro_rules! define_cipher_context {
             fn from(value: $other) -> Self {
                 match value {
                     $other::Iv128(iv) => $name::Iv128(iv),
+                    $other::None => $name::None,
                 }
             }
         }
@@ -388,6 +396,7 @@ impl Algorithm {
                 OperatingMode::CBC | OperatingMode::CTR | OperatingMode::CFB128 => {
                     Ok(EncryptionContext::Iv128(FixedLength::new()?))
                 }
+                OperatingMode::ECB => Ok(EncryptionContext::None),
             },
         }
     }
@@ -399,6 +408,9 @@ impl Algorithm {
                 OperatingMode::CBC | OperatingMode::CTR | OperatingMode::CFB128 => {
                     matches!(input, EncryptionContext::Iv128(_))
                 }
+                OperatingMode::ECB => {
+                    matches!(input, EncryptionContext::None)
+                }
             },
         }
     }
@@ -410,6 +422,9 @@ impl Algorithm {
                 OperatingMode::CBC | OperatingMode::CTR | OperatingMode::CFB128 => {
                     matches!(input, DecryptionContext::Iv128(_))
                 }
+                OperatingMode::ECB => {
+                    matches!(input, DecryptionContext::None)
+                }
             },
         }
     }
@@ -496,8 +511,8 @@ impl EncryptingKey {
     //
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error constructing the `EncryptingKey`.
-    pub fn ctr(key: UnboundCipherKey) -> Result<EncryptingKey, Unspecified> {
-        EncryptingKey::new(key, OperatingMode::CTR)
+    pub fn ctr(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::CTR)
     }
 
     /// Constructs an `EncryptingKey` operating in cipher feedback 128-bit mode (CFB128) using the provided key.
@@ -509,15 +524,32 @@ impl EncryptingKey {
     //
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error constructing the `EncryptingKey`.
-    pub fn cfb128(key: UnboundCipherKey) -> Result<EncryptingKey, Unspecified> {
-        EncryptingKey::new(key, OperatingMode::CFB128)
+    pub fn cfb128(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::CFB128)
+    }
+
+    /// Constructs an `EncryptingKey` operating in electronic code book mode (ECB) using the provided key.
+    /// 
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    // # FIPS
+    // Use this function with an `UnboundCipherKey` constructed with one of the following algorithms:
+    // * `AES_128`
+    // * `AES_256`
+    //
+    /// # Errors
+    /// * [`Unspecified`]: Returned if there is an error constructing the `EncryptingKey`.
+    pub fn ecb(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::ECB)
     }
 
     #[allow(clippy::unnecessary_wraps)]
-    fn new(key: UnboundCipherKey, mode: OperatingMode) -> Result<EncryptingKey, Unspecified> {
+    fn new(key: UnboundCipherKey, mode: OperatingMode) -> Result<Self, Unspecified> {
         let algorithm = key.algorithm();
         let key = key.try_into()?;
-        Ok(EncryptingKey {
+        Ok(Self {
             algorithm,
             key,
             mode,
@@ -539,6 +571,9 @@ impl EncryptingKey {
     /// Encrypts the data provided in `in_out` in-place.
     /// Returns a [`DecryptionContext`] with the randomly generated IV that was used to encrypt
     /// the data provided.
+    /// 
+    /// If `EncryptingKey` is operating in `OperatingMode::ECB`, then `in_out.len()` must be a multiple
+    /// of the block length.
     ///
     /// # Errors
     /// * [`Unspecified`]: Returned if cipher mode requires input to be a multiple of the block length,
@@ -552,6 +587,9 @@ impl EncryptingKey {
     /// This is considered "less safe" because the caller could potentially construct
     /// a `EncryptionContext` from a previously used IV (initialization vector).
     /// Returns a [`DecryptionContext`] produced from the provided `EncryptionContext`.
+    /// 
+    /// If `EncryptingKey` is operating in `OperatingMode::ECB`, then `in_out.len()` must be a multiple
+    /// of the block length.
     ///
     /// # Errors
     /// * [`Unspecified`]: Returned if cipher mode requires input to be a multiple of the block length,
@@ -598,7 +636,7 @@ impl DecryptingKey {
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error during decryption.
     pub fn ctr(key: UnboundCipherKey) -> Result<DecryptingKey, Unspecified> {
-        DecryptingKey::new(key, OperatingMode::CTR)
+        Self::new(key, OperatingMode::CTR)
     }
 
     /// Constructs a cipher decrypting key operating in cipher feedback 128-bit mode (CFB128) using the provided key and context.
@@ -610,15 +648,32 @@ impl DecryptingKey {
     //
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error during decryption.
-    pub fn cfb128(key: UnboundCipherKey) -> Result<DecryptingKey, Unspecified> {
-        DecryptingKey::new(key, OperatingMode::CFB128)
+    pub fn cfb128(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::CFB128)
+    }
+
+    /// Constructs an `DecryptingKey` operating in electronic code book (ECB) mode using the provided key.
+    ///
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    // # FIPS
+    // Use this function with an `UnboundCipherKey` constructed with one of the following algorithms:
+    // * `AES_128`
+    // * `AES_256`
+    //
+    /// # Errors
+    /// * [`Unspecified`]: Returned if there is an error constructing the `DecryptingKey`.
+    pub fn ecb(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::ECB)
     }
 
     #[allow(clippy::unnecessary_wraps)]
-    fn new(key: UnboundCipherKey, mode: OperatingMode) -> Result<DecryptingKey, Unspecified> {
+    fn new(key: UnboundCipherKey, mode: OperatingMode) -> Result<Self, Unspecified> {
         let algorithm = key.algorithm();
         let key = key.try_into()?;
-        Ok(DecryptingKey {
+        Ok(Self {
             algorithm,
             key,
             mode,
@@ -639,6 +694,9 @@ impl DecryptingKey {
 
     /// Decrypts the data provided in `in_out` in-place.
     /// Returns a references to the decrypted data.
+    /// 
+    /// If `DecryptingKey` is operating in `OperatingMode::ECB`, then `in_out.len()` must be a multiple
+    /// of the block length.
     ///
     /// # Errors
     /// * [`Unspecified`]: Returned if cipher mode requires input to be a multiple of the block length,
@@ -670,21 +728,35 @@ fn encrypt(
 ) -> Result<DecryptionContext, Unspecified> {
     let block_len = algorithm.block_len();
 
-    if mode == OperatingMode::CBC && (in_out.len() % block_len) != 0 {
-        return Err(Unspecified);
+    match mode {
+        OperatingMode::CBC | OperatingMode::ECB => {
+            if in_out.len() % block_len != 0 {
+                return Err(Unspecified);
+            }
+        }
+        _ => {}
     }
 
     match mode {
         OperatingMode::CBC => match algorithm.id() {
-            AlgorithmId::Aes128 | AlgorithmId::Aes256 => encrypt_aes_cbc_mode(key, context, in_out),
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::encrypt_cbc_mode(key, context, in_out)
+            }
         },
         OperatingMode::CTR => match algorithm.id() {
-            AlgorithmId::Aes128 | AlgorithmId::Aes256 => encrypt_aes_ctr_mode(key, context, in_out),
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::encrypt_ctr_mode(key, context, in_out)
+            }
         },
         // TODO: Hopefully support CFB1, and CFB8
         OperatingMode::CFB128 => match algorithm.id() {
             AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
-                encrypt_aes_cfb_mode(key, mode, context, in_out)
+                aes::encrypt_cfb_mode(key, mode, context, in_out)
+            }
+        },
+        OperatingMode::ECB => match algorithm.id() {
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::encrypt_ecb_mode(key, context, in_out)
             }
         },
     }
@@ -699,254 +771,40 @@ fn decrypt<'in_out>(
 ) -> Result<&'in_out mut [u8], Unspecified> {
     let block_len = algorithm.block_len();
 
-    if mode == OperatingMode::CBC && (in_out.len() % block_len) != 0 {
-        return Err(Unspecified);
+    match mode {
+        OperatingMode::CBC | OperatingMode::ECB => {
+            if in_out.len() % block_len != 0 {
+                return Err(Unspecified);
+            }
+        }
+        _ => {}
     }
 
     match mode {
         OperatingMode::CBC => match algorithm.id() {
-            AlgorithmId::Aes128 | AlgorithmId::Aes256 => decrypt_aes_cbc_mode(key, context, in_out),
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::decrypt_cbc_mode(key, context, in_out)
+            }
         },
         OperatingMode::CTR => match algorithm.id() {
-            AlgorithmId::Aes128 | AlgorithmId::Aes256 => decrypt_aes_ctr_mode(key, context, in_out),
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::decrypt_ctr_mode(key, context, in_out)
+            }
         },
         // TODO: Hopefully support CFB1, and CFB8
         OperatingMode::CFB128 => match algorithm.id() {
             AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
-                decrypt_aes_cfb_mode(key, mode, context, in_out)
+                aes::decrypt_cfb_mode(key, mode, context, in_out)
+            }
+        },
+        OperatingMode::ECB => match algorithm.id() {
+            AlgorithmId::Aes128 | AlgorithmId::Aes256 => {
+                aes::decrypt_ecb_mode(key, context, in_out)
             }
         },
     }
 }
 
-fn encrypt_aes_ctr_mode(
-    key: &SymmetricCipherKey,
-    context: EncryptionContext,
-    in_out: &mut [u8],
-) -> Result<DecryptionContext, Unspecified> {
-    #[allow(clippy::match_wildcard_for_single_variants)]
-    let key = match &key {
-        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
-            enc_key
-        }
-        _ => unreachable!(),
-    };
-
-    let mut iv = {
-        let mut iv = [0u8; AES_CTR_IV_LEN];
-        iv.copy_from_slice((&context).try_into()?);
-        iv
-    };
-
-    let mut buffer = [0u8; AES_BLOCK_LEN];
-
-    aes_ctr128_encrypt(key, &mut iv, &mut buffer, in_out);
-    iv.zeroize();
-
-    Ok(context.into())
-}
-
-fn decrypt_aes_ctr_mode<'in_out>(
-    key: &SymmetricCipherKey,
-    context: DecryptionContext,
-    in_out: &'in_out mut [u8],
-) -> Result<&'in_out mut [u8], Unspecified> {
-    // it's the same in CTR, just providing a nice named wrapper to match
-    encrypt_aes_ctr_mode(key, context.into(), in_out).map(|_| in_out)
-}
-
-fn encrypt_aes_cbc_mode(
-    key: &SymmetricCipherKey,
-    context: EncryptionContext,
-    in_out: &mut [u8],
-) -> Result<DecryptionContext, Unspecified> {
-    #[allow(clippy::match_wildcard_for_single_variants)]
-    let key = match &key {
-        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
-            enc_key
-        }
-        _ => unreachable!(),
-    };
-
-    let mut iv = {
-        let mut iv = [0u8; AES_CBC_IV_LEN];
-        iv.copy_from_slice((&context).try_into()?);
-        iv
-    };
-
-    aes_cbc_encrypt(key, &mut iv, in_out);
-    iv.zeroize();
-
-    Ok(context.into())
-}
-
-#[allow(clippy::needless_pass_by_value)]
-fn decrypt_aes_cbc_mode<'in_out>(
-    key: &SymmetricCipherKey,
-    context: DecryptionContext,
-    in_out: &'in_out mut [u8],
-) -> Result<&'in_out mut [u8], Unspecified> {
-    #[allow(clippy::match_wildcard_for_single_variants)]
-    let key = match &key {
-        SymmetricCipherKey::Aes128 { dec_key, .. } | SymmetricCipherKey::Aes256 { dec_key, .. } => {
-            dec_key
-        }
-        _ => unreachable!(),
-    };
-
-    let mut iv = {
-        let mut iv = [0u8; AES_CBC_IV_LEN];
-        iv.copy_from_slice((&context).try_into()?);
-        iv
-    };
-
-    aes_cbc_decrypt(key, &mut iv, in_out);
-    iv.zeroize();
-
-    Ok(in_out)
-}
-
-#[allow(clippy::needless_pass_by_value)]
-fn encrypt_aes_cfb_mode(
-    key: &SymmetricCipherKey,
-    mode: OperatingMode,
-    context: EncryptionContext,
-    in_out: &mut [u8],
-) -> Result<DecryptionContext, Unspecified> {
-    #[allow(clippy::match_wildcard_for_single_variants)]
-    let key = match &key {
-        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
-            enc_key
-        }
-        _ => unreachable!(),
-    };
-
-    let mut iv = {
-        let mut iv = [0u8; AES_CFB_IV_LEN];
-        iv.copy_from_slice((&context).try_into()?);
-        iv
-    };
-
-    let cfb_encrypt: fn(&AES_KEY, &mut [u8], &mut [u8]) = match mode {
-        // TODO: Hopefully support CFB1, and CFB8
-        OperatingMode::CFB128 => aes_cfb128_encrypt,
-        _ => unreachable!(),
-    };
-
-    cfb_encrypt(key, &mut iv, in_out);
-    iv.zeroize();
-
-    Ok(context.into())
-}
-
-#[allow(clippy::needless_pass_by_value)]
-fn decrypt_aes_cfb_mode<'in_out>(
-    key: &SymmetricCipherKey,
-    mode: OperatingMode,
-    context: DecryptionContext,
-    in_out: &'in_out mut [u8],
-) -> Result<&'in_out mut [u8], Unspecified> {
-    #[allow(clippy::match_wildcard_for_single_variants)]
-    let key = match &key {
-        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
-            enc_key
-        }
-        _ => unreachable!(),
-    };
-
-    let mut iv = {
-        let mut iv = [0u8; AES_CFB_IV_LEN];
-        iv.copy_from_slice((&context).try_into()?);
-        iv
-    };
-
-    let cfb_decrypt: fn(&AES_KEY, &mut [u8], &mut [u8]) = match mode {
-        // TODO: Hopefully support CFB1, and CFB8
-        OperatingMode::CFB128 => aes_cfb128_decrypt,
-        _ => unreachable!(),
-    };
-
-    cfb_decrypt(key, &mut iv, in_out);
-
-    iv.zeroize();
-
-    Ok(in_out)
-}
-
-fn aes_ctr128_encrypt(key: &AES_KEY, iv: &mut [u8], block_buffer: &mut [u8], in_out: &mut [u8]) {
-    let mut num: u32 = 0;
-
-    indicator_check!(unsafe {
-        AES_ctr128_encrypt(
-            in_out.as_ptr(),
-            in_out.as_mut_ptr(),
-            in_out.len(),
-            key,
-            iv.as_mut_ptr(),
-            block_buffer.as_mut_ptr(),
-            &mut num,
-        );
-    });
-
-    Zeroize::zeroize(block_buffer);
-}
-
-fn aes_cbc_encrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
-    indicator_check!(unsafe {
-        AES_cbc_encrypt(
-            in_out.as_ptr(),
-            in_out.as_mut_ptr(),
-            in_out.len(),
-            key,
-            iv.as_mut_ptr(),
-            AES_ENCRYPT,
-        );
-    });
-}
-
-fn aes_cbc_decrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
-    indicator_check!(unsafe {
-        AES_cbc_encrypt(
-            in_out.as_ptr(),
-            in_out.as_mut_ptr(),
-            in_out.len(),
-            key,
-            iv.as_mut_ptr(),
-            AES_DECRYPT,
-        );
-    });
-}
-
-fn aes_cfb128_encrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
-    let mut num: i32 = 0;
-    indicator_check!(unsafe {
-        AES_cfb128_encrypt(
-            in_out.as_ptr(),
-            in_out.as_mut_ptr(),
-            in_out.len(),
-            key,
-            iv.as_mut_ptr(),
-            &mut num,
-            AES_ENCRYPT,
-        );
-    });
-}
-
-fn aes_cfb128_decrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
-    let mut num: i32 = 0;
-    indicator_check!(unsafe {
-        AES_cfb128_encrypt(
-            in_out.as_ptr(),
-            in_out.as_mut_ptr(),
-            in_out.len(),
-            key,
-            iv.as_mut_ptr(),
-            &mut num,
-            AES_DECRYPT,
-        );
-    });
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -1040,6 +898,23 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_aes_128_cfb128() {
+        let key = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
+        for i in 0..=50 {
+            helper_test_cipher_n_bytes(key.as_slice(), &AES_128, OperatingMode::CFB128, i);
+        }
+    }
+
+    #[test]
+    fn test_aes_256_cfb128() {
+        let key =
+            from_hex("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f").unwrap();
+        for i in 0..=50 {
+            helper_test_cipher_n_bytes(key.as_slice(), &AES_256, OperatingMode::CFB128, i);
+        }
+    }
+
     #[test]
     fn test_aes_256_ctr() {
         let key =
@@ -1049,6 +924,12 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_aes_128_ecb() {
+        let key = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
+        _ = key;
+    }
+
     macro_rules! cipher_kat {
         ($name:ident, $alg:expr, $mode:expr, $key:literal, $iv: literal, $plaintext:literal, $ciphertext:literal) => {
             #[test]
@@ -1084,6 +965,34 @@ mod tests {
                 let unbound_key2 = UnboundCipherKey::new(alg, &key).unwrap();
                 let decrypting_key = DecryptingKey::new(unbound_key2, $mode).unwrap();
 
+                let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
+                assert_eq!(input.as_slice(), plaintext);
+            }
+        };
+        ($name:ident, $alg:expr, $mode:expr, $key:literal, $plaintext:literal, $ciphertext:literal) => {
+            #[test]
+            fn $name() {
+                let key = from_hex($key).unwrap();
+                let input = from_hex($plaintext).unwrap();
+                let expected_ciphertext = from_hex($ciphertext).unwrap();
+
+                let alg = $alg;
+
+                let unbound_key = UnboundCipherKey::new(alg, &key).unwrap();
+
+                let encrypting_key = EncryptingKey::new(unbound_key, $mode).unwrap();
+
+                let mut in_out = input.clone();
+
+                let context = encrypting_key
+                    .less_safe_encrypt(&mut in_out, EncryptionContext::None)
+                    .unwrap();
+
+                assert_eq!(expected_ciphertext, in_out);
+
+                let unbound_key2 = UnboundCipherKey::new(alg, &key).unwrap();
+                let decrypting_key = DecryptingKey::new(unbound_key2, $mode).unwrap();
+
                 let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
                 assert_eq!(input.as_slice(), plaintext);
             }
@@ -1149,4 +1058,22 @@ mod tests {
         "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
         "dc7e84bfda79164b7ecd8486985d386039ffed143b28b1c832113c6331e5407bdf10132415e54b92a13ed0a8267ae2f975a385741ab9cef82031623d55b1e471"
     );
+
+    cipher_kat!(
+        test_sp800_38a_ecb_aes128,
+        &AES_128,
+        OperatingMode::ECB,
+        "2b7e151628aed2a6abf7158809cf4f3c",
+        "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
+        "3ad77bb40d7a3660a89ecaf32466ef97f5d3d58503b9699de785895a96fdbaaf43b1cd7f598ece23881b00e3ed0306887b0c785e27e8ad3f8223207104725dd4"
+    );
+
+    cipher_kat!(
+        test_sp800_38a_ecb_aes256,
+        &AES_256,
+        OperatingMode::ECB,
+        "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4",
+        "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
+        "f3eed1bdb5d2a03c064b5a7e3db181f8591ccb10d410ed26dc5ba74a31362870b6ed21b99ca6f4f9f153e7b1beafed1d23304b7a39f9f3ff067d8d8f9e24ecc7"
+    );
 }
diff --git a/aws-lc-rs/src/cipher/aes.rs b/aws-lc-rs/src/cipher/aes.rs
index d13941a820b..d2e8202373e 100644
--- a/aws-lc-rs/src/cipher/aes.rs
+++ b/aws-lc-rs/src/cipher/aes.rs
@@ -3,12 +3,14 @@
 // Modifications copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0 OR ISC
 
-use crate::{
-    cipher::block::{Block, BLOCK_LEN},
-    fips::indicator_check,
+use crate::{cipher::block::Block, error::Unspecified, fips::indicator_check};
+use aws_lc::{
+    AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt, AES_ecb_encrypt, AES_DECRYPT,
+    AES_ENCRYPT, AES_KEY,
 };
-use aws_lc::{AES_ecb_encrypt, AES_ENCRYPT, AES_KEY};
-use core::mem::MaybeUninit;
+use zeroize::Zeroize;
+
+use super::{DecryptionContext, EncryptionContext, OperatingMode, SymmetricCipherKey};
 
 /// Length of an AES-128 key in bytes.
 pub const AES_128_KEY_LEN: usize = 16;
@@ -16,19 +18,327 @@ pub const AES_128_KEY_LEN: usize = 16;
 /// Length of an AES-256 key in bytes.
 pub const AES_256_KEY_LEN: usize = 32;
 
+/// The number of bytes for an AES-CBC initialization vector (IV)
+pub const AES_CBC_IV_LEN: usize = 16;
+
+/// The number of bytes for an AES-CTR initialization vector (IV)
+pub const AES_CTR_IV_LEN: usize = 16;
+
+/// The number of bytes for an AES-CFB initialization vector (IV)
+pub const AES_CFB_IV_LEN: usize = 16;
+
+pub const AES_BLOCK_LEN: usize = 16;
+
 #[inline]
-pub(crate) fn encrypt_block_aes(aes_key: &AES_KEY, block: Block) -> Block {
-    unsafe {
-        let mut cipher_text = MaybeUninit::<[u8; BLOCK_LEN]>::uninit();
-        let plain_bytes = block.as_ref();
-
-        indicator_check!(AES_ecb_encrypt(
-            plain_bytes.as_ptr(),
-            cipher_text.as_mut_ptr().cast(),
-            aes_key,
-            AES_ENCRYPT,
-        ));
+pub(crate) fn encrypt_block(aes_key: &AES_KEY, mut block: Block) -> Block {
+    {
+        let block_ref = block.as_mut();
+        debug_assert_eq!(block_ref.len(), AES_BLOCK_LEN);
+        aes_ecb_encrypt(aes_key, block_ref);
+    }
+    block
+}
+
+pub(super) fn encrypt_ctr_mode(
+    key: &SymmetricCipherKey,
+    context: EncryptionContext,
+    in_out: &mut [u8],
+) -> Result<DecryptionContext, Unspecified> {
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+            enc_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut iv = {
+        let mut iv = [0u8; AES_CTR_IV_LEN];
+        iv.copy_from_slice((&context).try_into()?);
+        iv
+    };
+
+    let mut buffer = [0u8; AES_BLOCK_LEN];
+
+    aes_ctr128_encrypt(key, &mut iv, &mut buffer, in_out);
+    iv.zeroize();
+
+    Ok(context.into())
+}
+
+pub(super) fn decrypt_ctr_mode<'in_out>(
+    key: &SymmetricCipherKey,
+    context: DecryptionContext,
+    in_out: &'in_out mut [u8],
+) -> Result<&'in_out mut [u8], Unspecified> {
+    // it's the same in CTR, just providing a nice named wrapper to match
+    encrypt_ctr_mode(key, context.into(), in_out).map(|_| in_out)
+}
+
+pub(super) fn encrypt_cbc_mode(
+    key: &SymmetricCipherKey,
+    context: EncryptionContext,
+    in_out: &mut [u8],
+) -> Result<DecryptionContext, Unspecified> {
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+            enc_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut iv = {
+        let mut iv = [0u8; AES_CBC_IV_LEN];
+        iv.copy_from_slice((&context).try_into()?);
+        iv
+    };
+
+    aes_cbc_encrypt(key, &mut iv, in_out);
+    iv.zeroize();
+
+    Ok(context.into())
+}
+
+#[allow(clippy::needless_pass_by_value)]
+pub(super) fn decrypt_cbc_mode<'in_out>(
+    key: &SymmetricCipherKey,
+    context: DecryptionContext,
+    in_out: &'in_out mut [u8],
+) -> Result<&'in_out mut [u8], Unspecified> {
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { dec_key, .. } | SymmetricCipherKey::Aes256 { dec_key, .. } => {
+            dec_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut iv = {
+        let mut iv = [0u8; AES_CBC_IV_LEN];
+        iv.copy_from_slice((&context).try_into()?);
+        iv
+    };
+
+    aes_cbc_decrypt(key, &mut iv, in_out);
+    iv.zeroize();
+
+    Ok(in_out)
+}
+
+#[allow(clippy::needless_pass_by_value)]
+pub(super) fn encrypt_cfb_mode(
+    key: &SymmetricCipherKey,
+    mode: OperatingMode,
+    context: EncryptionContext,
+    in_out: &mut [u8],
+) -> Result<DecryptionContext, Unspecified> {
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+            enc_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut iv = {
+        let mut iv = [0u8; AES_CFB_IV_LEN];
+        iv.copy_from_slice((&context).try_into()?);
+        iv
+    };
+
+    let cfb_encrypt: fn(&AES_KEY, &mut [u8], &mut [u8]) = match mode {
+        // TODO: Hopefully support CFB1, and CFB8
+        OperatingMode::CFB128 => aes_cfb128_encrypt,
+        _ => unreachable!(),
+    };
+
+    cfb_encrypt(key, &mut iv, in_out);
+    iv.zeroize();
+
+    Ok(context.into())
+}
+
+#[allow(clippy::needless_pass_by_value)]
+pub(super) fn decrypt_cfb_mode<'in_out>(
+    key: &SymmetricCipherKey,
+    mode: OperatingMode,
+    context: DecryptionContext,
+    in_out: &'in_out mut [u8],
+) -> Result<&'in_out mut [u8], Unspecified> {
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+            enc_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut iv = {
+        let mut iv = [0u8; AES_CFB_IV_LEN];
+        iv.copy_from_slice((&context).try_into()?);
+        iv
+    };
+
+    let cfb_decrypt: fn(&AES_KEY, &mut [u8], &mut [u8]) = match mode {
+        // TODO: Hopefully support CFB1, and CFB8
+        OperatingMode::CFB128 => aes_cfb128_decrypt,
+        _ => unreachable!(),
+    };
+
+    cfb_decrypt(key, &mut iv, in_out);
 
-        Block::from(cipher_text.assume_init())
+    iv.zeroize();
+
+    Ok(in_out)
+}
+
+#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
+pub(super) fn encrypt_ecb_mode(
+    key: &SymmetricCipherKey,
+    context: EncryptionContext,
+    in_out: &mut [u8],
+) -> Result<DecryptionContext, Unspecified> {
+    if !matches!(context, EncryptionContext::None) {
+        unreachable!();
+    }
+
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { enc_key, .. } | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+            enc_key
+        }
+        _ => unreachable!(),
+    };
+
+    let mut in_out_iter = in_out.chunks_exact_mut(AES_BLOCK_LEN);
+
+    for block in in_out_iter.by_ref() {
+        aes_ecb_encrypt(key, block);
+    }
+
+    // This is a sanity check that should not happen. We validate in `encrypt` that in_out.len() % block_len == 0
+    // for this mode.
+    debug_assert!(in_out_iter.into_remainder().is_empty());
+
+    Ok(context.into())
+}
+
+#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
+pub(super) fn decrypt_ecb_mode<'in_out>(
+    key: &SymmetricCipherKey,
+    context: DecryptionContext,
+    in_out: &'in_out mut [u8],
+) -> Result<&'in_out mut [u8], Unspecified> {
+    if !matches!(context, DecryptionContext::None) {
+        unreachable!();
     }
+
+    #[allow(clippy::match_wildcard_for_single_variants)]
+    let key = match &key {
+        SymmetricCipherKey::Aes128 { dec_key, .. } | SymmetricCipherKey::Aes256 { dec_key, .. } => {
+            dec_key
+        }
+        _ => unreachable!(),
+    };
+
+    {
+        let mut in_out_iter = in_out.chunks_exact_mut(AES_BLOCK_LEN);
+
+        for block in in_out_iter.by_ref() {
+            aes_ecb_decrypt(key, block);
+        }
+
+        // This is a sanity check hat should not fail. We validate in `decrypt` that in_out.len() % block_len == 0 for
+        // this mode.
+        debug_assert!(in_out_iter.into_remainder().is_empty());
+    }
+
+    Ok(in_out)
+}
+
+fn aes_ecb_encrypt(key: &AES_KEY, in_out: &mut [u8]) {
+    indicator_check!(unsafe {
+        AES_ecb_encrypt(in_out.as_ptr(), in_out.as_mut_ptr(), key, AES_ENCRYPT);
+    });
+}
+
+fn aes_ecb_decrypt(key: &AES_KEY, in_out: &mut [u8]) {
+    indicator_check!(unsafe {
+        AES_ecb_encrypt(in_out.as_ptr(), in_out.as_mut_ptr(), key, AES_DECRYPT);
+    });
+}
+
+fn aes_ctr128_encrypt(key: &AES_KEY, iv: &mut [u8], block_buffer: &mut [u8], in_out: &mut [u8]) {
+    let mut num: u32 = 0;
+
+    indicator_check!(unsafe {
+        AES_ctr128_encrypt(
+            in_out.as_ptr(),
+            in_out.as_mut_ptr(),
+            in_out.len(),
+            key,
+            iv.as_mut_ptr(),
+            block_buffer.as_mut_ptr(),
+            &mut num,
+        );
+    });
+
+    Zeroize::zeroize(block_buffer);
+}
+
+fn aes_cbc_encrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
+    indicator_check!(unsafe {
+        AES_cbc_encrypt(
+            in_out.as_ptr(),
+            in_out.as_mut_ptr(),
+            in_out.len(),
+            key,
+            iv.as_mut_ptr(),
+            AES_ENCRYPT,
+        );
+    });
+}
+
+fn aes_cbc_decrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
+    indicator_check!(unsafe {
+        AES_cbc_encrypt(
+            in_out.as_ptr(),
+            in_out.as_mut_ptr(),
+            in_out.len(),
+            key,
+            iv.as_mut_ptr(),
+            AES_DECRYPT,
+        );
+    });
+}
+
+fn aes_cfb128_encrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
+    let mut num: i32 = 0;
+    indicator_check!(unsafe {
+        AES_cfb128_encrypt(
+            in_out.as_ptr(),
+            in_out.as_mut_ptr(),
+            in_out.len(),
+            key,
+            iv.as_mut_ptr(),
+            &mut num,
+            AES_ENCRYPT,
+        );
+    });
+}
+
+fn aes_cfb128_decrypt(key: &AES_KEY, iv: &mut [u8], in_out: &mut [u8]) {
+    let mut num: i32 = 0;
+    indicator_check!(unsafe {
+        AES_cfb128_encrypt(
+            in_out.as_ptr(),
+            in_out.as_mut_ptr(),
+            in_out.len(),
+            key,
+            iv.as_mut_ptr(),
+            &mut num,
+            AES_DECRYPT,
+        );
+    });
 }
diff --git a/aws-lc-rs/src/cipher/block.rs b/aws-lc-rs/src/cipher/block.rs
index 84a798d1fdb..714751f1b93 100644
--- a/aws-lc-rs/src/cipher/block.rs
+++ b/aws-lc-rs/src/cipher/block.rs
@@ -38,6 +38,14 @@ impl AsRef<[u8; BLOCK_LEN]> for Block {
     }
 }
 
+impl AsMut<[u8; BLOCK_LEN]> for Block {
+    #[allow(clippy::transmute_ptr_to_ptr)]
+    #[inline]
+    fn as_mut(&mut self) -> &mut [u8; BLOCK_LEN] {
+        unsafe { core::mem::transmute(self) }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     #[test]
@@ -51,4 +59,16 @@ mod tests {
             assert_eq!(block_a.as_ref()[i], block_b.as_ref()[i]);
         }
     }
+
+    #[test]
+    fn test_block_clone_mut_ref() {
+        use super::{Block, BLOCK_LEN};
+        let mut block_a = Block::from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+        #[allow(clippy::clone_on_copy)]
+        let mut block_b = block_a.clone();
+
+        for i in 0..BLOCK_LEN {
+            assert_eq!(block_a.as_mut()[i], block_b.as_mut()[i]);
+        }
+    }
 }
diff --git a/aws-lc-rs/src/cipher/key.rs b/aws-lc-rs/src/cipher/key.rs
index c7d735b331f..73ed178c0fd 100644
--- a/aws-lc-rs/src/cipher/key.rs
+++ b/aws-lc-rs/src/cipher/key.rs
@@ -1,7 +1,6 @@
 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0 OR ISC
 
-use crate::cipher::aes::encrypt_block_aes;
 use crate::cipher::block::Block;
 use crate::cipher::chacha::ChaCha20Key;
 use crate::cipher::{AES_128_KEY_LEN, AES_256_KEY_LEN};
@@ -133,7 +132,9 @@ impl SymmetricCipherKey {
     pub(crate) fn encrypt_block(&self, block: Block) -> Block {
         match self {
             SymmetricCipherKey::Aes128 { enc_key, .. }
-            | SymmetricCipherKey::Aes256 { enc_key, .. } => encrypt_block_aes(enc_key, block),
+            | SymmetricCipherKey::Aes256 { enc_key, .. } => {
+                super::aes::encrypt_block(enc_key, block)
+            }
             SymmetricCipherKey::ChaCha20 { .. } => panic!("Unsupported algorithm!"),
         }
     }
diff --git a/aws-lc-rs/src/cipher/padded.rs b/aws-lc-rs/src/cipher/padded.rs
index 379532ffaa8..0dc137437a0 100644
--- a/aws-lc-rs/src/cipher/padded.rs
+++ b/aws-lc-rs/src/cipher/padded.rs
@@ -85,8 +85,21 @@ impl PaddedBlockEncryptingKey {
     //
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error constructing a `PaddedBlockEncryptingKey`.
-    pub fn cbc_pkcs7(key: UnboundCipherKey) -> Result<PaddedBlockEncryptingKey, Unspecified> {
-        PaddedBlockEncryptingKey::new(key, OperatingMode::CBC, PaddingStrategy::PKCS7)
+    pub fn cbc_pkcs7(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::CBC, PaddingStrategy::PKCS7)
+    }
+
+    /// Constructs a new `PaddedBlockEncryptingKey` cipher with electronic code book (ECB) mode.
+    /// Plaintext data is padded following the PKCS#7 scheme.
+    /// 
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    /// # Errors
+    /// * [`Unspecified`]: Returned if there is an error constructing a `PaddedBlockEncryptingKey`.
+    pub fn ecb_pkcs7(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::ECB, PaddingStrategy::PKCS7)
     }
 
     #[allow(clippy::unnecessary_wraps)]
@@ -97,7 +110,7 @@ impl PaddedBlockEncryptingKey {
     ) -> Result<PaddedBlockEncryptingKey, Unspecified> {
         let algorithm = key.algorithm();
         let key = key.try_into()?;
-        Ok(PaddedBlockEncryptingKey {
+        Ok(Self {
             algorithm,
             key,
             mode,
@@ -191,8 +204,26 @@ impl PaddedBlockDecryptingKey {
     //
     /// # Errors
     /// * [`Unspecified`]: Returned if there is an error constructing the `PaddedBlockDecryptingKey`.
-    pub fn cbc_pkcs7(key: UnboundCipherKey) -> Result<PaddedBlockDecryptingKey, Unspecified> {
-        PaddedBlockDecryptingKey::new(key, OperatingMode::CBC, PaddingStrategy::PKCS7)
+    pub fn cbc_pkcs7(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::CBC, PaddingStrategy::PKCS7)
+    }
+
+    /// Constructs a new `PaddedBlockDecryptingKey` cipher with electronic code book (ECB) mode.
+    /// Decrypted data is unpadded following the PKCS#7 scheme.
+    /// 
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    // # FIPS
+    // Use this function with an `UnboundCipherKey` constructed with one of the following algorithms:
+    // * `AES_128`
+    // * `AES_256`
+    //
+    /// # Errors
+    /// * [`Unspecified`]: Returned if there is an error constructing the `PaddedBlockDecryptingKey`.
+    pub fn ecb_pkcs7(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::ECB, PaddingStrategy::PKCS7)
     }
 
     #[allow(clippy::unnecessary_wraps)]
diff --git a/aws-lc-rs/src/cipher/streaming.rs b/aws-lc-rs/src/cipher/streaming.rs
index e990678bc07..175acddcbab 100644
--- a/aws-lc-rs/src/cipher/streaming.rs
+++ b/aws-lc-rs/src/cipher/streaming.rs
@@ -10,9 +10,11 @@ use crate::ptr::LcPtr;
 use aws_lc::{
     EVP_CIPHER_CTX_new, EVP_CIPHER_iv_length, EVP_CIPHER_key_length, EVP_DecryptFinal_ex,
     EVP_DecryptInit_ex, EVP_DecryptUpdate, EVP_EncryptFinal_ex, EVP_EncryptInit_ex,
-    EVP_EncryptUpdate, EVP_CIPHER_CTX,
+    EVP_EncryptUpdate, EVP_CIPHER, EVP_CIPHER_CTX,
 };
-use std::ptr::null_mut;
+use std::ptr::{null, null_mut};
+
+use super::ConstPointer;
 
 /// A key for streaming encryption operations.
 pub struct StreamingEncryptingKey {
@@ -56,6 +58,62 @@ impl BufferUpdate<'_> {
     }
 }
 
+fn evp_encrypt_init(
+    cipher_ctx: &mut LcPtr<EVP_CIPHER_CTX>,
+    cipher: &ConstPointer<EVP_CIPHER>,
+    key: &[u8],
+    iv: Option<&[u8]>,
+) -> Result<(), Unspecified> {
+    let iv_ptr: *const u8 = if let Some(iv) = iv {
+        iv.as_ptr()
+    } else {
+        null()
+    };
+
+    // AWS-LC copies the key and iv values into the EVP_CIPHER_CTX, and thus can be dropped after this.
+    if 1 != unsafe {
+        EVP_EncryptInit_ex(
+            *cipher_ctx.as_mut(),
+            **cipher,
+            null_mut(),
+            key.as_ptr(),
+            iv_ptr,
+        )
+    } {
+        return Err(Unspecified);
+    }
+
+    Ok(())
+}
+
+fn evp_decrypt_init(
+    cipher_ctx: &mut LcPtr<EVP_CIPHER_CTX>,
+    cipher: &ConstPointer<EVP_CIPHER>,
+    key: &[u8],
+    iv: Option<&[u8]>,
+) -> Result<(), Unspecified> {
+    let iv_ptr: *const u8 = if let Some(iv) = iv {
+        iv.as_ptr()
+    } else {
+        null()
+    };
+
+    // AWS-LC copies the key and iv values into the EVP_CIPHER_CTX, and thus can be dropped after this.
+    if 1 != unsafe {
+        EVP_DecryptInit_ex(
+            *cipher_ctx.as_mut(),
+            **cipher,
+            null_mut(),
+            key.as_ptr(),
+            iv_ptr,
+        )
+    } {
+        return Err(Unspecified);
+    }
+
+    Ok(())
+}
+
 impl StreamingEncryptingKey {
     #[allow(clippy::needless_pass_by_value)]
     fn new(
@@ -71,23 +129,19 @@ impl StreamingEncryptingKey {
             key_bytes.len(),
             <usize>::try_from(unsafe { EVP_CIPHER_key_length(*cipher) }).unwrap()
         );
-        let iv = <&[u8]>::try_from(&context)?;
-        debug_assert_eq!(
-            iv.len(),
-            <usize>::try_from(unsafe { EVP_CIPHER_iv_length(*cipher) }).unwrap()
-        );
 
-        // AWS-LC copies the key and iv values into the EVP_CIPHER_CTX, and thus can be dropped after this.
-        if 1 != unsafe {
-            EVP_EncryptInit_ex(
-                *cipher_ctx.as_mut(),
-                *cipher,
-                null_mut(),
-                key_bytes.as_ptr(),
-                iv.as_ptr(),
-            )
-        } {
-            return Err(Unspecified);
+        match &context {
+            ctx @ EncryptionContext::Iv128(..) => {
+                let iv = <&[u8]>::try_from(ctx)?;
+                debug_assert_eq!(
+                    iv.len(),
+                    <usize>::try_from(unsafe { EVP_CIPHER_iv_length(*cipher) }).unwrap()
+                );
+                evp_encrypt_init(&mut cipher_ctx, &cipher, key_bytes, Some(iv))?;
+            }
+            EncryptionContext::None => {
+                evp_encrypt_init(&mut cipher_ctx, &cipher, key_bytes, None)?;
+            }
         }
 
         Ok(Self {
@@ -229,6 +283,20 @@ impl StreamingEncryptingKey {
         Self::less_safe_cfb128(key, context)
     }
 
+    /// Constructs a `StreamingEncryptingKey` for encrypting using ECB cipher mode with PKCS7 padding.
+    /// The resulting plaintext will be the same length as the ciphertext.
+    ///
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    /// # Errors
+    /// Returns an error on an internal failure.
+    pub fn ecb_pkcs7(key: UnboundCipherKey) -> Result<Self, Unspecified> {
+        let context = key.algorithm().new_encryption_context(OperatingMode::ECB)?;
+        Self::new(key, OperatingMode::ECB, context)
+    }
+
     /// Constructs a `StreamingEncryptingKey` for encrypting data using the CFB128 cipher mode.
     /// The resulting ciphertext will be the same length as the plaintext.
     ///
@@ -283,23 +351,19 @@ impl StreamingDecryptingKey {
             key_bytes.len(),
             <usize>::try_from(unsafe { EVP_CIPHER_key_length(*cipher) }).unwrap()
         );
-        let iv = <&[u8]>::try_from(&context)?;
-        debug_assert_eq!(
-            iv.len(),
-            <usize>::try_from(unsafe { EVP_CIPHER_iv_length(*cipher) }).unwrap()
-        );
 
-        // AWS-LC copies the key and iv values into the EVP_CIPHER_CTX, and thus can be dropped after this.
-        if 1 != unsafe {
-            EVP_DecryptInit_ex(
-                *cipher_ctx.as_mut(),
-                *cipher,
-                null_mut(),
-                key_bytes.as_ptr(),
-                iv.as_ptr(),
-            )
-        } {
-            return Err(Unspecified);
+        match &context {
+            ctx @ DecryptionContext::Iv128(..) => {
+                let iv = <&[u8]>::try_from(ctx)?;
+                debug_assert_eq!(
+                    iv.len(),
+                    <usize>::try_from(unsafe { EVP_CIPHER_iv_length(*cipher) }).unwrap()
+                );
+                evp_decrypt_init(&mut cipher_ctx, &cipher, key_bytes, Some(iv))?;
+            }
+            DecryptionContext::None => {
+                evp_decrypt_init(&mut cipher_ctx, &cipher, key_bytes, None)?;
+            }
         }
 
         Ok(Self {
@@ -416,6 +480,22 @@ impl StreamingDecryptingKey {
     pub fn cfb128(key: UnboundCipherKey, context: DecryptionContext) -> Result<Self, Unspecified> {
         Self::new(key, OperatingMode::CFB128, context)
     }
+
+    /// Constructs a `StreamingDecryptingKey` for decrypting using the ECB cipher mode.
+    /// The resulting plaintext will be the same length as the ciphertext.
+    ///
+    /// # ☠️ ️️️DANGER ☠️
+    /// Offered for computability purposes only. This is an extremely dangerous mode, and
+    /// very likely not what you want to use.
+    ///
+    /// # Errors
+    /// Returns an error on an internal failure.
+    pub fn ecb_pkcs7(
+        key: UnboundCipherKey,
+        context: DecryptionContext,
+    ) -> Result<Self, Unspecified> {
+        Self::new(key, OperatingMode::ECB, context)
+    }
 }
 
 #[cfg(test)]
@@ -466,7 +546,7 @@ mod tests {
         let outlen = output.written().len();
         ciphertext.truncate(out_idx + outlen);
         match mode {
-            OperatingMode::CBC => {
+            OperatingMode::CBC | OperatingMode::ECB => {
                 assert!(ciphertext.len() > plaintext.len());
                 assert!(ciphertext.len() <= plaintext.len() + alg.block_len());
             }
@@ -515,7 +595,7 @@ mod tests {
         let outlen = output.written().len();
         plaintext.truncate(out_idx + outlen);
         match mode {
-            OperatingMode::CBC => {
+            OperatingMode::CBC | OperatingMode::ECB => {
                 assert!(ciphertext.len() > plaintext.len());
                 assert!(ciphertext.len() <= plaintext.len() + alg.block_len());
             }
@@ -556,6 +636,7 @@ mod tests {
     helper_stream_step_encrypt_test!(cbc_pkcs7);
     helper_stream_step_encrypt_test!(ctr);
     helper_stream_step_encrypt_test!(cfb128);
+    helper_stream_step_encrypt_test!(ecb_pkcs7);
 
     #[test]
     fn test_step_cbc() {
@@ -723,6 +804,61 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_step_ecb_pkcs7() {
+        let random = SystemRandom::new();
+        let mut key = [0u8; AES_256_KEY_LEN];
+        random.fill(&mut key).unwrap();
+
+        let encrypting_key_creator = || {
+            let key = UnboundCipherKey::new(&AES_256, &key.clone()).unwrap();
+            StreamingEncryptingKey::ecb_pkcs7(key).unwrap()
+        };
+        let decrypting_key_creator = |decryption_ctx: DecryptionContext| {
+            let key = UnboundCipherKey::new(&AES_256, &key.clone()).unwrap();
+            StreamingDecryptingKey::ecb_pkcs7(key, decryption_ctx).unwrap()
+        };
+
+        for i in 13..=21 {
+            for j in 124..=131 {
+                helper_test_ecb_pkcs7_stream_encrypt_step_n_bytes(
+                    encrypting_key_creator,
+                    decrypting_key_creator,
+                    j,
+                    i,
+                );
+            }
+            for j in 124..=131 {
+                helper_test_ecb_pkcs7_stream_encrypt_step_n_bytes(
+                    encrypting_key_creator,
+                    decrypting_key_creator,
+                    j,
+                    j - i,
+                );
+            }
+        }
+        for j in 124..=131 {
+            helper_test_ecb_pkcs7_stream_encrypt_step_n_bytes(
+                encrypting_key_creator,
+                decrypting_key_creator,
+                j,
+                j,
+            );
+            helper_test_ecb_pkcs7_stream_encrypt_step_n_bytes(
+                encrypting_key_creator,
+                decrypting_key_creator,
+                j,
+                256,
+            );
+            helper_test_ecb_pkcs7_stream_encrypt_step_n_bytes(
+                encrypting_key_creator,
+                decrypting_key_creator,
+                j,
+                1,
+            );
+        }
+    }
+
     macro_rules! streaming_cipher_kat {
         ($name:ident, $alg:expr, $mode:expr, $key:literal, $iv: literal, $plaintext:literal, $ciphertext:literal, $from_step:literal, $to_step:literal) => {
             #[test]
@@ -746,6 +882,33 @@ mod tests {
 
                     assert_eq!(expected_ciphertext.as_slice(), ciphertext.as_ref());
 
+                    let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+                    let decrypting_key =
+                        StreamingDecryptingKey::new(unbound_key2, $mode, decrypt_ctx).unwrap();
+
+                    let plaintext = step_decrypt(decrypting_key, &ciphertext, step);
+                    assert_eq!(input.as_slice(), plaintext.as_ref());
+                }
+            }
+        };
+        ($name:ident, $alg:expr, $mode:expr, $key:literal, $plaintext:literal, $ciphertext:literal, $from_step:literal, $to_step:literal) => {
+            #[test]
+            fn $name() {
+                let key = from_hex($key).unwrap();
+                let input = from_hex($plaintext).unwrap();
+                let expected_ciphertext = from_hex($ciphertext).unwrap();
+
+                for step in ($from_step..=$to_step) {
+                    let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+                    let encrypting_key =
+                        StreamingEncryptingKey::new(unbound_key, $mode, EncryptionContext::None)
+                            .unwrap();
+
+                    let (ciphertext, decrypt_ctx) = step_encrypt(encrypting_key, &input, step);
+
+                    assert_eq!(expected_ciphertext.as_slice(), ciphertext.as_ref());
+
                     let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
                     let decrypting_key =
                         StreamingDecryptingKey::new(unbound_key2, $mode, decrypt_ctx).unwrap();
@@ -923,4 +1086,48 @@ mod tests {
         2,
         9
     );
+
+    streaming_cipher_kat!(
+        test_openssl_aes_128_ecb_pkcs7_16_bytes,
+        &AES_128,
+        OperatingMode::ECB,
+        "a1b7cd124f9824a1532d8440f8136788",
+        "388118e6848b0cea97401707a754d7a1",
+        "19b7c7f5d9c2bda3f957e9e7d20847828d5eb5624bcbf221014063a87b38d133",
+        2,
+        9
+    );
+
+    streaming_cipher_kat!(
+        test_openssl_aes_128_ecb_pkcs7_15_bytes,
+        &AES_128,
+        OperatingMode::ECB,
+        "d10e12accb837aaffbb284448e53138c",
+        "b21cfd1c9e6e7e6e912c82c7dd1aa8",
+        "3d1168e61df34b51c6ab6745c20ee881",
+        2,
+        9
+    );
+
+    streaming_cipher_kat!(
+        test_openssl_aes_256_ecb_pkcs7_16_bytes,
+        &AES_256,
+        OperatingMode::ECB,
+        "0600f4ad4eda4bc8e3e99592abdfce7eb08fee0ccc801c5ccee26134bcaafbbd",
+        "516b45cb1342239a549bd8c1d5998f98",
+        "854c593555a213e4a862c6f66aa4a79631faca131eba6f163e5cd3940e9c0a57",
+        2,
+        9
+    );
+
+    streaming_cipher_kat!(
+        test_openssl_aes_256_ecb_pkcs7_15_bytes,
+        &AES_256,
+        OperatingMode::ECB,
+        "80f235756c8f70094ae1f99a95a599c27c4452a4b8412fd934e2b253f7098508",
+        "2235590b90190d7a1dc2464a0205ad",
+        "8547d8ac8dc6d9cebb2dc77a7034bb67",
+        2,
+        9
+    );
 }


Test patch:
diff --git a/aws-lc-rs/tests/cipher_test.rs b/aws-lc-rs/tests/cipher_test.rs
index bbf844d89c2..4d496e69087 100644
--- a/aws-lc-rs/tests/cipher_test.rs
+++ b/aws-lc-rs/tests/cipher_test.rs
@@ -47,11 +47,11 @@ fn step_encrypt(
     let outlen = output.written().len();
     ciphertext.truncate(out_idx + outlen);
     match mode {
-        OperatingMode::CBC => {
+        OperatingMode::CBC | OperatingMode::ECB => {
             assert!(ciphertext.len() > plaintext.len());
             assert!(ciphertext.len() <= plaintext.len() + alg.block_len());
         }
-        OperatingMode::CTR => {
+        OperatingMode::CTR | OperatingMode::CFB128 => {
             assert_eq!(ciphertext.len(), plaintext.len());
         }
         _ => panic!("Unknown cipher mode"),
@@ -97,11 +97,11 @@ fn step_decrypt(
     let outlen = output.written().len();
     plaintext.truncate(out_idx + outlen);
     match mode {
-        OperatingMode::CBC => {
+        OperatingMode::CBC | OperatingMode::ECB => {
             assert!(ciphertext.len() > plaintext.len());
             assert!(ciphertext.len() <= plaintext.len() + alg.block_len());
         }
-        OperatingMode::CTR => {
+        OperatingMode::CTR | OperatingMode::CFB128 => {
             assert_eq!(ciphertext.len(), plaintext.len());
         }
         _ => panic!("Unknown cipher mode"),
@@ -135,6 +135,32 @@ macro_rules! streaming_cipher_rt {
     };
 }
 
+macro_rules! streaming_ecb_pkcs7_rt {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal, $from_step:literal, $to_step:literal) => {
+        paste! {
+        #[test]
+        fn [<$name _streaming>]() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+
+            for step in ($from_step..=$to_step) {
+                let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+                let encrypting_key = StreamingEncryptingKey::ecb_pkcs7(unbound_key).unwrap();
+
+                let (ciphertext, decrypt_ctx) = step_encrypt(encrypting_key, &input, step);
+
+                let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+                    let decrypting_key =
+                        StreamingDecryptingKey::ecb_pkcs7(unbound_key2, decrypt_ctx).unwrap();
+
+                let plaintext = step_decrypt(decrypting_key, &ciphertext, step);
+                assert_eq!(input.as_slice(), plaintext.as_ref());
+            }
+        }
+        }
+    };
+}
+
 macro_rules! streaming_cipher_kat {
     ($name:ident, $alg:expr, $mode:expr, $constructor:ident, $key:literal, $iv: literal, $plaintext:literal, $ciphertext:literal, $from_step:literal, $to_step:literal) => {
         paste! {
@@ -168,6 +194,36 @@ macro_rules! streaming_cipher_kat {
         }
     };
 }
+
+macro_rules! streaming_ecb_pkcs7_kat {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal, $ciphertext:literal, $from_step:literal, $to_step:literal) => {
+        paste! {
+        #[test]
+        fn [<$name _streaming>]() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+            let expected_ciphertext = from_hex($ciphertext).unwrap();
+
+            for step in ($from_step..=$to_step) {
+                let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+                    let encrypting_key = StreamingEncryptingKey::ecb_pkcs7(unbound_key).unwrap();
+
+                let (ciphertext, decrypt_ctx) = step_encrypt(encrypting_key, &input, step);
+
+                assert_eq!(expected_ciphertext.as_slice(), ciphertext.as_ref());
+
+                let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+                    let decrypting_key =
+                        StreamingDecryptingKey::ecb_pkcs7(unbound_key2, decrypt_ctx).unwrap();
+
+                let plaintext = step_decrypt(decrypting_key, &ciphertext, step);
+                assert_eq!(input.as_slice(), plaintext.as_ref());
+            }
+        }
+        }
+    };
+}
+
 macro_rules! padded_cipher_kat {
     ($name:ident, $alg:expr, $mode:expr, $constructor:ident, $key:literal, $iv: literal, $plaintext:literal, $ciphertext:literal) => {
         #[test]
@@ -214,6 +270,37 @@ macro_rules! padded_cipher_kat {
     };
 }
 
+macro_rules! padded_ecb_pkcs7_kat {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal, $ciphertext:literal) => {
+        #[test]
+        fn $name() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+            let expected_ciphertext = from_hex($ciphertext).unwrap();
+
+            let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+            let encrypting_key = PaddedBlockEncryptingKey::ecb_pkcs7(unbound_key).unwrap();
+            assert_eq!(OperatingMode::ECB, encrypting_key.mode());
+            assert_eq!($alg, encrypting_key.algorithm());
+            let mut in_out = input.clone();
+            let context = encrypting_key
+                .less_safe_encrypt(&mut in_out, EncryptionContext::None)
+                .unwrap();
+            assert_eq!(expected_ciphertext.as_slice(), in_out.as_slice());
+
+            let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+            let decrypting_key = PaddedBlockDecryptingKey::ecb_pkcs7(unbound_key2).unwrap();
+            assert_eq!(OperatingMode::ECB, decrypting_key.mode());
+            assert_eq!($alg, decrypting_key.algorithm());
+            let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
+            assert_eq!(input.as_slice(), plaintext);
+        }
+
+        streaming_ecb_pkcs7_kat!($name, $alg, $key, $plaintext, $ciphertext, 2, 9);
+    };
+}
+
 macro_rules! cipher_kat {
     ($name:ident, $alg:expr, $mode:expr, $constructor:ident, $key:literal, $iv: literal, $plaintext:literal, $ciphertext:literal) => {
         #[test]
@@ -260,6 +347,52 @@ macro_rules! cipher_kat {
     };
 }
 
+macro_rules! ecb_kat {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal, $ciphertext:literal) => {
+        #[test]
+        fn $name() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+            let expected_ciphertext = from_hex($ciphertext).unwrap();
+
+            let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+            let encrypting_key = EncryptingKey::ecb(unbound_key).unwrap();
+            assert_eq!(OperatingMode::ECB, encrypting_key.mode());
+            assert_eq!($alg, encrypting_key.algorithm());
+            let mut in_out = input.clone();
+            let context = encrypting_key
+                .less_safe_encrypt(in_out.as_mut_slice(), EncryptionContext::None)
+                .unwrap();
+            assert_eq!(expected_ciphertext.as_slice(), in_out);
+
+            let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+            let decrypting_key = DecryptingKey::ecb(unbound_key2).unwrap();
+            assert_eq!(OperatingMode::ECB, decrypting_key.mode());
+            assert_eq!($alg, decrypting_key.algorithm());
+            let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
+            assert_eq!(input.as_slice(), plaintext);
+        }
+    };
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal) => {
+        #[test]
+        fn $name() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+
+            let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+            let encrypting_key = EncryptingKey::ecb(unbound_key).unwrap();
+            assert_eq!(OperatingMode::ECB, encrypting_key.mode());
+            assert_eq!($alg, encrypting_key.algorithm());
+            let mut in_out = input.clone();
+            encrypting_key
+                .less_safe_encrypt(in_out.as_mut_slice(), EncryptionContext::None)
+                .expect_err("expected encryption failure");
+        }
+    };
+}
+
 macro_rules! padded_cipher_rt {
     ($name:ident, $alg:expr, $mode:expr, $constructor:ident, $key:literal, $plaintext:literal) => {
         #[test]
@@ -311,6 +444,56 @@ macro_rules! cipher_rt {
     };
 }
 
+macro_rules! ecb_rt {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal) => {
+        #[test]
+        fn $name() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+            let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+            let encrypting_key = EncryptingKey::ecb(unbound_key).unwrap();
+            assert_eq!(OperatingMode::ECB, encrypting_key.mode());
+            assert_eq!($alg, encrypting_key.algorithm());
+            let mut in_out = input.clone();
+            let context = encrypting_key.encrypt(in_out.as_mut_slice()).unwrap();
+
+            let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+            let decrypting_key = DecryptingKey::ecb(unbound_key2).unwrap();
+            assert_eq!(OperatingMode::ECB, decrypting_key.mode());
+            assert_eq!($alg, decrypting_key.algorithm());
+            let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
+            assert_eq!(input.as_slice(), plaintext);
+        }
+    };
+}
+
+macro_rules! padded_ecb_pkcs7_rt {
+    ($name:ident, $alg:expr, $key:literal, $plaintext:literal) => {
+        #[test]
+        fn $name() {
+            let key = from_hex($key).unwrap();
+            let input = from_hex($plaintext).unwrap();
+            let unbound_key = UnboundCipherKey::new($alg, &key).unwrap();
+
+            let encrypting_key = PaddedBlockEncryptingKey::ecb_pkcs7(unbound_key).unwrap();
+            assert_eq!(OperatingMode::ECB, encrypting_key.mode());
+            assert_eq!($alg, encrypting_key.algorithm());
+            let mut in_out = input.clone();
+            let context = encrypting_key.encrypt(&mut in_out).unwrap();
+
+            let unbound_key2 = UnboundCipherKey::new($alg, &key).unwrap();
+            let decrypting_key = PaddedBlockDecryptingKey::ecb_pkcs7(unbound_key2).unwrap();
+            assert_eq!(OperatingMode::ECB, decrypting_key.mode());
+            assert_eq!($alg, decrypting_key.algorithm());
+            let plaintext = decrypting_key.decrypt(&mut in_out, context).unwrap();
+            assert_eq!(input.as_slice(), plaintext);
+        }
+
+        streaming_ecb_pkcs7_rt!($name, $alg, $key, $plaintext, 2, 9);
+    };
+}
+
 padded_cipher_kat!(
     test_kat_aes_128_cbc_16_bytes,
     &AES_128,
@@ -526,3 +709,151 @@ padded_cipher_rt!(
     "d4a8206dcae01242f9db79a4ecfe277d0f7bb8ccbafd8f9809adb39f35aa9b41",
     "a39c1fdf77ea3e1f18178c0ec237c70a34"
 );
+
+padded_ecb_pkcs7_kat!(
+    test_kat_aes_128_ecb_pkcs7_16_bytes,
+    &AES_128,
+    "4da1ed87937836de98836433b11eb5a7",
+    "61f17a594bd5b55ae3fa8efaae6e83d6",
+    "e5e18734e84530de94b1636d938e5d6f6b4027b4321685a9195b4ddbf25530bf"
+);
+
+padded_ecb_pkcs7_kat!(
+    test_kat_aes_128_ecb_pkcs7_15_bytes,
+    &AES_128,
+    "50f0fb9c8bfcedd0424a4932fdb4578d",
+    "4badb333837326d75406a0cd6149f0",
+    "593a39d148e106ebc4a429b97b5033bc"
+);
+
+padded_ecb_pkcs7_kat!(
+    test_kat_aes_256_ecb_pkcs7_16_bytes,
+    &AES_256,
+    "13b2cc03ba601f45b7b1927a7b8566abfae0d97220cb7d5193725ab12e1b23ac",
+    "6a3867fbd39bd3345df4aec929c8843a",
+    "615c152b5655499a1d94993e9c220a7e9430ed4d48f2c5b408878beed2c90cf7"
+);
+
+padded_ecb_pkcs7_kat!(
+    test_kat_aes_256_ecb_pkcs7_15_bytes,
+    &AES_256,
+    "f636aefc30bfe19e7fda3ea399be6529f102b965523719e7e717648ec8451c86",
+    "8d6e85663f99f22bb293582f81ae45",
+    "f6dc9e368d2cdf6a2e97a022876eb9f2"
+);
+
+ecb_kat!(
+    test_kat_aes_128_ecb_16_bytes,
+    &AES_128,
+    "f8efb984d9e813c96a79020bdfbb6032",
+    "c4a500e39307dbe7727b5b3a36660f70",
+    "1eea416d959f747da26d48d2df11d205"
+);
+
+ecb_kat!(
+    test_kat_aes_128_ecb_15_bytes,
+    &AES_128,
+    "f8efb984d9e813c96a79020bdfbb6032",
+    "c4a500e39307dbe7727b5b3a36660f"
+);
+
+ecb_kat!(
+    test_kat_aes_256_ecb_16_bytes,
+    &AES_256,
+    "d3c9173cbfc65d0e2b6f43ae57c2a6550b756f487bbb7b6404efec69aa74d411",
+    "109082176cf2a9488b0cd887386bb84a",
+    "c8c9fece9883b26c0ca58e610493a318"
+);
+
+ecb_kat!(
+    test_kat_aes_256_ecb_15_bytes,
+    &AES_256,
+    "d3c9173cbfc65d0e2b6f43ae57c2a6550b756f487bbb7b6404efec69aa74d411",
+    "109082176cf2a9488b0cd887386bb8"
+);
+
+ecb_rt!(
+    test_rt_aes_128_16_bytes,
+    &AES_128,
+    "9dfb64e0d2b94ba6df41ef5f72413cb1",
+    "81c7241bbbf37d9b50e5072858fc498d"
+);
+
+ecb_rt!(
+    test_rt_aes_256_16_bytes,
+    &AES_256,
+    "e6be82de1addbf40550abad4b613b2e77dd498ecaeff5251d4773fcfa00cc1f4",
+    "eb80cab07da4d9ce53c27903dd070b28"
+);
+
+padded_ecb_pkcs7_rt!(
+    test_rt_aes_128_ecb_pkcs7_16_bytes,
+    &AES_128,
+    "c6fcad04ac45dc5801277484279396c0",
+    "e6a32546cb537cf589ac65aac84815ae"
+);
+
+padded_ecb_pkcs7_rt!(
+    test_rt_aes_128_ecb_pkcs7_15_bytes,
+    &AES_128,
+    "41647c63411930c483be063ca890472e",
+    "6194b065db9003381c0c736130188e"
+);
+
+padded_ecb_pkcs7_rt!(
+    test_rt_aes_256_ecb_pkcs7_16_bytes,
+    &AES_256,
+    "c0b52e26961ec041bcea9b3d066c42ea97023a7cfb3e1f3b2e3a8a6427284a47",
+    "10420ee900c0045bde1218e17008bdb0"
+);
+
+padded_ecb_pkcs7_rt!(
+    test_rt_aes_256_ecb_pkcs7_15_bytes,
+    &AES_256,
+    "48c8511ea06d6b4f594870cdf30fd60e7a88eee09f62d7c359e26e475292ec64",
+    "9ac8559493ee5274ae9ca03a47618d"
+);
+
+cipher_kat!(
+    test_kat_aes_128_cfb128_16_bytes,
+    &AES_128,
+    OperatingMode::CFB128,
+    cfb128,
+    "679816318e2f095262ee93c4552490a2",
+    "dd41c2fb73e61bdc02da6c70eb5ac729",
+    "6a841482cf079c4a4b8c59b6c6bda6a4",
+    "0cebe979ac61df66bb190bf9ed22e363"
+);
+
+cipher_kat!(
+    test_kat_aes_128_cfb128_15_bytes,
+    &AES_128,
+    OperatingMode::CFB128,
+    cfb128,
+    "27d4285e8315857653833f63b0bcc034",
+    "bbdeef0d1837c33971f04eb0a8cde0a2",
+    "750cf377093fb05f84875a240154f7",
+    "187a9d7d922afa9f3294ad12669df1"
+);
+
+cipher_kat!(
+    test_kat_aes_256_cfb128_16_bytes,
+    &AES_256,
+    OperatingMode::CFB128,
+    cfb128,
+    "c7faca2e29734b069d19c0e95b9a93efa2512925ccebba8622fb321a93d50cd0",
+    "6f5aad20a97c694b0e0c93457f81b5c8",
+    "2bc1196172ca0bad4a349198f8abd925",
+    "21e1e308bfec28a24520a963aa0f4c57"
+);
+
+cipher_kat!(
+    test_kat_aes_256_cfb128_15_bytes,
+    &AES_256,
+    OperatingMode::CFB128,
+    cfb128,
+    "8ac86dfe6fa15d71c5c5c4a88ae182fd3a1636818f6a8bcd62c85a599329649c",
+    "f1b6b55e908d39b769968ae6c3c05c4f",
+    "9c1675a95f573b4504e6bc5275d0df",
+    "b8e816bd9e74adebdacf9036cbda41"
+);