| @@ -0,0 +1,7 @@ | |||||
| .coverage | |||||
| __pycache__ | |||||
| cryptography.egg-info | |||||
| pycaenv | |||||
| venv | |||||
| @@ -0,0 +1,16 @@ | |||||
| VIRTUALENV ?= python3 -m venv | |||||
| VRITUALENVARGS = | |||||
| MODULES=cryptography.tests | |||||
| test: venv pycaenv | |||||
| find cryptography -name '*.py' | entr make test-noentr | |||||
| test-noentr: | |||||
| ( . ./pycaenv/bin/activate && cd cryptography && python -m unittest tests) && ( . ./venv/bin/activate && pip install -e . && python -m coverage run -m unittest $(MODULES) && coverage report --omit=p/\* -m -i) | |||||
| venv: | |||||
| ($(VIRTUALENV) $(VIRTUALENVARGS) venv && . ./venv/bin/activate && pip install -r requirements.txt) | |||||
| pycaenv: | |||||
| ($(VIRTUALENV) $(VIRTUALENVARGS) pycaenv && . ./pycaenv/bin/activate && pip install cryptography) | |||||
| @@ -0,0 +1,33 @@ | |||||
| pycryptowrap | |||||
| ============ | |||||
| This is a translation layer to let software written for | |||||
| [cryptography](https://cryptography.io/en/latest/) to used with | |||||
| [pycryptodome](https://www.pycryptodome.org/). | |||||
| It currently only implements a minimal interface to get | |||||
| [pywebpush](https://github.com/web-push-libs/pywebpush) working. | |||||
| Currently implemented, tested, and working: | |||||
| * AES-GCM | |||||
| * ECC SECP256R1 - DSS and ECDH | |||||
| It shouldn't be too hard to add some additional ciphers or curves. | |||||
| Testing | |||||
| ======= | |||||
| The `Makefile` does the neccessary work to build an environment. To | |||||
| run the tests: | |||||
| ``` | |||||
| make test-noentr | |||||
| ``` | |||||
| This will first run the tests against the cryptography module, and then | |||||
| run the tests against this module. This makes sure that the tests are | |||||
| valid, and also makes it easier to add known answers to make sure | |||||
| you don't end up only self compatible. | |||||
| To make developing easier, the `test` target has one dependency, | |||||
| [entr](https://github.com/eradman/entr). This will watch for changes | |||||
| in the `py` files, and automatically rerun the test. | |||||
| @@ -0,0 +1,5 @@ | |||||
| class InvalidTag(Exception): | |||||
| pass | |||||
| class InvalidSignature(Exception): | |||||
| pass | |||||
| @@ -0,0 +1,2 @@ | |||||
| def default_backend(): | |||||
| pass | |||||
| @@ -0,0 +1,87 @@ | |||||
| from cryptography.exceptions import InvalidSignature | |||||
| from Crypto.PublicKey import ECC | |||||
| from Crypto.Protocol.DH import _compute_ecdh | |||||
| from Crypto.Signature import DSS | |||||
| # https://www.pycryptodome.org/src/public_key/ecc# | |||||
| SECP256R1 = lambda: 'secp256r1' | |||||
| class ECDSA: | |||||
| def __init__(self, algorithm): | |||||
| self._algo = algorithm | |||||
| class ECDH: | |||||
| pass | |||||
| class EllipticCurvePrivateNumbers: | |||||
| def __init__(self, key): | |||||
| self._key = key | |||||
| @property | |||||
| def public_numbers(self): | |||||
| return self._key._ecc.pointQ | |||||
| @property | |||||
| def private_value(self): | |||||
| return self._key._ecc.d | |||||
| class ECCWrapper: | |||||
| def __init__(self, ecckey): | |||||
| self._ecc = ecckey | |||||
| def private_numbers(self): | |||||
| return EllipticCurvePrivateNumbers(self) | |||||
| def public_key(self): | |||||
| return EllipticCurvePublicKey(self._ecc.public_key()) | |||||
| _format_to_compress = dict(nocompress=False, compress=True) | |||||
| def public_bytes(self, encoding, format): | |||||
| return self.public_key()._ecc.export_key(format=encoding, compress=self._format_to_compress[format]) | |||||
| def private_bytes(self, encoding, format, encryption_algorithm): | |||||
| return self._ecc.export_key(format=encoding, compress=format) | |||||
| # https://www.pycryptodome.org/src/protocol/dh | |||||
| def exchange(self, typ, pubkey): | |||||
| assert isinstance(typ, ECDH) | |||||
| return _compute_ecdh(self._ecc, pubkey._ecc) | |||||
| @classmethod | |||||
| def generate_private_key(cls, keytype, backend=None): | |||||
| if callable(keytype): | |||||
| keytype = keytype() | |||||
| return cls(ECC.generate(curve=keytype)) | |||||
| def sign(self, data, signature_algorithm): | |||||
| h = signature_algorithm._algo.new(data) | |||||
| signer = DSS.new(self._ecc, 'fips-186-3', 'der') | |||||
| return signer.sign(h) | |||||
| def verify(self, signature, data, signature_algorithm): | |||||
| h = signature_algorithm._algo.new(data) | |||||
| verifier = DSS.new(self._ecc, 'fips-186-3', 'der') | |||||
| try: | |||||
| verifier.verify(h, signature) | |||||
| except ValueError: | |||||
| raise InvalidSignature | |||||
| class EllipticCurvePublicKey(ECCWrapper): | |||||
| @classmethod | |||||
| def from_encoded_point(cls, curve, key): | |||||
| return cls(ECC.import_key(key, curve_name=curve)) | |||||
| generate_private_key = ECCWrapper.generate_private_key | |||||
| def load_der_private_key(key, password, backend=None): | |||||
| if password is not None: | |||||
| raise ValueError('unsupported') | |||||
| return ECCWrapper(ECC.import_key(key)) | |||||
| @@ -0,0 +1,6 @@ | |||||
| from Crypto.Util.asn1 import DerSequence | |||||
| def decode_dss_signature(signature): | |||||
| obj = DerSequence().decode(der_encoded=signature) | |||||
| return tuple(obj) | |||||
| @@ -0,0 +1,70 @@ | |||||
| from Crypto.Cipher import AES as ccAES | |||||
| from cryptography.exceptions import InvalidTag | |||||
| # https://www.pycryptodome.org/src/cipher/modern#gcm-mode | |||||
| class CipherEncryptor: | |||||
| def __init__(self, encor): | |||||
| self._encor = encor | |||||
| self.authenticate_additional_data = encor.update | |||||
| self.update = encor.encrypt | |||||
| @property | |||||
| def tag(self): | |||||
| return self._encor.digest() | |||||
| def finalize(self): | |||||
| return b'' | |||||
| class CipherDecryptor: | |||||
| def __init__(self, decor, tag=None): | |||||
| self._decor = decor | |||||
| self._tag = tag | |||||
| self.authenticate_additional_data = decor.update | |||||
| self.update = decor.decrypt | |||||
| def finalize(self): | |||||
| try: | |||||
| #print(repr(self._decor)) | |||||
| self._decor.verify(self._tag) | |||||
| except ValueError: | |||||
| raise InvalidTag('tag mismatch') | |||||
| return b'' | |||||
| class Cipher: | |||||
| def __init__(self, algo, mode, backend=None): | |||||
| self._algo = algo | |||||
| self._mode = mode | |||||
| def _getmode(self): | |||||
| if isinstance(self._mode, GCM): | |||||
| return ccAES.MODE_GCM | |||||
| def _nonce(self): | |||||
| return self._mode._iv | |||||
| def encryptor(self): | |||||
| return CipherEncryptor(ccAES.new(self._algo._key, | |||||
| self._getmode(), nonce=self._nonce())) | |||||
| def decryptor(self): | |||||
| return CipherDecryptor(ccAES.new(self._algo._key, | |||||
| self._getmode(), nonce=self._nonce()), tag=self._mode._tag) | |||||
| class AES: | |||||
| def __init__(self, key): | |||||
| self._key = key | |||||
| class algorithms: | |||||
| AES = AES | |||||
| class GCM: | |||||
| def __init__(self, iv, tag=None): | |||||
| self._iv = iv | |||||
| self._tag = tag | |||||
| class modes: | |||||
| GCM = GCM | |||||
| @@ -0,0 +1,4 @@ | |||||
| def SHA256(): | |||||
| from Crypto.Hash import SHA256 | |||||
| return SHA256 | |||||
| @@ -0,0 +1,15 @@ | |||||
| from Crypto.Protocol.KDF import HKDF as baseHKDF | |||||
| # https://www.pycryptodome.org/src/protocol/kdf#hkdf | |||||
| # https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#hkdf | |||||
| class HKDF: | |||||
| def __init__(self, algorithm, length, salt, info, backend=None): | |||||
| self._algo = algorithm | |||||
| self._len = length | |||||
| self._salt = salt | |||||
| self._info = info | |||||
| def derive(self, key): | |||||
| return baseHKDF(key, self._len, self._salt, self._algo, context=self._info) | |||||
| @@ -0,0 +1,15 @@ | |||||
| from .asymmetric.ec import load_der_private_key | |||||
| class Encoding: | |||||
| X962 = 'SEC1' | |||||
| Raw = 'raw' | |||||
| DER = 'DER' | |||||
| class PublicFormat: | |||||
| UncompressedPoint = 'nocompress' | |||||
| class PrivateFormat: | |||||
| PKCS8 = 'pkcs8' | |||||
| def NoEncryption(): | |||||
| return None | |||||
| @@ -0,0 +1,2 @@ | |||||
| from .ecc import TestECC | |||||
| from .aes import TestAES | |||||
| @@ -0,0 +1,61 @@ | |||||
| from cryptography.hazmat.backends import default_backend | |||||
| from cryptography.hazmat.primitives.ciphers import ( | |||||
| Cipher, algorithms, modes | |||||
| ) | |||||
| from cryptography.exceptions import InvalidTag | |||||
| import random | |||||
| import unittest | |||||
| TAG_LENGTH = 16 | |||||
| class TestAES(unittest.TestCase): | |||||
| def test_aesgcm_cav(self): | |||||
| pass | |||||
| def test_aesgcm(self): | |||||
| buf = bytes.fromhex('00000000000000000000000000000000') | |||||
| key = bytes.fromhex('00000000000000000000000000000000') | |||||
| iv = bytes.fromhex('000000000000000000000000') | |||||
| origdata = buf | |||||
| # encryption | |||||
| encryptor = Cipher(algorithms.AES(key), | |||||
| modes.GCM(iv), backend=default_backend() | |||||
| ).encryptor() | |||||
| last = True | |||||
| #if version == 'aes128gcm': | |||||
| data = encryptor.update(buf) | |||||
| self.assertEqual(data, bytes.fromhex('0388dace60b6a392f328c2b971b2fe78')) | |||||
| data += encryptor.finalize() | |||||
| self.assertEqual(encryptor.tag, bytes.fromhex('ab6e47d42cec13bdf53a67b21257bddf')) | |||||
| data += encryptor.tag | |||||
| encdata = data | |||||
| # decryption | |||||
| content = encdata | |||||
| decryptor = Cipher(algorithms.AES(key), | |||||
| modes.GCM(iv, tag=content[-TAG_LENGTH:]), | |||||
| backend=default_backend() | |||||
| ).decryptor() | |||||
| decdata = decryptor.update(content[:-TAG_LENGTH]) + decryptor.finalize() | |||||
| self.assertEqual(origdata, decdata) | |||||
| # decryption | |||||
| content = encdata | |||||
| decryptor = Cipher(algorithms.AES(key), | |||||
| modes.GCM(iv, tag=b'\x00' * TAG_LENGTH), | |||||
| backend=default_backend() | |||||
| ).decryptor() | |||||
| decdata = decryptor.update(content[:-TAG_LENGTH]) | |||||
| self.assertRaises(InvalidTag, decryptor.finalize) | |||||
| @@ -0,0 +1,128 @@ | |||||
| import unittest | |||||
| from cryptography.exceptions import InvalidSignature | |||||
| from cryptography.hazmat.backends import default_backend | |||||
| from cryptography.hazmat.primitives import serialization | |||||
| from cryptography.hazmat.primitives import hashes | |||||
| from cryptography.hazmat.primitives.asymmetric import ec | |||||
| from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature | |||||
| from cryptography.hazmat.primitives.kdf.hkdf import HKDF | |||||
| class TestECC(unittest.TestCase): | |||||
| def test_dh(self): | |||||
| # slightly modified from: | |||||
| # https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#elliptic-curve-key-exchange-algorithm | |||||
| # private keys for use in the exchange. | |||||
| keya = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \x04\xa3X\xbd\x0e\xff*\x8cw\xf8\x9f\x05BD<\nY\xb3\xf1\xd2\xc1\xb0\r\x1e\xedu\x92]4M?\x01\xa1D\x03B\x00\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4' | |||||
| keyb = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \xfb\xf8\xf7\x9f\xa3\xb7\xed\x8cT@`\xf6\x9c\xbbv\x0e?\x87\xb1(\xf6\xa8\xb3`\x91\xb4\x92W\xc6\xaa\xf5~\xa1D\x03B\x00\x04\xb8\xfe\xe4\x8dkukc\xa4^\x87\x98\x9c\xb9\xa8\xec\x86\xf8\xc2\x89\xaeF\xe8q\xb9q\x92I\x98n\xfe\xe3<{\x1c&R\x82\xb1\x94=\xa5h*)m/\x13\xfb\x05\x1d\x98u\xec\x1ew\xdfW\x84\xfe\x9eSl\x83' | |||||
| #server_private_key = ec.generate_private_key(ec.SECP256R1()) | |||||
| #print('spk:', repr(server_private_key.private_bytes( | |||||
| # encoding=serialization.Encoding.DER, | |||||
| # format=serialization.PublicFormat.UncompressedPoint, | |||||
| # algorithm=None))) | |||||
| server_private_key = serialization.load_der_private_key(keya, | |||||
| None, backend=default_backend()) | |||||
| pubpoint = server_private_key.private_numbers().public_numbers | |||||
| self.assertEqual(server_private_key.private_numbers(). \ | |||||
| private_value, | |||||
| 2097859916579721232322403601989230314767884081400167668022347085958538411777) | |||||
| self.assertEqual(pubpoint.x, | |||||
| 36569272757924220784927781299997671003354453138026426189464987345196826688394) | |||||
| self.assertEqual(pubpoint.y, | |||||
| 95759458837377270694950453035845273914475242769728982836658929275588228093908) | |||||
| self.assertEqual(keya, server_private_key.private_bytes( | |||||
| encoding=serialization.Encoding.DER, | |||||
| format=serialization.PrivateFormat.PKCS8, | |||||
| encryption_algorithm=serialization.NoEncryption())) | |||||
| # In a real handshake the peer is a remote client. For this | |||||
| # example we'll generate another local private key though. | |||||
| peer_private_key = serialization.load_der_private_key(keyb, | |||||
| None) | |||||
| shared_key = server_private_key.exchange( | |||||
| ec.ECDH(), peer_private_key.public_key()) | |||||
| self.assertEqual(shared_key, | |||||
| b'\x1a\x9f\x93c\xb0s\xa2\x15]{\xa3\xcc\xcf&Q\xd6g\x83\x86%\x7f\t\xfem@\xcb\xe9:U\x16\x07\x02') | |||||
| # Perform key derivation. | |||||
| derived_key = HKDF(algorithm=hashes.SHA256(), | |||||
| length=32, salt=None, info=b'handshake data', backend=None | |||||
| ).derive(shared_key) | |||||
| # And now we can demonstrate that the handshake performed in | |||||
| # the opposite direction gives the same final value | |||||
| same_shared_key = peer_private_key.exchange( | |||||
| ec.ECDH(), server_private_key.public_key()) | |||||
| self.assertEqual(shared_key, same_shared_key) | |||||
| # Perform key derivation. | |||||
| same_derived_key = HKDF(algorithm=hashes.SHA256(), | |||||
| length=32, salt=None, info=b'handshake data', | |||||
| ).derive(same_shared_key) | |||||
| self.assertEqual(derived_key, | |||||
| b'\x89\r\xf7\xf0\xa6\xb9Z\xb9\xd7\xd0\x9b\x95y\xe0M\x11,\xb4\xe1Z\xe5\xa2j\xee)\xa0I\xb5Q\x18\x94\xd1') | |||||
| self.assertEqual(derived_key, same_derived_key) | |||||
| def test_decode_sig(self): | |||||
| sig = b"0D\x02 P\x92\xaf\xffoN\xadq\r=\x92\xb5\r\xe0l3\xf2\x80*\xdd|\xfe\xd8'\xb8\\\xe8\x94\xd6\xa1\xdb\xea\x02 \x18\x89j\xa8P\x83jk*\xb8\xa2\x15r&d\xa1\x9e\xf6\xec\xd2\xf4 \xd6\x08\x91bs\x18\xb5\x11/\x04" | |||||
| res = (36444202250238074078057463719437572015031876874679459625290239663827367091178, 11098302536735876471048588108325227764001515075886106999029234198831298588420) | |||||
| self.assertEqual(decode_dss_signature(sig), res) | |||||
| def test_sign(self): | |||||
| private_key = ec.generate_private_key(ec.SECP256R1()) | |||||
| data = b"this is some data I'd like to sign" | |||||
| signature = private_key.sign(data, ec.ECDSA(hashes.SHA256())) | |||||
| # make sure we can decode our own signatures | |||||
| decode_dss_signature(signature) | |||||
| public_key = private_key.public_key() | |||||
| public_key.verify(signature, data, ec.ECDSA(hashes.SHA256())) | |||||
| wrongsig = bytearray(signature) | |||||
| wrongsig[0] ^= 1 | |||||
| wrongsig[1] ^= 4 | |||||
| wrongsig = bytes(wrongsig) | |||||
| self.assertRaises(InvalidSignature, public_key.verify, wrongsig, data, ec.ECDSA(hashes.SHA256())) | |||||
| def test_misc(self): | |||||
| keya = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \x04\xa3X\xbd\x0e\xff*\x8cw\xf8\x9f\x05BD<\nY\xb3\xf1\xd2\xc1\xb0\r\x1e\xedu\x92]4M?\x01\xa1D\x03B\x00\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4' | |||||
| skey = serialization.load_der_private_key(keya, | |||||
| None, backend=default_backend()) | |||||
| ckey = skey.public_key().public_bytes( | |||||
| encoding=serialization.Encoding.X962, | |||||
| format=serialization.PublicFormat.UncompressedPoint | |||||
| ) | |||||
| self.assertEqual(ckey, b'\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4') | |||||
| pubkey = ec.EllipticCurvePublicKey.from_encoded_point( | |||||
| ec.SECP256R1(), ckey) | |||||
| self.assertTrue(isinstance(pubkey, ec.EllipticCurvePublicKey)) | |||||
| self.assertEqual(ckey, pubkey.public_bytes( | |||||
| encoding=serialization.Encoding.X962, | |||||
| format=serialization.PublicFormat.UncompressedPoint | |||||
| )) | |||||
| @@ -0,0 +1,3 @@ | |||||
| -e . | |||||
| -e .[dev] | |||||
| @@ -0,0 +1,15 @@ | |||||
| from setuptools import setup, Extension | |||||
| setup(name = "cryptography", version = "41.0.5", | |||||
| description = "Emulate cryptography enough for some needs", | |||||
| author = "John-Mark Gurney", | |||||
| author_email = "jmg@funkthat.com", | |||||
| url = 'about:blank', | |||||
| packages = [ 'cryptography' ], | |||||
| extras_require = { | |||||
| 'dev': [ 'coverage' ], | |||||
| }, | |||||
| install_requires=[ | |||||
| 'pycryptodome', | |||||
| ], | |||||
| ) | |||||