Spring」タグアーカイブ

JavaのSpringSecurityでAESの暗号化/復号化処理を実装する

サクっと作るなら以下が参考になります。

http://terasolunaorg.github.io/guideline/current/ja/Security/Encryption.html

暗号化

    public static String encryptTextByAesWithGcm(String secret, String salt, String plainText) {
        TextEncryptor aesTextEncryptor = Encryptors.delux(secret, salt);

        return aesTextEncryptor.encrypt(plainText);
    }

復号化

    public static String decryptTextByAesWithGcm(String secret, String salt, String cipherText) {
        TextEncryptor aesTextEncryptor = Encryptors.delux(secret, salt);

        return aesTextEncryptor.decrypt(cipherText);
    }

補足

ここからが個人的には重要

1.secretはプレーンテキストでいいのですが、saltはプレーンテキストではダメなので以下みたいになります。

String salt = new String(Hex.encodeHexString("salt".getBytes()));

2.参考サイト

暗号化の結果について
encryptメソッドの返り値 (暗号化の結果) は実行毎に異なる値を返すが、鍵とソルトが同一であれば復号処理の結果は同一になる (正しく復号できる) 。

の記載、初期化ベクトル (iv)の役割よるものです。
ivは、同じ鍵を使用して同じ平文データを複数回暗号化するときに異なる暗号文を生成します。
ivはランダムな値で、セッションごとに生成される。

セッションごと?確かにソースをSpringSecurityのソースを追っていくと、以下になっている。

    /**
     * Creates a standard password-based bytes encryptor using 256 bit AES encryption.
     * Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation
     * Function #2). Salts the password to prevent dictionary attacks against the key. The
     * provided salt is expected to be hex-encoded; it should be random and at least 8
     * bytes in length. Also applies a random 16-byte initialization vector to ensure each
     * encrypted message will be unique. Requires Java 6. NOTE: This mode is not
     * <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
     * and does not provide any guarantees about the authenticity of the data. For a more
     * secure alternative, users should prefer
     * {@link #stronger(CharSequence, CharSequence)}.
     * @param password the password used to generate the encryptor's secret key; should
     * not be shared
     * @param salt a hex-encoded, random, site-global salt value to use to generate the
     * key
     *
     * @see Encryptors#stronger(CharSequence, CharSequence)
     */
    public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
        return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
    }

以下のJavaDocによると、、、

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/crypto/encrypt/AesBytesEncryptor.html

new AesBytesEncryptorの第三引数の KeyGenerators.secureRandom(16) がiv

確かに乱数

では、復号化にも同じ値のivがいるのかなと思ったらソース見たら以下のように暗号化後のテキストから取得しているようなので大丈夫そうでした。

    @Override
    public byte[] decrypt(byte[] encryptedBytes) {
        synchronized (this.decryptor) {
            byte[] iv = iv(encryptedBytes);
            CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey, this.alg.getParameterSpec(iv));
            return CipherUtils.doFinal(this.decryptor,
                    (this.ivGenerator != NULL_IV_GENERATOR) ? encrypted(encryptedBytes, iv.length) : encryptedBytes);
        }
    }

    private byte[] iv(byte[] encrypted) {
        return (this.ivGenerator != NULL_IV_GENERATOR)
                ? EncodingUtils.subArray(encrypted, 0, this.ivGenerator.getKeyLength())
                : NULL_IV_GENERATOR.generateKey();
    }

まとめ

ということで改めて復習です。

Secret:
Secretは一般的にユーザーが選択する文字列で、秘密情報を保護するために使用されます。
Secretベースのセキュリティでは、ユーザーのSecretから導かれた鍵を生成します。このとき、パスワードを安全に保管し、強力な鍵導出関数(PBKDF2、bcrypt、Scryptなど)を使用することが重要です。

Salt:
SaltはSecretベースのセキュリティで使用され、Secretに追加されるランダムなデータです。
Saltはユーザーごとに異なり、同じSecretを持つユーザーのハッシュ値を異なるものにします。これにより、レインボーテーブル攻撃から保護されます。

初期化ベクトル (iv):
ivは、同じ鍵を使用して同じ平文データを複数回暗号化するときに異なる暗号文を生成します。
ivはランダムな値で、セッションごとに生成される。

SpringMVCを使ったJavaプログラムにおけるシングルトンとスタティック変数・メソッドの効果を検証してみた

マルチスレッドでのスタティック変数・メソッドの効果を検証してみた

JavaのSpringMVCを使用して、シングルトンパターンとスタティック変数、スタティックメソッドを実装してみました。これらはJavaアプリケーションでよく使用される概念なんですが、ちょっと混乱してきたので、自分なりの整理結果になります。詳細は以下をご覧ください。

検証ソース

  • コントローラソース
@Controller
@RequestMapping("/hello")
public class HelloWorldController {
    private int counter = 0;

    @RequestMapping("/thread")
    public ModelAndView thread(@RequestParam("id") String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        System.out.println("Parameter:" + id);

        counter = CommonUtil.countup(counter, id);

        CommonUtil com = new CommonUtil();
        counter = com.countdown(counter, id);

        String NewTime1 = CommonUtil.newtime(id);
        String NewTime2 = CommonUtil.newtime(id);

        System.out.println("NewTime1:" + NewTime1);
        System.out.println("NewTime2:" + NewTime2);
        ModelAndView modelAndView = new ModelAndView("thread");
        modelAndView.addObject("id", id);
        modelAndView.addObject("NewTime1", NewTime1);
        modelAndView.addObject("NewTime2", NewTime2);
        return modelAndView;
    }
}
  • 共通処理クラス
public class CommonUtil {

    public static int countup(int var, String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        var++;
        System.out.println(var);
        return(var);
    }

    public int countdown(int var, String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        var--;
        System.out.println(var);
        return(var);
    }

    public static String newtime(String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        // 現在日時を取得
        LocalDateTime now = LocalDateTime.now();
        // フォーマットを指定して文字列に変換
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        String nowStr = now.format(formatter);      
        System.out.println(nowStr);
        return(nowStr);
    }
}
  • 画面表示用HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Thread</title>
</head>
<body>
  <h2>title</h2>
  <p th:text="${id}"></p>
  <p th:text="${NewTime1}"></p>
  <p th:text="${NewTime2}"></p>
</body>
</html>

ソースコードは単純だと思います。
キーはコントローラクラスの変数「private int counter = 0;」
これは SpringのDIコンテナで管理されている Controller のデフォルトのスコープだとシングルトンのためスレッドセーフではありません
では、それを検証していきましょう。

アクセスURL

/hello/thread?id=001
/hello/thread?id=002

マルチスレッドでの検証となるため、URLは二つ用意します。
ログが分かりやすいようにパラメータに id を付与します。
id 自体には処理結果に全く関係ありません。
では、結果です。

結果

順次処理の実行結果

まずは別々にアクセスが来た場合
カウントアップされ直後にカウントダウンされ 1 -> 0、1 -> 0 を繰り返す結果となります。

Method: thread
Parameter:001
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:04:50
Method: newtime
2023/04/30 10:04:50
NewTime1:2023/04/30 10:04:50
NewTime2:2023/04/30 10:04:50
Method: thread
Parameter:002
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:04:52
Method: newtime
2023/04/30 10:04:52
NewTime1:2023/04/30 10:04:52
NewTime2:2023/04/30 10:04:52

同時実行の実行結果

では、次に同時実行のケースです。
同時にリクエストが来た場合、以下の結果をなることがあります。
一時的に「counter = 2」になっていることが分かると思います。
要件として「counter」に何をカウントしたいかによりますが、このケースは予期せず起こったケースとなることが多いのではないでしょうか。
もし別々のリクエストでも同じカウントでトータルをカウントしたいケースを想定した場合、「private static int counter = 0;」と明示的にstatic変数にすべきと思います。

Method: thread
Parameter:001
Method: countup
1
Method: thread
Parameter:002
Method: countup
2
Method: countdown
1
Method: newtime
2023/04/30 10:14:05
Method: newtime
2023/04/30 10:14:05
NewTime1:2023/04/30 10:14:05
NewTime2:2023/04/30 10:14:05
Method: countdown
0
Method: newtime
2023/04/30 10:14:13
Method: newtime
2023/04/30 10:14:13
NewTime1:2023/04/30 10:14:13
NewTime2:2023/04/30 10:14:13

ちなみにこの動作確認のやり方としては、1つ目のリクエスト処理中に2つ目のリクエスト処理を先に処理させています。
具体的にはEclipseのデバック機能を使って以下のブレイクポイントを設定しておき、1つ目のリクエスト処理を止めておき、最初に2つ目のリクエスト処理を進めています。

今回は、変数「private int counter = 0;」がキーであるため、
CommonUtilのメソッド「public static int countup(int var, String id)」と「public int countdown(int var, String id)」は
敢えて、countupメソッドの方をstaticメソッドとしていますが、
いずれのケースでも変数「private int counter = 0;」をスタティック変数として使用している結果がわかると思います。
(もし、countdownメソッドの方がスタティック変数を使っていないければ、結果が0にならないはずなので)

補足結果

「public static String newtime(String id)」メソッドでは、変数「private int counter = 0;」を使用していません。
変わりに内部で作成した「String nowStr」を使用しています。
この変数はstaticメソッドのnewtimeメソッド内で作成されていますが、スレッドセーフな変数になります。

public static String newtime(String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        // 現在日時を取得
        LocalDateTime now = LocalDateTime.now();
        // フォーマットを指定して文字列に変換
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        String nowStr = now.format(formatter);      
        System.out.println(nowStr);
        return(nowStr);
    }

同時実行の実行結果

今回も同時実行して1つ目のリクエスト処理中に2つ目のリクエスト処理を先に処理させています。
ただし、今回は時間がそれぞれ正しく(上書きされずに)取得されて出力されていることがわかると思います。

Method: thread
Parameter:001
Method: countup
1
Method: countdown
0
Method: newtime
Method: thread
Parameter:002
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:46:22
Method: newtime
2023/04/30 10:46:42
NewTime1:2023/04/30 10:46:22
NewTime2:2023/04/30 10:46:42
2023/04/30 10:46:03
Method: newtime
2023/04/30 10:47:12
NewTime1:2023/04/30 10:46:03
NewTime2:2023/04/30 10:47:12

今回のブレイクポイントは以下に設定しています。

ちなみに画面の表示結果は以下のようになります。

まとめ

最初に書いた通り、キーは変数「private int counter = 0;」です。
これはぱっと見ると、メンバ変数であり、スレッドセーフのように見えますが、SpringのDIコンテナで管理されているインスタントのデフォルトのスコープだと
シングルトン
のためスレッドセーフではありません。
これを常に意識して使いましょう。
補足としてメソッドがstaticかそうではないかは影響しないことが確認できました。