「大石泉すき」を公開鍵暗号標準(RSA)で暗号化/復号する

この記事は[「大石泉すき」アドベントカレンダー](https://qiita.com/advent-calendar/2019/ohishi-izumi-suki) 4日目の記事となります。
4日目は、「大石泉すきを暗号化しよう！」とします。

**2019/12/04 21:37追記**
当初「復号」を「復号化」と記載していたため、修正いたしました。ご指摘いただき、ありがとうございます。

## 何をしたいのか

「大石泉すき」という言葉を、一度暗号に直し、再度意味の通る文字に戻したい(復号したい)です。
今回は、公開鍵暗号を用いてこれを行っていこうと思います。

## 公開鍵暗号とは

仕組み自体は本筋ではないので、詳しくは下記の記事などを参照してください。

> 公開鍵暗号方式とは、暗号化と復号に別々の鍵を用いる暗号方式である。「非対称鍵暗号方式」とも呼ばれる。
>～(中略)～
>公開鍵暗号方式では、「暗号文を作り出す鍵」と「暗号文を元に戻す鍵」が異なる。暗号通信を行いたい人は、まず独自に2つの鍵のペアを作成する。同時に生成された一対の鍵のうち一方を公開鍵として公開し、他方を秘密鍵として厳重に管理する。送信者は受信者の公開鍵で暗号文を作成して送る。受信者は、自分の秘密鍵で受け取った暗号文を復号する。
> 引用元：[@IT 公開鍵暗号方式とは](https://www.atmarkit.co.jp/ait/articles/0401/01/news099.html)

AliceとBobという名前の二者間の通信を例に出すと、

* Bobは、Aliceに**秘密の文字列(平文)**を送りたい
* Bobは、Aliceが公開している公開鍵(誰でも知ることが出来る)を使って、**文字列を暗号化**する
* Aliceは、Bobから暗号化した文字列を受け取る
* Aliceは、自分が持っている秘密鍵(Aliceしか知らない)で**文字列を復号**する
* Aliceは、Bobから送られた秘密の文字列を受け取る

こんな風に、文字列の送受信が秘密に行えます。
(実際に利用される場合は、公開鍵が本当に正しいか、Aliceが所持しているかを証明する第三者機関が居たりしますが、今回は割愛します)

## 環境

今回、二者間の通信を表現するために、以下のような構成でソースコードを書きました。
C#で書く場合、AliceとBobでソリューションを分けてください。

### Alice(メッセージ受信側)

* 役割
  * HTTPサーバ上で公開鍵を公開する
  * 受信した暗号オブジェクトを復号する
* 言語・フレームワーク
  * C# 8.0
  * ASP .net Core 3.0

### Bob(メッセージ送信側)

* 役割
  * Aliceが公開している公開鍵を入手し、秘密の文字列を暗号化する
  * 暗号化した文字列をAliceに送信する
* 言語・フレームワーク
  * C# 8.0
  * .net Core 3.0 コンソールアプリケーション

## 実装

### 暗号化・復号の実装

下記のライブラリを使いました。

* [Bouncy Castle](https://www.bouncycastle.org/)

使い方は下記の記事を参照。
更新日付を見る限り .net Framework用のライブラリを使っての紹介記事だと思われるが、.net Core用のライブラリでも同様の使い方で暗号化・復号が可能。

* [BouncyCastle で公開鍵暗号標準(RSA)を扱う](https://qiita.com/snaka/items/0829c37494b61353445e)

使い方は大体こんな感じ。暗号文・復号文共にbyte[]配列で表される。

```C#:EncryptDecrypt.cs
        // 暗号化
        public static byte[] Encrypt(byte[]) bytes, string publickey)
        {
            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化（第1引数trueは暗号化、falseは復号）
            RSA.Init(true, publicKeyParam);

            // 暗号化対象のバイト列・長さを渡し、暗号化した結果のバイト列を受け取る
            byte[] encrypted = RSA.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }

        // 復号
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化（第1引数trueは暗号化、falseは復号）
            RSA.Init(false, privateKeyParam.Private);

            // 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }
```

#### BouncyCastle入手方法

* ソリューションエクスプローラから「依存関係」を右クリック
* 「NuGet パッケージの管理」をクリック　＞　「参照」をクリック
* 「BouncyCastle.NetCore」を検索欄に入力し、出てきたものをインストール
  * 「BouncyCastle」は .net Framework用なので注意

### 鍵ペアの生成

動確検証用の公開鍵/暗号鍵のペアは、このサイトで生成しました。
<https://travistidwell.com/jsencrypt/demo/index.html>

### 個別実装

#### メッセージ受信側(HTTPサーバ)

```C#:RsaRemoteController.cs
        /// <summary>
        /// 暗号byte[]配列の復号
        /// </summary>
        /// <returns></returns>
        [HttpPost("")]
        public async System.Threading.Tasks.Task<string> DecryptoAsync()
        {
            byte[] encrypto;
            using (var ms = new MemoryStream(2048))
            {
                await Request.Body.CopyToAsync(ms);
                encrypto = ms.ToArray();  // returns base64 encoded string JSON result
            }

            var cert = new Cert();
            var decryptoByte = cert.Decrypto(encrypto, Cert.PRIVATE_KEY);
            
            // ログ
            _logger.LogInformation($"Decrypto [{Encoding.UTF8.GetString( decryptoByte )}]");
            return Encoding.UTF8.GetString( decryptoByte );
        }

        /// <summary>
        /// 公開鍵
        /// </summary>
        /// <returns></returns>
        [HttpGet("Alice/cert")]
        public string GetCert()
        {
            return Cert.PUBLIC_KEY;
        }
```

* {ルートパス}/Alice/certに、Getリクエスト：公開鍵を生のstring型で返却する
* ルートパスに、bodyに生のbyte配列(暗号文)を添付しPostリクエスト：リクエストのbyte配列を復号したbyte配列を、文字列に変換する。
  * 今回は、どのように変換したのか分かるように、stringで結果を返却する実装にした


```C#:RsaRemoteController.cs
    class Cert
    {
        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PUBLIC_KEY = @"(略)";

        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PRIVATE_KEY =  @"(略)";

        internal Pkcs1Encoding RSA { get; }

        public Cert()
        {
            RSA = new Pkcs1Encoding(new RsaEngine());
        }

        /// <summary>
        /// 対称鍵暗号で暗号文を復号する
        /// </summary>
        /// <param name="cipher">平文の文字列</param>
        /// <param name="privatekey">秘密鍵</param>
        /// <returns>復号された文字列</returns>
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化（第1引数trueは暗号化、falseは復号）
            RSA.Init(false, privateKeyParam.Private);

            // 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }
    }
```

* 単純な復号処理。公開鍵・秘密鍵は絶対にハードコーディングしないこと

#### メッセージ送信側(コンソールアプリ)

```C#:Bob.cs
        static void Main()
        {
            // HttpClientを使うための準備。今回はあまり関係ない
            // HTTPConnectionFactoryを使うため、DI設定を行う
            var serviceCollection = new ServiceCollection()
                .AddHttpClient()                                         // IHttpClientFactoryの依存設定
                .AddSingleton<IHttpConnection, HttpConnectionSample>()    // IHTTPConnectionの依存設定
                .BuildServiceProvider();

            // DI設定済みのIHttpConnectionを実装したクラスを取得
            var connector = serviceCollection.GetService<IHttpConnection>();


            // 大石泉すき
            string plainText = "大石泉すき";
            Console.WriteLine($"PlainText\r\n{plainText}\r\n");

            // サーバから公開鍵を取得する
            // SendGetメソッドの中身はただのHttpClient.GetAsyncです
            var publicKey = connector.SendGet($"https://{メッセージ受信側HTTPサーバのIP:Port}/Alice/cert").Result;

            // RSA暗号標準オブジェクト(PKCS#1)を生成
            var rsa = new Pkcs1Encoding(new RsaEngine());

            // 暗号化
            var encrypted = Encrypt(plainText, publicKey, rsa);

            // byte配列は化けるのでBase64でエンコードしておく
            Console.WriteLine($"Encrypted(Base64 Encoded)\r\n{Convert.ToBase64String(encrypted)}\r\n");

            // 暗号文(配列)を復号するべく、サーバに暗号文を送信
            // SendPostメソッドの中身はただのHttpClient.PostAsyncです
            var decrypted = connector.SendPost($"https://{メッセージ受信側HTTPサーバのIP:Port}/rsaremote/", encrypted).Result;

            // サーバで復号した結果を表示
            Console.WriteLine($"Decrypted\r\n{decrypted}\r\n");
        }
```

* 公開鍵取ってきてーの暗号化してーの復号してもらいーののコントローラクラス
* 「大石泉すき」を知っているのはBobだけ。暗号化してAliceに伝わるだろうか。

```C#:Bob.cs

        /// <summary>
        /// 公開鍵で文字列を暗号化する
        /// </summary>
        /// <param name="text">平文の文字列</param>
        /// <param name="publickey">Pem形式の公開鍵</param>
        /// <returns>暗号化されたByte</returns>
        public static byte[] Encrypt(string text, string publickey, Pkcs1Encoding rsa)
        {
            var bytes = Encoding.UTF8.GetBytes(text);

            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            // RSA暗号オブジェクトを初期化（第１引数 true は「暗号化」を示す）
            rsa.Init(true, publicKeyParam);

            // 対象のバイト列を渡し暗号化した結果のバイト列を受け取る
            byte[] encrypted = rsa.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }
```

* 単純な暗号化処理。公開鍵・秘密鍵は今回クライアントは持っていない

### 動作確認

サーバ側を起動させた状態で、クライアント側を実行

```Console:クライアント側
PlainText
大石泉すき

Encrypted(Base64 Encoded)
W2joxjgxL+Q6CtCYaSGCzpx4fJYspCb7KRI/2Ddlnt//70o0R/039Hx6R2fywqCEF0Q21MqpF4/BbjzDM8lAKJgPEIFx5Gp2kYBO08B6bjYdrhSPgIeWEIj7ulwZPO4TD+G5bGwrZn/ogapQfUbTY748B49h1/d4t0IowxRartc=

Decrypted
大石泉すき
```

```Console:サーバ側
RsaServer.Controllers.RsaRemoteController: Information: Decrypto [大石泉すき]
```

## 参考資料

* [Accepting Raw Request Body Content in ASP.NET Core API Controllers](https://weblog.west-wind.com/posts/2017/sep/14/accepting-raw-request-body-content-in-aspnet-core-api-controllers)
  * Asp .net Coreで生のByte配列を取得するのに躓いたので、半日くらいこの文書を探していました。