概要

業務データの暗号化は、個人情報の保護やファイルの秘匿送信など、セキュリティ要件が絡む場面で避けて通れません。Java の javax.crypto パッケージには AES 暗号が標準で組み込まれており、外部ライブラリなしで暗号化と復号が実装できます。この記事では、現在推奨される AES-GCM モード(認証付き暗号)を使った実装を扱います。GCM は暗号化と同時にデータの改ざん検出を行えるため、CBC + HMAC の組み合わせより実装がシンプルになります。IV(初期化ベクトル)の正しい扱い方、鍵の生成方法、暗号文の保存形式、そして改ざんを検出する仕組みまでを一通り整理します。ECB モードを使ってはいけない理由や、IV の再利用がなぜ致命的なのかといった、セキュリティ上の判断材料も補足します。

使いどころ

データベースに保存する個人情報(メールアドレス・電話番号)をカラムレベルで AES-GCM 暗号化する

外部システムへ送信するファイルを AES-GCM で暗号化し、受信側で復号と改ざんチェックを同時に行う

設定ファイル内のパスワードや API キーを暗号化して保存し、アプリケーション起動時に復号して使用する

コード例

AesGcmEncryptionExample.java
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

public class AesGcmEncryptionExample {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;

    record EncryptedData(byte[] iv, byte[] cipherText) {}

    /** AES-256 鍵を生成 */
    public static SecretKey generateKey() throws Exception {
        var keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256, new SecureRandom());
        return keyGen.generateKey();
    }

    /** 暗号化 */
    public static EncryptedData encrypt(String plainText, SecretKey key)
            throws Exception {
        var iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        var cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key,
                new GCMParameterSpec(GCM_TAG_LENGTH, iv));

        var cipherText = cipher.doFinal(
                plainText.getBytes("UTF-8"));
        return new EncryptedData(iv, cipherText);
    }

    /** 復号 */
    public static String decrypt(EncryptedData data, SecretKey key)
            throws Exception {
        var cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key,
                new GCMParameterSpec(GCM_TAG_LENGTH, data.iv()));

        return new String(cipher.doFinal(data.cipherText()), "UTF-8");
    }

    /** IV + 暗号文を結合した byte[] に変換 */
    public static byte[] toBytes(EncryptedData data) {
        var result = new byte[data.iv().length + data.cipherText().length];
        System.arraycopy(data.iv(), 0, result, 0, data.iv().length);
        System.arraycopy(data.cipherText(), 0, result,
                data.iv().length, data.cipherText().length);
        return result;
    }

    public static void main(String[] args) throws Exception {
        var key = generateKey();
        var plainText = "機密データ: 顧客ID=12345";

        // 暗号化
        var encrypted = encrypt(plainText, key);
        System.out.println("暗号文: " +
                Base64.getEncoder().encodeToString(toBytes(encrypted)));

        // 復号
        var decrypted = decrypt(encrypted, key);
        System.out.println("復号: " + decrypted);

        // 改ざん検出
        var tampered = toBytes(encrypted);
        tampered[20] ^= 0xFF;
        try {
            var iv = Arrays.copyOfRange(tampered, 0, GCM_IV_LENGTH);
            var ct = Arrays.copyOfRange(tampered, GCM_IV_LENGTH,
                    tampered.length);
            decrypt(new EncryptedData(iv, ct), key);
        } catch (Exception e) {
            System.out.println("改ざん検出: " + e.getClass().getSimpleName());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で IV と暗号文をまとめて EncryptedData として扱える。var による型推論でボイラープレートも減り、コードの意図が読みやすくなる。

Java 17
// Java 17: record で IV と暗号文をまとめる
record EncryptedData(byte[] iv, byte[] cipherText) {}
var iv = new byte[12];
new SecureRandom().nextBytes(iv);
var cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key,
    new GCMParameterSpec(128, iv));
var cipherText = cipher.doFinal(plainText.getBytes("UTF-8"));
var encrypted = new EncryptedData(iv, cipherText);

Library Comparison

標準 API(javax.crypto)AES-GCM の暗号化・復号だけで十分な場合。依存ゼロで Java 8 以降のどの環境でも動く。高レベルの暗号化ユーティリティは自前で組み立てる必要がある。IV 管理や鍵保管のベストプラクティスを自分で実装することになる。
Google Tink暗号化のベストプラクティスをライブラリに任せたいとき。鍵のローテーションや複数アルゴリズムの切り替えを安全に管理できる。Google Cloud との連携が前提の設計になっており、オンプレミス環境では鍵管理の別途検討が必要。
Bouncy Castle標準 API にないアルゴリズム(ChaCha20-Poly1305 等)や、楕円曲線暗号の拡張が必要なとき。API が低レベルで学習コストが高い。AES-GCM だけなら標準 API で十分に実装できる。

注意点

IV(初期化ベクトル)は暗号化のたびに必ず異なる値を使うこと。同じ鍵で同じ IV を再利用すると、GCM の認証機能が完全に破綻し、平文が推測可能になる

ECB モードは同じ平文ブロックが同じ暗号文になるため、パターンが漏洩する。AES を使うなら GCM か CBC を選ぶこと

暗号文と IV はセットで保存・送信する必要がある。IV を暗号文の先頭に結合する方式が一般的だが、フォーマットをドキュメント化しておかないと復号時に混乱する

SecretKey オブジェクトはシリアライズせず、KeyStore で管理するのが望ましい。コード内にハードコードした鍵は、デコンパイルで簡単に読み取られる

GCM の認証タグ長は128ビットが推奨。短くすると改ざん検出の精度が下がる。NIST SP 800-38D では96ビット以上を求めている

FAQ

AES-128 と AES-256 のどちらを使うべきですか。

セキュリティ要件が高い場合は AES-256 を選びます。AES-128 でも現時点では十分な強度がありますが、長期保存データや規制対応では256ビットが求められることがあります。

GCM モードの IV は何バイトにすべきですか。

12バイト(96ビット)が NIST 推奨です。12バイト以外も技術的には可能ですが、内部で追加のハッシュ計算が発生しパフォーマンスが落ちるため、12バイト固定が無難です。

暗号化した鍵をどこに保存すればよいですか。

Java KeyStore(JKS や PKCS12)に格納するのが標準的です。環境変数やシークレットマネージャー(AWS KMS、HashiCorp Vault 等)と組み合わせる方法もあります。コード内へのハードコードは避けてください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます