暗号化データをPythonとC#間でやりとりする

# 暗号化データをPythonとC#間でやりとりする

## はじめに

公開鍵暗号、**浪漫**ですよね<sub>(個人の感想)</sub>。個人開発のソフトに組み込んだりとか、実用性は別にして愉快なことができそうです。

しかしながら、ざっくりググって見る限り、~~自分みたいなスクリプトキディが嬉々としてコピペしたくなる~~シンプル側に極振りした実装がひとところに纏まっているというのが見当たらなかったため、練習のために作ってみました。

5%位は実用性を考慮しても良いだろうと思ったので、PythonとC#の2つでシンプルに書いてみて、2者間で暗号データをやりとりしてみます。コレができれば、例えばクライアント側(C#)で暗号化したデータを、サーバ側(Python)で復号、とかができるので、ほんの少しですが実用に与することができます。

**もっと実用に良い手段があるとか言わない。**

### 想定する状況

1. サーバ側(Python)でRSAのキーを作成
1. クライアント側(C#)は作られた公開鍵を得ており、これでテキストファイルを暗号化する
1. サーバ側に送られた(ことにする)暗号化ファイルを、秘密鍵で復号化する

## 環境

### Python側

- Windows 10
- Anaconda3(4.8.2)
  - Python 3.7.6
    - pycrypto 2.6.1

Anacondaならだいたい全部入りで、暗号化モジュールも入ってるので楽です。

Python単体から始める場合、pycryptoをインストールします。[^1]

[^1]: 同種の機能を持つモジュールが複数存在していて(かつ、関数名が微妙に異なったりして)検索時等に混乱の元になってるようです。ここではAnacondaに標準で入っているものを使います。

```cmd.exe
pip install -U pycrypto
```

### C#側

- Windows 10
- Visual Studio 2019 community
  - .NET Core 3.1
  - コンソールアプリケーション

一部の関数が.NET Standard 2.1の適用のため、.NET Core 3.0以上(.NET Frameworkは5.0 RC1)が必要です。

## 実装

### 公開鍵・秘密鍵の作成

```Python:CreateKey.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

key_length = 1024

def createKey():

    key = RSA.generate(key_length)

    private_key = key.exportKey().decode('utf-8')
    with open(file="private.pem", mode="w", encoding='utf-8') as file_out:
        file_out.write(private_key)

    public_key = key.publickey().exportKey().decode('utf-8')
    with open(file="public.pem", mode="w", encoding='utf-8') as file_out:
        file_out.write(public_key)
```

これでキーが生成されます。変数key_lengthは1024以上を指定しますが、[別の似たモジュールのマニュアル](https://pycryptodome.readthedocs.io/en/latest/src/public_key/rsa.html)によれば、2048以上を推奨、1024,2048,3072のいずれかにすべき、とのお話。[^2] 今回は長くする必要が無いので1024。

生成されたキーは、秘密鍵と公開鍵の2つに分けて保存します。[^3]

[^2]: [pycryptoのマニュアル](https://www.dlitz.net/software/pycrypto/apidoc/)が404になってるんですが、どこに正本があるんでしょう？

[^3]: 実際には、秘密鍵ファイルの中には公開鍵のデータも入っているらしい(?)

### 暗号化(C#)

Pythonで作成した公開鍵ファイルを読み込み、短文を暗号化してみます。

サンプルはコンソールアプリ(.NET Core3.1)で作成しています。

```cs:main.cs
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

class Encrypt_sample
{
    static void Main(string[] args)
    {
        const string message = @"This is test message!";

        const string key_begin = "-----BEGIN PUBLIC KEY-----\r\n";
        const string key_end = "\r\n-----END PUBLIC KEY-----";

        string public_key = File.ReadAllText(@"public.pem");
        public_key = public_key.Replace(key_begin, "").Replace(key_end, "");

        var publicKeyBytes = Convert.FromBase64String(public_key);

        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);

            byte[] encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(message), false);

            File.WriteAllText(@"encrypted.txt", Convert.ToBase64String(encrypted));
        }
    }
}
```

変数messageはこれから暗号化する文字列です。

Python側で作成した公開鍵ファイルpublic.pemを読み込んだ後、ヘッダ、フッタを除いてからbyteに変換、RSACryptoServiceProvider型の変数rsaにImportSubjectPublicKeyInfo()で読み込んでいます。[^4]

[^4]: ImportRSAPublicKey()という、いかにもな名前の関数があるが、こっちが正解。

あとは、バイト列に変換したmessageをEncrypt()で暗号化して、base64変換したテキストを保存します。変換しているのは、テキストファイルとして読めるようにするためです。

Encrypt()の第2引数をfalseにすると、PKCS#1 v1.5のパディングを利用します。

### 暗号化(Python)

想定状況では暗号化はC#側で行いますが、Python上でも暗号化/復号化ができることを確認出来るよう、関数を作っておきます。

```Python:Encrypt.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

message = R"This is test message!"

def encrypt():
    with open(file="public.pem", mode='rb') as file_read:
        public_pem = file_read.read()

    public_key = RSA.importKey(public_pem)
    public_cipher = PKCS1_v1_5.new(key=public_key)

    encrypted = public_cipher.encrypt(message=message.encode())

    with open(file="encrypted.txt", mode='w', encoding='utf-8') as w:
        w.write(base64.b64encode(encrypted).decode('utf-8'))
```

実作業としては、

1. 公開鍵ファイルをインポートする(public_key)
1. public_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(public_cipher)
1. public_cipherを使ってmessageを暗号化

です。

### 復号化(Python)

最後は復号化です。

```Python:Decrypt.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

def decrypt():
    with open(file="private.pem", mode='rb') as file_read:
        private_pem = file_read.read()

    private_key = RSA.importKey(private_pem)
    private_cipher = PKCS1_v1_5.new(key=private_key)

    with open(file="encrypted.txt", mode='r', encoding='utf-8') as r:
        encrypt_data = base64.b64decode(s=r.read())

    decrypt_data = private_cipher.decrypt(ct=encrypt_data, sentinel="")

    print(decrypt_data.decode('utf-8'))
```

1. 秘密鍵ファイルをインポート(private_key)
1. private_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(private_cipher)
1. private_cipherを使って、暗号化ファイルの中身を復号化

最後のprint()で表示される文字列が、C#で作成したときのmessageと同一なら、復号化成功です。

## 感想

思った以上に短いコードでなんとかなるもんだ、と言うのが率直な感想です。**エラー処理とか実際のセキュリティ面とか実用上の暗号強度とか、**細かな設定をすっ飛ばせば、比較的簡単に言語を跨いで暗号化/復号化ができるのが分かってニッコリです。

なお、**コードは本当に適当なので、実用にしないでください。**

### 加えた方がよろしいもの

- エラー処理全般
- 署名に関するあれこれ
- より暗号強度の高い設定
- *このやり方で実装して大丈夫だと信じれる強い心*

### Q&A

- Q: なんでC#側のキー作成と復号化が無いの？<br>
    → 復号化はさておき(想定上のクライアント側に秘密鍵預けるの？)、キーの作成は共用できる形式への出力が微妙にめんどうくさい感じだったのでパス。良い手段があれば追記します。