我正在尝试使用ChaCha20-Poly1305
模块中的cryptography
密码,
但仅提供ChaCha20
密码和Poly1305
MAC。
这是我最初尝试将它们组合的方式:
from cryptography.hazmat.primitives.poly1305 import Poly1305
from cryptography.hazmat.primitives.ciphers import (
Cipher,
algorithms as algo,
)
from cryptography.hazmat.backends import default_backend as defb
class ChaCha20Poly1305:
def __init__(self, locking, key, nonce):
self._locking = locking
# only accepts 16 bytes nonce
cipher = Cipher(algo.ChaCha20(key, nonce), None, defb())
if locking:
self._cipher = cipher.encryptor()
else:
self._cipher = cipher.decryptor()
self._auth = Poly1305(key)
self._auth.update(nonce)
def update(self, data):
ctxt = self._cipher.update(data)
self._auth.update(ctxt)
return ctxt
def finalize(self, tag=None):
if not self._locking
if tag is None:
raise ValueError('tag required')
self._auth.verify(tag)
def calculate_tag(self):
return self._auth.calculate_tag()
这是与Poly1305一起使用此密码的正确方法吗?
编辑:尽管cryptography
提供了ChaCha20Poly1305
,
它不不支持连续加密数据。
它只是获取一条数据,对其进行加密并返回密文
附有MAC。那不是我想要的。
答案 0 :(得分:2)
可以使用 Cryptography 实现ChaCha20
和Poly1305
来实现流身份验证的加密/解密,类似于 PyCryptodome < / em>实现ChaCha20_Poly1305
。所发布的代码已经基本上完成了此操作,从而缺少了以下几点或有问题:
ChaCha20
期望完整的16字节IV(即随机数(12字节)和计数器(4字节)),采用小尾数形式s。 RFC 7539 sec 2.3。0
用于生成Poly1305
密钥,计数器值1
用于加密,例如。 RFC 7539 sec 2.4和sec 2.6。Poly1305
密钥不仅与加密密钥相对应,还必须从此密钥和带有计数器0
的随机数s派生。 RFC 7539 sec 2.6。Poly1305
密钥外,现时不再参与标签的计算。以下代码考虑了这些要点,应该说明基本原理,并且必须/可以适应个人需求:
from cryptography.hazmat.backends import default_backend as defb
from cryptography.hazmat.primitives.poly1305 import Poly1305
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms as algo
class ChaCha20Poly1305:
def __init__(self, encrypt, key, nonce):
self._encrypt = encrypt
self._dataLength = 0;
self._aadLength = 0;
self._nonceCounter = (0).to_bytes(4, byteorder='little') + nonce # Create 16 bytes IV for Poly1305 key derivation
self._nonceEncrypt = (1).to_bytes(4, byteorder='little') + nonce # Create 16 bytes IV for encryption / decryption
cipher = Cipher(algo.ChaCha20(key, self._nonceEncrypt), None, defb())
if encrypt:
self._cipher = cipher.encryptor()
else:
self._cipher = cipher.decryptor()
polyKey = self.__getPolyKey(key) # Get Poly1305 key
self._auth = Poly1305(polyKey)
# Add AAD and zero pad if nnecessary (optional, may only be called once and before first 'update' call)
def updateAAD(self, aad):
self._auth.update(aad)
self._aadLength = len(aad)
self._auth.update(self.__getZeroBytes(self._aadLength))
# Add ciphertext / plaintext for encryption / decryption and actualize tag
def update(self, data):
ctxt = self._cipher.update(data)
self._dataLength += len(ctxt)
if self._encrypt:
self._auth.update(ctxt)
else:
self._auth.update(data)
return ctxt
# Complete padding and verify tag (only decryption)
def verify_tag(self, tag=None):
if not self._encrypt:
self.__pad()
if tag is None:
raise ValueError('tag required')
self._auth.verify(tag)
else:
raise ValueError('Tag verification only during decryption')
# Complete padding and calculate tag (only encryption)
def calculate_tag(self):
if self._encrypt:
self.__pad()
return self._auth.finalize()
else:
raise ValueError('Tag calculation only during encryption')
# Complete formatting: zero pad ciphertext, append AAD and ciphertext lengths
def __pad(self):
self._auth.update(self.__getZeroBytes(self._dataLength))
self._auth.update(self._aadLength.to_bytes(8, byteorder='little'))
self._auth.update(self._dataLength.to_bytes(8, byteorder='little'))
# Zero pad data (AAD or ciphertext)
def __getZeroBytes(self, len):
spareBytes = len % 16
if (spareBytes != 0):
length = 16 - spareBytes
return bytes([0]) * length
return b''
# Derive Poly1305 key
def __getPolyKey(self, key):
cipher = Cipher(algo.ChaCha20(key, self._nonceCounter), None, defb())
cipher = cipher.encryptor()
key = cipher.update(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
return key
该实现满足RFC 7539, sec 2.8.2中的测试向量:
# Test vector from RFC 7539, sec 2.8.2
plaintext1 = b"Ladies and Gentlemen of the class "
plaintext2 = b"of '99: If I could offer you only one"
plaintext3 = b" tip for the future, sunscreen would be it."
nonce = bytes.fromhex("070000004041424344454647")
key = bytes.fromhex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
# Encryption
ccEnc = ChaCha20Poly1305(True, key, nonce)
ccEnc.updateAAD(bytes.fromhex('50515253c0c1c2c3c4c5c6c7'))
ct1 = ccEnc.update(plaintext1)
ct2 = ccEnc.update(plaintext2)
ct3 = ccEnc.update(plaintext3)
tag = ccEnc.calculate_tag()
print("Ciphertext:\n%s\n" % (ct1 + ct2 + ct3).hex())
print("Tag:\n%s\n" % tag.hex())
# Decryption
ccDec = ChaCha20Poly1305(False, key, nonce)
ccDec.updateAAD(bytes.fromhex('50515253c0c1c2c3c4c5c6c7'))
dt1 = ccDec.update(ct1)
dt2 = ccDec.update(ct2)
dt3 = ccDec.update(ct3)
ccDec.verify_tag(tag)
print("Decrypted:\n%s\n" % (dt1 + dt2 + dt3))
注意:当然重要的是,在成功通过身份验证之前,不要信任解密的数据!就像 PyCryptodome 实现一样,该构造也会尝试使用未经身份验证的(并可能已损坏)数据。评论中已经详细指出了该问题,并且还提出了更可靠的替代方法(另请参见其他答案中的链接文章)。
答案 1 :(得分:0)