4

Python 实现 RSA 非对称加解密

 1 year ago
source link: https://yanbin.blog/python-implement-rsa-encryption/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Python 实现 RSA 非对称加解密

2022-08-18 — Yanbin

在阅读《HTTP/2 in Action》的 HTTPS 一节后,不觉一脚踏入到非对称加密这一领地而不能自拔。与非对称加密相对应的是对称加密,有点像是由一把钥匙反锁的门,只能用同一把钥匙打开; 而非对称加密是用一把钥匙反锁门,但只能用另一把特定的钥匙才能打开它,锁门的叫做公钥,开门的叫做私钥。

在此之前我理解的非对称加密以为是像 MD5 那种摘要(Digest), 由明文生成的 MD5 摘要信息是无法还原出原始数据的,谬以为那就是所谓的非对称。

1976 年,两位美国计算机学家 Whitefield Diffie 和 Martin Hellman 提出了非对称加解密的的构思。1977 年三位数学家 Ron Rivest, Adi Shamir 和 Leonard Adleman 实现了非对称加密算法,即 RSA,取自这三个的姓的首字母

具体的 RSA 算法原理可参考阮一峰的两篇网络日志:RSA算法原理 (一)RSA算法原理 (二), 大致就是通过互质的两个数,计算欧拉函数, 模反元素,最终算法公钥和私钥,公钥加密的数据只能用用私钥解密,以当前的算力,只要 RSA 的密钥足够长,如 1024 位以上,私钥是无法通过公钥推断出来的。

下面是用 Python 演示 RSA 加密和解密,要用到的库是 pycryptodome。早先有 cryptoPyCrypto, 但它们都已不再维护,而 PyCrypto 建议使用 CryptographyPyCryptodome

一个完整的例子

安装 pycryptodome 库

pip install pycryptodoem

下面的代码生成公钥与私钥,并用公钥加密,私钥解密

from Crypto import Random
from Crypto.PublicKey import RSA
import base64
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
# 生成公钥与私钥
rsa = RSA.generate(1024, Random.new().read)
private_key = rsa.exportKey()
public_key = rsa.publickey().exportKey()
def encrypt_data(msg, key):
    cipher = PKCS1_cipher.new(key)
    encrypt_text = base64.b64encode(cipher.encrypt(msg.encode()))
    return encrypt_text.decode()
def decrypt_data(encrypt_msg, key):
    cipher = PKCS1_cipher.new(key)
    decrypt_text = cipher.decrypt(base64.b64decode(encrypt_msg), 0)
    return decrypt_text.decode()
encrypted = encrypt_data("hello world", rsa.public_key()) # 或者 RSA.importKey(public_key)
print(encrypted)
decrypted = decrypt_data(encrypted,  RSA.importKey(private_key))
print(decrypted)

运行的输出类似如下

sAcpyMx6N9L11dDER37xnM8KUIis5EUmpiacEPealzuNnmt85S1h8NQUevcomnBzYkv5/7E0kr0oyVJK4sos8J5lm5VR803c5RMLzLLJxZe4Sbhna000TQ3lgmkaLYbQ2pJ7EAdjutUwWHATCQoPd21E7XskGqOWMA/zzf7o1sY=
hello world

  1. 因为每次的公,私钥不一样,所以加密后的表现形式不一样,但都能用私钥解密出来
  2. 用 base64 编解码是为了能在控制台打印出二进制字节数据
  3. rsa.public_key() 能直接获得 public_key, 如果是用 exportKey() 方法导出的 key, 需用 RSA.importKey() 导入后才能使用

RsaKey 所蕴藏的秘密

我们在上面的 RSA.generate(1024, Random.new().read) 后打个断点,睢瞧一个 RsaKey 包含些什么,或者说是怎么不断运算生成的

rsakey-inside-800x552.png

参考 RSA 的算法,对上面的 d, e, n, p, q 和 u 的解释

  1. p 和 q 为随意选择的两个质数
  2. n 为 p * q 的乘机, n 的二进制值的位数即密钥的长度 rsakey-length-1.png
  3. u 是 n 的欧拉函数值 φ(n) 
  4. e 可以随便一个在 1 与 n 的欧拉函数φ(n) 之间的数,这里直接用  65537
  5. d 为 e 对于 φ(n) 的模板元素

最后 n, e 封装为公钥,n, d 封装为私钥

输出公钥和私钥

如果我们打印出公钥和钥是什么样子的呢?来

print(private_key.decode())
print("\n")
print(public_key.decode())

控制台看到

-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDOqKaGRUrz0MmOEzB128o2zRhOw6CFSR0p3rcqFUTKGvfMj9le
w3CLjlYHBScZanUe5aBXQ2xDxrdCfEM9BjizAL4+Pdf8l75FNWZMgusjPGn+vGuA
niPid2zyLEQghGLB1S5f3WxWbionc3H941OFNnEGqtochN/dwoAT4fCSBwIDAQAB
AoGATmijwgcRcJ+TkaHLPbR2LUO0yNGlmlyKwaOcaE2oi2we/9DGXxOVJIYNMt2s
H5MKO/5QzzsoHTEMwB+InWM6aFXBkshsT+SA06e33K2lun6gRUB8TgHfw8VoAEgK
20M6j0FydOh+PNh4AdNrYh44loe4simWg3YpzEBglInCx60CQQDkzrW9BL6tsplh
oTUk9HxGcTkXWSp+2dMUv/lJFRdw4p957+jskqmmRCKyoimScQnj+VLfc/Q4TTGx
ChWY8yZrAkEA5zgVnKMeMjAylpgxs9qN+wYplv36NAGHD/xGkiMTAANf9XTZ+6S8
2Y5+kvl/4zYAH9PPq+impb+5w7D3Vl2R1QJAeMQ+GnFJr2aIHIa5BTNh8NBMAO3Y
RzHzfo1BJ3jRcYy7/eFKAKv8jTyDT+PVq2yser6bJkQOkDT2WGppMdyM1wJBAN7M
LhcHHtuhkb2G3a2+lT0jTQQPqv5d0nVW0/GRFofWuKpedIWE0eyY3+JjxBV+PVRt
1xiBT8M7IZcteMeh1hkCQQCrgQFOFOm165jNDSIYaipHSM/XJlcjh7ljucqJwe7R
PM1MgGWlMJUDMZAxgB523asmcpGEThvxm3sbtr76VMV/
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOqKaGRUrz0MmOEzB128o2zRhO
w6CFSR0p3rcqFUTKGvfMj9lew3CLjlYHBScZanUe5aBXQ2xDxrdCfEM9BjizAL4+
Pdf8l75FNWZMgusjPGn+vGuAniPid2zyLEQghGLB1S5f3WxWbionc3H941OFNnEG
qtochN/dwoAT4fCSBwIDAQAB
-----END PUBLIC KEY-----

是不是很熟悉的味道,后面还要继续联想

上面的公钥私钥可以保存成二进制文件,以及从文件中读回的代码如下

def write_key(filename, export_key):
    with open(filename, 'wb') as f:
        f.write(export_key)
write_key('rsa_private_key.pem', private_key)
write_key('rsa_public_key.pem', public_key)
def get_key(filename):
    with open(filename) as f:
       return RSA.importKey(f.read())
private_key = get_key("rsa_private_key.pem")

其他工具生成的 RSA keys

前面我们用的 pycryptodome 组件生成的 RSA 公私钥,记得之前在 Mac OS X 下用 ssh-keygen 生成 RSA 密钥,运行

ssh-keygen -t rsa

就会在 ~/.ssh 目录下生成公钥和私钥文件 id_rsaid_rsa.pub。如果用 ssh key 免密码连接 SSH 服务的话,我们需要把 id_rsa.pub 的内容上传到 SSH 服务器。

那么我们是否能用ssh-keygen 生成的公钥和私钥进行加密,解密呢?下面来试下

encrypted = encrypt_data("hello world", get_key("/Users/yanbin/.ssh/id_rsa.pub"))
print(encrypted)
decrypted = decrypt_data(encrypted, get_key("/Users/yanbin/.ssh/id_rsa"))
print(decrypted)

dgrWzMYZL2OIx/Xu0hSRTn7Q0yhyjDJRygKgMCXN/KxLJAb8nGnovmnkAhUH16glAQgzUNw7j0R73EaKT5bg8iwzMDFH5ufJx2+iOUdjXQOf6ZBQ7yyFi+Jz9ZFF2TLT8DGscS/cvh2whxvZZ+zA+xF5tVlgeIY4HPWXXSOuJ0LsCKaRxg25MTSjOWmaiWDlGLi4lGjXJu0wCMxjBCFR4PbtqIaleYL7bQndv03CCWQxNmTMgoBAjQ7TZXfDmpfYc17qgANfZ86gqhM1ILqfu7SlQBWWBL0URDcy6ED6AwsccI5W7l/k0abBX2aQ8YUfh6ZD5wX0/39sOqJ0Am1LbESeI7p3G1iDFy/i0s/3uk+RuzzK4oDxzbpLAXoy+GgNk3cf9gRP1+/pP2H83fTu0H1L5cKYVcgPXQJ/5l+aIoZrZLz+f7+hfAkphuy76jZKGJ0a4nTVJwMuJHQUz81mvYNi6jJB2iF//id+LsG9lZ4Byvio8X+ymKlhHuSjiSb1
hello world

完全没问题。

查看 id_rsa 和 id_rsa.pub 中的内容,大致的样子是

ssh-key-rsa-800x189.png

也可以用 openssl 命令生成的公钥私钥

openssl genrsa -out rsa_private_key.pem 1024

生成与私钥相关联的公钥

openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

$ cat rsa_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDs47aLDbMH1RWpEYrLYlUAYV1AYS4q+eIieCxmjW8zWdMuejBN
2qSXejV4C1kt/w/goLwR+c++snJyMcRSaJINKKx29H/ZM099DXeKOXnSfmJBdKn7
S+ojQ7/jciGWxVhpw5NpLJy9ai+Bus/EO+ThbFkYDfsA04+GfJxhHKFT1wIDAQAB
AoGAZA8jMcUNlAdbaNhyCcp1IP3/R4xE1f5KmEiuaHrhN41/eChAcwIhd6I1J4mT
l6we5sC979HXADObx8RhqnVlCr3Ec5IT17iLs5+OQHfVr5sLbpP2LQslPJvL3Lx/
XA3SAtiAMxTE71rC3nJaA9MwTVX0yikoNX440UJlfAwBUXECQQD8RqKyI6QEBaHy
otT3F0HT9yjTWSX6t1TiR3zbDwTkrsnvg/DP3NuOWFY4ys6CcSLgDkKHkPyMpAFM
XbkbEAQDAkEA8GLuccB06uV/CirXt1UWPEQz/bH2uhEgrWjb/3r0cvtoDxIecgT/
9EV3sv3uCZyUAV6RZ9ZSau2rqrxByOJKnQJBAL4yewMXP9cQcBLAlRNdY3Hti8gc
FDg79DFNeGKnpibLaM+9h9cPSjC9hPP4Y02RApwt5BbVRrK6C4iJuL8gigUCQD5y
ZOEWDwlqfvskMA/HQdR8H0l7bs3dXzDNOcF/rnskRl8L5O7Xz6okVbkg8DJ9A5Hr
gDiKW7S9c0gSScCm0J0CQCfcZ2MRiKdBpvujzZoaC6UFBbC3mVkm5ybYRkJ5uVUw
q/bKOfD41fJ8vQQgquxJXPMIfxiiMscq5Cox+UHakzY=
-----END RSA PRIVATE KEY-----
$ cat rsa_public_key.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDs47aLDbMH1RWpEYrLYlUAYV1A
YS4q+eIieCxmjW8zWdMuejBN2qSXejV4C1kt/w/goLwR+c++snJyMcRSaJINKKx2
9H/ZM099DXeKOXnSfmJBdKn7S+ojQ7/jciGWxVhpw5NpLJy9ai+Bus/EO+ThbFkY
DfsA04+GfJxhHKFT1wIDAQAB
-----END PUBLIC KEY-----

使用这里生成的公钥加密,私钥解密也没有问题

是否能用私钥加密公钥解密呢?

先来尝试一下

encrypted = encrypt_data("hello world", RSA.importKey(private_key))
print(encrypted)
decrypted = decrypt_data(encrypted, RSA.importKey(public_key))
print(decrypted)

gVc1t/HEJFdh9WeBQKpW4fpcfTkIiQvCY3Pnly38vARB6F2rlsIhkFRHQtSeZwdcUoN+qsX8Bfa8CMuP0Gr8RIJj7LaVkNpSSZ80tLbBC1Bfbc34qOtowfbmBBoMdCu9cpTwO8ePepGB06+MQTKgUwmzXRo0AeXxzjhvGTtz3y8=
.....
TypeError: This is not a private key

说明可以用私钥加密,但不能用公钥解密

那么只用私钥加密和解密呢?

encrypted = encrypt_data("hello world", RSA.importKey(private_key))
print(encrypted)
decrypted = decrypt_data(encrypted, RSA.importKey(private_key))
print(decrypted)

GpeISnhTUTHoJbGGF02PB9+pHtrjL2pfo+AiOGas7Euit/e+njhxWU7bLcurX7KbtHWgjxkp2ObAQh1JbebkckLKTHQA3hebuA4zH5DXStovYwX80EMFm2bKH7slz0Dql0Oc4JPRKqTJhttrizrrsFAyDYFCNBj2RNvzaHyZCvM=
hello world

通过,但这又回到对称加密的原地了,还要 RSA 做什么,况且生成密钥的一方是不会把私钥告知另一方的。

私钥制作签名, 公钥验证签名

前面测试过用私钥加密的数据不能用公钥解密,用私钥加密的数据仍能用私钥解密。RSA 的正确做法都是用公钥加密数据,然后用私钥解密,在进行 TLS 握手时,初步猜想(有待于以后验证)应该是在各自一端生成公钥和私钥,然后交换公钥,所以数据总是由对方提供过来的公钥加密,最后由保存在本地私钥进行解密。这样就无需发送私钥。

比如 A 和与 B 进行加密通信,在进行 RSA 握手时

  1. A 生成 private_key_A, public_key_A, 并发送 public_key_A 给 B
  2. 同样的, B 生成 private_key_B, public_key_B, 也把 public_key_B 发给 A

加密,传送,解密过程,比如 A 要发数据给 B

  1. A  先用收到的 public_key_B 对数据加密
  2. 发送加密数据给 B
  3. B 收到数据后用本地的 private_key_B 进行解密

私钥虽不用来加密数据,但可以用私钥生成签名,另一方再用公钥进行签名验证。数据发送端持有自己生成私钥,并把公钥发送给了对方。对数据进行签名的目的是为了保证数据在传送过程中的完整性,如果数据被篡改,签名未随之修改验证就不能通过,要是签名也被改了,还能被验证通过,问题就大了,说明私钥都被别人掌握了。

比如同样是 A 发送数据给 B

  1. 数据的构成为: public _key_B 加密的数据 + private_key_A 对数据生成的签名
  2. B 接收到数据后用 private_key_B 解密
  3. B 再用 public_key_A 对数据进行签名验证

私钥签名,公钥验证签名的代码如下:

import base64
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
def sign_with_private_key(msg):
    signer = PKCS1_signature.new(RSA.importKey(private_key))
    digest = SHA.new()
    digest.update(msg.encode())
    sign = signer.sign(digest)
    signature = base64.b64encode(sign)
    signature = signature.decode()
    return signature
def check_sign_with_public_key(msg, signature):
    verifier = PKCS1_signature.new(rsa.publickey())
    digest = SHA.new()
    digest.update(msg.encode())
    return verifier.verify(digest, base64.b64decode(signature))
my_signature = sign_with_private_key("hello world")
print(my_signature)
print(check_sign_with_public_key("hello world", my_signature))
print(check_sign_with_public_key("hello worldx", my_signature))

yVFiA9z7uPCVum0D4pQI/yvfCiArPF+b+Kr2IO0v1g9mOdFZ4YxOlnedtK75b2Slz6p7xwpr4OATrwopadCcnfdbeHlY24CKYzFOZh8W+uK8FPMSNE9//RNvNLy+Znlx+mGbcDQQBjzPsYfoDQ0DGoSpTGgrFFN3FOaj0G4b//M=
True
False

任何对数据的修改都无法通过签名的验证。

注:非对称的设计之初是用公钥加密私钥解密,但由于公钥加密很慢,所以它常用来协商一个共享密钥,以后只用该共享密钥加密数所。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK