暗号化データをPythonとC#間でやりとりする
はじめに
公開鍵暗号、浪漫ですよね(個人の感想)。個人開発のソフトに組み込んだりとか、実用性は別にして愉快なことができそうです。
しかしながら、ざっくりググって見る限り、自分みたいなスクリプトキディが嬉々としてコピペしたくなるシンプル側に極振りした実装がひとところに纏まっているというのが見当たらなかったため、練習のために作ってみました。
5%位は実用性を考慮しても良いだろうと思ったので、PythonとC#の2つでシンプルに書いてみて、2者間で暗号データをやりとりしてみます。コレができれば、例えばクライアント側(C#)で暗号化したデータを、サーバ側(Python)で復号、とかができるので、ほんの少しですが実用に与することができます。
もっと実用に良い手段があるとか言わない。
想定する状況
- サーバ側(Python)でRSAのキーを作成
- クライアント側(C#)は作られた公開鍵を得ており、これでテキストファイルを暗号化する
- サーバ側に送られた(ことにする)暗号化ファイルを、秘密鍵で復号化する
環境
Python側
- Windows 10
- Anaconda3(4.8.2)
- Python 3.7.6
- pycrypto 2.6.1
Anacondaならだいたい全部入りで、暗号化モジュールも入ってるので楽です。
Python単体から始める場合、pycryptoをインストールします。1
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)が必要です。
実装
公開鍵・秘密鍵の作成
fromCrypto.PublicKeyimportRSAfromCrypto.CipherimportPKCS1_v1_5key_length=1024defcreateKey():key=RSA.generate(key_length)private_key=key.exportKey().decode('utf-8')withopen(file="private.pem",mode="w",encoding='utf-8')asfile_out:file_out.write(private_key)public_key=key.publickey().exportKey().decode('utf-8')withopen(file="public.pem",mode="w",encoding='utf-8')asfile_out:file_out.write(public_key)
これでキーが生成されます。変数key_lengthは1024以上を指定しますが、別の似たモジュールのマニュアルによれば、2048以上を推奨、1024,2048,3072のいずれかにすべき、とのお話。2今回は長くする必要が無いので1024。
生成されたキーは、秘密鍵と公開鍵の2つに分けて保存します。3
暗号化(C#)
Pythonで作成した公開鍵ファイルを読み込み、短文を暗号化してみます。
サンプルはコンソールアプリ(.NET Core3.1)で作成しています。
usingSystem;usingSystem.IO;usingSystem.Security.Cryptography;usingSystem.Text;classEncrypt_sample{staticvoidMain(string[]args){conststringmessage=@"This is test message!";conststringkey_begin="-----BEGIN PUBLIC KEY-----\r\n";conststringkey_end="\r\n-----END PUBLIC KEY-----";stringpublic_key=File.ReadAllText(@"public.pem");public_key=public_key.Replace(key_begin,"").Replace(key_end,"");varpublicKeyBytes=Convert.FromBase64String(public_key);using(RSACryptoServiceProviderrsa=newRSACryptoServiceProvider()){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
あとは、バイト列に変換したmessageをEncrypt()で暗号化して、base64変換したテキストを保存します。変換しているのは、テキストファイルとして読めるようにするためです。
Encrypt()の第2引数をfalseにすると、PKCS#1 v1.5のパディングを利用します。
暗号化(Python)
想定状況では暗号化はC#側で行いますが、Python上でも暗号化/復号化ができることを確認出来るよう、関数を作っておきます。
fromCrypto.PublicKeyimportRSAfromCrypto.CipherimportPKCS1_v1_5message=R"This is test message!"defencrypt():withopen(file="public.pem",mode='rb')asfile_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())withopen(file="encrypted.txt",mode='w',encoding='utf-8')asw:w.write(base64.b64encode(encrypted).decode('utf-8'))
実作業としては、
- 公開鍵ファイルをインポートする(public_key)
- public_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(public_cipher)
- public_cipherを使ってmessageを暗号化
です。
復号化(Python)
最後は復号化です。
fromCrypto.PublicKeyimportRSAfromCrypto.CipherimportPKCS1_v1_5defdecrypt():withopen(file="private.pem",mode='rb')asfile_read:private_pem=file_read.read()private_key=RSA.importKey(private_pem)private_cipher=PKCS1_v1_5.new(key=private_key)withopen(file="encrypted.txt",mode='r',encoding='utf-8')asr:encrypt_data=base64.b64decode(s=r.read())decrypt_data=private_cipher.decrypt(ct=encrypt_data,sentinel="")print(decrypt_data.decode('utf-8'))
- 秘密鍵ファイルをインポート(private_key)
- private_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(private_cipher)
- private_cipherを使って、暗号化ファイルの中身を復号化
最後のprint()で表示される文字列が、C#で作成したときのmessageと同一なら、復号化成功です。
感想
思った以上に短いコードでなんとかなるもんだ、と言うのが率直な感想です。エラー処理とか実際のセキュリティ面とか実用上の暗号強度とか、細かな設定をすっ飛ばせば、比較的簡単に言語を跨いで暗号化/復号化ができるのが分かってニッコリです。
なお、コードは本当に適当なので、実用にしないでください。
加えた方がよろしいもの
- エラー処理全般
- 署名に関するあれこれ
- より暗号強度の高い設定
- このやり方で実装して大丈夫だと信じれる強い心
Q&A
- Q: なんでC#側のキー作成と復号化が無いの?
→ 復号化はさておき(想定上のクライアント側に秘密鍵預けるの?)、キーの作成は共用できる形式への出力が微妙にめんどうくさい感じだったのでパス。良い手段があれば追記します。
同種の機能を持つモジュールが複数存在していて(かつ、関数名が微妙に異なったりして)検索時等に混乱の元になってるようです。ここではAnacondaに標準で入っているものを使います。 ↩
pycryptoのマニュアルが404になってるんですが、どこに正本があるんでしょう? ↩
実際には、秘密鍵ファイルの中には公開鍵のデータも入っているらしい(?) ↩
ImportRSAPublicKey()という、いかにもな名前の関数があるが、こっちが正解。 ↩