加密和签名是开发涉及敏感信息的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 发起请求:

  1. 先创建一个字典, 加上时间戳字段: {“params”: params, “timestamp”: timestamp}

  2. 排序后序列化成一个新字符串, 格式为 params=xxx&timestamp=1666421907.729505, 计算它的 md5摘要值 md5data

  3. 用自己的私钥 privateKey2 对 md5data签名, 得到 signature

  4. 随机生成一个 AES密钥 aesKey, 对 params加密, 得到 aesEncryptedParams

  5. 用对方的公钥 publicKey1 对 aesKey 进行 RSA 加密, 得到 rsaEncryptedAesKey

  6. 构造参数 {“params”: aesEncryptedParams, “timestamp”: timestamp, “sign”: signature, “key”: rsaEncryptedAesKey}, 发送请求.

Alice 验证请求:

  1. 用自己的私钥 privateKey1 解密 rsaEncryptedAesKey, 得到 aesKey

    😈 rsa加解密 确保了 aesKey的机密传输.

  2. 用 aesKey 解密 aesEncryptedParams, 得到 params.

    😈 aes加解密 确保了params机密传输.

  3. 用 params 和 收到的 timestamp计算 md5data, 方法同 Bob 的Step 2

  4. 用 Bob 的公钥 publicKey2 对 md5data 验签, 成功则证明一切正常.

    😈 签名验签 可以确认数据源身份, 确保了真实性和数据完整性.

完整示例代码

git clone git@github.com:nolanzhao/API_Encrypt.git