Avoid using PyCryptodome for AES-SIV encryption of zero-length plaintext

This commit is contained in:
RunasSudo 2024-05-27 22:14:50 +10:00
parent bba6e782f1
commit 2af1d5385b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
1 changed files with 64 additions and 22 deletions

View File

@ -14,8 +14,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from Crypto.Cipher import AES
from cryptography.hazmat.primitives.ciphers.aead import AESSIV
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.cmac import CMAC
import struct
def aes_siv_encrypt(primary_master_key, hmac_master_key, plaintext, associated_data):
"""
@ -23,33 +26,72 @@ def aes_siv_encrypt(primary_master_key, hmac_master_key, plaintext, associated_d
"""
if len(plaintext) == 0:
# Must use PyCryptodome
# https://github.com/pyca/cryptography/issues/10958 - cryptography AESSIV does not accept empty plaintext (e.g. root directory has empty directory ID)
if associated_data:
if len(associated_data) > 1:
# Incompatible with PyCryptodome
raise ValueError('Cannot encrypt zero-length plaintext with AES-SIV with >1 associated data')
if not associated_data[0]:
# Incompatible with PyCryptodome
raise ValueError('Cannot encrypt zero-length plaintext with AES-SIV with zero-length associated data')
# Manually calculate the synthetic IV
siv = aes_siv_s2v(hmac_master_key, plaintext, associated_data)
# If there is only one associated data, this is equivalent to the nonce, so we can use PyCryptodome
ciphertext, tag = AES.new(hmac_master_key + primary_master_key, AES.MODE_SIV, nonce=associated_data[0]).encrypt_and_digest(plaintext)
return tag + ciphertext
# Zero-length plaintext with no AAD - encrypt with PyCryptodome
ciphertext, tag = AES.new(hmac_master_key + primary_master_key, AES.MODE_SIV).encrypt_and_digest(plaintext)
return tag + ciphertext
# Empty plaintext equals empty ciphertext, so result is just the SIV
return siv
# In all other cases, use cryptography AESSIV
tag_and_ciphertext = AESSIV(hmac_master_key + primary_master_key).encrypt(plaintext, associated_data)
return tag_and_ciphertext
siv_and_ciphertext = AESSIV(hmac_master_key + primary_master_key).encrypt(plaintext, associated_data)
return siv_and_ciphertext
def aes_siv_decrypt(primary_master_key, hmac_master_key, tag_and_ciphertext, associated_data):
def aes_siv_decrypt(primary_master_key, hmac_master_key, siv_and_ciphertext, associated_data):
"""
Decrypt the given AES-SIV ciphertext
"""
# Use cryptography AESSIV
return AESSIV(hmac_master_key + primary_master_key).decrypt(tag_and_ciphertext, associated_data)
return AESSIV(hmac_master_key + primary_master_key).decrypt(siv_and_ciphertext, associated_data)
def aes_cmac(hmac_master_key, data):
mac = CMAC(AES(hmac_master_key))
mac.update(data)
return mac.finalize()
def aes_siv_dbl(data):
# Based on miscreant.py by Phil Rogaway, MIT License
overflow = 0
words = struct.unpack(b'!LLLL', data)
output_words = []
for word in reversed(words):
new_word = (word << 1) & 0xFFFFFFFF
new_word |= overflow
overflow = int((word & 0x80000000) >= 0x80000000)
output_words.append(new_word)
result = bytearray(struct.pack(b'!LLLL', *reversed(output_words)))
if overflow:
result[-1] ^= 0x87 # Foot-gun! Not constant time
return result
def aes_siv_s2v(hmac_master_key, plaintext, associated_data):
# Based on miscreant.py by Phil Rogaway, MIT License
# Note: The standalone S2V returns CMAC(1) if the number of passed vectors is zero, however in SIV construction this case is never triggered, since we always pass plaintext as the last vector, so we omit this case.
d = bytes(128//8) # 128-bit blocks
d = aes_cmac(hmac_master_key, d)
if associated_data:
for ad in associated_data:
d = aes_siv_dbl(d)
d = bytes(x ^ y for x, y in zip(d, aes_cmac(hmac_master_key, ad))) # d ^= aes_cmac(hmac_master_key, ad)
if len(plaintext) >= 128//8: # 128 bits
mac = CMAC(AES(hmac_master_key))
difference = len(plaintext) - 128/8
mac.update(plaintext[:difference])
d = bytes(x ^ y for x, y in zip(d, plaintext[difference:])) # d ^= plaintext[difference:]
mac.update(d)
return mac.finalize()
d = aes_siv_dbl(d)
for i in range(len(plaintext)):
d[i] ^= plaintext[i]
d[len(plaintext)] ^= 0x80
return aes_cmac(hmac_master_key, d)