ECC 加密 / 解密
本节将说明如何实现基于椭圆曲线的公钥加密 / 解密(基于 ECC 的非对称加密方案)。这并非易事,通常需要设计一种混合加密方案,其中涉及 ECC 密码学、ECDH 密钥交换和对称加密算法。
假设我们有一个 ECC 公私钥对,并希望使用这些密钥加密和解密数据。根据定义,非对称加密的工作方式如下:如果使用公钥加密数据,随后就能使用对应的私钥解密密文:

上述过程可以直接应用于 RSA 密码系统,但不能直接应用于 ECC。椭圆曲线密码学(ECC)并不直接提供加密方法。我们可以改为设计一种混合加密方案,使用 ECDH(椭圆曲线 Diffie–Hellman)密钥交换方案派生一个共享秘密密钥,用于对称数据加密和解密。
大多数混合加密方案的工作方式如下(加密过程):

大多数混合加密方案的工作方式如下(解密过程):

下面详细说明如何设计和实现基于 ECC 的混合加密方案。
基于 ECC 的秘密密钥派生(使用 ECDH)
假设我们有一条定义在有限域上的密码学椭圆曲线及其生成点 G。可以使用以下两个函数计算用于加密和解密的共享秘密密钥(派生自 ECDH 方案):
- calculateEncryptionKey(pubKey) --> (sharedECCKey, ciphertextPubKey)
- 生成 ciphertextPrivKey = 新的随机私钥。
- 计算 ciphertextPubKey = ciphertextPrivKey * G。
- 计算 ECDH 共享秘密:sharedECCKey = pubKey * ciphertextPrivKey。
- 同时返回 sharedECCKey + ciphertextPubKey。使用 sharedECCKey 进行对称加密;之后使用随机生成的 ciphertextPubKey 计算解密密钥。
- calculateDecryptionKey(privKey, ciphertextPubKey) --> sharedECCKey
- 计算 ECDH 共享秘密:sharedECCKey = ciphertextPubKey * privKey。
- 返回 sharedECCKey 并将其用于解密。
上述计算采用与 ECDH 算法相同的数学原理(参见上一节)。回想一下,EC 点具有以下性质:
- (a * G) * b = (b * G) * a
现在假设 a = privKey, a * G = pubKey, b = ciphertextPrivKey, b * G = ciphertextPubKey。
上述等式变为以下形式:
- pubKey * ciphertextPrivKey = ciphertextPubKey * privKey = sharedECCKey
这正是上述两个函数所计算的内容,它们直接遵循 ECDH 密钥协商方案。在混合加密方案中,封装的 ciphertextPubKey 也称为“临时密钥”,因为它仅供临时使用,通过 ECDH 密钥协商方案派生对称加密密钥。
基于 ECC 的秘密密钥派生——Python 示例
下面的 Python 代码使用 tinyec 库为消息接收方生成一个 ECC 公私钥对(基于 brainpoolP256r1 曲线),然后根据接收方的公钥派生一个共享秘密密钥(用于加密)和一个临时密文公钥(用于 ECDH);之后,再根据接收方的私钥和此前生成的临时密文公钥派生出同一个共享秘密密钥(用于解密):
from tinyec import registry
import secrets
curve = registry.get_curve('brainpoolP256r1')
def compress_point(point):
return hex(point.x) + hex(point.y % 2)[2:]
def ecc_calc_encryption_keys(pubKey):
ciphertextPrivKey = secrets.randbelow(curve.field.n)
ciphertextPubKey = ciphertextPrivKey * curve.g
sharedECCKey = pubKey * ciphertextPrivKey
return (sharedECCKey, ciphertextPubKey)
def ecc_calc_decryption_key(privKey, ciphertextPubKey):
sharedECCKey = ciphertextPubKey * privKey
return sharedECCKey
privKey = secrets.randbelow(curve.field.n)
pubKey = privKey * curve.g
print("private key:", hex(privKey))
print("public key:", compress_point(pubKey))
(encryptKey, ciphertextPubKey) = ecc_calc_encryption_keys(pubKey)
print("ciphertext pubKey:", compress_point(ciphertextPubKey))
print("encryption key:", compress_point(encryptKey))
decryptKey = ecc_calc_decryption_key(privKey, ciphertextPubKey)
print("decryption key:", compress_point(decryptKey))
运行上述代码示例:https://repl.it/@nakov/ECC-based-secret-key-derivation-in-Python。
代码非常简单,它说明我们可以根据给定的 EC 公钥生成一对 { 秘密密钥 + 密文公钥 },之后再根据 { 密文公钥 + 私钥 } 恢复出同一个秘密密钥。上述代码会生成如下输出:
private key: 0x2e2921b4cde59cdf01e7a014a322abd530b3015085c31cb6e59502da761d29e9
public key: 0x850d3873cf4ac50ddb54ddbd27f8225fc43bd3f4c2cc0a4f9d1f9ce15fc4eb711
ciphertext pubKey: 0x71586f9999d3ee050005054bc681c1d96c5eb054ca15b080ba245e495627003b0
encryption key: 0x9d13d3f8f9747669432f575731926b5ed99a6883f00146cbd3203ffa7ff8b1ae1
decryption key: 0x9d13d3f8f9747669432f575731926b5ed99a6883f00146cbd3203ffa7ff8b1ae1
从上述输出可以清楚地看出,加密密钥(由公钥派生)和解密密钥(由对应私钥派生)完全相同。这是由前面讨论的 ECC 性质决定的:pubKey * ciphertextPrivKey = ciphertextPubKey * privKey。这些密钥将用于集成加密方案中的数据加密和解密。每次运行代码时,上述输出都会不同(因为生成 ciphertextPrivKey 时使用了随机性),但加密密钥与解密密钥始终相同(即 ECDH 共享秘密)。
上述基于 ECC 密钥对生成共享临时秘密密钥的机制,是一种基于 ECC 和 ECDH 的 KEM(密钥封装机制)示例。
基于 ECC 的混合加密 / 解密——Python 示例
获得秘密密钥后,就可以使用 AES-GCM 或 ChaCha20-Poly1305 等对称加密方案进行对称数据加密。下面实现一个功能完整的非对称 ECC 加密与解密混合方案,它将基于 brainpoolP256r1 曲线和 AES-256-GCM 认证对称密码。
我们将分别使用 Python 库 tinyec 和 pycryptodome 进行 ECC 计算和 AES 加密:
pip install tinyec
pip install pycryptodome
下面查看这个完整的 ECC + AES 混合加密示例:
from tinyec import registry
from Crypto.Cipher import AES
import hashlib, secrets, binascii
def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)
def decrypt_AES_GCM(ciphertext, nonce, authTag, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM, nonce)
plaintext = aesCipher.decrypt_and_verify(ciphertext, authTag)
return plaintext
def ecc_point_to_256_bit_key(point):
sha = hashlib.sha256(int.to_bytes(point.x, 32, 'big'))
sha.update(int.to_bytes(point.y, 32, 'big'))
return sha.digest()
curve = registry.get_curve('brainpoolP256r1')
def encrypt_ECC(msg, pubKey):
ciphertextPrivKey = secrets.randbelow(curve.field.n)
sharedECCKey = ciphertextPrivKey * pubKey
secretKey = ecc_point_to_256_bit_key(sharedECCKey)
ciphertext, nonce, authTag = encrypt_AES_GCM(msg, secretKey)
ciphertextPubKey = ciphertextPrivKey * curve.g
return (ciphertext, nonce, authTag, ciphertextPubKey)
def decrypt_ECC(encryptedMsg, privKey):
(ciphertext, nonce, authTag, ciphertextPubKey) = encryptedMsg
sharedECCKey = privKey * ciphertextPubKey
secretKey = ecc_point_to_256_bit_key(sharedECCKey)
plaintext = decrypt_AES_GCM(ciphertext, nonce, authTag, secretKey)
return plaintext
msg = b'Text to be encrypted by ECC public key and ' \
b'decrypted by its corresponding ECC private key'
print("original msg:", msg)
privKey = secrets.randbelow(curve.field.n)
pubKey = privKey * curve.g
encryptedMsg = encrypt_ECC(msg, pubKey)
encryptedMsgObj = {
'ciphertext': binascii.hexlify(encryptedMsg[0]),
'nonce': binascii.hexlify(encryptedMsg[1]),
'authTag': binascii.hexlify(encryptedMsg[2]),
'ciphertextPubKey': hex(encryptedMsg[3].x) + hex(encryptedMsg[3].y % 2)[2:]
}
print("encrypted msg:", encryptedMsgObj)
decryptedMsg = decrypt_ECC(encryptedMsg, privKey)
print("decrypted msg:", decryptedMsg)
运行上述代码示例:https://repl.it/@nakov/ECC-based-hybrid-encryption-decryption-in-Python。
上述示例首先使用 tinyec 库为消息接收方生成 ECC 公私钥对:pubKey + privKey。这些密钥将通过混合加密方案(非对称 ECC + 对称 AES)加密消息 msg,并在之后将其解密回原始形式。
接下来,使用 pubKey 加密 msg,得到以下一组输出:{ ciphertext, nonce, authTag, ciphertextPubKey }。ciphertext 由对称 AES-GCM 加密生成,同时还会得到 nonce(随机 AES 初始化向量)和 authTag(由 GCM 分组模式生成的密文认证标签)。此外,还会得到一个随机生成的临时公钥 ciphertextPubKey,它将被封装在加密消息中,并在解密期间用于恢复 AES 对称密钥(采用前面介绍的 ECDH 密钥协商方案)。
要解密加密消息,需要使用加密期间生成的数据 { ciphertext, nonce, authTag, ciphertextPubKey } 以及用于解密的 privateKey。结果是解密后的明文消息。由于使用了认证加密(GCM 分组模式),如果解密密钥或其他参数不正确,解密将失败并抛出异常。
在内部,encrypt_ECC(msg, pubKey) 函数首先为密文生成一个临时 ECC 密钥对,并计算用于对称加密的共享 ECC 密钥 sharedECCKey = ciphertextPrivKey * pubKey。该密钥是一个 EC 点,因此随后通过对该点的 x 和 y 坐标进行哈希,将其转换为 256 位 AES 秘密密钥(整数)。最后,AES-256-GCM 密码(来自 pycryptodome)使用 256 位共享秘密密钥 secretKey 加密消息,并生成输出 ciphertext + nonce + authTag。
decrypt_ECC(encryptedMsg{ciphertext, nonce, authTag, ciphertextPubKey}, privKey) 函数首先在内部计算用于对称加密的共享 ECC 密钥 sharedECCKey = privKey * ciphertextPubKey。它是一个 EC 点,因此应先通过对该点的 x 和 y 坐标进行哈希,将其转换为 256 位 AES 秘密密钥。然后,使用 AES-256-GCM 密码和 256 位共享秘密密钥 secretKey 解密 ciphertext + nonce + authTag。最终输出是原始明文消息(如果解密密钥不正确或 authTag 不匹配,则抛出异常)。
上述代码的输出如下:
original msg: b'Text to be encrypted by ECC public key and decrypted by its corresponding ECC private key'
encrypted msg: {'ciphertext': b'b5953b3082fcefdbde91dd3c03cf83dde0822c19be6ae906a634db65115295e7cbcd7a1a492d69ba5be91990c70d8df9dc84360cf554f155ef81ce1f0ad44bd9fdabbc5f960517089262b3390e61b37610012bee4e6bcae335', 'nonce': b'9d55f4b5c87fff773d0457f3b23a953e', 'authTag': b'5c9d339778925aa4e44f43252a28681d', 'ciphertextPubKey': '0x21dbc985b625f2a42d0f86fc234b49b55477928bae73dfac73bafd9bed50abe70'}
decrypted msg: b'Text to be encrypted by ECC public key and decrypted by its corresponding ECC private key'
请尽情尝试上述示例,动手修改它:理解其确切工作原理,尝试更换底层 ECC 曲线或对称加密算法,并尝试使用错误的私钥解密密文。