CLOVER🍀

That was when it all began.

JavaでAES(ECB/CBC)を使う

これは、なにをしたくて書いたもの?

こちらのエントリを書いていて、「JavaでAESを使う時のコードを全然覚えてないな」と思いまして。

MessageDigestに"SHA"とか、Cipherに"AES"とだけ指定した場合、どうなるの? - CLOVER🍀

メモしておこうかな、と。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.9.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-60-generic", arch: "amd64", family: "unix"

JavaでAESを使う

JavaでAESを扱うには、Cipherクラスを主として使います。

Cipher (Java SE 11 & JDK 11 )

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / Cipherクラス

あと、こちらも。

KeyGenerator (Java SE 11 & JDK 11 )

SecretKeySpec (Java SE 11 & JDK 11 )

IvParameterSpec (Java SE 11 & JDK 11 )

今回、アルゴリズム、モード、パディングは以下の2つを使ってプログラムを書くとしましょう。

  • AES/ECB/PKCS5Padding
  • AES/CBC/PKCS5Padding

鍵の長さは最初は128ビットで行い、最後に192ビット、256ビットまで扱います。

パディングなしの場合と、他のモードは今回は考えないことにします。

Javaセキュリティ標準アルゴリズム名 / Cipherアルゴリズム名

JDKプロバイダ・ドキュメント / SunJCEプロバイダ

この前提で、テストコードで使い方をメモしていこうかな、と。

テストコードは、使用するクラスを変えつつ、暗号化した値を復号できるところまでを確認します。

準備

テストコード用にJUnit 5とAssertJを依存関係に追加し、Maven Surefire Pluginの設定を行います。

    <dependencies>
        <dependency>
          <groupId>org.assertj</groupId>
          <artifactId>assertj-core</artifactId>
          <version>3.18.1</version>
          <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

テストコードの雛形は、こちら。
src/test/java/org/littlewings/aes/AesEncryptDecryptTest.java

package org.littlewings.aes;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class AesEncryptDecryptTest {

    // ここに、テストを書く!!
}

では、書いていきましょう。

KeyGeneratorを使って秘密鍵を作成する

AESは共通鍵暗号アルゴリズムなわけですが、秘密鍵の作成をKeyGeneratorに任せることができます。

KeyGenerator (Java SE 11 & JDK 11 )

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / KeyGeneratorクラス

AES/ECB/PKCS5Paddingを使う場合。

    @Test
    public void ecbUsingKeyGenerator() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // SecretKey
        int keySizeAsBit = 128;  // 128 bit
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(keySizeAsBit);
        SecretKey secretKey = keyGenerator.generateKey();

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        encryptor.init(Cipher.ENCRYPT_MODE, secretKey);

        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");

        decryptor.init(Cipher.DECRYPT_MODE, secretKey);

        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

暗号化対象の値は、いずれのテストケースでもこの値にします。

        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

KeyGeneratorを使用した、秘密鍵の生成はこちら。

        // SecretKey
        int keySizeAsBit = 128;  // 128 bit
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(keySizeAsBit);
        SecretKey secretKey = keyGenerator.generateKey();

KeyGenerator#getInstanceの引数にどのアルゴリズム用のキーを作るのかを指定する必要があるのですが、AESとだけ指定します。
この部分に関しては、モードやパディングは関係ありません。

Javaセキュリティ標準アルゴリズム名 / KeyGeneratorアルゴリズム

KeyGenerator#initでは、作成する秘密鍵のサイズを指定します。今回は鍵のサイズは128ビットなので、128と指定します。

最後にKeyGenerator#generateKeyを呼び出すと、SecretKey、すなわち秘密鍵が作成されます。

あとは、暗号化します。

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        encryptor.init(Cipher.ENCRYPT_MODE, secretKey);

        byte[] encrypted = encryptor.doFinal(value);

復号の際には、同じ秘密鍵を使います。

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");

        decryptor.init(Cipher.DECRYPT_MODE, secretKey);

        byte[] decrypted = decryptor.doFinal(encrypted);

AES/CBC/PKCS5Paddingの場合はこちら。

    @Test
    public void cbcUsingKeyGeneratorNoProvideIv() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // SecretKey
        int keySizeAsBit = 128;  // 128 bit
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(keySizeAsBit);
        SecretKey secretKey = keyGenerator.generateKey();

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        encryptor.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] ivBytes = encryptor.getIV();  // auto generate
        byte[] encrypted = encryptor.doFinal(value);

        assertThat(ivBytes).hasSize(16);  // block size
        assertThat(ivBytes).hasSize(encryptor.getBlockSize());  // block size

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, secretKey, iv);  // iv required
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

CBCの場合、IV(初期化ベクトル)が必要になります。

Cipher#init時にIVを明示的に与えない場合はランダムに作成されますが、このIVは復号時に必要になります。
よって、生成されたIVを取得する必要があります(Cipher#getIV)。

        byte[] ivBytes = encryptor.getIV();  // auto generate

IVを作っているのは、ここですね。

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/CipherCore.java#L397-L405

得られたIV(バイト配列)は、IvParameterSpecのインスタンス生成に使います。

IvParameterSpec (Java SE 11 & JDK 11 )

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

このIvParameterSpecをCipher#init時に与え、復号を行います。暗号化に使用したIVなしでは、復号できません。

        decryptor.init(Cipher.DECRYPT_MODE, secretKey, iv);  // iv required
        byte[] decrypted = decryptor.doFinal(encrypted);

KeyGeneratorに関しては、こんな感じで。

ちなみに、KeyGeneratorが実際に秘密鍵を作成を依頼するのはAESKeyGeneratorなのですが、こちらはSecureRandom#nextBytesを
使っているだけだったりします。

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/AESKeyGenerator.java#L105-L116

SecretKeySpecを使って秘密鍵を作る

次は、ScretKeySpecを使って秘密鍵を作ってみます。先ほどはKeyGeneratorに秘密鍵の作成を完全に任せましたが、
こちらは秘密鍵のデータを自分で用意することになります。

SecretKeySpec (Java SE 11 & JDK 11 )

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / KeySpecインタフェース

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / KeySpecサブインタフェース

まずはAES/ECB/PKCS5Paddingから。

    @Test
    public void ecbUseUserDefinedSecretKey() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 16;  // 128 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey);
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey);
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

秘密鍵となる16バイト(128ビット)分のバイト配列が必要なのですが、今回はSecureRandomで作ることにしました。

        // User Defined SecretKey
        int keySize = 16;  // 128 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

まあ、この方法を選んだ時点で先ほどのKeyGeneratorとやっていることは変わらないのですが、鍵のデータを自分で用意した体で
今回は書いています。

このバイト配列は、SecretKeySpecのコンストラクタに引き渡します。2つ目の引数にはAESと指定します。こちらもKeyGeneratorの時と
同様に、モードやパディングについては関係なくAESとだけ指定すればOKです。

        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

あとは、KeyGeneratorを使った場合と同じです。

AES/CBC/PKCS5Paddingの場合は、こちら。

    @Test
    public void cbcUseUserDefinedSecretKeyNoProvideIv() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 16;  // 128 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey);
        byte[] ivBytes = encryptor.getIV();  // auto generate
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey, iv);  // iv required
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

KeyGeneratorを使っていた部分がSecretKeySpecを使う内容に書き換わっているだけなので、特段変わったところは
ありません。

なお、SecretKeySpec作成時に使ったバイト配列はSecretKey#getEncodedで取り出せますし、AESの鍵を
KeyGenerator#generateKeyで作った場合に得られるのもSecretKeySpecです。

    @Test
    public void secretKeySpecEncodedBytes() throws NoSuchAlgorithmException {
        // direct SecretKeySpec
        int keySize = 16;  // 128 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");

        assertThat(secretKeyBytes).isEqualTo(secretKey.getEncoded());

        // KeyGenerator
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(keySize * 8);
        SecretKey secretKeyFromKeyGenerator = keyGenerator.generateKey();

        assertThat(secretKeyFromKeyGenerator).isInstanceOf(SecretKeySpec.class);
    }

なので、秘密鍵の作成だけKeyGeneratorで行って、生成したバイト配列を後から取り出すというのもできます、と。

IvParameterSpecを使ってIV(初期化ベクトル)を指定する

CBCのような、IV(初期化ベクトル)を必要とするモードの場合の話です。ここまでは、暗号化する時にIVはCipherに
生成してもらっていましたが、ここではIVを自分で用意することにします。

使うのは、IvParameterSpecクラスです。

IvParameterSpec (Java SE 11 & JDK 11 )

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / AlgorithmParameterSpecインタフェース

作成したコードはこちら。AES/CBC/PKCS5Paddingです。

    @Test
    public void cbcUseUserDefinedSecretKeyProvideIv() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 16;  // 128 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // Initial Vector
        int blockSize = 16;
        byte[] ivBytes = new byte[blockSize];
        random.nextBytes(ivBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");

        assertThat(encryptor.getBlockSize()).isEqualTo(blockSize);  // block-size = 16 byte

        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec encryptIv = new IvParameterSpec(ivBytes);

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey, encryptIv);  // using iv
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec decryptIv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey, decryptIv);  // using iv
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

IV用のデータは、ブロックサイズ(16バイト)用意します。今回は、他に習ってSecureRandomで用意しました。

        // Initial Vector
        int blockSize = 16;
        byte[] ivBytes = new byte[blockSize];
        random.nextBytes(ivBytes);

ブロックサイズは、Cipherのインスタンスを作成した後であればCipher#getBlockSizeで得ることができます。

        assertThat(encryptor.getBlockSize()).isEqualTo(blockSize);  // block-size = 16 byte

AESのブロックサイズは、16バイトで固定です。

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/AESCipher.java#L185

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/AESConstants.java#L40

IVとして用意したバイト配列は、IvParameterSpecのコンストラクタに渡してインスタンスを作成し、Cipher#initの引数として
使います。

        IvParameterSpec encryptIv = new IvParameterSpec(ivBytes);

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey, encryptIv);  // using iv
        byte[] encrypted = encryptor.doFinal(value);

復号の時も、同じIVの値を使います。

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec decryptIv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey, decryptIv);  // using iv
        byte[] decrypted = decryptor.doFinal(encrypted);

AESを扱う時に使いそうなクラスは、こんな感じではないでしょうか。

128ビット以外の鍵を使う

AESは鍵の長さとして、128ビット、192ビット、256ビットの3つを利用できます。

Java 9より前は128ビット以外の鍵を使う場合は変更作業が必要だったのですが、Java 9以降はデフォルトで使えるようになっています。

Java暗号化アーキテクチャ(JCA)リファレンス・ガイド / 暗号強度の構成

$JAVA_HOME/conf/security/java.securityというファイルのcrypto.policyプロパティが、unlimitedになったからです。

$ grep ^crypto.policy /usr/lib/jvm/java-11-openjdk-amd64/conf/security/java.security
crypto.policy=unlimited

$JAVA_HOME/conf/security/java.securityファイル内の、crypto.policyのコメントを記載しておきます。

#
# Cryptographic Jurisdiction Policy defaults
#
# Import and export control rules on cryptographic software vary from
# country to country.  By default, Java provides two different sets of
# cryptographic policy files[1]:
#
#     unlimited:  These policy files contain no restrictions on cryptographic
#                 strengths or algorithms
#
#     limited:    These policy files contain more restricted cryptographic
#                 strengths
#
# The default setting is determined by the value of the "crypto.policy"
# Security property below. If your country or usage requires the
# traditional restrictive policy, the "limited" Java cryptographic
# policy is still available and may be appropriate for your environment.
#
# If you have restrictions that do not fit either use case mentioned
# above, Java provides the capability to customize these policy files.
# The "crypto.policy" security property points to a subdirectory
# within <java-home>/conf/security/policy/ which can be customized.
# Please see the <java-home>/conf/security/policy/README.txt file or consult
# the Java Security Guide/JCA documentation for more information.
#
# YOU ARE ADVISED TO CONSULT YOUR EXPORT/IMPORT CONTROL COUNSEL OR ATTORNEY
# TO DETERMINE THE EXACT REQUIREMENTS.
#
# [1] Please note that the JCE for Java SE, including the JCE framework,
# cryptographic policy files, and standard JCE providers provided with
# the Java SE, have been reviewed and approved for export as mass market
# encryption item by the US Bureau of Industry and Security.
#
# Note: This property is currently used by the JDK Reference implementation.
# It is not guaranteed to be examined and used by other implementations.
#
crypto.policy=unlimited

Java 9より前は、こうでした。

Oracle Java JDK 9より前は、Oracle実装によって許可されているデフォルト暗号強度は、強力だが制限付きでした(たとえば、128ビットに制限されたAESキー)。この制限をなくすには、管理者が、無制限強度の管轄ポリシー・ファイルのバンドルを別にダウンロードしてインストールします。

では、どうして制限されていたかというと、これです。

これまでどおり、管理者およびユーザーは、自分の地理的な場所に応じたすべての輸入/輸出ガイドラインに従う必要があります。

これを理解するにはEAR(Export Administration Regulations)という米国輸出規則、その中の品目と国のマッピングを読み解いていく
必要があるのですが…。

EAR超入門‐米国の輸出規制を学ぼう‐一般財団法人安全保障貿易情報センター 2019年度版

Commerce Control List Overview and the Country Chart Supplement No. 1 to Part 738 page

Commerce Control List Supplement No. 1 to Part 774

途中で諦めました…。日本ではOKなはずなので、今回はそのまま使ってみます。

秘密鍵を192ビットにする

秘密鍵の長さを変えるといっても、鍵として用意するデータの長さを変えればよいのです。まずは192ビットの鍵にします。

AES/ECB/PKCS5Paddingの場合。秘密鍵は自分で用意する形にしました。

    @Test
    public void ecbUsing192BitSecretKey() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 24;  // 192 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey);
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey);
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

今回は、SecureRandomで用意する鍵を24バイト(192ビット)にしています。

        // User Defined SecretKey
        int keySize = 24;  // 192 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

ポイントはここだけです。

AES/CBC/PKCS5Paddingの場合。IVは自分で用意しています。

    @Test
    public void cbcUsing192BitSecretKey() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 24;  // 192 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // Initial Vector
        int blockSize = 16;
        byte[] ivBytes = new byte[blockSize];
        random.nextBytes(ivBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec encryptIv = new IvParameterSpec(ivBytes);

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey, encryptIv);  // using iv
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec decryptIv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey, decryptIv);  // using iv
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }
秘密鍵を256ビットにする

秘密鍵を256ビットにしてみます。用意する鍵のデータのサイズが変わるだけなので、コードを載せるだけにします…。

AES/ECB/PKCS5Paddingの場合。

    @Test
    public void ecbUsing256BitSecretKey() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 32;  // 256 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey);
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey);
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }

AES/CBC/PKCS5Paddingの場合。

    @Test
    public void cbcUsing256BitSecretKey() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
        // target
        byte[] value = "こんにちは、世界".getBytes(StandardCharsets.UTF_8);

        // User Defined SecretKey
        int keySize = 32;  // 256 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // Initial Vector
        int blockSize = 16;
        byte[] ivBytes = new byte[blockSize];
        random.nextBytes(ivBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec encryptIv = new IvParameterSpec(ivBytes);

        encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey, encryptIv);  // using iv
        byte[] encrypted = encryptor.doFinal(value);

        // decrypt
        Cipher decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey decryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec decryptIv = new IvParameterSpec(ivBytes);

        decryptor.init(Cipher.DECRYPT_MODE, decryptSecretKey, decryptIv);  // using iv
        byte[] decrypted = decryptor.doFinal(encrypted);

        // assertion
        assertThat(new String(decrypted, StandardCharsets.UTF_8)).isEqualTo("こんにちは、世界");
    }
変な鍵の長さにしてみる

こういうふうに単純に動かしているとやや不安になるので、鍵のサイズをサポートされていないものにするとどうなるか、
確認してみました。

ECBで鍵の長さを64バイト(512ビット)にしてみます。

    @Test
    public void invalidKeyLength() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        // User Defined SecretKey
        int keySize = 64;  // 512 bit
        SecureRandom random = new SecureRandom();
        byte[] secretKeyBytes = new byte[keySize];
        random.nextBytes(secretKeyBytes);

        // encrypt
        Cipher encryptor = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKey encryptSecretKey = new SecretKeySpec(secretKeyBytes, "AES");

        assertThatThrownBy(() -> encryptor.init(Cipher.ENCRYPT_MODE, encryptSecretKey))
                .isInstanceOf(InvalidKeyException.class)
                .hasMessage("Invalid AES key length: 64 bytes");
    }

すると、InvalidKeyExceptionがスローされます。

鍵の長さをチェックしているのは、ここですね。

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/AESCrypt.java#L89-L92

そして、使える鍵の長さが定義されているのは、ここです。

https://github.com/openjdk/jdk11u/blob/jdk-11.0.9.1%2B1/src/java.base/share/classes/com/sun/crypto/provider/AESConstants.java#L45

16バイト(128ビット)、24バイト(192ビット)、32バイト(256ビット)の3つが定義されています。

まとめ

Javaで、AES(ECB、CBCのパディングありに限ってですが)を扱うコードを書いてみました。

時々使ったりする時に完全に忘れているので、メモとして。

AESのソースコードを読んだり、Cipherまわり(というかJCA)に関するドキュメントを読むいい機会にもなりました。