エラーメッセージ
OperationError: The operation failed for an operation-specific reason
WebCrypto crypto.subtle — browsers & Node
このエラーの意味
WebCrypto が意図的にあいまいにしている包括的なエラーで、プリミティブ自体が失敗したことを示します。AES-GCM の decrypt() では、認証タグの検証に失敗したことを意味します。鍵、IV、追加データのいずれかが間違っているか、暗号文が改変されています。仕様では、暗号のチェックが失敗した理由を実装が漏らさないよう、メッセージを汎用的なものにしています。sign() と deriveBits() では、同じ名前がプリミティブ内部の失敗を表すため、メッセージのテキストよりも、スタックトレースに出ている操作のほうが重要です。
発生する理由
AES-GCM の認証失敗
よくある暗号化に使ったものとは別の鍵での復号、一致しない IV、additionalData の欠落または相違、転送中に末尾の 16 バイトのタグが失われた暗号文などです。GCM は認証付きの方式なので、これらのどれでも、でたらめなデータを返すのではなく操作全体が失敗します。
暗号文が切り詰められた、または再エンコードされた
よくあるBase64 の扱いの不一致(URL セーフと標準のアルファベット)、データベースのカラムによるバイトの切り捨て、文字列とバイナリの変換によるバッファーの破損などです。タグのチェックはこうした変更をすべて検出します。それがこのチェックの役割です。
導出パラメーターが範囲外
ときどき満たせない長さを指定した deriveBits/deriveKey、反復回数 0 の PBKDF2、矛盾する入力での ECDH の計算は、いくつかのランタイムで OperationError として表に出ます。
修正方法
- 1.
IV と暗号文を一緒に送り、両側で同じ方式でデコードする
IV を暗号文の先頭に付けて復号時に分割すれば、両側の認識が常に一致します。base64 の種類はすべての場所で 1 つに統一してください。
js // encrypt const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data); const packed = new Uint8Array([...iv, ...new Uint8Array(ct)]); // decrypt const buf = packedFromWire; const pt = await crypto.subtle.decrypt( { name: "AES-GCM", iv: buf.slice(0, 12) }, key, buf.slice(12), ); - 2.
データ破損ではなく完全性の判定として扱う
decrypt が OperationError をスローしたときの正しい対応は、「この暗号文はこの鍵のもとでは真正ではない」と判断して拒否することです。パディングを調整して再試行したり、認証のないモードにフォールバックしたりしてはいけません。チェックが失敗すること自体が、セキュリティ機能なのです。
- 3.
両側で鍵のフィンガープリントを比較する
両方の鍵をエクスポートしてハッシュを計算してください。ダイジェストが異なれば、それだけで謎が解けます。
js const raw = await crypto.subtle.exportKey("raw", key); const fp = await crypto.subtle.digest("SHA-256", raw); console.log(Buffer.from(fp).toString("hex").slice(0, 16)); // compare on both sides