JAVAでRSA暗号化を使用する方法

# はじめに
業務をしながら暗号化を扱うことが多くてこの機会に整理しました。


### 1. **暗号化方式 一目で確認できる表**

| 暗号化方式   | タイプ     | 鍵管理                | 主な特徴と利点                                        | 使用例                                  |
|--------------|------------|-----------------------|-----------------------------------------------------|----------------------------------------|
| **RSA**      | 非対称鍵   | 公開鍵 & 秘密鍵       | 公開鍵で暗号化し、秘密鍵で復号化および署名検証          | サーバー認証、電子署名、SSL/TLS通信   |
| **AES**      | 対称鍵     | 同じ秘密鍵を使用      | 高速で安全、大量データの暗号化に適している              | ファイル暗号化、VPN、無線通信暗号化    |
| **HMAC**     | 対称鍵 + ハッシュ化 | 秘密鍵を使用       | メッセージの完全性を保証し、改ざんを防止              | APIリクエスト検証、メッセージ認証       |
| **SHA-256**  | ハッシュ化 | 鍵なし                | 一方向ハッシュ関数で、入力データに基づく固有のハッシュを生成 | パスワードのハッシュ化、デジタル署名    |
| **PBKDF2**   | ハッシュ化 | Saltを使用            | 繰り返しのハッシュ化で強力なキーを導出し、パスワードのハッシュ化に最適 | パスワード保存、キー導出                |
| **Argon2**   | ハッシュ化 | Saltを使用            | メモリや時間のコストを調整可能、ハッシュ化速度が遅い    | パスワードのハッシュ化、キー導出        |
| **ChaCha20** | 対称鍵     | 同じ秘密鍵を使用      | 低電力デバイスでも高速でストリーム暗号化可能            | モバイルデバイス、ネットワーク暗号化   |

### 2. **RSA（非対称鍵暗号化）**

#### 説明：
**RSA**は、公開鍵と秘密鍵を使用してデータを暗号化および復号化、または署名を行いデータの完全性を保証する非対称鍵暗号化方式です。公開鍵で暗号化されたデータは、秘密鍵でのみ復号化できます。

#### サンプルコード：

```java
import java.security.*;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class RSAEncryptionExample {

    // RSA鍵ペアの生成
    public static KeyPair generateRSAKeyPair() throws Exception {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        return keyGen.generateKeyPair();
    }

    // RSA公開鍵でデータを暗号化
    public static String encryptWithRSA(String data, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    // RSA秘密鍵でデータを復号化
    public static String decryptWithRSA(String encryptedData, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);
        byte[] decryptedBytes = cipher.doFinal(decodedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = generateRSAKeyPair();
        String data = "Sensitive data";
        String encryptedData = encryptWithRSA(data, keyPair.getPublic());
        System.out.println("暗号化されたデータ: " + encryptedData);

        String decryptedData = decryptWithRSA(encryptedData, keyPair.getPrivate());
        System.out.println("復号化されたデータ: " + decryptedData);
    }
}
```

### 3. **Saltを使用したユーザー認証（SHA-256とSaltの結合）**

#### 説明：
**Salt**は、ハッシュ関数にランダムな値を追加し、同じ入力データであっても異なるハッシュ値を生成できるようにして、セキュリティを強化します。特に、**ユーザー認証**や**パスワードハッシュ化**において効果的です。

#### サンプルコード：

```java
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;

public class SaltedHashingExample {

    // Saltの生成
    public static String generateSalt() {
        return UUID.randomUUID().toString();
    }

    // SHA-256ハッシュ + Saltの適用
    public static String hashWithSalt(String data, String salt) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        String dataWithSalt = data + salt;
        byte[] hashedBytes = digest.digest(dataWithSalt.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hashedBytes);
    }

    public static void main(String[] args) throws Exception {
        String userId = "user123";
        String password = "password";
        String salt = generateSalt();
        System.out.println("生成されたSalt: " + salt);

        String saltedUserKey = hashWithSalt(userId + password, salt);
        System.out.println("Saltが適用されたハッシュ値: " + saltedUserKey);
    }
}
```

### 4. **Saltベースのユーザー認証 + RSA暗号化の結合**

#### 説明：
この構造では、**RSA**でデータを暗号化し、**Salt**を適用したユーザーキーを生成してユーザー認証を行います。**RSA**はデータの機密性を保証し、**Salt**はユーザーの固有性を保証します。

#### サンプルコード：

```java
import java.security.*;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;

public class RSAAndSaltExample {

    // RSA鍵ペアの生成
    public static KeyPair generateRSAKeyPair() throws Exception {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        return keyGen.generateKeyPair();
    }

    // Saltの生成
    public static String generateSalt() {
        return UUID.randomUUID().toString();
    }

    // ユーザーキーにSaltを適用（ユーザーIDまたはパスワードを基に生成）
    public static String generateSaltedUserKey(String userId, String salt) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        String dataWithSalt = userId + salt;
        byte[] hashedBytes = digest.digest(dataWithSalt.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hashedBytes);
    }

    // RSA公開鍵でデータを暗号化
    public static String encryptWithRSA(String data, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = generateRSAKeyPair();
        String salt = generateSalt();
        System.out.println("生成されたSalt: " + salt);
        String userId = "user123";
        String saltedUserKey = generateSaltedUserKey(userId, salt);
        System.out.println("Saltが適用されたユーザーキー: " + saltedUserKey);

        String sensitiveData = "This is sensitive data";
        String encryptedData = encryptWithRSA(sensitiveData, keyPair.getPublic());
        System.out.println("暗号化されたデータ: " + encryptedData);
    }
}
```

### **Saltを使用する理由**

**Salt**はセキュリティを強化するために、特にパスワードや重要なデータのハッシュ化においてよく使用されます。Saltを使用する主な理由は以下の通りです。

#### 1. **辞書攻撃（Dictionary Attack）やレインボーテーブル攻撃の防止**
- **辞書攻撃**とは、予め用意された一般的なパスワードリストを使用して、ハッシュ化されたパスワードと照合する攻撃です。Saltがないと、同じパスワードに対して同じハッシュ値が生成されるため、攻撃者が簡単にパスワードを逆算できます。
- **レインボーテーブル攻撃**は、事前に生成されたハッシュ値のデータベース（レインボーテーブル）を使って、ハッシュ化されたデータを解読する攻撃です。
- **Salt**を使用することで、同じパスワードでも異なるハッシュ値が生成されるため、攻撃者がこれらのテーブルを使ってパスワードを割り出すことが困難になります。

#### 2. **同一のパスワードが常に異なるハッシュ値を持つようにする**
- Saltがない場合、異なるユーザーが同じパスワードを設定すると、ハッシュ化されたパスワードは同じになります。これにより、同じハッシュ値が使われているユーザー同士が同じパスワードを使用していることが分かってしまいます。
- Saltを追加することで、同一のパスワードでも異なるハッシュ値が生成され、個々のユーザーのパスワードがユニークに保たれます。

#### 3. **セキュリティ強度の向上**
- Saltを使用してパスワードをハッシュ化することで、攻撃者が一つのハッシュ値を特定したとしても、他のユーザーのハッシュ値やパスワードに影響を与えることがありません。これにより、パスワードが盗まれるリスクを大幅に軽減できます。

#### 4. **総当たり攻撃（Brute-force Attack）のコスト増加**
- Saltが追加されたパスワードのハッシュ化は、攻撃者が総当たり攻撃を行う際のコストを増加させます。攻撃者は、各パスワードに対して異なるSaltを適用してハッシュ化を試みなければならず、試行回数が大幅に増えるため、時間とリソースが必要になります。

### **Saltの重要なポイント**

- **Saltはランダムであるべき**: Saltは予測不可能である必要があります。Saltが決まりきったパターンであれば、そのパターンを利用して攻撃される可能性があります。
- **Saltの長さ**: 十分に長いSaltを使用することで、セキュリティがさらに強化されます。一般的に16バイト以上のSaltが推奨されます。
- **Saltはユーザーごとに異なるべき**: 各ユーザーに対して異なるSaltを割り当てることで、同じパスワードでも異なるハッシュ値が生成されます。