RSA暗号についてpython-RSAを使って動作確認をする~暗号化と復号について~

2023/01/10

python セキュリティ プログラミング

 前回の記事でRSA暗号について勉強しました。前回勉強に使った具体例では実践に耐えうる公開鍵、暗号鍵ではなかったため、今回はpythonのライブラリ"python-RSA"をつかって実践的なRSA暗号に触れてみたいと思います。

↓前回の記事

Python-RSAをインストールする

pipコマンドでインストールすることが出来ます。
pip install rsa
私の場合はAnaconda経由でpythonを入れたのですが、すでに入ってました。
ただバージョンが古かったため、2023年1月時点での最新の4.9にアップデートしました。

鍵ペア生成をしてみる

pythonのバージョンは3.9.15で実施してます。
鍵ペアの生成については公式ドキュメントを参照してます。
鍵ペアの生成はrsa.newkeysというものでできるみたいです。引数の数字は鍵の長さを指定することが出来ます。鍵が長いほどセキュリティ強度が高くなると思いますが、生成するのに時間がかかってしまうみたいです。

公式ドキュメントによると鍵長と生成時間の関係は下記のとおりです。

前置きが長くなりましたが、今回は公式ドキュメントの例と同じ512ビットで鍵を生成し、中身を確認します。実行したコードは下記の通りです。
import rsa
(pubkey,privkey) = rsa.newkeys(512)
print("pubkey = ")
print(pubkey)
print("privkey = ")
print(privkey)

実行した結果は下記の通りです。
鍵はランダムに生成するため、実行するたび中身は変わります。
また、本来は秘密鍵は公開してはいけない情報ですが、今回は勉強のためなので公開しています。
pubkey = 
PublicKey(7860788454055865772880591325788540720239307336070977793860987994621983778070919566058194646641091024091611308310809994833637298391885151018077907450682171, 65537)    
privkey =
PrivateKey(7860788454055865772880591325788540720239307336070977793860987994621983778070919566058194646641091024091611308310809994833637298391885151018077907450682171, 65537, 4871417096800661191384597038399313255287840886021433573704474822063525168090160871052026560473326308494317293504866018537018835700721153496432025357294977, 5452490623706065642882733098295339215934593818200044001900178470050722603685662043, 1441687661025793138289696220400335833479493640330822201805104216803848097)
pubkey(公開鍵)の方は2つの値が含まれていることが分かります。
公式ドキュメントによるとこの2つの値はnとeみたいです。eについては前回の記事でも紹介しましたが、65537が使用されているみたいです。
publickeyについて公式ドキュメントより引用
次にprivkey(秘密鍵)の方についてですが、こちらは5つの値が含まれてることが分かります。こちらも公式ドキュメントを参照するとそれぞれ、n、e、d、p、qとなるみたいです。
privatekeyについて公式ドキュメントより引用

ここで今回生成した鍵について簡単な検算をしてみたいと思います。
確認する内容としては以下の通りです。
  1. $n=pq$となっているか
  2. $ed \equiv 1 \mod \phi(n)$となっているか
これについて下記のような検算用のコードで確認します。
n = 7860788454055865772880591325788540720239307336070977793860987994621983778070919566058194646641091024091611308310809994833637298391885151018077907450682171
e = 65537
d = 4871417096800661191384597038399313255287840886021433573704474822063525168090160871052026560473326308494317293504866018537018835700721153496432025357294977
p = 5452490623706065642882733098295339215934593818200044001900178470050722603685662043
q = 1441687661025793138289696220400335833479493640330822201805104216803848097
phi = (p-1)*(q-1)
if n == p*q:
    print("n equal pq")
else:
    print("n not equal pq")
if (e*d) % phi == 1:
    print("ed mod phi equal 1")
else:
    print("ed mod phi not equal 1")
コードの説明をする必要は無いと思いますが、$n=eq$を満たしていたら、"n equal pq"と出力し、$ed \equiv 1 \mod \phi(n)$を満たしていたら"ed mod phi equal 1"と出力するコードとなってます。

出力結果は下記のとおりとなり、問題なく鍵生成が出来ていることが分かります。
n equal pq
ed mod phi equal 1

文字列の符号化

公開鍵、暗号鍵を生成することが出来たため、次はいよいよメッセージを暗号化していきます。暗号化する平文がビット列であればそのまま暗号化すれば問題ないのですが、文字列を暗号化するためには文字列をビット列に変換する符号化という処理が必要となります。

今回はUTF-8を使って文字列を数字に変換したいと思います。UTF-8の符号化についてのイメージは下記サイトを参照お願いします。

ここでは簡単な例で符号化を実施していきます。
"こんにちわ!"という文字列をutf-8に変換してどのような出力になるかを見ます。コードは下記のとおりです。
message = "こんにちわ!"
message_utf = message.encode("utf8")#messageをutf-8で符号化
print(message_utf)
message_decode = message_utf.decode("utf8")#messageをutf-8で復号
print(message_decode)
出力結果は下記の通りになりました。
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x82\x8f\xef\xbc\x81'
こんにちわ!
興味がある方は下記サイトをもとに自分の手で復号してみてください。

ここでは最初の"こ”という文字を復号してみます。
utf-8に変換した結果を見てみるとe3,81,93という文字があると思います。
この3文字で"こ"という文字を表しています。

先ほどのリンク先のe38190の列の左から4番目(e38943)の場所に"こ"という文字列があります(一番が0なので、3番目ではなく4番目を見ています。)

実際に文字列の暗号化、復号を行ってみる

前置きが長くなりましたが、公開鍵を使ってメッセージを暗号化し、秘密鍵を使ってメッセージを復号してみたいと思います。
先ほどは"こんにちわ!"というメッセージを暗号化しましたが、ここでは"こんにちわ!私の名前はAliceです。"という漢字、英語、ひらがなが混じった文章を暗号化、復号してみます。

暗号化して復号するコードは下記のとおりです。
詳細はコードを見ていただければわかると思いますが、rsa.encryptの第一引数に符号化したメッセージ、第二引数に公開鍵を渡すことで、公開鍵を使って暗号化をすることが出来ます。復号する場合はrsa.decryptの第一引数に暗号化したメッセージ、第二引数に暗号鍵を渡すと復号することが出来ます。
import rsa
(pubkey,privkey) = rsa.newkeys(512)
print("pubkey = ")
print(pubkey)
print("privkey = ")
print(privkey)
message = "こんにちわ!私の名前はAliceです。"
message_utf = message.encode("utf8")#messageをutf-8で符号化
print(message_utf)

message_encrypt = rsa.encrypt(message_utf,pubkey)#生成した公開鍵を使ってメッセージを暗号化

message_decrypt = rsa.decrypt(message_encrypt,privkey)#秘密鍵を使って暗号化されたメッセージを復号
message_decode = message_decrypt.decode("utf8")#messageをutf-8で復号
print(message_decode)

出力結果は下記の通りになりました。(公開鍵、暗号鍵はもう一度生成しなおしているため、先ほど作った鍵ペアと別物になっております。)

pubkey = 
PublicKey(8508378234616501057220861539930317178210680656363029003827567064016795790671549085136063541079394908116328499534562580389082055427548532641192393634719177, 65537)
privkey =
PrivateKey(8508378234616501057220861539930317178210680656363029003827567064016795790671549085136063541079394908116328499534562580389082055427548532641192393634719177, 65537, 5275851422835121686426619638672631177626770229233574509934001714260868625488444823517807380805037104602016358528353089879187943187088848691513546389374457, 5725532832877677583933609927145531963275248008555273334628619317087930417961826403, 1486041296586217169456884776603999929100102824035992016953534545934213859)
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x82\x8f\xef\xbc\x81\xe7\xa7\x81\xe3\x81\xae\xe5\x90\x8d\xe5\x89\x8d\xe3\x81\xafAlice\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82'
こんにちわ!私の名前はAliceです。
暗号化したメッセージを復号したところ問題なく元のメッセージを取り出すことが出来ました。

補足になりますが、暗号化できるメッセージ長は鍵サイズ以下であることに注意してください。(復号する際に$\phi (n)$で割った余りを求めるため、$\phi (n)$以上の値を暗号化してしまうと一周してしまうため。)

公開鍵から暗号鍵を求めてみる

前回の記事でも少し書きましたが、公開鍵を素因数分解することが出来れば秘密鍵のdを求めることが出来てしまいます。ここでは弱い鍵を作ってみて素因数分解をしてみたいと思います。
実際に実行したコードは下記の通りです。今回は素因数分解がしやすいように128bitの鍵を生成して、sympyのfactorintを利用して素因数分解するまでの時間を求めてみます。
import rsa
import rsa
import sympy
import time

(pubkey,privkey) = rsa.newkeys(128)#鍵ぺア生成
print("pubkey = ")
print(pubkey)
print("privkey = ")
print(privkey)
n = pubkey.n
print(n)
start = time.time()
print("p,q =",end=" ")
print(sympy.factorint(n))#sympyのfactorintを使用して素因数分解を実施
print("time is :",end=" ")
print(time.time() -start )
実行した結果は下記の通りです。
pubkey = 
PublicKey(258408772746054587747383326989415955601, 65537)
privkey =
PrivateKey(258408772746054587747383326989415955601, 65537, 64624865119059991894809700054140621473, 265620770574384827141, 972848517031500061)
258408772746054587747383326989415955601
p,q = {972848517031500061: 1, 265620770574384827141: 1}
time is : 165.37815380096436
今回公開鍵のnが258408772746054587747383326989415955601で秘密鍵を作る際に必要なp,qはそれぞれ
265620770574384827141
972848517031500061
でしたが、165秒程度で素因数分解が出来てしまい、それぞれの値が求まってしまいました。最初に提示した図より128bitの鍵生成は0.01秒程度で完了するが、鍵の解読には165秒程度かかってしまうため、一方向性関数となっていることが分かります。

まとめ

今回はpython-RSAというライブラリを使って、実際にメッセージを暗号化、復号をしました。

今回は正しい送信者がメッセージを送った場合を想定して動作を確認しましが、
実際は公開鍵は誰でも入手できる情報であるため、悪意を持った第三者が成りすましてメッセージを送るという事が出来てしまいます。

次回は正しい送信者から送られたメッセージであることを確認するための署名について勉強していきたいと思います。

↓次回記事

自己紹介

はじめまして 社会人になってからバイクやプログラミングなどを始めました。 プログラミングや整備の記事を書いていますが、独学なので間違った情報が多いかもしれません。 間違っている情報や改善点がありましたらコメントしていただけると幸いです。

X(旧Twitter)

フォローお願いします!

QooQ