エラーメッセージ
Malformed UTF-8 data
CryptoJS — thrown by .toString(CryptoJS.enc.Utf8) after decrypt
このエラーの意味
CryptoJS が暗号文を復号した結果、有効な UTF-8 ではないバイト列になり、それを文字列に変換できませんでした。CryptoJS の AES(CBC モード)には完全性のチェックがないため、鍵が間違っていても復号は失敗せず、ランダムなバイト列が生成されるだけです。それに最初に気づくのが UTF-8 デコーダーです。同じ間違った鍵で、このエラーの代わりに空文字列が返ることもあります(パディングのチェックがたまたまきれいに失敗した場合)。そのため、どちらの症状も診断は同じです。
発生する理由
鍵またはパスフレーズが間違っている
よくある暗号化に使った鍵と異なる鍵で復号すると、擬似ランダムな出力になります。どちらの症状が出るかはメッセージの長さによって異なります。ecdsa.com のテストでは、間違った鍵での復号のうちこのエラーをスローしたのは、29 バイトのメッセージで約 8 回に 1 回、100 バイトで約半数、300 バイトではすべてでした。残りのほぼすべては空文字列("")を返しました。いずれにしても、疑うべきは鍵です。
双方で鍵導出の方法が一致していない
よくあるCryptoJS.AES.encrypt(msg, "passphrase") は、OpenSSL 互換の KDF とランダムなソルトを使って鍵と IV を導出します。相手側がパスフレーズを生の鍵として扱う場合(または別のプラットフォームがその KDF を使わずに復号する場合)は、導出される鍵が異なり、出力は無意味なデータになります。
転送中に暗号文が壊れた
ときどきURL エンコードによる Base64 の破損(+ がスペースになる)、保存時の切り詰め、二重エンコードによって、暗号文のバイト列が変わります。改変されたバイト列を CBC で復号すると、暗号処理の層では何のエラーも出ずに無意味なデータになります。
修正方法
- 1.
鍵の扱いを明示的にし、双方でそろえる
パスフレーズの KDF による曖昧さを避けるため、双方でまったく同じように、本物の鍵を WordArray として渡し、IV も明示的に指定してください。
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.
空の出力も同じ失敗として扱う
結果を確認し、本当のエラーとして表に出してください。何も通知されない空文字列は、間違った鍵によるバグを何か月も隠してしまいます。
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.
認証付きの暗号方式を優先する
WebCrypto の AES-GCM は暗号文を認証します。鍵が間違っていれば、無意味なデータを生成するのではなく、明確かつ確実に失敗するため、この種類のエラーは完全になくなります。
js const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); // wrong key → OperationError, never silent garbage