C#でRSA鍵ペアをPEM形式で保存・読み込み、メッセージの暗号化と復号化【.NET 7以降対応】

## 概要

RSA（Rivest-Shamir-Adleman）は、公開鍵暗号方式の一つで、暗号化やデジタル署名などで広く利用されています。以前は、C#でRSA鍵をPEM形式で扱う際に、**BouncyCastle**などの外部ライブラリを利用することが一般的でした。しかし、.NET 7以降、**System.Security.Cryptography** で PEM 形式を直接サポートするメソッドが追加され、RSA鍵の生成や保存・読み込みがより簡単かつ安全に行えるようになりました。本記事では、.NET 7 の新機能を活用してRSA鍵をPEM形式で保存・読み込み、さらにメッセージの暗号化・復号化を行う方法を解説します。

## 1. RSA暗号の基本

RSA暗号は、**公開鍵**と**秘密鍵**のペアを使用してデータを暗号化・復号化する公開鍵暗号方式の一つです。公開鍵を用いて暗号化されたデータは、対応する秘密鍵でのみ復号できるため、機密データのやり取りやデジタル署名などで使用されています。

## 2. BouncyCastleや自作実装との違い

これまで、C#でPEM形式のRSA鍵を扱う際には、外部ライブラリの**BouncyCastle**を利用したり、独自にPEM形式のエンコード・デコード処理を実装することが一般的でした。しかし、.NET 7 以降では、**System.Security.Cryptography** 名前空間にPEM形式のサポートが追加され、外部ライブラリを使用せずにシンプルなコードでRSA鍵をPEM形式で扱えるようになりました。

具体的には、`ExportRSAPublicKeyPem()`や`ExportPkcs8PrivateKeyPem()`などのメソッドを利用して、RSA鍵をPEM形式で保存できます。

```csharp
using RSA rsa = RSA.Create(2048); // 2048ビットのRSA鍵を生成
RsaPemUtils.SaveRsaPublicKeyToPem(rsa, "publicKey.pem");
RsaPemUtils.SaveRsaPrivateKeyToPem(rsa, "privateKey.pem", RsaPemUtils.PKCS.PKCS8);
```

このコードは、RSA鍵ペアを生成し、公開鍵と秘密鍵をそれぞれ`publicKey.pem`、`privateKey.pem`というファイルに保存するものです。

## 3. PEM形式の公開鍵・秘密鍵の保存

RSA鍵ペアを生成し、それをPEM形式で保存することができます。PEM形式は、鍵を**Base64エンコード**した文字列を`-----BEGIN RSA PUBLIC KEY-----`や`-----END PRIVATE KEY-----`などのヘッダー・フッターで囲った形式です。

以下は、公開鍵と秘密鍵をPEM形式でファイルに保存するコードです。

```csharp
public static void SaveRsaPublicKeyToPem(RSA rsa, string publicKeyPath)
{
    // 公開鍵をPEM形式でエクスポート
    string publicKeyPem = rsa.ExportRSAPublicKeyPem();

    // PEM形式の公開鍵をファイルに保存
    File.WriteAllText(publicKeyPath, publicKeyPem);
}

public static void SaveRsaPrivateKeyToPem(RSA rsa, string privateKeyPath, PKCS pkcs)
{
    // 秘密鍵をPEM形式でエクスポート（PKCS1 または PKCS8）
    string privateKeyPem = pkcs == PKCS.PKCS1
        ? rsa.ExportRSAPrivateKeyPem()
        : rsa.ExportPkcs8PrivateKeyPem();

    // PEM形式の秘密鍵をファイルに保存
    File.WriteAllText(privateKeyPath, privateKeyPem);
}
```

.NET 7では、`ExportRSAPublicKeyPem()`と`ExportPkcs8PrivateKeyPem()`メソッドが提供されており、これにより簡単にPEM形式で鍵をエクスポートできるようになりました。

## 4. PEM形式の公開鍵・秘密鍵の読み込み

保存したPEM形式の公開鍵・秘密鍵をファイルから読み込むことも、非常にシンプルな方法で実現できます。次のコードは、公開鍵と秘密鍵をファイルから読み込み、それをRSAオブジェクトにインポートする例です。

```csharp
public static RSA ImportRsaPublicKeyFromPem(string publicKeyPath)
{
    // PEM形式の公開鍵をファイルから読み込む
    string publicKeyPem = File.ReadAllText(publicKeyPath);

    // PEM形式からバイナリ形式に変換
    byte[] publicKeyBytes = ConvertPemToByteArray(publicKeyPem);

    // 公開鍵をインポート
    RSA rsa = RSA.Create();
    rsa.ImportRSAPublicKey(publicKeyBytes, out _);
    return rsa;
}

public static RSA ImportRsaPrivateKeyFromPem(string privateKeyPath, PKCS pkcs)
{
    // PEM形式の秘密鍵をファイルから読み込む
    string privateKeyPem = File.ReadAllText(privateKeyPath);

    // PEM形式からバイナリ形式に変換
    byte[] privateKeyBytes = ConvertPemToByteArrayPrivateKey(privateKeyPem);

    // 秘密鍵をインポート
    RSA rsa = RSA.Create();
    if (pkcs == PKCS.PKCS1)
    {
        rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
    }
    else
    {
        rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
    }
    return rsa;
}
```

PEM形式の鍵を読み込む際には、改行コードやヘッダー・フッターを除去し、Base64エンコードされた部分をデコードしてバイナリ形式に変換します。これにより、RSAオブジェクトに鍵をインポートできます。

## 5. メッセージの暗号化と復号化

公開鍵と秘密鍵を使用して、メッセージを暗号化・復号化する手順を見ていきます。公開鍵で暗号化されたメッセージは、対応する秘密鍵でのみ復号可能です。

```csharp
// メッセージをUTF-8でエンコード
string message = "Hello, this is a test message!";
byte[] messageBytes = Encoding.UTF8.GetBytes(message);

// 公開鍵でメッセージを暗号化
byte[] encryptedMessage;
using (RSA rsaEncrypt = RSA.Create())
{
    rsaEncrypt.ImportParameters(importedPublicKey.ExportParameters(false));
    encryptedMessage = rsaEncrypt.Encrypt(messageBytes, RSAEncryptionPadding.OaepSHA256);
}

// 秘密鍵で暗号化されたメッセージを復号化
byte[] decryptedMessage;
using (RSA rsaDecrypt = RSA.Create())
{
    rsaDecrypt.ImportParameters(importedPrivateKey.ExportParameters(true));
    decryptedMessage = rsaDecrypt.Decrypt(encryptedMessage, RSAEncryptionPadding.OaepSHA256);
}

// 復号化されたメッセージを表示
Console.WriteLine("Original Message: " + message);
Console.WriteLine("Decrypted Message: " + Encoding.UTF8.GetString(decryptedMessage));
```

このコードでは、公開鍵を使用してメッセージを暗号化し、秘密鍵を使用して復号化しています。メッセージの暗号化・復号化には、`RSAEncryptionPadding.OaepSHA256`を使用し、より安全な暗号化を実現しています。

## 6. System.Environment.NewLine の使用について

PEM形式のファイルでは、改行コードが異なる環境（Windows, Linux, macOS）により異なる場合があります。そのため、改行を削除する際に、`System.Environment.NewLine` を使うことで、環境に依存せずに正しく動作させることができます。`System.Environment.NewLine` を使う場合のコードは以下の通りです。

```csharp
var base64String = pemString
    .Replace("-----BEGIN RSA PUBLIC KEY-----", "")
    .Replace("-----END RSA PUBLIC KEY-----", "")
    .Replace(System.Environment.NewLine, "") // OS依存の改行コードを削除
    .Replace("\n", "") // Unix系の改行を削除
    .Replace("\r", ""); // Windowsのキャリッジリターンを削除
```

これにより、どのOS上でも正しくPEM形式のファイルを処理できます。

## 7. ソースコード全体
ソースコード全体は以下になります。
```csharp
using System.Security.Cryptography;

public class Program
{
    public static void Main()
    {
        // RSA鍵ペアの生成
        using RSA rsa = RSA.Create(2048);

        // 公開鍵と秘密鍵をPEM形式でファイルに保存
        RsaPemUtils.SaveRsaPublicKeyToPem(rsa, "publicKey.pem");
        RsaPemUtils.SaveRsaPrivateKeyToPem(rsa, "privateKey.pem", RsaPemUtils.PKCS.PKCS8);

        // ファイルから公開鍵と秘密鍵を読み込む
        RSA importedPublicKey = RsaPemUtils.ImportRsaPublicKeyFromPem("publicKey.pem");
        RSA importedPrivateKey = RsaPemUtils.ImportRsaPrivateKeyFromPem("privateKey.pem", RsaPemUtils.PKCS.PKCS8);

        // テストメッセージを暗号化し、復号化する
        string message = "Hello, this is a test message!";
        byte[] messageBytes = System.Text.Encoding.UTF8.GetBytes(message);

        // 秘密鍵でメッセージを暗号化
        byte[] encryptedMessage;
        using (RSA rsaEncrypt = RSA.Create())
        {
            rsaEncrypt.ImportParameters(importedPublicKey.ExportParameters(false));
            encryptedMessage = rsaEncrypt.Encrypt(messageBytes, RSAEncryptionPadding.OaepSHA256);
        }

        // 公開鍵でメッセージを復号化
        byte[] decryptedMessage;
        using (RSA rsaDecrypt = RSA.Create())
        {
            rsaDecrypt.ImportParameters(importedPrivateKey.ExportParameters(true));
            decryptedMessage = rsaDecrypt.Decrypt(encryptedMessage, RSAEncryptionPadding.OaepSHA256);
        }

        // 結果を表示
        Console.WriteLine("Original Message: " + message);
        Console.WriteLine("Decrypted Message: " + System.Text.Encoding.UTF8.GetString(decryptedMessage));
    }
}
public class RsaPemUtils
{
    /// <summary>
    /// RSA鍵ペアのPEM形式での保存と読み込みを行うユーティリティクラス
    /// </summary>
    public enum PKCS
    {
        PKCS1,
        PKCS8
    }

    /// <summary>
    /// RSAの公開鍵をPEM形式で保存するメソッド
    /// </summary>
    /// <param name="rsa">公開鍵を持つRSAオブジェクト</param>
    /// <param name="publicKeyPath">保存先のファイルパス</param>
    public static void SaveRsaPublicKeyToPem(RSA rsa, string publicKeyPath)
    {
        // 公開鍵をPEM形式でエクスポート
        string publicKeyPem = rsa.ExportRSAPublicKeyPem();

        // PEM形式の公開鍵をファイルに保存
        File.WriteAllText(publicKeyPath, publicKeyPem);
    }

    /// <summary>
    /// RSAの秘密鍵をPEM形式で保存するメソッド
    /// </summary>
    /// <param name="rsa">秘密鍵を持つRSAオブジェクト</param>
    /// <param name="privateKeyPath">保存先のファイルパス</param>
    /// <param name="pkcs">PKCS形式の指定 (PKCS1 または PKCS8)</param>
    public static void SaveRsaPrivateKeyToPem(RSA rsa, string privateKeyPath, PKCS pkcs)
    {
        // 秘密鍵をPEM形式でエクスポート
        string privateKeyPem = pkcs switch
        {
            PKCS.PKCS1 => rsa.ExportRSAPrivateKeyPem(),
            PKCS.PKCS8 => rsa.ExportPkcs8PrivateKeyPem(),
        };

        // PEM形式の秘密鍵をファイルに保存
        File.WriteAllText(privateKeyPath, privateKeyPem);
    }

    /// <summary>
    /// PEM形式の公開鍵を読み込み、RSAオブジェクトにインポートするメソッド
    /// </summary>
    /// <param name="publicKeyPath">PEM形式の公開鍵ファイルのパス</param>
    /// <returns>公開鍵をインポートしたRSAオブジェクト</returns>
    public static RSA ImportRsaPublicKeyFromPem(string publicKeyPath)
    {
        // PEM形式の公開鍵をファイルから読み込む
        string publicKeyPem = File.ReadAllText(publicKeyPath);

        // PEM形式からバイナリ形式に変換
        byte[] publicKeyBytes = ConvertPemToByteArray(publicKeyPem);

        // RSAオブジェクトを作成し、公開鍵をインポート
        RSA rsa = RSA.Create();
        rsa.ImportRSAPublicKey(publicKeyBytes, out _);

        return rsa;
    }

    /// <summary>
    /// PEM形式の秘密鍵を読み込み、RSAオブジェクトにインポートするメソッド
    /// </summary>
    /// <param name="privateKeyPath">PEM形式の秘密鍵ファイルのパス</param>
    /// <param name="pkcs">PKCS形式の指定 (PKCS1 または PKCS8)</param>
    /// <returns>秘密鍵をインポートしたRSAオブジェクト</returns>
    public static RSA ImportRsaPrivateKeyFromPem(string privateKeyPath, PKCS pkcs)
    {
        // PEM形式の秘密鍵をファイルから読み込む
        string privateKeyPem = File.ReadAllText(privateKeyPath);

        // PEM形式からバイナリ形式に変換
        byte[] privateKeyBytes = ConvertPemToByteArrayPrivateKey(privateKeyPem);

        // RSAオブジェクトを作成し、秘密鍵をインポート
        RSA rsa = RSA.Create();

        switch (pkcs)
        {
            case PKCS.PKCS1:
                rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
                break;
            case PKCS.PKCS8:
                rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
                break;
        }

        return rsa;
    }

    /// <summary>
    /// PEM形式の公開鍵文字列をバイナリ形式に変換する
    /// </summary>
    /// <param name="pemString">PEM形式の公開鍵文字列</param>
    /// <returns>バイナリ形式の公開鍵</returns>字列をバイナリ形式に変換
    private static byte[] ConvertPemToByteArray(string pemString)
    {
        // PEM形式からヘッダーとフッターを削除し、Base64でデコード
        var base64String = pemString
            .Replace("-----BEGIN RSA PUBLIC KEY-----", "")
            .Replace("-----END RSA PUBLIC KEY-----", "")
            .Replace(System.Environment.NewLine, "") // OSごとの改行コードを削除
            .Replace("\n", "") // Unix系の改行コードを削除
            .Replace("\r", ""); // 追加で改行コードの\rも削除

        return Convert.FromBase64String(base64String);
    }

    /// <summary>
    /// PEM形式の秘密鍵文字列をバイナリ形式に変換する
    /// </summary>
    /// <param name="pemString">PEM形式の秘密鍵文字列</param>
    /// <returns>バイナリ形式の秘密鍵</returns>
    private static byte[] ConvertPemToByteArrayPrivateKey(string pemString)
    {
        var base64String = pemString
            .Replace("-----BEGIN RSA PRIVATE KEY-----", "")
            .Replace("-----END RSA PRIVATE KEY-----", "")
            .Replace("-----BEGIN PRIVATE KEY-----", "")
            .Replace("-----END PRIVATE KEY-----", "")
            .Replace(System.Environment.NewLine, "") // OSごとの改行コードを削除
            .Replace("\n", "") // Unix系の改行コードを削除
            .Replace("\r", ""); // 追加で改行コードの\rも削除

        return Convert.FromBase64String(base64String);
    }
}
```

## 8. まとめ

今回のコードでは、.NET 7 で追加された`ExportRSAPublicKeyPem()`や`ExportPkcs8PrivateKeyPem()`メソッドを利用して、RSA鍵をPEM形式で保存・読み込みする方法を解説しました。BouncyCastleなどの外部ライブラリを使わずに、標準ライブラリだけで安全にRSA鍵を扱えるようになったのは非常に便利です。これを活用することで、セキュアな通信やデータの暗号化を容易に実現できます。