From 1e97961843811a2417755d007c297e3540dc91f8 Mon Sep 17 00:00:00 2001 From: Weiliang Li Date: Mon, 3 Mar 2025 15:08:34 +0900 Subject: [PATCH] Refactor utils --- .cspell.jsonc | 8 +++- .github/dependabot.yml | 10 ++--- CHANGELOG.md | 8 ++++ DETAILS.md | 2 +- LICENSE | 2 +- README.md | 44 ++++++++++--------- ecies/__init__.py | 30 ++++++++----- ecies/__main__.py | 8 ++-- ecies/utils/__init__.py | 28 ++++++------ ecies/utils/elliptic.py | 47 +++++++++----------- ecies/utils/eth.py | 54 +++++++++++++++++++++++ ecies/utils/hash.py | 35 +++++++++++++++ ecies/utils/hex.py | 21 --------- ecies/utils/symmetric.py | 59 ++++++++++++++------------ scripts/ci.sh | 2 +- tests/conftest.py | 11 +++++ tests/test_crypt.py | 35 ++++++--------- tests/test_crypt_eth.py | 20 +++++---- tests/test_utils.py | 80 ----------------------------------- tests/utils/__init__.py | 0 tests/utils/test_elliptic.py | 17 ++++++++ tests/utils/test_eth.py | 7 +++ tests/utils/test_hash.py | 10 +++++ tests/utils/test_symmetric.py | 41 ++++++++++++++++++ 24 files changed, 336 insertions(+), 243 deletions(-) create mode 100644 ecies/utils/eth.py create mode 100644 ecies/utils/hash.py create mode 100644 tests/conftest.py delete mode 100644 tests/test_utils.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_elliptic.py create mode 100644 tests/utils/test_eth.py create mode 100644 tests/utils/test_hash.py create mode 100644 tests/utils/test_symmetric.py diff --git a/.cspell.jsonc b/.cspell.jsonc index 14b4c06..103d3fa 100644 --- a/.cspell.jsonc +++ b/.cspell.jsonc @@ -1,6 +1,7 @@ { "words": [ "bitcointalk", + "chacha", "Cipolla", "Codacy", "Codecov", @@ -14,12 +15,17 @@ "helloworld", "hexdigest", "hkdf", + "keccak", "pycryptodome", + "pytest", "readablize", "secp", "urandom", "xcfl", "xchacha" ], - "ignorePaths": [".cspell.jsonc", "LICENSE"] + "ignorePaths": [ + ".cspell.jsonc", + "LICENSE" + ] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c990752..e8f33e8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 + - package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9117989..22fb283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release Notes +## 0.4.4 + +- Make `eth-keys` optional +- Drop Python 3.8 +- Refactor `utils` +- Revamp documentation +- Bump dependencies + ## 0.4.1 ~ 0.4.3 - Bump dependencies diff --git a/DETAILS.md b/DETAILS.md index 9e810f1..11bdba6 100644 --- a/DETAILS.md +++ b/DETAILS.md @@ -136,6 +136,6 @@ Now we have the shared key, and we can use the `nonce` and `tag` to decrypt. Thi b'helloworld' ``` -> Strictly speaking, `nonce` != `iv`, but this is a little bit off topic, if you are curious, you can check [the comment in `utils/symmetric.py`](./ecies/utils/symmetric.py#L79). +> Strictly speaking, `nonce` != `iv`, but this is a little bit off topic, if you are curious, you can check [the comment in `utils/symmetric.py`](./ecies/utils/symmetric.py#L86). > > Warning: it's dangerous to reuse nonce, if you don't know what you are doing, just follow the default setting. diff --git a/LICENSE b/LICENSE index a835df5..b035f72 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2024 Weiliang Li +Copyright (c) 2018-2025 Weiliang Li Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f48831b..e3765b9 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,66 @@ # eciespy [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a11aeb9939244019d2c64bce3ff3c4e)](https://app.codacy.com/gh/ecies/py/dashboard) +[![License](https://img.shields.io/github/license/ecies/py.svg)](https://github.com/ecies/py) +[![PyPI](https://img.shields.io/pypi/v/eciespy.svg)](https://pypi.org/project/eciespy/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/eciespy)](https://pypistats.org/packages/eciespy) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/eciespy.svg)](https://pypi.org/project/eciespy/) [![CI](https://img.shields.io/github/actions/workflow/status/ecies/py/ci.yml?branch=master)](https://github.com/ecies/py/actions) [![Codecov](https://img.shields.io/codecov/c/github/ecies/py.svg)](https://codecov.io/gh/ecies/py) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/eciespy.svg)](https://pypi.org/project/eciespy/) -[![PyPI](https://img.shields.io/pypi/v/eciespy.svg)](https://pypi.org/project/eciespy/) -[![License](https://img.shields.io/github/license/ecies/py.svg)](https://github.com/ecies/py) Elliptic Curve Integrated Encryption Scheme for secp256k1 in Python. Other language versions: -- [Rust](https://github.com/ecies/rs) - [TypeScript](https://github.com/ecies/js) +- [Rust](https://github.com/ecies/rs) - [Golang](https://github.com/ecies/go) - [WASM](https://github.com/ecies/rs-wasm) +- [Java](https://github.com/ecies/java) +- [Dart](https://github.com/ecies/dart) -You can also check a FastAPI web backend demo [here](https://github.com/ecies/py-demo). +You can also check a web backend demo [here](https://github.com/ecies/py-demo). ## Install `pip install eciespy` +Or `pip install 'eciespy[eth]'` to install `eth-keys` as well. + ## Quick Start ```python ->>> from ecies.utils import generate_eth_key, generate_key +>>> from ecies.utils import generate_key >>> from ecies import encrypt, decrypt ->>> eth_k = generate_eth_key() ->>> sk_hex = eth_k.to_hex() # hex string ->>> pk_hex = eth_k.public_key.to_hex() # hex string ->>> data = b'this is a test' ->>> decrypt(sk_hex, encrypt(pk_hex, data)) -b'this is a test' ->>> secp_k = generate_key() ->>> sk_bytes = secp_k.secret # bytes ->>> pk_bytes = secp_k.public_key.format(True) # bytes ->>> decrypt(sk_bytes, encrypt(pk_bytes, data)) -b'this is a test' +>>> data = 'hello world🌍'.encode() +>>> sk = generate_key() +>>> sk_bytes = sk.secret # bytes +>>> pk_bytes = sk.public_key.format(True) # bytes +>>> decrypt(sk_bytes, encrypt(pk_bytes, data)).decode() +'hello world🌍' ``` Or just use a builtin command `eciespy` in your favorite [command line](#command-line-interface). ## API -### `ecies.encrypt(receiver_pk: Union[str, bytes], msg: bytes) -> bytes` +### `ecies.encrypt(receiver_pk: Union[str, bytes], data: bytes, config: Config = ECIES_CONFIG) -> bytes` Parameters: - **receiver_pk** - Receiver's public key (hex str or bytes) -- **msg** - Data to encrypt +- **data** - Data to encrypt +- **config** - Optional configuration object Returns: **bytes** -### `ecies.decrypt(receiver_sk: Union[str, bytes], msg: bytes) -> bytes` +### `ecies.decrypt(receiver_sk: Union[str, bytes], data: bytes, config: Config = ECIES_CONFIG) -> bytes` Parameters: - **receiver_sk** - Receiver's private key (hex str or bytes) -- **msg** - Data to decrypt +- **data** - Data to decrypt +- **config** - Optional configuration object Returns: **bytes** diff --git a/ecies/__init__.py b/ecies/__init__.py index 6b90e68..252933c 100644 --- a/ecies/__init__.py +++ b/ecies/__init__.py @@ -4,7 +4,7 @@ from .config import ECIES_CONFIG, Config from .utils import ( - compat_eth_public_key, + bytes2pk, decapsulate, encapsulate, generate_key, @@ -18,7 +18,7 @@ def encrypt( - receiver_pk: Union[str, bytes], msg: bytes, config: Config = ECIES_CONFIG + receiver_pk: Union[str, bytes], data: bytes, config: Config = ECIES_CONFIG ) -> bytes: """ Encrypt with receiver's secp256k1 public key @@ -27,8 +27,10 @@ def encrypt( ---------- receiver_pk: Union[str, bytes] Receiver's public key (hex str or bytes) - msg: bytes + data: bytes Data to encrypt + config: Config + Optional configuration object Returns ------- @@ -38,20 +40,22 @@ def encrypt( if isinstance(receiver_pk, str): pk = hex2pk(receiver_pk) elif isinstance(receiver_pk, bytes): - pk = PublicKey(compat_eth_public_key(receiver_pk)) + pk = bytes2pk(receiver_pk) else: raise TypeError("Invalid public key type") ephemeral_sk = generate_key() ephemeral_pk = ephemeral_sk.public_key.format(config.is_ephemeral_key_compressed) - sym_key = encapsulate(ephemeral_sk, pk, config) - encrypted = sym_encrypt(sym_key, msg, config) + sym_key = encapsulate(ephemeral_sk, pk, config.is_hkdf_key_compressed) + encrypted = sym_encrypt( + sym_key, data, config.symmetric_algorithm, config.symmetric_nonce_length + ) return ephemeral_pk + encrypted def decrypt( - receiver_sk: Union[str, bytes], msg: bytes, config: Config = ECIES_CONFIG + receiver_sk: Union[str, bytes], data: bytes, config: Config = ECIES_CONFIG ) -> bytes: """ Decrypt with receiver's secp256k1 private key @@ -60,8 +64,10 @@ def decrypt( ---------- receiver_sk: Union[str, bytes] Receiver's private key (hex str or bytes) - msg: bytes + data: bytes Data to decrypt + config: Config + Optional configuration object Returns ------- @@ -76,7 +82,9 @@ def decrypt( raise TypeError("Invalid secret key type") key_size = config.ephemeral_key_size - ephemeral_pk, encrypted = PublicKey(msg[0:key_size]), msg[key_size:] + ephemeral_pk, encrypted = PublicKey(data[0:key_size]), data[key_size:] - sym_key = decapsulate(ephemeral_pk, sk, config) - return sym_decrypt(sym_key, encrypted, config) + sym_key = decapsulate(ephemeral_pk, sk, config.is_hkdf_key_compressed) + return sym_decrypt( + sym_key, encrypted, config.symmetric_algorithm, config.symmetric_nonce_length + ) diff --git a/ecies/__main__.py b/ecies/__main__.py index a6376ba..e659e3d 100644 --- a/ecies/__main__.py +++ b/ecies/__main__.py @@ -14,7 +14,7 @@ import sys from ecies import decrypt, encrypt -from ecies.utils import generate_eth_key +from ecies.utils import generate_key, to_eth_address, to_eth_public_key __description__ = "Elliptic Curve Integrated Encryption Scheme for secp256k1 in Python" @@ -68,11 +68,11 @@ def main(): args = parser.parse_args() if args.generate: - k = generate_eth_key() + k = generate_key() sk, pk, addr = ( k.to_hex(), - k.public_key.to_hex(), - k.public_key.to_checksum_address(), + f"0x{to_eth_public_key(k.public_key).hex()}", + to_eth_address(k.public_key), ) print("Private: {}\nPublic: {}\nAddress: {}".format(sk, pk, addr)) return diff --git a/ecies/utils/__init__.py b/ecies/utils/__init__.py index d06448b..4b3cae7 100644 --- a/ecies/utils/__init__.py +++ b/ecies/utils/__init__.py @@ -1,25 +1,25 @@ -from .elliptic import ( - compat_eth_public_key, - decapsulate, - encapsulate, - generate_eth_key, - generate_key, - hex2pk, - hex2sk, -) -from .hex import decode_hex, sha256 +from .elliptic import bytes2pk, decapsulate, encapsulate, generate_key, hex2pk, hex2sk +from .eth import generate_eth_key, to_eth_address, to_eth_public_key +from .hash import derive_key, sha256 +from .hex import decode_hex from .symmetric import sym_decrypt, sym_encrypt __all__ = [ - "sha256", - "decode_hex", "sym_encrypt", "sym_decrypt", "generate_key", - "generate_eth_key", "hex2sk", "hex2pk", + "bytes2pk", "decapsulate", "encapsulate", - "compat_eth_public_key", + # eth + "generate_eth_key", + "to_eth_address", + "to_eth_public_key", + # hex + "decode_hex", + # hash + "sha256", + "derive_key", ] diff --git a/ecies/utils/elliptic.py b/ecies/utils/elliptic.py index 7108e7c..9338614 100644 --- a/ecies/utils/elliptic.py +++ b/ecies/utils/elliptic.py @@ -1,9 +1,9 @@ from coincurve import PrivateKey, PublicKey from coincurve.utils import get_valid_secret -from ..config import ECIES_CONFIG, Config +from .eth import convert_eth_public_key +from .hash import derive_key from .hex import decode_hex -from .symmetric import derive_key def generate_key() -> PrivateKey: @@ -20,31 +20,34 @@ def generate_key() -> PrivateKey: return PrivateKey(get_valid_secret()) -def generate_eth_key(): +def hex2pk(pk_hex: str) -> PublicKey: """ - Generate a random `eth_keys.keys.PrivateKey` + Convert public key hex to `coincurve.PublicKey` + The hex should be 65 bytes (uncompressed) or 33 bytes (compressed), but ethereum public key has 64 bytes. + `0x04` will be appended if it's an ethereum public key. + + Parameters + ---------- + pk_hex: str + Public key hex string Returns ------- - eth_keys.keys.PrivateKey - An ethereum key + coincurve.PublicKey + A secp256k1 public key """ - from eth_keys import keys - - return keys.PrivateKey(get_valid_secret()) + return PublicKey(convert_eth_public_key(decode_hex(pk_hex))) -def hex2pk(pk_hex: str) -> PublicKey: +def bytes2pk(pk_bytes: bytes) -> PublicKey: """ - Convert ethereum hex to `coincurve.PublicKey` - The hex should be 65 bytes (uncompressed) or 33 bytes (compressed), but ethereum public key has 64 bytes. - `0x04` will be appended if it's an ethereum public key. + Convert public key bytes to `coincurve.PublicKey` Parameters ---------- - pk_hex: str - Public key hex string + pk_bytes: bytes + Public key bytes Returns ------- @@ -52,13 +55,7 @@ def hex2pk(pk_hex: str) -> PublicKey: A secp256k1 public key """ - return PublicKey(compat_eth_public_key(decode_hex(pk_hex))) - - -def compat_eth_public_key(data: bytes): - if len(data) == 64: # eth public key format - data = b"\x04" + data - return data + return PublicKey(convert_eth_public_key(pk_bytes)) def hex2sk(sk_hex: str) -> PrivateKey: @@ -81,9 +78,8 @@ def hex2sk(sk_hex: str) -> PrivateKey: # private below def encapsulate( - private_key: PrivateKey, peer_public_key: PublicKey, config: Config = ECIES_CONFIG + private_key: PrivateKey, peer_public_key: PublicKey, is_compressed: bool = False ) -> bytes: - is_compressed = config.is_hkdf_key_compressed shared_point = peer_public_key.multiply(private_key.secret) master = private_key.public_key.format(is_compressed) + shared_point.format( is_compressed @@ -92,9 +88,8 @@ def encapsulate( def decapsulate( - public_key: PublicKey, peer_private_key: PrivateKey, config: Config = ECIES_CONFIG + public_key: PublicKey, peer_private_key: PrivateKey, is_compressed: bool = False ) -> bytes: - is_compressed = config.is_hkdf_key_compressed shared_point = public_key.multiply(peer_private_key.secret) master = public_key.format(is_compressed) + shared_point.format(is_compressed) return derive_key(master) diff --git a/ecies/utils/eth.py b/ecies/utils/eth.py new file mode 100644 index 0000000..d485b2e --- /dev/null +++ b/ecies/utils/eth.py @@ -0,0 +1,54 @@ +from coincurve import PublicKey +from coincurve.utils import get_valid_secret + +from .hash import keccak_256 + +ETH_PUBLIC_KEY_LENGTH = 64 + + +def generate_eth_key(): + """ + Note: `eth-keys` needs to be installed in advance. + + Generate a random `eth_keys.keys.PrivateKey` + + Returns + ------- + eth_keys.keys.PrivateKey + An ethereum flavored secp256k1 key + + """ + from eth_keys import keys + + return keys.PrivateKey(get_valid_secret()) + + +def to_eth_public_key(pk: PublicKey) -> bytes: + return pk.format(False)[1:] + + +def to_eth_address(pk: PublicKey) -> str: + pk_bytes = to_eth_public_key(pk) + return encode_checksum(keccak_256(pk_bytes)[-20:].hex()) + + +# private below +def convert_eth_public_key(data: bytes): + if len(data) == ETH_PUBLIC_KEY_LENGTH: + data = b"\x04" + data + return data + + +def encode_checksum(raw_address: str) -> str: + # https://github.com/ethereum/ercs/blob/master/ERCS/erc-55.md + address = raw_address.lower().replace("0x", "") + address_hash = keccak_256(address.encode()).hex() + + res = [] + for a, h in zip(address, address_hash): + if int(h, 16) >= 8: + res.append(a.upper()) + else: + res.append(a) + + return "0x" + "".join(res) diff --git a/ecies/utils/hash.py b/ecies/utils/hash.py new file mode 100644 index 0000000..0682a4d --- /dev/null +++ b/ecies/utils/hash.py @@ -0,0 +1,35 @@ +import hashlib + +from Crypto.Hash import SHA256, keccak +from Crypto.Protocol.KDF import HKDF + + +def sha256(data: bytes) -> bytes: + """ + Calculate sha256 hash. + + Parameters + ---------- + data: bytes + data to hash + + Returns + ------- + bytes + sha256 hash in bytes + + >>> sha256(b'0'*16).hex()[:8] == 'fcdb4b42' + True + """ + return hashlib.sha256(data).digest() + + +def derive_key(master: bytes, salt: bytes = b"") -> bytes: + derived = HKDF(master, 32, salt, SHA256, num_keys=1) + return derived # type: ignore + + +# private below +def keccak_256(b: bytes) -> bytes: + h = keccak.new(data=b, digest_bits=256) + return h.digest() diff --git a/ecies/utils/hex.py b/ecies/utils/hex.py index 35ca2a3..8adcaa1 100644 --- a/ecies/utils/hex.py +++ b/ecies/utils/hex.py @@ -1,25 +1,4 @@ import codecs -import hashlib - - -def sha256(msg: bytes) -> bytes: - """ - Calculate sha256 hash. - - Parameters - ---------- - msg: bytes - message to hash - - Returns - ------- - bytes - sha256 hash in bytes - - >>> sha256(b'0'*16).hex()[:8] == 'fcdb4b42' - True - """ - return hashlib.sha256(msg).digest() def decode_hex(s: str) -> bytes: diff --git a/ecies/utils/symmetric.py b/ecies/utils/symmetric.py index 04837c0..9e00e2c 100644 --- a/ecies/utils/symmetric.py +++ b/ecies/utils/symmetric.py @@ -1,17 +1,20 @@ import os from Crypto.Cipher import AES, ChaCha20_Poly1305 -from Crypto.Hash import SHA256 -from Crypto.Protocol.KDF import HKDF -from ..config import ECIES_CONFIG, Config +from ..config import NonceLength, SymmetricAlgorithm AES_CIPHER_MODE = AES.MODE_GCM AEAD_TAG_LENGTH = 16 XCHACHA20_NONCE_LENGTH = 24 -def sym_encrypt(key: bytes, plain_text: bytes, config: Config = ECIES_CONFIG) -> bytes: +def sym_encrypt( + key: bytes, + plain_text: bytes, + algorithm: SymmetricAlgorithm = "aes-256-gcm", + aes_nonce_length: NonceLength = 16, +) -> bytes: """ Symmetric encryption. AES-256-GCM or XChaCha20-Poly1305. @@ -29,18 +32,17 @@ def sym_encrypt(key: bytes, plain_text: bytes, config: Config = ECIES_CONFIG) -> bytes nonce + tag(16 bytes) + encrypted data """ - algorithm = config.symmetric_algorithm if algorithm == "aes-256-gcm": - nonce_length = config.symmetric_nonce_length - nonce = os.urandom(nonce_length) - cipher = AES.new(key, AES_CIPHER_MODE, nonce) + nonce = os.urandom(aes_nonce_length) + aes_cipher = AES.new(key, AES_CIPHER_MODE, nonce) + encrypted, tag = aes_cipher.encrypt_and_digest(plain_text) elif algorithm == "xchacha20": nonce = os.urandom(XCHACHA20_NONCE_LENGTH) - cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) # type:ignore + chacha_cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + encrypted, tag = chacha_cipher.encrypt_and_digest(plain_text) else: raise NotImplementedError - encrypted, tag = cipher.encrypt_and_digest(plain_text) cipher_text = bytearray() cipher_text.extend(nonce) cipher_text.extend(tag) @@ -48,7 +50,12 @@ def sym_encrypt(key: bytes, plain_text: bytes, config: Config = ECIES_CONFIG) -> return bytes(cipher_text) -def sym_decrypt(key: bytes, cipher_text: bytes, config: Config = ECIES_CONFIG) -> bytes: +def sym_decrypt( + key: bytes, + cipher_text: bytes, + algorithm: SymmetricAlgorithm = "aes-256-gcm", + nonce_length: NonceLength = 16, +) -> bytes: """ AES-GCM decryption. AES-256-GCM or XChaCha20-Poly1305. @@ -84,25 +91,23 @@ def sym_decrypt(key: bytes, cipher_text: bytes, config: Config = ECIES_CONFIG) - # If it's 12 bytes, the nonce can be incremented by 1 for each encryption # If it's 16 bytes, the nonce will be used to hash, so it's meaningless to increment - algorithm = config.symmetric_algorithm if algorithm == "aes-256-gcm": - nonce_length = config.symmetric_nonce_length - nonce_tag_length = nonce_length + AEAD_TAG_LENGTH - nonce = cipher_text[:nonce_length] - tag = cipher_text[nonce_length:nonce_tag_length] - ciphered_data = cipher_text[nonce_tag_length:] - cipher = AES.new(key, AES_CIPHER_MODE, nonce) + nonce, tag, ciphered_data = _split_cipher_text(cipher_text, nonce_length) + aes_cipher = AES.new(key, AES_CIPHER_MODE, nonce) + return aes_cipher.decrypt_and_verify(ciphered_data, tag) elif algorithm == "xchacha20": - nonce_tag_length = XCHACHA20_NONCE_LENGTH + AEAD_TAG_LENGTH - nonce = cipher_text[:XCHACHA20_NONCE_LENGTH] - tag = cipher_text[XCHACHA20_NONCE_LENGTH:nonce_tag_length] - ciphered_data = cipher_text[nonce_tag_length:] - cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) # type:ignore + nonce, tag, ciphered_data = _split_cipher_text( + cipher_text, XCHACHA20_NONCE_LENGTH + ) + xchacha_cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + return xchacha_cipher.decrypt_and_verify(ciphered_data, tag) else: raise NotImplementedError - return cipher.decrypt_and_verify(ciphered_data, tag) -def derive_key(master: bytes) -> bytes: - derived = HKDF(master, 32, b"", SHA256) - return derived # type: ignore +def _split_cipher_text(cipher_text: bytes, nonce_length: int): + nonce_tag_length = nonce_length + AEAD_TAG_LENGTH + nonce = cipher_text[:nonce_length] + tag = cipher_text[nonce_length:nonce_tag_length] + ciphered_data = cipher_text[nonce_tag_length:] + return nonce, tag, ciphered_data diff --git a/scripts/ci.sh b/scripts/ci.sh index 20d6c53..0202700 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -5,6 +5,6 @@ poetry run eciespy -h poetry run eciespy -g echo '0x95d3c5e483e9b1d4f5fc8e79b2deaf51362980de62dbb082a9a4257eef653d7d' > sk echo '0x98afe4f150642cd05cc9d2fa36458ce0a58567daeaf5fde7333ba9b403011140a4e28911fcf83ab1f457a30b4959efc4b9306f514a4c3711a16a80e3b47eb58b' > pk -echo 'hello ecies' | poetry run eciespy -e -k pk -O out +echo 'hello world 🌍' | poetry run eciespy -e -k pk -O out poetry run eciespy -d -k sk -D out rm sk pk out diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..adc3eb4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture(scope="session") +def data(): + return "hello world🌍".encode() + + +@pytest.fixture(scope="session") +def big_data(): + return b"1" * 1024 * 1024 * 100 # 100 MB diff --git a/tests/test_crypt.py b/tests/test_crypt.py index f26b106..26ed33c 100644 --- a/tests/test_crypt.py +++ b/tests/test_crypt.py @@ -4,19 +4,12 @@ from ecies import ECIES_CONFIG, decrypt, encrypt from ecies.utils import decode_hex, generate_key -data = b"this is a test" - -def __check_bytes(k, compressed=False): +def __check(data: bytes, k: PrivateKey, compressed: bool = False): sk_hex = k.to_hex() pk_hex = k.public_key.format(compressed).hex() assert data == decrypt(sk_hex, encrypt(pk_hex, data)) - - -def __check_hex(sk_hex: str, pk_hex: str): - sk_bytes = decode_hex(sk_hex) - pk_bytes = decode_hex(pk_hex) - assert data == decrypt(sk_bytes, encrypt(pk_bytes, data)) + assert data == decrypt(decode_hex(sk_hex), encrypt(decode_hex(pk_hex), data)) @pytest.mark.parametrize( @@ -26,13 +19,11 @@ def __check_hex(sk_hex: str, pk_hex: str): (generate_key(), True), ], ) -def test_elliptic_ok(key: PrivateKey, compressed: bool): - sk_hex = key.to_hex() - pk_hex = key.public_key.format(compressed).hex() - __check_hex(sk_hex, pk_hex) +def test_elliptic_ok(data, key: PrivateKey, compressed: bool): + __check(data, key, compressed) -def test_elliptic_error(): +def test_elliptic_error(data): with pytest.raises(TypeError): encrypt(1, data) @@ -43,25 +34,25 @@ def test_elliptic_error(): decrypt(1, encrypt(bytes.fromhex(pk_hex), data)) -def test_hkdf_config(): +def test_hkdf_config(data): ECIES_CONFIG.is_hkdf_key_compressed = True - __check_bytes(generate_key()) + __check(data, generate_key()) ECIES_CONFIG.is_hkdf_key_compressed = False -def test_ephemeral_key_config(): +def test_ephemeral_key_config(data): ECIES_CONFIG.is_ephemeral_key_compressed = True - __check_bytes(generate_key()) + __check(data, generate_key()) ECIES_CONFIG.is_ephemeral_key_compressed = False -def test_aes_nonce_config(): +def test_aes_nonce_config(data): ECIES_CONFIG.symmetric_nonce_length = 12 - __check_bytes(generate_key()) + __check(data, generate_key()) ECIES_CONFIG.symmetric_nonce_length = 16 -def test_sym_config(): +def test_sym_config(data): ECIES_CONFIG.symmetric_algorithm = "xchacha20" - __check_bytes(generate_key()) + __check(data, generate_key()) ECIES_CONFIG.symmetric_algorithm = "aes-256-gcm" diff --git a/tests/test_crypt_eth.py b/tests/test_crypt_eth.py index 6fd6f32..06eb520 100644 --- a/tests/test_crypt_eth.py +++ b/tests/test_crypt_eth.py @@ -1,24 +1,28 @@ import pytest from ecies import decrypt, encrypt -from ecies.utils import decode_hex, generate_eth_key, hex2pk, hex2sk, sha256 +from ecies.utils import ( + decode_hex, + generate_eth_key, + hex2pk, + hex2sk, + sha256, + to_eth_public_key, +) eth_keys = pytest.importorskip("eth_keys") -data = b"this is a test" - @pytest.fixture(scope="session") def sk(): return generate_eth_key() -def test_elliptic_ok_eth(sk): +def test_elliptic_ok_eth(data, sk): sk_hex = sk.to_hex() pk_hex = sk.public_key.to_hex() - sk_bytes = decode_hex(sk_hex) - pk_bytes = decode_hex(pk_hex) - assert data == decrypt(sk_bytes, encrypt(pk_bytes, data)) + assert data == decrypt(sk_hex, encrypt(pk_hex, data)) + assert data == decrypt(decode_hex(sk_hex), encrypt(decode_hex(pk_hex), data)) def test_hex_to_pk(sk): @@ -37,4 +41,4 @@ def test_hex_to_sk(sk): pk_hex = sk.public_key.to_hex() computed_sk = hex2sk(sk_hex) assert computed_sk.to_int() == int(sk.to_hex(), 16) - assert computed_sk.public_key.format(False) == b"\x04" + decode_hex(pk_hex) + assert to_eth_public_key(computed_sk.public_key) == decode_hex(pk_hex) diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 0907b8c..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,80 +0,0 @@ -import os - -from coincurve import PrivateKey - -from ecies import ECIES_CONFIG -from ecies.utils import ( - decapsulate, - decode_hex, - encapsulate, - sha256, - sym_decrypt, - sym_encrypt, -) -from ecies.utils.symmetric import derive_key - - -def __check_symmetric_random(data: bytes): - key = os.urandom(32) - sym_decrypt(key, sym_encrypt(key, data)) == data - - -def test_hash(): - assert sha256(b"0" * 16).hex()[:8] == "fcdb4b42" - - -def test_hkdf(): - derived = derive_key(b"secret").hex() - assert derived == "2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf" - - -def test_encapsulate(): - k1 = PrivateKey(secret=bytes([2])) - assert k1.to_int() == 2 - - k2 = PrivateKey(secret=bytes([3])) - assert k2.to_int() == 3 - - assert encapsulate(k1, k2.public_key) == decapsulate(k1.public_key, k2) - assert ( - encapsulate(k1, k2.public_key).hex() - == "6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82" - ) - - -def test_aes(): - # test random - __check_symmetric_random("helloworld🌍".encode()) - - # test big - data = b"1" * 1024 * 1024 * 100 # 100 MB - __check_symmetric_random(data) - - # test known - key = decode_hex("0000000000000000000000000000000000000000000000000000000000000000") - nonce = decode_hex("0xf3e1ba810d2c8900b11312b7c725565f") - tag = decode_hex("0Xec3b71e17c11dbe31484da9450edcf6c") - encrypted = decode_hex("02d2ffed93b856f148b9") - data = b"".join([nonce, tag, encrypted]) - assert b"helloworld" == sym_decrypt(key, data) - - -def test_xchacha20(): - ECIES_CONFIG.symmetric_algorithm = "xchacha20" - - # test random - __check_symmetric_random("helloworld🌍".encode()) - - # test big - data = b"1" * 1024 * 1024 * 100 # 100 MB - __check_symmetric_random(data) - - # test known - key = decode_hex("27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828") - nonce = decode_hex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6") - tag = decode_hex("0X5b5ccc27324af03b7ca92dd067ad6eb5") - encrypted = decode_hex("aa0664f3c00a09d098bf") - data = b"".join([nonce, tag, encrypted]) - assert b"helloworld" == sym_decrypt(key, data) - - ECIES_CONFIG.symmetric_algorithm = "aes-256-gcm" diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_elliptic.py b/tests/utils/test_elliptic.py new file mode 100644 index 0000000..fd309a2 --- /dev/null +++ b/tests/utils/test_elliptic.py @@ -0,0 +1,17 @@ +from coincurve import PrivateKey + +from ecies.utils import decapsulate, encapsulate + + +def test_encapsulate_decapsulate(): + k1 = PrivateKey(secret=bytes([2])) + assert k1.to_int() == 2 + + k2 = PrivateKey(secret=bytes([3])) + assert k2.to_int() == 3 + + assert encapsulate(k1, k2.public_key) == decapsulate(k1.public_key, k2) + assert ( + encapsulate(k1, k2.public_key).hex() + == "6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82" + ) diff --git a/tests/utils/test_eth.py b/tests/utils/test_eth.py new file mode 100644 index 0000000..006bf79 --- /dev/null +++ b/tests/utils/test_eth.py @@ -0,0 +1,7 @@ +from ecies.utils import hex2pk, to_eth_address + + +def test_checksum_address(): + pk = "02d9ed78008e7b6c4bdc2beea13230fb3ccb8072728c0986894a3d544485e9b727" + address = "0x7aD23D6eD9a1D98E240988BED0d78e8C81Ec296C" + assert to_eth_address(hex2pk(pk)) == address diff --git a/tests/utils/test_hash.py b/tests/utils/test_hash.py new file mode 100644 index 0000000..7c2bacf --- /dev/null +++ b/tests/utils/test_hash.py @@ -0,0 +1,10 @@ +from ecies.utils import derive_key, sha256 + + +def test_hash(): + assert sha256(b"0" * 16).hex()[:8] == "fcdb4b42" + + +def test_hkdf(): + derived = derive_key(b"secret").hex() + assert derived == "2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf" diff --git a/tests/utils/test_symmetric.py b/tests/utils/test_symmetric.py new file mode 100644 index 0000000..314bef9 --- /dev/null +++ b/tests/utils/test_symmetric.py @@ -0,0 +1,41 @@ +import os + +import pytest + +from ecies.config import SymmetricAlgorithm +from ecies.utils import decode_hex, sym_decrypt, sym_encrypt + + +def __check_symmetric_random( + data: bytes, algorithm: SymmetricAlgorithm = "aes-256-gcm" +): + key = os.urandom(32) + sym_decrypt(key, sym_encrypt(key, data, algorithm), algorithm) == data + + +@pytest.mark.parametrize("algorithm", ["aes-256-gcm", "xchacha20"]) +def test_symmetric_random(data, algorithm): + __check_symmetric_random(data, algorithm) + + +@pytest.mark.parametrize("algorithm", ["aes-256-gcm", "xchacha20"]) +def test_symmetric_big(algorithm, big_data): + __check_symmetric_random(big_data, algorithm) + + +def test_aes_known(): + key = decode_hex("0000000000000000000000000000000000000000000000000000000000000000") + nonce = decode_hex("0xf3e1ba810d2c8900b11312b7c725565f") + tag = decode_hex("0Xec3b71e17c11dbe31484da9450edcf6c") + encrypted = decode_hex("02d2ffed93b856f148b9") + data = b"".join([nonce, tag, encrypted]) + assert b"helloworld" == sym_decrypt(key, data) + + +def test_xchacha20_known(): + key = decode_hex("27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828") + nonce = decode_hex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6") + tag = decode_hex("0X5b5ccc27324af03b7ca92dd067ad6eb5") + encrypted = decode_hex("aa0664f3c00a09d098bf") + data = b"".join([nonce, tag, encrypted]) + assert b"helloworld" == sym_decrypt(key, data, "xchacha20")