Online Tools Toolshu.com Log In Sign Up

AES Decryption Returns Garbage? Five Integration Pitfalls Between Frontend and Backend

Author:bhnw Released on 2026-04-24 09:49 5 views Star (0)

AES Integration Between Frontend and Backend Breaks More Often Than Not

That's not an exaggeration. AES has too many moving parts — mode, padding, key length, IV, encoding format — and any single mismatch produces either garbage output or an outright error. What makes it worse is that both sides can work perfectly in isolation, and only fall apart when they talk to each other.

This article goes through the most common failure points one by one. Use it as a checklist when something goes wrong.


Problem 1: Key Length Determines Which AES You're Using

AES comes in three variants: AES-128, AES-192, and AES-256, requiring keys of exactly 16, 24, and 32 bytes respectively.

Here's the trap — a lot of people pass a plain string as the key, like "mysecretkey". That's only 11 characters, short of the 16-byte minimum. Different libraries handle this differently: some silently pad with zeros, some truncate, some throw an error.

# Python AES keys must be exactly 16, 24, or 32 bytes
from Crypto.Cipher import AES

key = b"mysecretkey12345"              # 16 bytes — AES-128
key = b"mysecretkey123456789abcd"      # 24 bytes — AES-192
key = b"mysecretkey123456789abcdefghijk"  # 32 bytes — AES-256

If the frontend uses a plain string key, the backend needs to apply the same transformation — either both truncate to a fixed length, or both hash the key with MD5 or SHA-256 to produce a fixed-length result. The method doesn't matter, but both sides must do the same thing.


Problem 2: How the IV Gets Transmitted

CBC mode requires an IV (Initialization Vector) — exactly 16 bytes.

Two failure modes show up constantly.

The IV is omitted, and both sides assume different defaults. Some libraries default to an all-zero IV when none is provided; others require an explicit IV. Both sides test fine on their own, but one uses all-zero IV and the other generates a random one — decryption produces garbage.

The IV transmission method was never agreed upon. The standard approach is to prepend the randomly generated IV to the ciphertext, then transmit them together. The receiver takes the first 16 bytes as the IV and decrypts the rest. Some systems put the IV in a separate HTTP header; others use a fixed IV. Any of these can work — but both sides must agree on the same approach.

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

# Encrypt: generate random IV, prepend it to ciphertext
key = b"mysecretkey12345"
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(b"hello world", AES.block_size))
result = base64.b64encode(iv + ciphertext).decode()  # IV + ciphertext, Base64-encoded

# Decrypt: take first 16 bytes as IV
raw = base64.b64decode(result)
iv = raw[:16]
ciphertext = raw[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

Problem 3: Padding Scheme

AES is a block cipher — it processes exactly 16 bytes at a time. When the plaintext length isn't a multiple of 16, padding fills the gap.

The most common scheme is PKCS7 (also called PKCS5 — they're identical for AES). The rule: if N bytes are missing, fill with N bytes each containing the value N. Missing 3 bytes? Append \x03 \x03 \x03.

If the decryptor uses the wrong padding scheme — or the encryptor used ZeroPadding but the decryptor expects PKCS7 — the output is either garbled or throws an error.

// CryptoJS (frontend) defaults to Pkcs7
var encrypted = CryptoJS.AES.encrypt(message, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7  // this is the default — can be omitted
});
# pycryptodome (Python) also defaults to PKCS7
from Crypto.Util.Padding import pad
padded = pad(plaintext.encode(), AES.block_size)  # pkcs7 by default

If your backend is Java and uses Cipher.getInstance("AES/CBC/PKCS5Padding") — the name says PKCS5 but the actual algorithm is PKCS7. They're the same thing for AES. Don't worry about it.


Problem 4: Base64 or Hex?

The encrypted output is raw binary — it can't be passed around as a plain string. Two encodings are common:

  • Base64: More compact, standard for Web APIs
  • Hex: More readable, common for debugging and some backend systems

Both sides must use the same one. CryptoJS's .toString() after encryption outputs Base64 by default. If the backend tries to decode it as Hex, it will fail every time.

There's also a Base64 variant trap: standard Base64 uses + and /, while URL-safe Base64 uses - and _. If the ciphertext goes into a URL parameter, you must use URL-safe encoding — otherwise + gets decoded as a space.

import base64

# Standard Base64
encoded = base64.b64encode(ciphertext).decode()
decoded = base64.b64decode(encoded)

# URL-safe Base64
encoded = base64.urlsafe_b64encode(ciphertext).decode()
decoded = base64.urlsafe_b64decode(encoded)

Problem 5: Character Encoding (UTF-8 or Something Else?)

Before encrypting a string, it has to be converted to bytes. Whatever encoding is used to convert to bytes must be used again to convert back after decryption.

JavaScript defaults to UTF-8. But if the backend is a legacy system using GBK, Chinese characters will come out as garbage after decryption.

// CryptoJS handles strings as UTF-8 internally
var message = CryptoJS.enc.Utf8.parse("hello world");
# Decode back to string after decryption — encoding must match
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size).decode("utf-8")
# For legacy GBK systems:
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size).decode("gbk")

Debugging Checklist

When AES integration breaks, go through this in order:

  1. Isolate the problem first. Encrypt and decrypt on the same side in isolation. Confirm each side works on its own before blaming the other.
  2. Lock down all parameters. Agree on mode, padding, key, and IV. Start with a fixed all-zero IV to eliminate IV transmission as a variable.
  3. Print intermediate values. Output the Base64 or Hex ciphertext from both sides and compare them. If the ciphertexts match, the problem is on the decryption side. If they don't match, the problem is in encryption parameters.
  4. Eliminate variables one by one. Key → IV → mode → padding → encoding. In that order.

The AES Online Encrypt & Decrypt tool is useful here as a neutral reference — plug in your key, IV, mode, and content, then compare its output against your code. It quickly tells you which side has the parameter wrong.

发现周边 发现周边
Comment area

Loading...