From 1c9e7a974f83367ab8ee79ca244426c7543240c1 Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Thu, 19 Sep 2024 12:49:45 +0200 Subject: [PATCH] Replace pysodium with libnacl The library `pysodium` is used as "a very simple wrapper around libsodium masquerading as nacl". `libnacl` is another wrapper around the nacl library developed and maintained by the Salt project and with extensive documentation on https://libnacl.readthedocs.io and has been update to use the `pyproject.toml` format. The status of this library is marked as `5 - Production/Stable` on PyPI. The `pysodium` library has a bare bone repository with no documentation available and still uses the old 'setup.py' format. The status of this library is marked as `4 - Beta` on PyPI. --- poetry.lock | 23 +++++++++-------- pyproject.toml | 2 +- requirements.dev.txt | 2 +- requirements.slim.txt | 2 +- requirements.txt | 2 +- src/pytezos/crypto/key.py | 28 ++++++++++----------- tests/unit_tests/test_crypto/test_crypto.py | 2 +- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index 981d6c26e..b3e8b60ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1593,6 +1593,17 @@ files = [ {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, ] +[[package]] +name = "libnacl" +version = "2.1.0" +description = "Python bindings for libsodium based on ctypes" +optional = false +python-versions = ">=3.4,<4.0" +files = [ + {file = "libnacl-2.1.0-py3-none-any.whl", hash = "sha256:a8546b221afe8b72b6a9f298cd92a4c1f90570d7b5baa295acb1913644e230a5"}, + {file = "libnacl-2.1.0.tar.gz", hash = "sha256:f3418da7df29e6d9b11fd7d990289d16397dc1020e4e35192e11aee826922860"}, +] + [[package]] name = "markupsafe" version = "2.1.5" @@ -2210,16 +2221,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pysodium" -version = "0.7.18" -description = "python libsodium wrapper" -optional = false -python-versions = "*" -files = [ - {file = "pysodium-0.7.18.tar.gz", hash = "sha256:781ada024456ac74c411193b82d94018c85c94130ea01a22dbec48b8ff458b07"}, -] - [[package]] name = "pytest" version = "8.3.2" @@ -3584,4 +3585,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.13" -content-hash = "8c202be4ef653aad9b925a835fa344dd67075166e2d8af2a610c6cce055c0549" +content-hash = "36fddafcdf4caaf97c88d769e2287164ae1bc0a11c782805cb459320f1862922" diff --git a/pyproject.toml b/pyproject.toml index cb9de32a2..7ae6a62f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ netstruct = ">=1.1.2" notebook = ">=6.5,<7" ply = ">=3.11" py-ecc = ">=7.0.0" -pysodium = ">=0.7.10" python-dateutil = ">=2.8.2" requests = ">=2.28.2" simplejson = ">=3.17.6" @@ -66,6 +65,7 @@ tabulate = ">=0.9.0" testcontainers = ">=3.7.0,<4.8.0" tqdm = ">=4.62.3" simple-bson = ">=0.0.3" +libnacl = "^2.1.0" [tool.poetry.dev-dependencies] black = "*" diff --git a/requirements.dev.txt b/requirements.dev.txt index 8531606ef..06ed855ca 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -100,7 +100,7 @@ pure-eval==0.2.3 ; python_full_version >= "3.8.1" and python_version < "3.13" py-ecc==7.0.1 ; python_full_version >= "3.8.1" and python_version < "3.13" pycparser==2.22 ; python_full_version >= "3.8.1" and python_version < "3.13" pygments==2.18.0 ; python_full_version >= "3.8.1" and python_version < "3.13" -pysodium==0.7.18 ; python_full_version >= "3.8.1" and python_version < "3.13" +libnacl==2.1.0 ; python_full_version >= "3.8.1" and python_version < "3.13" pytest-cov==5.0.0 ; python_full_version >= "3.8.1" and python_version < "3.13" pytest-xdist==3.6.1 ; python_full_version >= "3.8.1" and python_version < "3.13" pytest==8.3.2 ; python_full_version >= "3.8.1" and python_version < "3.13" diff --git a/requirements.slim.txt b/requirements.slim.txt index 77e387949..1f0fa2971 100644 --- a/requirements.slim.txt +++ b/requirements.slim.txt @@ -30,7 +30,7 @@ pkgutil-resolve-name==1.3.10 ; python_full_version >= "3.8.1" and python_version ply==3.11 ; python_full_version >= "3.8.1" and python_version < "3.13" py-ecc==7.0.1 ; python_full_version >= "3.8.1" and python_version < "3.13" pycparser==2.22 ; python_full_version >= "3.8.1" and python_version < "3.13" -pysodium==0.7.18 ; python_full_version >= "3.8.1" and python_version < "3.13" +libnacl==2.1.0 ; python_full_version >= "3.8.1" and python_version < "3.13" python-dateutil==2.9.0.post0 ; python_full_version >= "3.8.1" and python_version < "3.13" pywin32==306 ; python_full_version >= "3.8.1" and python_version < "3.13" and sys_platform == "win32" referencing==0.35.1 ; python_full_version >= "3.8.1" and python_version < "3.13" diff --git a/requirements.txt b/requirements.txt index cc2b48b16..3ff439e8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -84,7 +84,7 @@ pure-eval==0.2.3 ; python_full_version >= "3.8.1" and python_version < "3.13" py-ecc==7.0.1 ; python_full_version >= "3.8.1" and python_version < "3.13" pycparser==2.22 ; python_full_version >= "3.8.1" and python_version < "3.13" pygments==2.18.0 ; python_full_version >= "3.8.1" and python_version < "3.13" -pysodium==0.7.18 ; python_full_version >= "3.8.1" and python_version < "3.13" +libnacl==2.1.0 ; python_full_version >= "3.8.1" and python_version < "3.13" python-dateutil==2.9.0.post0 ; python_full_version >= "3.8.1" and python_version < "3.13" python-json-logger==2.0.7 ; python_full_version >= "3.8.1" and python_version < "3.13" pywin32==306 ; python_full_version >= "3.8.1" and python_version < "3.13" and sys_platform == "win32" diff --git a/src/pytezos/crypto/key.py b/src/pytezos/crypto/key.py index d7baac212..253f49529 100644 --- a/src/pytezos/crypto/key.py +++ b/src/pytezos/crypto/key.py @@ -44,7 +44,7 @@ class CryptoExtraFallback: def __getattr__(self, item): raise ImportError( "Please, install packages libsodium-dev, and libgmp-dev, " - "and Python libraries pysodium, coincurve, and fastecdsa" + "and Python libraries libnacl, coincurve, and fastecdsa" ) def __call__(self, *args, **kwargs): @@ -57,13 +57,13 @@ def __call__(self, *args, **kwargs): import fastecdsa.ecdsa # type: ignore import fastecdsa.encoding.sec1 # type: ignore import fastecdsa.keys # type: ignore - import pysodium # type: ignore + import libnacl # type: ignore from coincurve import ecdsa # type: ignore from fastecdsa.encoding.util import bytes_to_int # type: ignore except ImportError as e: coincurve = CryptoExtraFallback() # type: ignore ecdsa = CryptoExtraFallback() # type: ignore - pysodium = CryptoExtraFallback() + libnacl = CryptoExtraFallback() fastecdsa = CryptoExtraFallback() bytes_to_int = CryptoExtraFallback() __crypto__ = False @@ -148,9 +148,9 @@ def from_secret_exponent( if curve == b'ed': # Dealing with secret exponent or seed? if len(secret_exponent) == 64: - public_point = pysodium.crypto_sign_sk_to_pk(sk=secret_exponent) + public_point = libnacl.crypto_sign_ed25519_sk_to_pk(sk=secret_exponent) else: - public_point, secret_exponent = pysodium.crypto_sign_seed_keypair(seed=secret_exponent) + public_point, secret_exponent = libnacl.crypto_sign_seed_keypair(seed=secret_exponent) # Secp256k1 elif curve == b'sp': sk = coincurve.PrivateKey(secret_exponent) @@ -218,7 +218,7 @@ def from_encoded_key( iterations=32768, dklen=32, ) - encoded_key = pysodium.crypto_secretbox_open( + encoded_key = libnacl.crypto_secretbox_open( c=encrypted_sk, nonce=b'\000' * 24, k=encryption_key, @@ -291,7 +291,7 @@ def from_mnemonic( seed = Mnemonic.to_seed(mnemonic, passphrase=email + passphrase) if curve == b'ed': - _, secret_exponent = pysodium.crypto_sign_seed_keypair(seed=seed[:32]) + _, secret_exponent = libnacl.crypto_sign_seed_keypair(seed=seed[:32]) elif curve == b'sp': secret_exponent = seed[:32] elif curve == b'p2': @@ -379,7 +379,7 @@ def secret_key( raise ValueError("Secret key is undefined") if self.curve == b'ed' and ed25519_seed: - key = pysodium.crypto_sign_sk_to_seed(self.secret_exponent) + key = libnacl.crypto_sign_ed25519_sk_to_seed(self.secret_exponent) else: key = self.secret_exponent @@ -390,7 +390,7 @@ def secret_key( passphrase = passphrase.encode() assert isinstance(passphrase, bytes), f'expected bytes or str, got {type(passphrase).__name__}' - salt = pysodium.randombytes(8) + salt = libnacl.randombytes(8) encryption_key = hashlib.pbkdf2_hmac( hash_name="sha512", password=passphrase, @@ -398,7 +398,7 @@ def secret_key( iterations=32768, dklen=32, ) - encrypted_sk = pysodium.crypto_secretbox(msg=key, nonce=b'\000' * 24, k=encryption_key) + encrypted_sk = libnacl.crypto_secretbox(msg=key, nonce=b'\000' * 24, k=encryption_key) key = salt + encrypted_sk # we have to combine salt and encrypted key in order to decrypt later prefix = self.curve + b'esk' else: @@ -442,8 +442,8 @@ def sign(self, message: Union[str, bytes], generic: bool = False): # Ed25519 if self.curve == b"ed": - digest = pysodium.crypto_generichash(encoded_message) - signature = pysodium.crypto_sign_detached(digest, self.secret_exponent) + digest = libnacl.crypto_generichash(encoded_message) + signature = libnacl.crypto_sign_detached(digest, self.secret_exponent) # Secp256k1 elif self.curve == b"sp": pk = coincurve.PrivateKey(self.secret_exponent) @@ -486,9 +486,9 @@ def verify(self, signature: Union[str, bytes], message: Union[str, bytes]) -> bo # Ed25519 if self.curve == b"ed": - digest = pysodium.crypto_generichash(encoded_message) + digest = libnacl.crypto_generichash(encoded_message) try: - pysodium.crypto_sign_verify_detached(decoded_signature, digest, self.public_point) + libnacl.crypto_sign_verify_detached(decoded_signature, digest, self.public_point) except ValueError as exc: raise ValueError('Signature is invalid.') from exc # Secp256k1 diff --git a/tests/unit_tests/test_crypto/test_crypto.py b/tests/unit_tests/test_crypto/test_crypto.py index a8f0f5f42..cfb3b5ea2 100644 --- a/tests/unit_tests/test_crypto/test_crypto.py +++ b/tests/unit_tests/test_crypto/test_crypto.py @@ -157,7 +157,7 @@ def test_encrypted_keys(self, sk, passphrase, salt, pk): key = Key.from_encoded_key(sk, passphrase=passphrase) self.assertEqual(pk, key.public_key()) - with patch('pytezos.crypto.key.pysodium.randombytes', return_value=salt): + with patch('pytezos.crypto.key.libnacl.randombytes', return_value=salt): self.assertEqual(sk, key.secret_key(passphrase)) @parameterized.expand(