Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
9
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

@takeday

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

概要

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

参考情報

英語だがspring.ioのドキュメントが参考になる
spring.io - Encryptorsのドキュメント

日本語のだとTERASOLUNAのドキュメントあたりが読みやすい
6. TERASOLUNA Server Framework for Java (5.x)によるセキュリティ対策 >> 6.9. 暗号化

結論(いきなり)

結論から言うと以下の様な感じ(あくまで個人的な見解)

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

検証

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

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);
  }
実行結果
noOpText()      :
  暗号化文字列      : Hello
  復号化確認        : Hello

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

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

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

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

noOpText

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

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

Encryptors.java
    public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
        return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
                salt));
    }
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

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));  // ← ここの引数が乱数
    }
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();
    }

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

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

delux

そして最後にdelux

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);
    }
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();
    }

textdeluxの違いは
CipherAlgorithmにCBCを指定するかGCMを指定するかの違いである。

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

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

所感

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
9
Help us understand the problem. What are the problem?