在阅读《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。早先有 crypto 和 PyCrypto, 但它们都已不再维护,而 PyCrypto 建议使用 Cryptography 或 PyCryptodome。
一个完整的例子
安装 pycryptodome 库
pip install pycryptodoem
下面的代码生成公钥与私钥,并用公钥加密,私钥解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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
说明
- 因为每次的公,私钥不一样,所以加密后的表现形式不一样,但都能用私钥解密出来
- 用 base64 编解码是为了能在控制台打印出二进制字节数据
- rsa.public_key() 能直接获得 public_key, 如果是用 exportKey() 方法导出的 key, 需用 RSA.importKey() 导入后才能使用
RsaKey 所蕴藏的秘密
我们在上面的 RSA.generate(1024, Random.new().read)
后打个断点,睢瞧一个 RsaKey 包含些什么,或者说是怎么不断运算生成的
参考 RSA 的算法,对上面的 d, e, n, p, q 和 u 的解释
- p 和 q 为随意选择的两个质数
- n 为 p * q 的乘机, n 的二进制值的位数即密钥的长度
- u 是 n 的欧拉函数值 φ(n)
- e 可以随便一个在 1 与 n 的欧拉函数φ(n) 之间的数,这里直接用 65537
- d 为 e 对于 φ(n) 的模板元素
最后 n, e 封装为公钥,n, d 封装为私钥
输出公钥和私钥
如果我们打印出公钥和钥是什么样子的呢?来
1 2 3 |
print(private_key.decode()) print("\n") print(public_key.decode()) |
控制台看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
-----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----- |
是不是很熟悉的味道,后面还要继续联想
上面的公钥私钥可以保存成二进制文件,以及从文件中读回的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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_rsa
和 id_rsa.pub
。如果用 ssh key 免密码连接 SSH 服务的话,我们需要把 id_rsa.pub
的内容上传到 SSH 服务器。
那么我们是否能用ssh-keygen
生成的公钥和私钥进行加密,解密呢?下面来试下
1 2 3 4 5 |
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 中的内容,大致的样子是
也可以用 openssl 命令生成的公钥私钥
生成私钥
openssl genrsa -out rsa_private_key.pem 1024
生成与私钥相关联的公钥
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ 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----- |
使用这里生成的公钥加密,私钥解密也没有问题
是否能用私钥加密公钥解密呢?
先来尝试一下
1 2 3 4 5 |
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
说明可以用私钥加密,但不能用公钥解密
那么只用私钥加密和解密呢?
测试
1 2 3 4 5 |
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 握手时
- A 生成 private_key_A, public_key_A, 并发送 public_key_A 给 B
- 同样的, B 生成 private_key_B, public_key_B, 也把 public_key_B 发给 A
加密,传送,解密过程,比如 A 要发数据给 B
- A 先用收到的 public_key_B 对数据加密
- 发送加密数据给 B
- B 收到数据后用本地的 private_key_B 进行解密
私钥虽不用来加密数据,但可以用私钥生成签名,另一方再用公钥进行签名验证。数据发送端持有自己生成私钥,并把公钥发送给了对方。对数据进行签名的目的是为了保证数据在传送过程中的完整性,如果数据被篡改,签名未随之修改验证就不能通过,要是签名也被改了,还能被验证通过,问题就大了,说明私钥都被别人掌握了。
比如同样是 A 发送数据给 B
- 数据的构成为: public _key_B 加密的数据 + private_key_A 对数据生成的签名
- B 接收到数据后用 private_key_B 解密
- B 再用 public_key_A 对数据进行签名验证
私钥签名,公钥验证签名的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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
任何对数据的修改都无法通过签名的验证。
注:非对称的设计之初是用公钥加密私钥解密,但由于公钥加密很慢,所以它常用来协商一个共享密钥,以后只用该共享密钥加密数所。
链接:
本文链接 https://yanbin.blog/python-implement-rsa-encryption/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] 关于非对称加密可参考本人之前写过的一篇 Python 实现 RSA 非对称加解密 […]
您好~我是腾讯云开发者社区运营,关注了您分享的技术文章,觉得内容很棒,我们诚挚邀请您加入腾讯云自媒体分享计划。完整福利和申请地址请见:https://cloud.tencent.com/developer/support-plan
作者申请此计划后将作者的文章进行搬迁同步到社区的专栏下,你只需要简单填写一下表单申请即可,我们会给作者提供包括流量、云服务器等,另外还有些周边礼物。
没用微信和QQ,也没有国内的手机号,申请不了。