diff --git a/README.rst b/README.rst
index b1eda839..6d2c4a8c 100644
--- a/README.rst
+++ b/README.rst
@@ -67,6 +67,10 @@ To use `sopgpy` you'll also need:
- `sop `_ >= 0.5.1
+To use EAX as an AEAD mode, you'll also need:
+
+- `Cryptodome `_
+
License
-------
diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py
index 7407af0e..73638a7e 100755
--- a/pgpy/sopgpy.py
+++ b/pgpy/sopgpy.py
@@ -38,6 +38,7 @@
import logging
import packaging.version
from importlib import metadata
+from types import ModuleType
from datetime import datetime, timezone
from typing import List, Literal, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable
@@ -48,14 +49,24 @@
import sop
import pgpy
+Maybe_Cryptodome: Optional[ModuleType]
+try:
+ import Cryptodome as Maybe_Cryptodome
+except ModuleNotFoundError:
+ Maybe_Cryptodome = None
+
class SOPGPy(sop.StatelessOpenPGP):
def __init__(self) -> None:
self.pgpy_version = packaging.version.Version(metadata.version('pgpy'))
self.cryptography_version = packaging.version.Version(metadata.version('cryptography'))
+ self.cryptodome_version = ''
+ if Maybe_Cryptodome is not None:
+ cdv: Tuple[int, int, str] = Maybe_Cryptodome.version_info
+ self.cryptodome_version = f'\nCryptodome (for EAX): {cdv[0]}.{cdv[1]}.{cdv[2]}'
super().__init__(name='sopgpy', version=f'{self.pgpy_version}',
backend=f'PGPy {self.pgpy_version}',
- extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}',
+ extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}{self.cryptodome_version}',
description=f'Stateless OpenPGP using PGPy {self.pgpy_version}')
@property
diff --git a/pgpy/symenc.py b/pgpy/symenc.py
index 281596ed..e8d011fc 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 1218b0c1..f135d792 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
argon2_cffi
cryptography>=3.3.2
sop>=0.5.1[sopgpy]
+pycryptodomex[eax]
diff --git a/setup.cfg b/setup.cfg
index 4b7f444a..a38c02cd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -53,6 +53,7 @@ python_requires = >=3.6
[options.extras_require]
sopgpy = sop>=0.5.1
+eax = pycryptodomex
[build_sphinx]
source-dir = docs/source
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 =