Spring SecurityのAES256文字列暗号化/復号化を試してみる

## 概要
仕事でデータの暗号化/復号化を前提とした案件の対応方法を検討することがあったので備忘も兼ねて試したことを書いてみる。

## 参考情報
英語だがspring.ioのドキュメントが参考になる
[spring.io - Encryptorsのドキュメント](https://docs.spring.io/spring-security/site/docs/4.2.12.RELEASE/apidocs/org/springframework/security/crypto/encrypt/Encryptors.html)

日本語のだとTERASOLUNAのドキュメントあたりが読みやすい
[6. TERASOLUNA Server Framework for Java (5.x)によるセキュリティ対策 >> 6.9. 暗号化](https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/Encryption.html)

## 結論（いきなり）
結論から言うと以下の様な感じ（あくまで個人的な見解）

|メソッド|内容|暗号利用モード|暗号化強度|
|:--|:--|:--|:--|
|noOpText|暗号化しない。実装のMock等として利用するもの。|なし|論外|
|queryableText|「標準の」パスワードベースの暗号化を使用する。不変の値を生成する。|CBC|低|
|text|「標準の」パスワードベースの暗号化を使用する。可変の値を生成する。|CBC|中|
|delux|「より強力な」パスワードベースの暗号化を使用する。可変の値を生成する。|GCM|高|

## 検証
以下のようなサンプルを作成し、実際に動かしてみる。

~~~ java:EncryptorTest.java
public class EncryptorTest {

  private static String secret = "1234";
  private static String salt = "5678";

  public static void main(String[] args) throws IOException {

    String text = "Hello";

    TextEncryptor encryptor1 = Encryptors.noOpText();
    String noOpTextEncryptText = encryptor1.encrypt(text);
    String noOpTextDecryptText = encryptor1.decrypt(noOpTextEncryptText);

    TextEncryptor encryptor2 = Encryptors.queryableText(secret, salt);
    String queryableTextEncryptText = encryptor2.encrypt(text);
    String queryableTextDecryptText = encryptor2.decrypt(queryableTextEncryptText);

    TextEncryptor encryptor3 = Encryptors.text(secret, salt);
    String textEncryptText = encryptor3.encrypt(text);
    String textDecryptText = encryptor3.decrypt(textEncryptText);

    TextEncryptor encryptor4 = Encryptors.delux(secret, salt);
    String deluxEncryptText = encryptor4.encrypt(text);
    String deluxDecryptText = encryptor4.decrypt(deluxEncryptText);

    System.out.println("noOpText()      :");
    System.out.println("  暗号化文字列      : " + noOpTextEncryptText);
    System.out.println("  復号化確認        : " + noOpTextDecryptText);
    System.out.println("");
    System.out.println("queryableText() : ");
    System.out.println("  暗号化文字列      : " + queryableTextEncryptText);
    System.out.println("  復号化確認        : " + queryableTextDecryptText);
    System.out.println("");
    System.out.println("text()          : ");
    System.out.println("  暗号化文字列      : " + textEncryptText);
    System.out.println("  復号化確認        : " + textDecryptText);
    System.out.println("");
    System.out.println("delux()         : ");
    System.out.println("  暗号化文字列      : " + deluxEncryptText);
    System.out.println("  復号化確認        : " + deluxDecryptText);
  }
~~~

~~~ console:実行結果
noOpText()      :
  暗号化文字列      : Hello
  復号化確認        : Hello

queryableText() : 
  暗号化文字列      : 524110739525b3f266a0064b4229a77d
  復号化確認        : Hello

text()          : 
  暗号化文字列      : ced1eb40b88e691f502d548ed17752bff82592da917804ea66ea1a49bfa77033
  復号化確認        : Hello

delux()         : 
  暗号化文字列      : 61e62f473d0cb228305081b927a3225d487a749b87af62071ad60dc6b27703410de45f7fbf
  復号化確認        : Hello
~~~

暗号化の文字長が異なるがいずれのケースも暗号化からの復号化が出来ていることが分かる。
どういう違いがあるのか確認するため、ここで各メソッドのロジックを見てみる。
※ 暗号化処理の専門家ではないのでアリゴリズム云々は語れないのでご容赦いただきたい

### noOpText
Encryptors.noOpText()でインスタンスを作成した場合、
以下のロジックが呼び出される。

~~~ java:Encryptors.java
  private static final class NoOpTextEncryptor implements TextEncryptor {

    public String encrypt(String text) {
      return text;
    }

    public String decrypt(String encryptedText) {
      return encryptedText;
    }

  }
~~~

論外と評したが何もしていない。
果たして何か使い道はあるのか…


### queryableText

続いて**queryableText**

~~~ java:Encryptors.java
	public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
		return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
				salt));
	}
~~~

~~~ java:AesBytesEncryptor.java
	public AesBytesEncryptor(String password, CharSequence salt) {
		this(password, salt, null); // ← ここの引数がnull
	}

	public AesBytesEncryptor(String password, CharSequence salt,
			BytesKeyGenerator ivGenerator) {
		this(password, salt, ivGenerator, CipherAlgorithm.CBC);
	}

	public AesBytesEncryptor(String password, CharSequence salt,
			BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
		PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt),
				1024, 256);
		SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec);
		this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
		this.alg = alg;
		this.encryptor = alg.createCipher();
		this.decryptor = alg.createCipher();
		this.ivGenerator = ivGenerator != null ? ivGenerator : alg.defaultIvGenerator();
	}
~~~

### text

**queryableText**のコメントは置いといて**text**を

~~~ java:Encryptors.java
	public static TextEncryptor text(CharSequence password, CharSequence salt) {
		return new HexEncodingTextEncryptor(standard(password, salt));
	}

	public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
		return new AesBytesEncryptor(password.toString(), salt,
				KeyGenerators.secureRandom(16));  // ← ここの引数が乱数
	}
~~~

~~~ java:AesBytesEncryptor.java
	public AesBytesEncryptor(String password, CharSequence salt,
			BytesKeyGenerator ivGenerator) {
		this(password, salt, ivGenerator, CipherAlgorithm.CBC);
	}

	public AesBytesEncryptor(String password, CharSequence salt,
			BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
		PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt),
				1024, 256);
		SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec);
		this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
		this.alg = alg;
		this.encryptor = alg.createCipher();
		this.decryptor = alg.createCipher();
		this.ivGenerator = ivGenerator != null ? ivGenerator : alg.defaultIvGenerator();
	}
~~~

**queryableText**と**text**の違いは
BytesKeyGeneratorを**null**で渡すか、**KeyGenerators.secureRandom(16)**で渡すかの違いである。

**queryableText**は**null**で渡しているので毎回、同じ暗号化文字列を生成しており、
**text**は**KeyGenerators.secureRandom(16)**を渡しているので毎回違う乱数が生成され、結果、毎回違う暗号化文字列が生成される。


### delux

そして最後に**delux**

~~~ java:Encryptors.java
	public static TextEncryptor delux(CharSequence password, CharSequence salt) {
		return new HexEncodingTextEncryptor(stronger(password, salt));
	}

	public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
		return new AesBytesEncryptor(password.toString(), salt,
				KeyGenerators.secureRandom(16), CipherAlgorithm.GCM);
	}
~~~

~~~ java:AesBytesEncryptor.java
	public AesBytesEncryptor(String password, CharSequence salt,
			BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
		PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt),
				1024, 256);
		SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec);
		this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
		this.alg = alg;
		this.encryptor = alg.createCipher();
		this.decryptor = alg.createCipher();
		this.ivGenerator = ivGenerator != null ? ivGenerator : alg.defaultIvGenerator();
	}
~~~

**text**と**delux**の違いは
CipherAlgorithmに**CBC**を指定するか**GCM**を指定するかの違いである。

GCM (Galois/Counter Mode) は並列処理が可能でCBCより処理効率が優れていると一般的にいわれており、
CBCより高度な暗号化を実施しているにも関わらず、処理時間は大差無い。

事実として100文字の文字列を暗号化→復号化を10,000回繰り返す処理を書いて数回サンプリングして
**text**と**delux**で比較してみたが、処理時間は殆ど変わらなかった。（誤差1秒未満）

## 所感

特段の理由がなければ**delux**を利用した方が良いと感じた。
（この時代でデータ長が長くなるからディスク容量が…なんて言っているシステムは少ないだろうし）

### 以上で今回の試行は終わり