RSA暗号入門〜ついでに文字化けについても触れながら〜


# RSA暗号化

RSA暗号化は、情報のセキュリティを保護するために広く使用されている公開鍵暗号化の一種です。

## ステップ1: 公開鍵と秘密鍵の生成

RSA暗号化の基本は、公開鍵と秘密鍵の生成です。これらの鍵は暗号化と復号化のために必要です。

```java
// 鍵ペアの生成
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(3072);
KeyPair keyPair = keyGen.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();

// 公開鍵と秘密鍵の表示
System.out.println("公開鍵: " + publicKey);
System.out.println("秘密鍵: " + privateKey);
```

**出力例**:
```
公開鍵: Sun RSA public key, 3072 bits
  modulus: 1234567890abcdef
  public exponent: 010001
秘密鍵: Sun RSA private CRT key, 3072 bits
  modulus: 1234567890abcdef
  private exponent: 1234567890abcdef
  primeP: 1234567890abcdef
  primeQ: 1234567890abcdef
  primeExponentP: 1234567890abcdef
  primeExponentQ: 1234567890abcdef
  crtCoefficient: 1234567890abcdef
```

**解説**: このステップでは、Javaのコードを使用してRSA鍵ペアを生成し、生成した鍵ペアから公開鍵と秘密鍵を取得して表示します。出力結果では、鍵の種類、ビット数、モジュラス、公開指数、秘密指数などが表示されます。

## ステップ2: メッセージの暗号化と文字化けの謎

次に、公開鍵を使用してメッセージを暗号化します。しかし、このままUTF-8に変換すると文字化けが起きます。

```java
// メッセージの暗号化
String message = "Hello, こんにちは！";
byte[] encryptedBytes = encryptMessage(message, publicKey);

// 暗号化されたメッセージを表示
System.out.println("暗号化されたメッセージ: " + new String(encryptedBytes, StandardCharsets.UTF_8));
```

**出力例**:
```
暗号化されたメッセージ: ���?���_k�+��r�:��]V������u�����X�=�:�1��_`��v���=���B>�`�l��A���{;���k��E 6�І�
```

**解説**: このステップでは、指定したメッセージを公開鍵で暗号化します。しかし、メッセージをそのまま表示することはできず、バイト配列として表示されます。このバイト配列には、UTF-8エンコーディングにおいて文字化けが発生する可能性があります。

UTF-8エンコーディングでは、一部の文字は複数のバイトで表現されます。通常、UTF-8はほとんどの言語で使用される文字をカバーしていますが、バイト配列の中に以下のような状況がある場合に文字化けが発生する可能性があります：

UTF-8エンコーディングの範囲外のバイトが含まれている場合：UTF-8では、特定のバイトパターンの組み合わせしか有効な文字として認識されません。したがって、それ以外のバイトが存在する場合は文字化けが発生します。

不正なUTF-8シーケンスが含まれている場合：正しいUTF-8シーケンスは特定のバイトパターンに従いますが、不正なシーケンスが存在する場合、文字化けが発生します。

マルチバイト文字の途中で切断されている場合：マルチバイト文字は複数のバイトで構成されており、途中でバイト配列が切断されている場合、正しい文字を復元できずに文字化けが発生します。

これらのケースでは、復号化されたバイト配列を正しく文字列に変換できないため、文字化けが発生します。
UTF-8エンコーディングでの具体的な例を示します。

1. 範囲外のバイトが含まれる場合：

   ```java
   byte[] invalidBytes = {(byte) 0xFF, (byte) 0xFE};
   String result = new String(invalidBytes, StandardCharsets.UTF_8);
   System.out.println(result);
   ```

   上記の例では、バイト配列に0xFFと0xFEというUTF-8エンコーディングの範囲外のバイトが含まれています。この場合、以下のような出力が表示されます：

   ```
   ���
   ```

   正しい文字を復元できないため、文字化けが発生しています。

2. 不正なUTF-8シーケンスが含まれる場合：

   ```java
   byte[] invalidSequence = {(byte) 0xC3, (byte) 0x28, (byte) 0xC3, (byte) 0xA9};
   String result = new String(invalidSequence, StandardCharsets.UTF_8);
   System.out.println(result);
   ```

   上記の例では、バイト配列に不正なUTF-8シーケンスが含まれています。この場合、以下のような出力が表示されます：

   ```
   (�é
   ```

   不正なシーケンスがあるため、文字列が正しく復元されずに文字化けが発生しています。

3. マルチバイト文字の途中で切断されている場合：

   ```java
   byte[] truncatedBytes = {(byte) 0xE6, (byte) 0x97};
   String result = new String(truncatedBytes, StandardCharsets.UTF_8);
   System.out.println(result);
   ```

   上記の例では、バイト配列がマルチバイト文字「漢」の途中で切断されています。この場合、以下のような出力が表示されます：

   ```
   漂
   ```

   マルチバイト文字が不完全な状態であるため、正しい文字を復元できずに文字化けが発生しています。

これらの具体的な例は、バイト配列の内容がUTF-8の範囲外である場合に文字化けが発生することを示しています。正しいエンコーディングが保証されていない状態でバイト配列を文字列に変換すると、文字化けのリスクが高まります。

## ステップ3: 暗号化されたメッセージの復号化と解決策

最後に、秘密鍵を使用して暗号化されたメッセージを復号化します。しかし、文字化けの問題はまだ解決していません。解決策を見つけましょう。

```java
// 暗号化されたメッセージの復号化
byte[] decryptedBytes = decryptMessage(encryptedBytes, privateKey);

// 復号化されたメッセージを表示
System.out.println("復号化されたメッセージ: " + new String(decryptedBytes, StandardCharsets.UTF_8));
```

**出力例**:
```
復号化されたメッセージ: Hello, こんにちは！
```

**解説**: このステップでは、秘密鍵を使用して暗号化されたメッセージを復号化します。復号化されたメッセージは正しい文字列として表示されます。

## 文字化けの原因と解決策

文字化けの原因は、バイト配列を文字列に変換する際に正しいエンコーディングが適用されていないことです。UTF-8エンコーディングにおいて範囲外のバイトや不正なシーケンスが含まれる場合、またはマルチバイト文字が途中で切断される場合に文字化けが発生します。

この文字化けの問題を解決するためには、正しいエンコーディングを使用してバイト配列を文字列に変換する必要があります。Javaの場合、`StandardCharsets.UTF_8`を使用することでUTF-8エンコーディングを指定できます。

```java
// 復号化されたメッセージを表示
System.out.println("復号化されたメッセージ: " + new String(decryptedBytes, StandardCharsets.UTF_8));
```

このようにして正しいエンコーディングを適用することで、文字化け

の問題を解決することができます。

## フルのコード
以下のコードは、修正後のチュートリアルにおいて、JavaのコードをIDEで実行できるように補完したものです。コードの補完には、必要なパッケージのインポートやメソッドの定義が含まれています。
### 文字化けするバージョン
```java
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import javax.crypto.Cipher;

public class RSAPublicKeyEncryptionTutorial {
    public static void main(String[] args) throws Exception {
        // 鍵ペアの生成
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(3072);
        KeyPair keyPair = keyGen.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        // メッセージの暗号化
        String message = "Hello, こんにちは！";
        byte[] encryptedBytes = encryptMessage(message, publicKey);

        // 暗号化されたメッセージを表示
        System.out.println("暗号化されたメッセージ: " + new String(encryptedBytes, StandardCharsets.UTF_8));

        // 暗号化されたメッセージの復号化
        byte[] decryptedBytes = decryptMessage(encryptedBytes, privateKey);

        // 復号化されたメッセージを表示
        System.out.println("復号化されたメッセージ: " + new String(decryptedBytes, StandardCharsets.UTF_8));
    }

    // メッセージの暗号化
    private static byte[] encryptMessage(String message, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
    }

    // メッセージの復号化
    private static byte[] decryptMessage(byte[] encryptedBytes, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(encryptedBytes);
    }
}
```
### 文字化けなし
```java
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import javax.crypto.Cipher;

public class RSAPublicKeyEncryptionTutorial {
    public static void main(String[] args) throws Exception {
        // 鍵ペアの生成
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(3072);
        KeyPair keyPair = keyGen.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        // メッセージの暗号化
        String message = "Hello, こんにちは！";
        byte[] encryptedBytes = encryptMessage(message, publicKey);

        // 暗号化されたメッセージをBase64エンコードして表示
        String encryptedMessage = Base64.getEncoder().encodeToString(encryptedBytes);
        System.out.println("暗号化されたメッセージ: " + encryptedMessage);

        // Base64デコードして暗号化されたメッセージを復号化
        byte[] decryptedBytes = decryptMessage(Base64.getDecoder().decode(encryptedMessage), privateKey);

        // 復号化されたメッセージを表示
        String decryptedMessage = new String(decryptedBytes, StandardCharsets.UTF_8);
        System.out.println("復号化されたメッセージ: " + decryptedMessage);
    }

    // メッセージの暗号化
    private static byte[] encryptMessage(String message, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
    }

    // メッセージの復号化
    private static byte[] decryptMessage(byte[] encryptedBytes, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(encryptedBytes);
    }
}

```