加密和签名是开发涉及敏感信息的API的必要步骤.
本文提供一个 Python 版的示例程序.
0. 相关概念
加密
加密 可以保证数据的机密性
.
常见加密算法包括 AES 对称加密, 和 RSA 非对称机密.
-
AES (Advanced Encryption Standard) 加解密使用同一个密钥.
-
RSA 有公钥
和私钥
, 成对使用. 用对方的公钥加密, 对方收到之后用他的私钥解密.
数字签名
数字签名 可以对数据的完整性(Integrity)
进行验证; 也能数据源进行身份验证
, 保证真实性(authenticity)
.
对报文加盖时间戳,添加序列号再签名, 也可实现防重放攻击(Replay Attack).
-
如果报文有被篡改或伪造, 无法通过验签, 因为不同的报文用同一个私钥签名的结果是不一样的. 这样就保证了数据的完整性
.
-
如果能用你的公钥
验签, 就证明这是你的签名, 而不是别人的. 即不可抵赖
. 因为你签名用的私钥
只有你所拥有, 唯一的验签方法是用你的公钥
,
你的公钥也无法验证别人的签名. 只需证明该公钥是你之前分发的(可通过第三方证明), 就能证明签名是你的.
-
如果别人用其他方式(非你的私钥)伪造一个签名, 你的公钥是无法验签的, 这就保证了你的签名不可伪造
.
-
重放攻击是黑客抓包拿到你的请求参数, 然后拿去请求服务器. 如果我们对每次请求加上时间戳, 对比相同序列号的请求的时间戳, 看是否重复, 就可发现重放事件. 对报文和时间戳序列号一起签名, 是为了防止黑客修改.
RSA 算法也可以用于数字签名, 用自己的私钥签名, 把公钥分发给对方, 用于验签.
通常对原始报文取摘要进行签名.
😈: 公钥加密, 私钥解密. 私钥签名, 公钥验签
.
散列
哈希摘要(散列)算法
常见的摘要算法有 MD5
, SHA1
, SHA256
等. 他们可以对原始信息生成一个唯一的简短的字符串,
这个原始信息只要有略微变化, 生成的摘要就有很大差别, 因此可以用来比对信息是否一致. 虽然有一定的碰撞概率, 但极低.
有些软件包发布的时候通常会附上摘要值, 这样当你下载到本地之后, 可以通过计算它的摘要值与公布的值对比, 来确保下载包是完整的,
并且没有被篡改, 即完整性(Integrity)
.
1
2
3
4
|
In [5]: import hashlib
In [6]: hashlib.md5("hello world".encode("utf-8")).hexdigest()
Out[6]: '5eb63bbbe01eeed093cb22bb8f5acdc3'
|
1
2
3
4
5
6
7
|
# On MAC
➜ ~ echo -n "hello world" | md5
5eb63bbbe01eeed093cb22bb8f5acdc3
# on Linux
➜ ~ echo -n "hello world" | md5sum
5eb63bbbe01eeed093cb22bb8f5acdc3
|
编码
base64编码
编码, 就是将原始信息映射为另一串信息. 和加密不同的是, 它只是将信息进行了简单的转换, 任何知道这种算法的人都能轻易还原, 不涉及到密钥问题.
base64是一种很常见的编码方式, 它可以把任意信息(文本或二进制)映射到只使用部分ASCII字符的文本. 对于有些只支持文本的信道, 可以将二进制信息(比如图片), 用base64编码之后传输.
Python 提供了 base64
包, 可供使用. Linux下也有命令可使用.
Python 示例:
1
2
3
4
5
6
7
|
In [6]: import base64
In [7]: base64.b64encode("hello world".encode("utf-8"))
Out[7]: b'aGVsbG8gd29ybGQ='
In [8]: base64.b64decode(b'aGVsbG8gd29ybGQ=')
Out[8]: b'hello world'
|
Linux命令示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 字符串编解码
➜ ~ echo -n "hello world" | base64
aGVsbG8gd29ybGQ=
➜ ~
➜ ~ echo -n "aGVsbG8gd29ybGQ=" | base64 -d
hello world%
# 😈 echo 后加 -n 参数表示 不输出换行, 不带它编码结果会不同!!!
# 文件编解码
➜ ~ base64 文件路径
➜ ~ base64 -d 文件路径
|
1. 数字签名
代码中用到的 Crypto
包的安装方法:
1
|
pip install pycryptodome==3.9.0
|
RSA 签名
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
|
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
from Crypto.Hash import MD5, SHA1, SHA256
import base64
def sign(data, privateKey):
"""
签名
"""
# from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
rsa_key = RSA.importKey(privateKey)
signer = PKCS1_signature.new(rsa_key)
## 哈希算法可选: MD5, SHA1, SHA256
# #1. SIGNATURE_ALGORITHM: "MD5withRSA"
# hash_obj = MD5.new(data.encode('utf-8'))
#2. SIGNATURE_ALGORITHM: "SHA1withRSA"
hash_obj = SHA1.new(data.encode('utf-8'))
# #3. SIGNATURE_ALGORITHM: "SHA256withRSA"
# hash_obj = SHA256.new(data.encode('utf-8'))
signature = base64.b64encode(signer.sign(hash_obj))
return signature.decode("utf-8")
|
RSA 验签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
from Crypto.Hash import MD5, SHA1, SHA256
import base64
def verify(data, signature, publicKey):
"""
验签
"""
# from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
rsa_key = RSA.importKey(publicKey)
verifier = PKCS1_signature.new(rsa_key)
h = SHA1.new(data.encode('utf-8'))
if verifier.verify(h, base64.b64decode(signature.encode("utf-8"))):
return True
return False
|
2. 对称加密算法
AES 加密
生成随机密钥
1
2
3
4
5
6
7
8
9
|
import random
import string
def generateAesKey():
"""
生成 AES 密钥: 16 位随机字符(字母大小写 + 数字)
"""
return ''.join(random.sample(string.ascii_letters + string.digits, 16))
|
定义一个函数, 对加解密内容补足 16 位, 防止报错.
1
2
3
4
5
6
7
|
def parse(value):
"""
补足 16 位
"""
while len(value) % 16 != 0:
value += '\0'
return str.encode(value)
|
加密
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from Crypto.Cipher import AES
import base64
def encrypt(content, key):
"""
AES 加密
"""
aes = AES.new(parse(key), AES.MODE_ECB)
encrypt_aes = aes.encrypt(parse(content))
encrypted_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypted_text
|
AES 解密算法
1
2
3
4
5
6
7
8
9
10
11
12
|
from Crypto.Cipher import AES
import base64
def decrypt(content, key):
"""
AES 解密
"""
aes = AES.new(parse(key), AES.MODE_ECB)
base64_decrypted = base64.decodebytes(content.encode(encoding='utf-8'))
decrypted_text = str(aes.decrypt(base64_decrypted), encoding='utf-8').replace('\0', '')
return decrypted_text
|
3. 非对称加密算法
RSA 加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
def encryptRSA(content, publicKey):
"""
RSA 加密
"""
# from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
rsa_key = RSA.importKey(publicKey)
cipher = PKCS1_cipher.new(rsa_key)
encrypted_text = base64.b64encode(cipher.encrypt(content.encode("utf-8")))
return encrypted_text.decode('utf-8')
|
RSA 解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
def decryptRSA(content, privateKey):
"""
RSA 解密
"""
# from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
rsa_key = RSA.importKey(privateKey)
cipher = PKCS1_cipher.new(rsa_key)
decrypted_text = cipher.decrypt(base64.b64decode(content), 0)
return decrypted_text.decode("utf-8")
|
4. API 加密和签名步骤
Alice 和 Bob 两人通信, Bob 发起请求, Alice 验证请求.
他们的密钥对分别为:
Alice: publicKey1, privateKey1
Bob: publicKey2, privateKey2
Bob 发起请求:
-
先创建一个字典, 加上时间戳字段: {“params”: params, “timestamp”: timestamp}
-
排序后序列化成一个新字符串, 格式为 params=xxx×tamp=1666421907.729505, 计算它的 md5摘要值 md5data
-
用自己的私钥 privateKey2 对 md5data签名, 得到 signature
-
随机生成一个 AES密钥 aesKey, 对 params加密, 得到 aesEncryptedParams
-
用对方的公钥 publicKey1 对 aesKey 进行 RSA 加密, 得到 rsaEncryptedAesKey
-
构造参数 {“params”: aesEncryptedParams, “timestamp”: timestamp, “sign”: signature, “key”: rsaEncryptedAesKey}, 发送请求.
Alice 验证请求:
-
用自己的私钥 privateKey1 解密 rsaEncryptedAesKey, 得到 aesKey
😈 rsa加解密 确保了 aesKey的机密传输.
-
用 aesKey 解密 aesEncryptedParams, 得到 params.
😈 aes加解密 确保了params机密传输.
-
用 params 和 收到的 timestamp计算 md5data, 方法同 Bob 的Step 2
-
用 Bob 的公钥 publicKey2 对 md5data 验签, 成功则证明一切正常.
😈 签名验签 可以确认数据源身份, 确保了真实性和数据完整性.
附
完整示例代码
git clone git@github.com:nolanzhao/API_Encrypt.git