Insecure encryption algorithm - Cipher Block Chaining

Need

Implementation of secure encryption algorithms with strong cipher block chaining

Context

• Usage of Rust 1.75+ for building application services

• Usage of the aes and cbc crates (RustCrypto block-cipher modes)

• Usage of the aes-gcm crate for authenticated encryption

Description

1. Non compliant code

use aes::Aes256;
use cbc::{
    cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit},
    Encryptor,
};

type Aes256CbcEnc = Encryptor<Aes256>;
...

The `encrypt` function below uses AES-256 in Cipher Block Chaining (CBC) mode via the RustCrypto `aes` and `cbc` crates. CBC is an unauthenticated mode: the ciphertext carries no cryptographic integrity tag, so an attacker who can intercept and modify ciphertext cannot be detected by the recipient. This enables two well-known attack families. - **Bit-flipping**: because each plaintext block is XORed with the previous ciphertext block during decryption, flipping a bit in ciphertext block N produces the same bit flip in plaintext block N+1 (while corrupting block N). An attacker who knows the structure of the plaintext can use this to silently alter messages — for example, toggling an `is_admin=false` flag to `is_admin=true`. - **Padding-oracle attacks**: combined with PKCS#7 padding (as used here via `encrypt_padded_vec_mut::<Pkcs7>`), any system that leaks whether decryption padding was valid (different error messages, response times, etc.) lets an attacker decrypt arbitrary ciphertexts one byte at a time without the key. The function also reuses a caller-supplied IV without enforcing uniqueness, which compounds the risk under repeated keys.

2. Steps

• Replace AES-CBC (via the `aes` + `cbc` crates) with an authenticated encryption construction — AES-GCM (`aes-gcm` crate) or ChaCha20-Poly1305 (`chacha20poly1305` crate).

• Generate the 96-bit GCM nonce (or 24-byte XChaCha20 nonce) from a cryptographically secure RNG such as `rand::rngs::OsRng`; never reuse a nonce under the same key.

• Store the nonce alongside the ciphertext (it is not secret); pass it back into `decrypt` together with the same key.

• Verify decryption returns `Ok(plaintext)` and treat any `Err(aead::Error)` as a tampered message — never return decrypted bytes on failure and never echo the error to the caller.

• Derive symmetric keys from a key-derivation function such as HKDF (`hkdf` crate) seeded with a high-entropy secret; never reuse application secrets as raw cipher keys.

3. Secure code example

use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};

fn encrypt(key: &[u8; 32], nonce: &[u8; 12], plaintext: &[u8]) -> Vec<u8> {
    // AES-GCM: authenticated encryption (AEAD)
    let cipher = Aes256Gcm::new(key.into());
    cipher.encrypt(Nonce::from_slice(nonce), plaintext).unwrap()
}

The corrected `encrypt` function uses AES-256 in Galois/Counter Mode (GCM) via the RustCrypto `aes-gcm` crate. AES-GCM is an Authenticated Encryption with Associated Data (AEAD) construction: every ciphertext is paired with a cryptographic authentication tag derived from the key, nonce, plaintext, and any associated data. Decryption verifies the tag before returning any plaintext, so any modification of the ciphertext (or of the nonce or associated data) causes decryption to fail with an error instead of producing attacker-controlled plaintext. This defeats both bit-flipping and padding-oracle attacks at the primitive level: there is no malleability that an attacker can exploit, and there is no padding to leak. The caller is still responsible for using a unique 96-bit nonce per (key, message) pair — typically by drawing it from a CSPRNG and storing it alongside the ciphertext, or by using a counter that is guaranteed not to repeat under a given key.