1. 概要

対称鍵ブロック暗号は、データの暗号化において重要な役割を果たします。 これは、暗号化と復号化の両方に同じキーが使用されることを意味します。 Advanced Encryption Standard (AES)は、広く使用されている対称鍵暗号化アルゴリズムです。

このチュートリアルでは、JDK内でJava暗号化アーキテクチャ(JCA)を使用してAES暗号化と復号化を実装する方法を学習します。

2. AESアルゴリズム

AESアルゴリズムは、反復対称鍵ブロック暗号であり、は128、192、および256ビットの暗号鍵(秘密鍵)をサポートして、128ビットのブロックでデータを暗号化および復号化します。 次の図は、高レベルのAESアルゴリズムを示しています。

暗号化するデータが128ビットのブロックサイズ要件を満たしていない場合は、パディングする必要があります。 パディングは、最後のブロックを128ビットで埋めるプロセスです。

3. AESバリエーション

AESアルゴリズムには、6つの動作モードがあります。

  1. ECB(電子コードブック)
  2. CBC(Cipher Block Chaining)
  3. CFB(暗号フィードバック)
  4. OFB(出力フィードバック)
  5. CTR(カウンター)
  6. GCM(ガロア/カウンターモード)

暗号化アルゴリズムの効果を強化するために、動作モードを適用できます。 さらに、動作モードは、ブロック暗号をストリーム暗号に変換する場合があります。 各モードには長所と短所があります。 それぞれを簡単に確認しましょう。

3.1. ECB

この操作モードは、すべての中で最も単純です。 平文は128ビットのサイズのブロックに分割されます。 次に、各ブロックは同じキーとアルゴリズムで暗号化されます。 したがって、同じブロックに対して同じ結果が生成されます。 これがこのモードの主な弱点であり、暗号化にはお勧めしません。 データのパディングが必要です。

3.2. CBC

ECBの弱点を克服するために、CBCモードは初期化ベクトル(IV)を使用して暗号化を強化します。 まず、CBCはIVで平文ブロックxorを使用します。 次に、結果を暗号文ブロックに暗号化します。 次のブロックでは、暗号化の結果を使用して、最後のブロックまでプレーンテキストブロックとxorします。

このモードでは、暗号化を並列化することはできませんが、復号化を並列化することはできます。 また、データのパディングも必要です。

3.3. CFB

このモードは、ストリーム暗号として使用できます。 まず、IVを暗号化し、次に平文ブロックとxorして暗号文を取得します。 次に、CFBは暗号化結果をxorプレーンテキストに暗号化します。 IVが必要です。

このモードでは、復号化を並列化できますが、暗号化を並列化することはできません。

3.4. OFB

このモードは、ストリーム暗号として使用できます。 まず、IVを暗号化します。 次に、暗号化の結果を使用して平文を排他的論理和し、暗号文を取得します。

パディングデータを必要とせず、ノイズの多いブロックの影響を受けません。

3.5. クリック率

このモードでは、カウンターの値をIVとして使用します。 OFBと非常によく似ていますが、IVの代わりに毎回暗号化されるカウンターを使用します。

このモードには、暗号化/復号化の並列化を含む2つの長所があり、1つのブロックのノイズが他のブロックに影響を与えることはありません。

3.6. GCM

このモードは、CTRモードの拡張です。 GCMは大きな注目を集めており、NISTによって推奨されています。 GCMモデルは、暗号文と認証タグを出力します。 このモードの主な利点は、アルゴリズムの他の操作モードと比較して、その効率です。

このチュートリアルでは、 AES / CBC / PKCS5Padding アルゴリズムを使用します。これは、多くのプロジェクトで広く使用されているためです。

3.7. 暗号化後のデータのサイズ

前述のように、AESのブロックサイズは128ビットまたは16バイトです。 AESはサイズを変更せず、暗号文のサイズは平文のサイズと同じです。 また、ECBおよびCBCモードでは、 PKCS 5. のようなパディングアルゴリズムを使用する必要があります。したがって、暗号化後のデータのサイズは次のようになります。

ciphertext_size (bytes) = cleartext_size + (16 - (cleartext_size % 16))

IVを暗号文で保存するには、さらに16バイトを追加する必要があります。

4. AESパラメータ

AESアルゴリズムでは、入力データ、秘密鍵、およびIVの3つのパラメーターが必要です。 IVはECBモードでは使用されません。

4.1. 入力データ

AESへの入力データは、文字列、ファイル、オブジェクト、およびパスワードベースにすることができます。

4.2. シークレットキー

AESで秘密鍵を生成するには、ランダムな番号から生成する方法と、特定のパスワードから取得する方法の2つがあります。

最初のアプローチでは、秘密鍵は、 SecureRandom クラスのような暗号的に安全な(疑似)乱数ジェネレーターから生成する必要があります。

秘密鍵を生成するために、KeyGeneratorクラスを使用できます。 n (128、192、および256)ビットのサイズのAESキーを生成する方法を定義しましょう。

public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(n);
    SecretKey key = keyGenerator.generateKey();
    return key;
}

2番目のアプローチでは、PBKDF2 のようなパスワードベースの鍵導出関数を使用して、特定のパスワードからAES秘密鍵を導出できます。 パスワードを秘密鍵に変換するためのソルト値も必要です。 ソルトもランダムな値です。

SecretKeyFactoryクラスをPBKDF2WithHmacSHA256アルゴリズムとともに使用して、指定されたパスワードからキーを生成できます。

65,536回の反復と256ビットのキー長で特定のパスワードからAESキーを生成する方法を定義しましょう。

public static SecretKey getKeyFromPassword(String password, String salt)
    throws NoSuchAlgorithmException, InvalidKeySpecException {
    
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
        .getEncoded(), "AES");
    return secret;
}

4.3. 初期化ベクトル(IV)

IVは疑似乱数値であり、暗号化されるブロックと同じサイズです。 SecureRandom クラスを使用して、ランダムIVを生成できます。

IVを生成する方法を定義しましょう:

public static IvParameterSpec generateIv() {
    byte[] iv = new byte[16];
    new SecureRandom().nextBytes(iv);
    return new IvParameterSpec(iv);
}

5. 暗号化と復号化

5.1. 弦

入力文字列暗号化を実装するには、最初に前のセクションに従って秘密鍵とIVを生成する必要があります。 次のステップとして、 getInstance()メソッドを使用して、Cipherクラスからインスタンスを作成します。

さらに、 init()メソッドを使用して、秘密鍵、IV、および暗号化モードを使用して暗号インスタンスを構成します。 最後に、 doFinal()メソッドを呼び出して、入力文字列を暗号化します。 このメソッドは、入力のバイトを取得し、暗号文をバイト単位で返します。

public static String encrypt(String algorithm, String input, SecretKey key,
    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] cipherText = cipher.doFinal(input.getBytes());
    return Base64.getEncoder()
        .encodeToString(cipherText);
}

入力文字列を復号化するために、DECRYPT_MODEを使用して暗号を初期化してコンテンツを復号化できます。

public static String decrypt(String algorithm, String cipherText, SecretKey key,
    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    byte[] plainText = cipher.doFinal(Base64.getDecoder()
        .decode(cipherText));
    return new String(plainText);
}

文字列入力を暗号化および復号化するためのテストメソッドを書いてみましょう。

@Test
void givenString_whenEncrypt_thenSuccess()
    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException { 
    
    String input = "baeldung";
    SecretKey key = AESUtil.generateKey(128);
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    String algorithm = "AES/CBC/PKCS5Padding";
    String cipherText = AESUtil.encrypt(algorithm, input, key, ivParameterSpec);
    String plainText = AESUtil.decrypt(algorithm, cipherText, key, ivParameterSpec);
    Assertions.assertEquals(input, plainText);
}

5.2. ファイル

次に、AESアルゴリズムを使用してファイルを暗号化します。 手順は同じですが、ファイルを操作するにはいくつかのIOクラスが必要です。 テキストファイルを暗号化してみましょう。

public static void encryptFile(String algorithm, SecretKey key, IvParameterSpec iv,
    File inputFile, File outputFile) throws IOException, NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    FileInputStream inputStream = new FileInputStream(inputFile);
    FileOutputStream outputStream = new FileOutputStream(outputFile);
    byte[] buffer = new byte[64];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        byte[] output = cipher.update(buffer, 0, bytesRead);
        if (output != null) {
            outputStream.write(output);
        }
    }
    byte[] outputBytes = cipher.doFinal();
    if (outputBytes != null) {
        outputStream.write(outputBytes);
    }
    inputStream.close();
    outputStream.close();
}

特にファイルが大きい場合は、ファイル全体をメモリに読み込もうとすることはお勧めしません。 代わりに、一度にバッファを暗号化します。

ファイルを復号化するために、同様の手順を使用し、前に見たようにDECRYPT_MODEを使用して暗号を初期化します。

繰り返しになりますが、テキストファイルを暗号化および復号化するためのテスト方法を定義しましょう。 この方法では、テストリソースディレクトリから baeldung.txt ファイルを読み取り、 baeldung.encrypted というファイルに暗号化してから、ファイルを新しいファイルに復号化します。

@Test
void givenFile_whenEncrypt_thenSuccess() 
    throws NoSuchAlgorithmException, IOException, IllegalBlockSizeException, 
    InvalidKeyException, BadPaddingException, InvalidAlgorithmParameterException, 
    NoSuchPaddingException {
    
    SecretKey key = AESUtil.generateKey(128);
    String algorithm = "AES/CBC/PKCS5Padding";
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    Resource resource = new ClassPathResource("inputFile/baeldung.txt");
    File inputFile = resource.getFile();
    File encryptedFile = new File("classpath:baeldung.encrypted");
    File decryptedFile = new File("document.decrypted");
    AESUtil.encryptFile(algorithm, key, ivParameterSpec, inputFile, encryptedFile);
    AESUtil.decryptFile(
      algorithm, key, ivParameterSpec, encryptedFile, decryptedFile);
    assertThat(inputFile).hasSameTextualContentAs(decryptedFile);
}

5.3. パスワードベース

指定されたパスワードから派生した秘密鍵を使用して、AESの暗号化と復号化を行うことができます。

秘密鍵の生成には、 getKeyFromPassword()メソッドを使用します。 暗号化と復号化の手順は、文字列入力セクションに示されている手順と同じです。 次に、インスタンス化された暗号と提供された秘密鍵を使用して暗号化を実行できます。

テストメソッドを書いてみましょう:

@Test
void givenPassword_whenEncrypt_thenSuccess() 
    throws InvalidKeySpecException, NoSuchAlgorithmException, 
    IllegalBlockSizeException, InvalidKeyException, BadPaddingException, 
    InvalidAlgorithmParameterException, NoSuchPaddingException {
    
    String plainText = "www.baeldung.com";
    String password = "baeldung";
    String salt = "12345678";
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    SecretKey key = AESUtil.getKeyFromPassword(password,salt);
    String cipherText = AESUtil.encryptPasswordBased(plainText, key, ivParameterSpec);
    String decryptedCipherText = AESUtil.decryptPasswordBased(
      cipherText, key, ivParameterSpec);
    Assertions.assertEquals(plainText, decryptedCipherText);
}

5.4. 物体

Javaオブジェクトを暗号化するには、SealedObjectクラスを使用する必要があります。 オブジェクトはSerializableである必要があります。 Studentクラスを定義することから始めましょう。

public class Student implements Serializable {
    private String name;
    private int age;

    // standard setters and getters
}

次に、Studentオブジェクトを暗号化しましょう。

public static SealedObject encryptObject(String algorithm, Serializable object,
    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, 
    InvalidKeyException, IOException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    SealedObject sealedObject = new SealedObject(object, cipher);
    return sealedObject;
}

暗号化されたオブジェクトは、後で正しい暗号を使用して復号化できます。

public static Serializable decryptObject(String algorithm, SealedObject sealedObject,
    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    ClassNotFoundException, BadPaddingException, IllegalBlockSizeException,
    IOException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    Serializable unsealObject = (Serializable) sealedObject.getObject(cipher);
    return unsealObject;
}

それでは、テストケースを書いてみましょう。

@Test
void givenObject_whenEncrypt_thenSuccess() 
    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    InvalidAlgorithmParameterException, NoSuchPaddingException, IOException, 
    BadPaddingException, ClassNotFoundException {
    
    Student student = new Student("Baeldung", 20);
    SecretKey key = AESUtil.generateKey(128);
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    String algorithm = "AES/CBC/PKCS5Padding";
    SealedObject sealedObject = AESUtil.encryptObject(
      algorithm, student, key, ivParameterSpec);
    Student object = (Student) AESUtil.decryptObject(
      algorithm, sealedObject, key, ivParameterSpec);
    assertThat(student).isEqualToComparingFieldByField(object);
}

6. 結論

この記事では、JavaのAESアルゴリズムを使用して、文字列、ファイル、オブジェクト、パスワードベースのデータなどの入力データを暗号化および復号化する方法を学習しました。 さらに、暗号化後のAESのバリエーションとデータのサイズについても説明しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。