【Java】標準ライブラリで暗号技術を使う

# この記事で紹介する暗号技術一覧
以下の暗号技術をJavaの標準ライブラリで扱う方法をまとめました。
各暗号技術について簡単に説明し、実装例を紹介します。

- 対称暗号（AES）
- 公開鍵暗号（RSA）
- 一方向ハッシュ関数（SHA-256）
- メッセージ認証コード（HMAC）
- デジタル署名（RSA + SHA-256）
- Diffie-Hellman鍵交換
- PBE -Password Based Encryption-

実装例で使用するアルゴリズムをカッコ内に記載しています。

# 対称暗号
## 対称暗号とは
対称暗号はメッセージの機密性を守るための技術です。
以下の特徴があります。

- 暗号化と復号の鍵が同じ
- 公開鍵暗号と比べて処理速度が速い

## 対称暗号の使い方
### 鍵の生成
対称暗号の鍵（共通鍵）の生成方法です。

```java
    public SecretKey generateKey() throws NoSuchAlgorithmException {

        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(128);

        return keyGen.generateKey();
    }
```

### 初期化ベクトルの生成
初期化ベクトルの生成方法です。
初期化ベクトルはランダムなビット列で、同じ鍵を使用して暗号化しても毎回異なる暗号文が得られるように使用するためのものです。

```java
    public IvParameterSpec generateIV() {

        SecureRandom random = new SecureRandom();
        byte[] iv = new byte[16];
        random.nextBytes(iv);

        return new IvParameterSpec(iv);
    }
```

### メッセージの暗号化
メッセージの暗号化の方法です。
`Cipher`インスタンス取得の際にアルゴリズム・ブロックモード・パディング方式を指定します。
アルゴリズム・ブロックモード・パディング方式の組み合わせについては`Cipher`クラスのJavaDocを参照してください。

```java
    public byte[] encrypto(String plainText, SecretKey key, IvParameterSpec iv) throws GeneralSecurityException {

        // 書式:"アルゴリズム/ブロックモード/パディング方式"
        Cipher encrypter = Cipher.getInstance("AES/CBC/PKCS5Padding");
        encrypter.init(Cipher.ENCRYPT_MODE, key, iv);

        return encrypter.doFinal(plainText.getBytes());
    }
```

### 暗号文の復号
暗号文の復号の方法です。
暗号化の時と同じようにアルゴリズム・ブロックモード・パディング方式を指定します。

```java
    public String decrypto(byte[] cryptoText, SecretKey key, IvParameterSpec iv) throws GeneralSecurityException {

        // 書式:"アルゴリズム/ブロックモード/パディング方式"
        Cipher decrypter = Cipher.getInstance("AES/CBC/PKCS5Padding");
        decrypter.init(Cipher.DECRYPT_MODE, key, iv);

        return new String(decrypter.doFinal(cryptoText));
    }
```

# 公開鍵暗号
## 公開鍵暗号とは
公開鍵暗号はメッセージの機密性を守るための技術です。
以下の特徴があります。

- 暗号化と復号の鍵が異なる
- 対称暗号と比べて処理速度が遅い

## 公開鍵暗号の使い方
### 鍵の生成
公開鍵暗号の鍵ペア（公開鍵と秘密鍵のペア）の生成方法です。

```java
    public KeyPair generateKeyPair() throws NoSuchAlgorithmException {

        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2024);

        return keyGen.generateKeyPair();
    }
```

鍵ペアから公開鍵・秘密鍵を取得します。

```java
    KeyPair keyPair = generateKeyPair();
    PublicKey publickey = keyPair.getPublic();
    PrivateKey privatekey = keyPair.getPrivate();
```

### メッセージの暗号化
メッセージの暗号化の方法です。**公開鍵**を使用して暗号化を行います。
`Cipher`インスタンス取得の際にアルゴリズム・ブロックモード・パディング方式を指定します。
アルゴリズム・ブロックモード・パディング方式の組み合わせについては`Cipher`クラスのJavaDocを参照してください。

```java
    public byte[] encrypto(String plainText, PublicKey publickey) throws GeneralSecurityException {

        // 書式:"アルゴリズム/ブロックモード/パディング方式"
        Cipher encrypter = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        encrypter.init(Cipher.ENCRYPT_MODE, publickey);

        return encrypter.doFinal(plainText.getBytes());
    }
```

### 暗号文の復号
暗号文の復号の方法です。**秘密鍵**を使用して復号を行います。
暗号化の時と同じようにアルゴリズム・ブロックモード・パディング方式を指定します。

```java
    public String decrypto(byte[] cryptoText, PrivateKey privatekey) throws GeneralSecurityException {

        Cipher decrypter = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        decrypter.init(Cipher.DECRYPT_MODE, privatekey);

        return new String(decrypter.doFinal(cryptoText));
    }
```

# 一方向ハッシュ関数
## 一方向ハッシュ関数とは
一方向ハッシュ関数はメッセージの改ざんを検出するための技術です。
以下の特徴があります。

- 任意の長さのメッセージから固定長のハッシュ値を取得できる
- ハッシュ値から元のメッセージを求めることは現実的に不可能（とても長い時間がかかる）

## 一方向ハッシュ関数の使い方
### メッセージのハッシュ化
メッセージのハッシュ化の方法です。Javaでは一方向ハッシュ関数は"メッセージダイジェスト"と呼ばれています。

```java
    private byte[] hash(String plainText) throws NoSuchAlgorithmException {

        MessageDigest md = MessageDigest.getInstance("SHA-256");

        return md.digest(plainText.getBytes());
    }
```

### 改ざんの検出
改ざんの検出の方法です。と言ってもハッシュ値を再度取得して比較するだけです。

```java
    private boolean verify(String plainText, byte[] givenHash) throws NoSuchAlgorithmException {

        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] newHash = md.digest(plainText.getBytes());

        return Arrays.equals(givenHash, newHash);
    }
```

# メッセージ認証コード
## メッセージ認証コードとは
メッセージ認証コード（MAC）はメッセージの改ざんと"なりすまし"を検出するための技術です。
以下の特徴があります。

- メッセージの送信者と受信者で共通の鍵を使って、メッセージのMAC値を取得する
- 任意の長さのメッセージから固定長のMAC値を取得できる
- MAC値から元のメッセージを求めることは現実的に不可能（とても長い時間がかかる）
- 受信者が送信者の"なりすまし"を検出できる
- 第三者は送信者の"なりすまし"を検出できない（受信者が送信者に"なりすまし"ているかもしれない）

## メッセージ認証コードの使い方
### 鍵の生成
メッセージ認証コードの鍵（共通鍵）の生成方法です。

```java
    public SecretKey generateKey() throws NoSuchAlgorithmException {

        KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");

        return keyGen.generateKey();
    }
```

### MAC値を取得
メッセージのMAC値を取得する方法です。

```java
    private byte[] mac(String plainText, SecretKey key) throws GeneralSecurityException {

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);

        return mac.doFinal(plainText.getBytes());
    }
```

### 改ざんの検出
改ざんの検出の方法です。と言ってもMAC値を再度取得して比較するだけです。

```java
    private boolean verify(String plainText, SecretKey key, byte[] givenMac) throws GeneralSecurityException {

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] newMac = mac.doFinal(plainText.getBytes());

        return Arrays.equals(givenMac, newMac);
    }
```

# デジタル署名
## デジタル署名とは
デジタル署名はメッセージの改ざんと"なりすまし"を検出するための技術です。
以下の特徴があります。

- メッセージの送信者と受信者で異なる鍵を使う
- 受信者が送信者の"なりすまし"を検出できる
- 第三者が送信者の"なりすまし"を検出できる
- 公開鍵暗号の公開鍵・秘密鍵を逆に使っている

## デジタル署名の使い方
### 鍵の生成
デジタル署名の鍵ペア（公開鍵と秘密鍵のペア）の生成方法です。

```java
    public KeyPair generateKey() throws NoSuchAlgorithmException {

        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2024);

        return keyGen.generateKeyPair();
    }
```

鍵ペアから公開鍵・秘密鍵を取得します。

```java
    KeyPair keyPair = generateKey();
    PublicKey publickey = keyPair.getPublic();
    PrivateKey privatekey = keyPair.getPrivate();
```

### メッセージに署名する
メッセージに署名する方法です。**秘密鍵**を使用して署名を行います。


```java
    public byte[] sign(String plainText, PrivateKey privatekey) throws GeneralSecurityException {

        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(privatekey);
        sign.update(plainText.getBytes());

        return sign.sign();
    }
```

### 署名の検証
署名を検証する方法です。**公開鍵**を使用して検証を行います。

```java
    public boolean verify(String plainText, byte[] sign, PublicKey publicKey) throws GeneralSecurityException {

        Signature verify = Signature.getInstance("SHA256withRSA");
        verify.initVerify(publicKey);
        verify.update(plainText.getBytes());

        return verify.verify(sign);
    }
```

# Diffie-Hellman鍵交換
## Diffie-Hellman鍵交換とは
Diffie-Hellman鍵交換は、メッセージの送信者と受信者の間で安全に共通鍵を共有するための技術です。
以下の特徴があります。

- メッセージの送信者と受信者が情報を交換し、その情報を基に共通鍵を生成する
- 交換した情報を第三者が盗聴していても、第三者は共通鍵を生成できない

## Diffie-Hellman鍵交換の手順
Diffie-Hellman鍵交換は手順が複雑ですので、詳細な使い方の説明の前に手順を整理しておきます。
手順を説明するためにアリス（送信者）とボブ（受信者）に登場してもらいます。

アリスとボブが鍵交換する場合、以下の手順で共通鍵を生成します。

1．アリスが、大きな素数Pと生成元Gを生成する
2．アリスからボブへPとGを送信する
3．アリスが秘密値Xと公開値Yを取得する
4．ボブが秘密値Xと公開値Yを取得する
5．アリスとボブが公開値Yを交換する
6. アリスが共通鍵を生成する
7. ボブが共通鍵を生成する


## Diffie-Hellman鍵交換の使い方
上で説明した手順に沿って鍵交換の方法を説明します。

### 1. アリスが、大きな素数Pと生成元Gを生成する
まず鍵ペアを生成し、鍵ペアからPとGを取得します。

```Java
    // 鍵ペアの生成
    public KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DiffieHellman");
        keyGen.initialize(2048);
        return keyGen.generateKeyPair();
    }
```

```java
    // PとGの取得
    KeyPair keyPair = generateKeyPair();

    DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
    DHParameterSpec paramSpec = publicKey.getParams();
    BigInteger p = paramSpec.getP();
    BigInteger g = paramSpec.getG();
```

なお、JavaではPとGの値が固定となっています。
PとGの値については「Java暗号化アーキテクチャ 標準アルゴリズム名のドキュメント」を参照してください。

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/security/StandardNames.html#algspec
アルゴリズムの仕様 → DSA鍵ペア生成アルゴリズム → パラメータのデフォルト値

### 2. アリスからボブへPとGを送信する
アリスからボブへPとGを送信します。この時、PとGの値は第三者へ見られてもかまいません。
ボブは受信したPとGを基に鍵ペアを生成します。

```Java
    public KeyPair generateKeyPair(BigInteger p, BigInteger g) throws GeneralSecurityException {

        DHParameterSpec paramSpec = new DHParameterSpec(p, g);
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DiffieHellman");
        keyGen.initialize(paramSpec);

        return keyGen.generateKeyPair();
    }
```

### 3. アリスが秘密値Xと公開値Yを取得する
アリスが秘密値Xと公開値Yを取得します。

```Java
    // 秘密値Xを取得
    DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate();
    BigInteger x = privateKey.getX();

    // 公開値Yを取得
    DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
    BigInteger y = publicKey.getY();
```

### 4. ボブが秘密値Xと公開値Yを取得する
ボブが秘密値Xと公開値Yを取得します。
取得方法はアリスと同じです。

### 5．アリスとボブが公開値Yを交換する
アリスとボブが公開値Yを交換します。この時、Yの値は第三者へ見られてもかまいません。

### 6. アリスが共通鍵を生成する
アリスが共通鍵を生成します。
共通鍵の生成には、PとGの値、そして相手（ボブ）の公開値Yと、自分（アリス）の秘密鍵（`DHPrivateKey`）を使用します。

```Java
    public SecretKey generateKey(BigInteger p, BigInteger g, BigInteger othersY, DHPrivateKey myPrivateKey)
            throws GeneralSecurityException {

        // 相手（ボブ）の公開鍵を生成する
        DHPublicKeySpec publicKeySpec = new DHPublicKeySpec(othersY, p, g);
        KeyFactory keyFactory = KeyFactory.getInstance("DiffieHellman");
        DHPublicKey othersPublicKey = (DHPublicKey) keyFactory.generatePublic(publicKeySpec);

        // 相手（ボブ）の公開鍵と自分（アリス）の秘密鍵を使って共通鍵を生成する
        KeyAgreement keyAgreement = KeyAgreement.getInstance("DiffieHellman");
        keyAgreement.init(myPrivateKey);
        keyAgreement.doPhase(othersPublicKey, true);
        SecretKey key = keyAgreement.generateSecret("AES");

        return key;
    }
```

### 7. ボブが共通鍵を生成する
ボブが共通鍵を生成します。
生成方法はアリスと同じですが、アリスの公開値Yとボブの秘密鍵（`DHPrivateKey`）を使用します。

# PBE -Password Based Encryption-
## PBEとは
PBEはパスワードを使って機密性を守るための技術です。
以下の特徴があります。

- 暗号化と復号の鍵が同じ
- パスワードから鍵を生成する

## PBEの使い方
### ソルトの生成
鍵の生成の前に、ソルトを用意します。ソルトはランダムなbyte配列です。
ソルトは辞書攻撃によってパスワードが解読されてしまうのを防ぐために使います。

```Java
    public byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[8];
        random.nextBytes(salt);

        return salt;
    }
```

### 鍵の生成
PBEの鍵（共通鍵）の生成方法です。
パスワード、ソルト、そしてハッシュ化の回数を指定します。
ハッシュ化の回数を大きくすることにより、ブルート・フォース・アタックにかかる処理時間を延ばすことができます。

```Java
    public SecretKey generateKey(String password, byte[] salt, int hashCount) throws GeneralSecurityException {

        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, hashCount);
        SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_128");
        SecretKey key = keyFac.generateSecret(keySpec);

        keySpec.clearPassword();

        return key;
    }
```

### アルゴリズムパラメータの保存
アルゴリズムパラメータをbyte配列で取得して保存します。
アルゴリズムパラメータには初期化ベクトル等の値が格納されています。

```Java
    private byte[] getAlgorithmParameters(SecretKey key) throws GeneralSecurityException, IOException {

        Cipher encrypter = Cipher.getInstance("PBEWithHmacSHA256AndAES_128");
        encrypter.init(Cipher.ENCRYPT_MODE, key);
        AlgorithmParameters params = encrypter.getParameters();

        return params.getEncoded();
    }
```

### メッセージの暗号化
メッセージの暗号化の方法です。
共通鍵とアルゴリズムパラメータを指定します。

```Java
    public byte[] encrypto(String plainText, SecretKey key, byte[] algParam)
            throws GeneralSecurityException, IOException {

        AlgorithmParameters params = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_128");
        params.init(algParam);

        Cipher encrypter = Cipher.getInstance("PBEWithHmacSHA256AndAES_128");
        encrypter.init(Cipher.ENCRYPT_MODE, key, params);

        return encrypter.doFinal(plainText.getBytes());
    }
```

### 暗号文の復号
暗号文の復号の方法です。
共通鍵とアルゴリズムパラメータを指定します。

```java
    public String decrypto(byte[] cryptoText, SecretKey key, byte[] algParam)
            throws GeneralSecurityException, IOException {

        AlgorithmParameters params = AlgorithmParameters.getInstance("PBEWithHmacSHA256AndAES_128");
        params.init(algParam);

        Cipher decrypter = Cipher.getInstance("PBEWithHmacSHA256AndAES_128");
        decrypter.init(Cipher.DECRYPT_MODE, key, params);

        return new String(decrypter.doFinal(cryptoText));
    }
```

# 終わりに
ブロックチェーンの台頭によってますます注目されている暗号技術について、勉強したことをまとめてみました。
Javaでは様々な暗号技術がカバーされていることが分かりますね。