暗号化データのフォーマットをJSONベースで規定したJSON Web Encryption(JWE)はJSON Web Token(JWT)の一部として取りあつかわれることが多く明示的に処理する機会は少ないと思われますが、仕様としてはJWTとは独立なので単独での使用も可能です。
最近業務でアルゴリズムにECDH-ESを指定したJWE暗号化データの復号処理をJavaで実装することになったのですが、まとまった参考になる情報がなかなか見あたらず難儀しました。同じように苦労される方がいそうなので、暗号化も含めメモとして残しておきます。
ECDH-ESとは?
ECDH-ESはElliptic Curve Diffie-Hellman Ephemeral Staticの略で楕円曲線Diffie-Hellman鍵共有の一種、ECDHE(一時的楕円曲線Diffie-Hellman鍵共有)にカテゴライズされるものです。ハイフン以降のESが鍵の管理方法を示していて、Ephemeralは暗号化側の鍵が一時的であること、Staticは復号側の鍵の管理が永続的であることを示します。
楕円曲線Diffie-Hellman鍵共有は楕円曲線暗号を応用した鍵共有の方法です。
- 暗号化側は公開鍵を都度生成、生成した公開鍵と復号側の公開鍵で暗号化
- 復号側には暗号化データと生成公開鍵をセットで送信
- 復号側は暗号化側から送信されてきた公開鍵と自身で管理する鍵の秘密鍵で復号
Javaで利用できるライブラリ
暗号にかかわる処理は実績と信頼が何よりも大事なので、自作は最後の手段と心得て、利用できるライブラリがあれば利用するのがベストプラクティスです。
JavaでJWTを取り扱うライブラリとしてはNimbus JOSE + JWTがもっとも普及しているようで、サンプルコードも比較的潤沢に確認できます。いまでもJava 7をコードベースとしているので古いシステムでも導入しやすいのではないかと思います。以降のコードではNimbus JOSE + JWTを用いています。
他にもいくつか確認しましたが、その中でもRESTEasyはECDH-ESにはそのままでは対応していないようで簡単に試してみた限りでは素直には使えませんでした。
暗号化処理
暗号化に必要な復号側の(秘密鍵を含まない)公開鍵はなんらかの方法で事前に暗号化側に渡しておきます。JSON Web Key(JWK)で渡した場合は JWK#parse()
メソッドで JWK 派生クラスのインスタンスを生成できます。
final var receiverKey = (ECKey) JWK.parse("(復号側公開鍵(秘密鍵は含まない)のJWK文字列)");
暗号化側の鍵は都度生成します。
final var senderKey = new ECKeyGenerator(Curve.P_256).generate();
ペイロードはなんでもよいのですが、Nimbus JOSE + JWT には Map<K, V> インターフェース実装クラスのインスタンスを設定すると Map<K, V>
インターフェースで参照できるという便利機能があります。
final var payloadMap = new HashMap<String, Object>();
payloadMap.put("key", "value");
暗号化はこれらを組みあわせて行います。
final var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM)
.ephemeralPublicKey(senderKey.toPublicJWK())
.build();
final var jweObject = new JWEObject(header, new Payload(payloadMap));
jweObject.encrypt(new ECDHEncrypter(receiverKey.toECPublicKey()));
final var jweCompactSerializationString = jweObject.serialize();
ポイントは次の2点。
- JWEHeader.Builder クラスによる JWEHeader クラスインスタンス生成時に
JWEHeader.Builder#ephemeralPublicKey()
メソッドで暗号化側の一時利用公開鍵を設定
- その
JWEHeader
クラスインスタンスを指定して JWEObject クラスインスタンスを生成、 復号側の公開鍵 を指定して JWEObject#encrypt()
メソッドを呼びだし暗号化
復号処理
JWEは暗号側の公開鍵を含むので、秘密鍵を含む復号側鍵を与えれば復号できます。
final var jweObject = JWEObject.parse(jweCompactSerializationString);
// JWE Headerは復号前に参照可能
final var algorithm = jweObject.getHeader().getAlgorithm();
final var receiverKey = (ECKey) JWK.parse("(復号側鍵(秘密鍵を含む)のJWK文字列)");
jweObject.decrypt(new ECDHDecrypter(receiverKey.toECPrivateKey()));
// JWE Payloadは復号後に参照可能
final var payload = jweObject.getPayload().toJSONObject();
終わりに
本稿では割愛した暗号全般に関する説明は別途信頼できる情報源にあたっていただければと思いますが、個人的にはずいぶん昔に本で得た知識では太刀打ちできなくなっていていろいろとまどいました。たとえば次。
- RSA鍵交換が推奨されなくなっていた(SSL / TLSのバージョンアップを追いかけて知りました)
- Diffie-Hellman鍵交換と楕円曲線Diffie-Hellman鍵交換は名前が一部重なるのに処理として考えると別物
- 楕円曲線暗号と楕円曲線Diffie-Hellman鍵交換も、同じ楕円曲線をベースにしているのにコードの水準で見るとけっこう違って見える
楕円曲線暗号関連に関しては、そもそも考えかた自体がむずかしい上に実装と関連づけられた解説が日本語ではなかなか見当たらず、どうすればよいものか最初は途方に暮れました。その意味では本稿は過去の自分に向けて書いたエントリですが、未来のどなたかの参考になりましたら幸いです。