概要
業務データの暗号化は、個人情報の保護やファイルの秘匿送信など、セキュリティ要件が絡む場面で避けて通れません。Java の javax.crypto パッケージには AES 暗号が標準で組み込まれており、外部ライブラリなしで暗号化と復号が実装できます。この記事では、現在推奨される AES-GCM モード(認証付き暗号)を使った実装を扱います。GCM は暗号化と同時にデータの改ざん検出を行えるため、CBC + HMAC の組み合わせより実装がシンプルになります。IV(初期化ベクトル)の正しい扱い方、鍵の生成方法、暗号文の保存形式、そして改ざんを検出する仕組みまでを一通り整理します。ECB モードを使ってはいけない理由や、IV の再利用がなぜ致命的なのかといった、セキュリティ上の判断材料も補足します。
使いどころ
データベースに保存する個人情報(メールアドレス・電話番号)をカラムレベルで AES-GCM 暗号化する
外部システムへ送信するファイルを AES-GCM で暗号化し、受信側で復号と改ざんチェックを同時に行う
設定ファイル内のパスワードや API キーを暗号化して保存し、アプリケーション起動時に復号して使用する
コード例
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());
}
}
}Version Coverage
record で IV と暗号文をまとめて EncryptedData として扱える。var による型推論でボイラープレートも減り、コードの意図が読みやすくなる。
// 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
注意点
IV(初期化ベクトル)は暗号化のたびに必ず異なる値を使うこと。同じ鍵で同じ IV を再利用すると、GCM の認証機能が完全に破綻し、平文が推測可能になる
ECB モードは同じ平文ブロックが同じ暗号文になるため、パターンが漏洩する。AES を使うなら GCM か CBC を選ぶこと
暗号文と IV はセットで保存・送信する必要がある。IV を暗号文の先頭に結合する方式が一般的だが、フォーマットをドキュメント化しておかないと復号時に混乱する
SecretKey オブジェクトはシリアライズせず、KeyStore で管理するのが望ましい。コード内にハードコードした鍵は、デコンパイルで簡単に読み取られる
GCM の認証タグ長は128ビットが推奨。短くすると改ざん検出の精度が下がる。NIST SP 800-38D では96ビット以上を求めている
FAQ
セキュリティ要件が高い場合は AES-256 を選びます。AES-128 でも現時点では十分な強度がありますが、長期保存データや規制対応では256ビットが求められることがあります。
12バイト(96ビット)が NIST 推奨です。12バイト以外も技術的には可能ですが、内部で追加のハッシュ計算が発生しパフォーマンスが落ちるため、12バイト固定が無難です。
Java KeyStore(JKS や PKCS12)に格納するのが標準的です。環境変数やシークレットマネージャー(AWS KMS、HashiCorp Vault 等)と組み合わせる方法もあります。コード内へのハードコードは避けてください。