diff --git a/pgpy/symenc.py b/pgpy/symenc.py index 281596ed..10f5c87c 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -2,6 +2,7 @@ """ from typing import Optional, Union +from types import ModuleType from cryptography.exceptions import UnsupportedAlgorithm @@ -15,6 +16,12 @@ from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError +AES_Cryptodome: Optional[ModuleType] +try: + from Cryptodome.Cipher import AES as AES_Cryptodome +except ModuleNotFoundError: + AES_Cryptodome = None + __all__ = ['_cfb_encrypt', '_cfb_decrypt', 'AEAD'] @@ -62,16 +69,49 @@ def _cfb_decrypt(ct: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional class AEAD: + class AESEAX: + '''This class supports the same interface as AESOCB3 and AESGCM from python's cryptography module + + We don't use that module because it doesn't support EAX + (see https://github.com/pyca/cryptography/issues/6903) + ''' + + def __init__(self, key: bytes) -> None: + self._key: bytes = key + + def decrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + if AES_Cryptodome is None: + raise NotImplementedError("AEAD Mode EAX needs the python cryptodome module installed") + if len(nonce) != AEADMode.EAX.iv_len: + raise ValueError(f"EAX nonce should be {AEADMode.EAX.iv_len} octets, got {len(nonce)}") + a = AES_Cryptodome.new(self._key, AES_Cryptodome.MODE_EAX, nonce, mac_len=AEADMode.EAX.tag_len) + if associated_data is not None: + a.update(associated_data) + return a.decrypt_and_verify(data[:-AEADMode.EAX.tag_len], data[-AEADMode.EAX.tag_len:]) + + def encrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + if AES_Cryptodome is None: + raise NotImplementedError("AEAD Mode EAX needs the python cryptodome module installed") + if len(nonce) != AEADMode.EAX.iv_len: + raise ValueError(f"EAX nonce should be {AEADMode.EAX.iv_len} octets, got {len(nonce)}") + a = AES_Cryptodome.new(self._key, AES_Cryptodome.MODE_EAX, nonce, mac_len=AEADMode.EAX.tag_len) + if associated_data is not None: + a.update(associated_data) + ciphertext, tag = a.encrypt_and_digest(data) + return ciphertext + tag + def __init__(self, cipher: SymmetricKeyAlgorithm, mode: AEADMode, key: bytes) -> None: - self._aead: Union[AESOCB3, AESGCM] + self._aead: Union[AESOCB3, AESGCM, AEAD.AESEAX] if cipher not in [SymmetricKeyAlgorithm.AES128, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES256]: raise NotImplementedError(f"Cannot do AEAD with non-AES cipher (requested cipher: {cipher!r})") if mode == AEADMode.OCB: self._aead = AESOCB3(key) elif mode == AEADMode.GCM: self._aead = AESGCM(key) + elif mode == AEADMode.EAX: + self._aead = AEAD.AESEAX(key) else: - raise NotImplementedError(f"Cannot do AEAD mode other than OCB, and GCM (requested mode: {mode!r})") + raise NotImplementedError(f"Cannot do AEAD mode other than OCB, GCM, and EAX (requested mode: {mode!r})") def encrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: return self._aead.encrypt(nonce, data, associated_data) diff --git a/requirements.txt b/requirements.txt index 1ba901f1..07f0eb36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ argon2_cffi cryptography>=3.3.2 +pycryptodomex[eax] diff --git a/setup.cfg b/setup.cfg index 4e7fde3d..c56f957b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,8 @@ install_requires = cryptography>=3.3.2 sop>=0.5.1 python_requires = >=3.6 +extras_require = + eax = pycryptodomex # doc_requires = # sphinx diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index a4249f42..2866602a 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -3,6 +3,7 @@ """ from typing import Dict, Optional, Tuple +from types import ModuleType import pytest @@ -11,6 +12,12 @@ from pgpy import PGPKey, PGPSignature, PGPMessage from pgpy.errors import PGPDecryptionError +Cryptodome:Optional[ModuleType] +try: + import Cryptodome +except ModuleNotFoundError: + Cryptodome = None + class TestPGP_CryptoRefresh(object): def test_v4_sigs(self) -> None: (k, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key') @@ -30,8 +37,8 @@ def test_v4_skesk_argon2(self, cipher: str) -> None: def test_v6_skesk(self, aead: str) -> None: msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v6skesk-aes128-{aead}.pgp') assert msg.is_encrypted - if aead == 'eax': - pytest.xfail('AEAD Mode EAX not supported') + if aead == 'eax' and Cryptodome is None: + pytest.xfail('AEAD Mode EAX is not supported unless the Cryptodome module is available') unlocked = msg.decrypt('password') assert not unlocked.is_encrypted assert unlocked.message == b'Hello, world!' diff --git a/tox.ini b/tox.ini index e6387bb9..8da29019 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,8 @@ deps = pytest pytest-cov pytest-order +extras = + eax install_command = pip install {opts} --no-cache-dir {packages} commands =