Error message
Malformed UTF-8 data
CryptoJS — thrown by .toString(CryptoJS.enc.Utf8) after decrypt
What it means
CryptoJS decrypted your ciphertext into bytes that are not valid UTF-8, and converting them to a string failed. Since CryptoJS's AES (CBC mode) has no integrity check, a wrong key does not fail the decryption — it just produces random bytes, and the UTF-8 decoder is the first thing to notice. The same wrong key can also yield an empty string instead of this error (when the padding check happens to fail cleanly), so both symptoms share one diagnosis.
Why it happens
Wrong key or passphrase
commonDecrypting with a key that differs from the encrypting one produces pseudo-random output. In testing, roughly one in six wrong-key decryptions throws this error and most of the rest return "" — either way, the key is the suspect.
Key-derivation mismatch between the two sides
commonCryptoJS.AES.encrypt(msg, "passphrase") derives key+IV via an OpenSSL-compatible KDF with a random salt. If the other side treats the passphrase as a raw key (or a different platform decrypts without that KDF), the derived keys differ and output is garbage.
Ciphertext corrupted in transport
occasionalBase64 mangled by URL encoding (+ becoming a space), truncation in storage, or double-encoding changes the ciphertext bytes; CBC decryption of altered bytes yields garbage without any error at the crypto layer.
How to fix it
- 1.
Make key handling explicit and symmetric
Skip the passphrase KDF ambiguity: pass a real key as a WordArray plus an explicit IV, identically on both sides.
js const key = CryptoJS.enc.Hex.parse(hexKey); // 32-byte key, both sides const iv = CryptoJS.enc.Hex.parse(hexIv); // 16-byte IV, both sides const ct = CryptoJS.AES.encrypt(plaintext, key, { iv }); const pt = CryptoJS.AES.decrypt(ct.toString(), key, { iv }) .toString(CryptoJS.enc.Utf8); - 2.
Treat empty output as the same failure
Check the result and surface a real error — silent empty strings hide wrong-key bugs for months.
js const pt = CryptoJS.AES.decrypt(ct, key, { iv }).toString(CryptoJS.enc.Utf8); if (!pt) throw new Error("decryption failed: wrong key/IV or corrupted ciphertext"); - 3.
Prefer an authenticated scheme
WebCrypto's AES-GCM authenticates ciphertext: a wrong key fails loudly and deterministically instead of producing garbage — eliminating this error class entirely.
js const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); // wrong key → OperationError, never silent garbage