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 24, 2023
1 parent 50efe13 commit c030e7a
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 5 deletions.
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ To use `sopgpy` you'll also need:

- `sop <https://pypi.org/project/sop/>`_ >= 0.5.1

To use EAX as an AEAD mode, you'll also need:

- `Cryptodome <https://pypi.org/project/pycryptodomex/>`_

License
-------

Expand Down
13 changes: 12 additions & 1 deletion pgpy/sopgpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
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,3 +1,4 @@
argon2_cffi
cryptography>=3.3.2
sop>=0.5.1[sopgpy]
pycryptodomex[eax]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ python_requires = >=3.6

[options.extras_require]
sopgpy = sop>=0.5.1
eax = pycryptodomex

[build_sphinx]
source-dir = docs/source
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 c030e7a

Please sign in to comment.