Skip to content

Commit

Permalink
AEAD: handle EAX, using Cryptodome if it is available
Browse files Browse the repository at this point in the history
  • Loading branch information
dkg committed Aug 18, 2023
1 parent a445c16 commit 96f82ab
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 4 deletions.
44 changes: 42 additions & 2 deletions pgpy/symenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""

from typing import Optional, Union
from types import ModuleType

from cryptography.exceptions import UnsupportedAlgorithm

Expand All @@ -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']
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
argon2_cffi
cryptography>=3.3.2
pycryptodomex[eax]
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions tests/test_12_crypto_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

from typing import Dict, Optional, Tuple
from types import ModuleType

import pytest

Expand All @@ -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')
Expand All @@ -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!'
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ deps =
pytest
pytest-cov
pytest-order
extras =
eax

install_command = pip install {opts} --no-cache-dir {packages}
commands =
Expand Down

0 comments on commit 96f82ab

Please sign in to comment.