diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..5efe8e2 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,75 @@ +name: Python CI + +on: [push] + +env: + PYTHON_SRC: "src" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + environment: + name: release + url: https://pypi.org/p/cenclave-lib-crypto + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install ".[dev]" + + - name: Package metadata + id: metadata + run: | + echo "PACKAGE_VERSION=$(python -c 'import cenclave_lib_crypto; print(cenclave_lib_crypto.__version__)')" >> $GITHUB_OUTPUT + + - name: Code format with black + run: | + python -m black --check $PYTHON_SRC + + - name: Import check with isort + run: | + python -m isort --check $PYTHON_SRC + + - name: Lint check with pylint + run: | + python -m pylint $PYTHON_SRC + + - name: Lint check with pycodestyle + run: | + python -m pycodestyle $PYTHON_SRC + + - name: Lint check with pydocstyle + run: | + python -m pydocstyle $PYTHON_SRC + + - name: Typecheck with MyPy + run: | + python -m mypy $PYTHON_SRC + + - name: Test with pytest + run: | + python -m pytest + + - name: Build package + if: ${{ startsWith(github.ref, 'refs/tags') && endsWith(github.ref, steps.metadata.outputs.PACKAGE_VERSION) }} + run: python -m build + + - name: Publish package to PyPi + if: ${{ startsWith(github.ref, 'refs/tags') && endsWith(github.ref, steps.metadata.outputs.PACKAGE_VERSION) }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57ec1d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +.vscode/ +.idea/ +.python-version + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d951b7b --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Cryptography Library for Cosmian Enclave + +## Overview + +Python cryptography library for [Cosmian Enclave](https://docs.cosmian.com/compute/cosmian_enclave/overview/) based on [PyNaCl](https://github.com/pyca/pynacl/) (binding to [libsodium](https://github.com/jedisct1/libsodium)) and [cryptography](https://github.com/pyca/cryptography). + +## Installation + +```console +$ pip install cenclave-lib-crypto +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0f22ff1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=68.0.0,<76.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "cenclave-lib-crypto" +authors = [ + { name = "Cosmian Tech", email = "tech@cosmian.com" }, +] +description = "Cryptography Library for Cosmian Enclave" +readme = "README.md" +requires-python = ">=3.8" +license = { text = "MIT" } +classifiers = [ + "Development Status :: 6 - Mature", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython" +] +dependencies = [ + "cryptography>=43.0.3,<44.0.0", + "pynacl>=1.5.0,<2.0.0" +] +dynamic = ["version"] + +[tool.setuptools.dynamic] +version = { attr = "cenclave_lib_crypto.__version__" } + +[project.optional-dependencies] +dev = [ + "black>=24.10.0,<25.0.0", + "isort>=5.13.2,<6.0.0", + "pylint>=3.3.1,<4.0.0", + "pycodestyle>=2.12.1,<3.0.0", + "pydocstyle>=6.3.0,<7.0.0", + "mypy>=1.13.0,<2.0.0", + "pytest>=8.3.3,<9.0.0", + "build>=1.2.2,<1.3.0", + "wheel>=0.45.0,<0.50.0" +] + +[tool.pylint.MAIN] +disable = [ + "C0103", # invalid-name + "R0913", # too-many-arguments + "R0917" # too-many-positional-arguments +] + +[tool.isort] +profile = "black" + +[tool.pytest] +testpaths = "tests" +pythonpath = "src" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a75e325 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[pycodestyle] +max-line-length = 90 +ignore = E203,W503 diff --git a/src/cenclave_lib_crypto/__init__.py b/src/cenclave_lib_crypto/__init__.py new file mode 100644 index 0000000..13c1aff --- /dev/null +++ b/src/cenclave_lib_crypto/__init__.py @@ -0,0 +1,3 @@ +"""cenclave_lib_crypto module.""" + +__version__ = "1.0.0" diff --git a/src/cenclave_lib_crypto/conversion.py b/src/cenclave_lib_crypto/conversion.py new file mode 100644 index 0000000..84d0abf --- /dev/null +++ b/src/cenclave_lib_crypto/conversion.py @@ -0,0 +1,122 @@ +"""cenclave_lib_crypto.conversion module.""" + +from typing import Tuple + +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, +) +from nacl.bindings import ( + crypto_scalarmult_ed25519_base, + crypto_sign_ed25519_pk_to_curve25519, + crypto_sign_ed25519_sk_to_curve25519, +) + + +def ed25519_pk_from_sk_(private_key: bytes) -> bytes: + """Recover public key from private key created for Ed25519. + + Parameters + ---------- + private_key: bytes + Bytes of the Ed25519 private key. + + Returns + ------- + bytes + Bytes of the Ed25519 public key corresponding to `private_key`. + + """ + return crypto_scalarmult_ed25519_base(private_key) + + +def ed25519_to_x25519_keypair_(public_key: bytes, seed: bytes) -> Tuple[bytes, bytes]: + """Map edwards25519 keypair to curve25519 keypair. + + Parameters + ---------- + public_key: bytes + Bytes of the Ed25519 public key. + seed : bytes + Bytes of the Ed25519 seed. + + Returns + ------- + Tuple[bytes, bytes] + Keypair (pk, sk) for X25519 + + """ + x25519_privkey: bytes = crypto_sign_ed25519_sk_to_curve25519(seed + public_key) + x25519_pubkey: bytes = crypto_sign_ed25519_pk_to_curve25519(public_key) + + return x25519_pubkey, x25519_privkey + + +def ed25519_to_x25519_pk_(public_key: bytes) -> bytes: + """Map edwards25519 point to curve25519 point. + + Parameters + ---------- + public_key: bytes + Bytes of the Ed25519 public key. + + Returns + ------- + bytes + Public key for X25519. + + """ + return crypto_sign_ed25519_pk_to_curve25519(public_key) + + +def ed25519_to_x25519_sk(private_key: Ed25519PrivateKey) -> X25519PrivateKey: + """Map Ed25519 private key to X25519 private key. + + Parameters + ---------- + private_key : Ed25519PrivateKey + Ed25519 private key. + + Returns + ------- + X25519PrivateKey + X25519 private key corresponding to `private_key`. + + """ + pk = private_key.public_key().public_bytes( + encoding=Encoding.Raw, format=PublicFormat.Raw + ) + seed = private_key.private_bytes( + encoding=Encoding.Raw, + format=PrivateFormat.Raw, + encryption_algorithm=NoEncryption(), + ) + x25519_sk: bytes = crypto_sign_ed25519_sk_to_curve25519(seed + pk) + + return X25519PrivateKey.from_private_bytes(x25519_sk) + + +def ed25519_to_x25519_pk(public_key: Ed25519PublicKey) -> bytes: + """Map Ed25519 public key to X25519 public key. + + Parameters + ---------- + public_key: Ed25519PublicKey + Ed25519 public key. + + Returns + ------- + bytes + Bytes of the X25519 public key corresponding to `public_key`. + + """ + return ed25519_to_x25519_pk_( + public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw) + ) diff --git a/src/cenclave_lib_crypto/ed25519.py b/src/cenclave_lib_crypto/ed25519.py new file mode 100644 index 0000000..47626c1 --- /dev/null +++ b/src/cenclave_lib_crypto/ed25519.py @@ -0,0 +1,99 @@ +"""cenclave_lib_crypto.ed25519 module.""" + +import hashlib +from typing import Tuple + +from nacl.bindings import ( + crypto_sign_keypair, + crypto_sign_seed_keypair, + crypto_sign_SEEDBYTES, +) +from nacl.bindings.crypto_scalarmult import crypto_scalarmult_ed25519_SCALARBYTES +from nacl.signing import SigningKey, VerifyKey + + +def ed25519_keygen() -> Tuple[bytes, bytes, bytes]: + """Keygen for Ed25519 (EdDSA using edwards25519). + + Returns + ------- + Tuple[bytes, bytes, bytes] + Triple (public_key, seed, private_key). + + """ + public_key, sk = crypto_sign_keypair() # type: bytes, bytes + seed: bytes = sk[:crypto_sign_SEEDBYTES] + private_key: bytes = hashlib.sha512(seed).digest()[ + :crypto_scalarmult_ed25519_SCALARBYTES + ] + + return public_key, seed, private_key + + +def ed25519_seed_keygen(seed: bytes) -> Tuple[bytes, bytes, bytes]: + """Seeded keygen for Ed25519 (EdDSA using edwards25519). + + Returns + ------- + Tuple[bytes, bytes, bytes] + Triple (public_key, seed, private_key). + + """ + public_key, sk = crypto_sign_seed_keypair(seed) + + assert seed == sk[:crypto_sign_SEEDBYTES] + + private_key: bytearray = bytearray( + hashlib.sha512(seed).digest()[:crypto_scalarmult_ed25519_SCALARBYTES] + ) + + # see: src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L19 + private_key[0] &= 248 + private_key[31] &= 127 + private_key[31] |= 64 + + return public_key, seed, bytes(private_key) + + +def sign(data: bytes, private_key: bytes) -> bytes: + """Sign `data` with `private_key` using Ed25519. + + Parameters + ---------- + data : bytes + Data to be signed. + private_key : bytes + Private key used to sign data. + + Returns + ------- + bytes + 64 bytes Ed25519 signature. + + """ + signing_key: SigningKey = SigningKey(private_key) + + return signing_key.sign(data).signature + + +def verify(data: bytes, sig: bytes, public_key: bytes) -> bytes: + """Verify `sig` with `data` and `public_key` using Ed25519. + + Parameters + ---------- + data : bytes + Data which has been signed. + sig : bytes + 64 bytes signature. + public_key : bytes + Public key used to verify the signature. + + Returns + ------- + bytes + Original bytes of the message if the verification succeeded. + + """ + verify_key: VerifyKey = VerifyKey(public_key) + + return verify_key.verify(data, sig) # raise nacl.exceptions.BadSignatureError diff --git a/src/cenclave_lib_crypto/error.py b/src/cenclave_lib_crypto/error.py new file mode 100644 index 0000000..6ad7c86 --- /dev/null +++ b/src/cenclave_lib_crypto/error.py @@ -0,0 +1,9 @@ +"""cenclave_lib_crypto.error module.""" + + +class NonceNotFound(Exception): + """Error when nonce does not exist for a specific path.""" + + +class ExtensionNotFound(Exception): + """Error when suffix extension is not found in a specific path.""" diff --git a/src/cenclave_lib_crypto/py.typed b/src/cenclave_lib_crypto/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/cenclave_lib_crypto/seal_box.py b/src/cenclave_lib_crypto/seal_box.py new file mode 100644 index 0000000..078c9bb --- /dev/null +++ b/src/cenclave_lib_crypto/seal_box.py @@ -0,0 +1,54 @@ +"""cenclave_lib_crypto.seal_box module.""" + +from nacl.public import PrivateKey, PublicKey, SealedBox + + +def seal(data: bytes, recipient_public_key: bytes) -> bytes: + """Seal with seal box of libsodium (X25519 and XSalsa20-Poly1305). + + Parameters + ---------- + data : bytes + The data to be sealed. + recipient_public_key : bytes + Recipent X25519 public key (32 bytes). + + Returns + ------- + bytes + `data` sealed for `recipient_public_key`. + + + Notes + ----- + ephemeral_pk ‖ box(m, + recipient_pk, + ephemeral_sk, + nonce=blake2b(ephemeral_pk ‖ recipient_pk)) + + """ + box = SealedBox(PublicKey(recipient_public_key)) + + return box.encrypt(data) + + +def unseal(encrypted_data: bytes, private_key: bytes) -> bytes: + """Unseal with seal box of libsodium (X25519 and XSalsa20-Poly1305). + + Parameters + ---------- + encrypted_data : bytes + The encrypted data to be unsealed: + ephemeral_pk (32 bytes) || MAC (16 bytes) || box(data) (var). + private_key : bytes + X25519 private key (32 bytes). + + Returns + ------- + bytes + cleartext data if success. + + """ + box = SealedBox(PrivateKey(private_key)) + + return box.decrypt(encrypted_data) diff --git a/src/cenclave_lib_crypto/x25519.py b/src/cenclave_lib_crypto/x25519.py new file mode 100644 index 0000000..949140b --- /dev/null +++ b/src/cenclave_lib_crypto/x25519.py @@ -0,0 +1,45 @@ +"""cenclave_lib_crypto.x25519 module.""" + +from typing import Tuple + +from nacl.bindings.crypto_scalarmult import crypto_scalarmult +from nacl.public import PrivateKey, PublicKey + + +def x25519_keygen() -> Tuple[bytes, bytes]: + """Keygen for X25519 (DH using Curve25519 in Montgomery form). + + Returns + ------- + Tuple[bytes, bytes] + Keypair (public_key, private_key). + + """ + private_key: PrivateKey = PrivateKey.generate() + public_key: PublicKey = private_key.public_key + + return bytes(public_key), bytes(private_key) + + +def x25519_pk_from_sk(private_key: bytes) -> bytes: + """X25519 public key from `private_key`.""" + return bytes(PrivateKey(private_key).public_key) + + +def x25519(private_key: bytes, public_key: bytes) -> bytes: + """Scalar multiplication over Curve25519. + + Parameters + ---------- + private_key: bytes + Scalar to be used as private key. + public_key: bytes + Bytes of the point on the Curve25519. + + Returns + ------- + bytes + Point on the Curve25519 to be used as shared secret. + + """ + return crypto_scalarmult(private_key, public_key) diff --git a/src/cenclave_lib_crypto/xsalsa20_poly1305.py b/src/cenclave_lib_crypto/xsalsa20_poly1305.py new file mode 100644 index 0000000..98edfef --- /dev/null +++ b/src/cenclave_lib_crypto/xsalsa20_poly1305.py @@ -0,0 +1,257 @@ +"""cenclave_lib_crypto.aead module.""" + +import logging +import shutil +from pathlib import Path +from typing import Dict, List, Optional, cast + +import nacl.utils +from nacl.secret import SecretBox + +from cenclave_lib_crypto.error import ExtensionNotFound, NonceNotFound + +KEY_LENGTH: int = nacl.secret.SecretBox.KEY_SIZE +NONCE_LENGTH: int = nacl.secret.SecretBox.NONCE_SIZE + + +def random_key() -> bytes: + """Generate a random symmetric key for XSalsa20-Poly1305. + + Returns + ------- + bytes + Random symmetric key of length AEAD_KEY_LENGHT. + + """ + return nacl.utils.random(KEY_LENGTH) + + +def encrypt(data: bytes, key: bytes, nonce: Optional[bytes] = None) -> bytes: + """Encrypt bytes `data` using XSalsa20-Poly1305. + + Parameters + ---------- + data : bytes + Data to be encrypted. + key : bytes + Symmetric key used for encryption. + nonce : Optional[bytes] + Arbitrary number that can be used just once. Randomly + generated if not provided. + + Returns + ------- + bytes + Ciphertext of `data` using `key`. + + """ + box: SecretBox = SecretBox(key) + + return box.encrypt(data, nonce) + + +def encrypt_file( + path: Path, key: bytes, nonce: Optional[bytes] = None, ext: str = ".enc" +) -> Path: + """Encrypt file `path` using XSalsa20-Poly1305. + + Parameters + ---------- + path : Path + Path to the data to be encrypted. + key : bytes + Symmetric key used for encryption. + nonce : Optional[bytes] + Arbitrary number that can be used just once. Randomly + generated if not provided. + ext : str + Extension for the encrypted file. + + Returns + ------- + Path + Path to the encrypted file `path`. + + """ + if not path.is_file(): + raise FileNotFoundError + + out_path: Path = path.with_suffix(f"{path.suffix}{ext}") + out_path.write_bytes(encrypt(path.read_bytes(), key, nonce)) + + return out_path + + +def encrypt_directory( + dir_path: Path, + pattern: str, + key: bytes, + nonces: Optional[Dict[str, bytes]], + exceptions: List[str], + ignore_patterns: List[str], + out_dir_path: Path, +) -> Dict[str, bytes]: + """Encrypt the content of directory `dir_path` using XSalsa20-Poly1305. + + Parameters + ---------- + dir_path : Path + Path to the directory to be encrypted. + pattern: str + A pattern to be matched in the directory. + key : bytes + Symmetric key used for encryption. + nonces : Optional[Dict[str, bytes]] + Map of string path to nonce. Randomly generated if None. + exceptions: List[str] + List of files which won't be encrypted. + ignore_patterns: List[str] + List of patterns which won't be copied. + out_dir_path: Path + Output directory path. Will be removed if already exists. + + Returns + ------- + Dict[str, bytes] + Map of path string to nonce used to encrypt. + + """ + if not dir_path.is_dir(): + raise NotADirectoryError + + if out_dir_path.exists(): + shutil.rmtree(out_dir_path) + + shutil.copytree( + dir_path, out_dir_path, ignore=shutil.ignore_patterns(*ignore_patterns) + ) + + nonce_map: Dict[str, bytes] = {} + + for path in out_dir_path.rglob(pattern): + if path.is_file() and path.name not in exceptions: + relpath: Path = path.relative_to(out_dir_path) + if nonces is not None and f"{relpath}" not in nonces: + raise NonceNotFound(f"Path '{relpath}' not found in nonces: {nonces}") + nonce: bytes = ( + nonces[f"{relpath}"] + if nonces is not None + else nacl.utils.random(NONCE_LENGTH) + ) + nonce_map[f"{relpath}"] = nonce + encrypt_file(path, key, nonce) + path.unlink() + + return nonce_map + + +def decrypt(encrypted_data: bytes, key: bytes, nonce: Optional[bytes] = None) -> bytes: + """Decrypt bytes `encrypted_data` using XSalsa20-Poly1305. + + Parameters + ---------- + encrypted_data : bytes + Encrypted data to be decrypted. + key : bytes + Symmetric key used for encryption. + nonce : Optional[bytes] + Arbitrary number that can be used just once. Part of the + ciphertext when omitted. + + Returns + ------- + bytes + Cleartext of `encrypted_data`. + + """ + box: SecretBox = SecretBox(key) + + return box.decrypt(encrypted_data, nonce) + + +def decrypt_file( + path: Path, + key: bytes, + nonce: Optional[bytes] = None, + ext: str = ".enc", + out_path: Optional[Path] = None, +) -> Path: + """Decrypt file `path` using XSalsa20-Poly1305. + + Parameters + ---------- + path : Path + Path to the data to be decrypted. + key : bytes + Symmetric key used for decryption. + nonce : Optional[bytes] + Arbitrary number that can be used just once. Part of the + ciphertext when omitted. + ext : str + Extension of encrypted file. + out_path : Optional[Path] + Output path if different from `path.with_suffix("")`. + + Returns + ------- + Path + Path to the decrypted file `path`. + + """ + if not path.is_file(): + raise FileNotFoundError + + if ext != path.suffix: + raise ExtensionNotFound(f"Extension {ext} not found in {path}") + + if out_path is not None: + out_path.parent.mkdir(parents=True, exist_ok=True) + + out_path = cast(Path, path.with_suffix("") if out_path is None else out_path) + out_path.write_bytes(decrypt(path.read_bytes(), key, nonce)) + + return out_path + + +def decrypt_directory( + dir_path: Path, key: bytes, ext: str = ".enc", out_dir_path: Optional[Path] = None +) -> bool: + """Decrypt the content of directory `dir_path` using XSalsa20-Poly1305. + + Parameters + ---------- + dir_path : Path + Path to the directory to be decrypted. + key : bytes + Symmetric key used for decryption. + ext : str + File extension of encrypted files. + out_dir_path : Optional[Path] + Output directory path if different from `dir_path`. + + Returns + ------- + bool + True if success, raise an exception otherwise. + + """ + if not dir_path.is_dir(): + raise NotADirectoryError + + for path in dir_path.rglob(f"*{ext}"): # type: Path + if path.is_file(): + out_path = decrypt_file( + path=path, + key=key, + nonce=None, + ext=ext, + out_path=( + (out_dir_path / path.relative_to(dir_path)).with_suffix("") + if out_dir_path is not None + else None + ), + ) + logging.debug("%s decrypted to %s", path.name, out_path) + path.unlink() + + return True diff --git a/tests/test_ed25519.py b/tests/test_ed25519.py new file mode 100644 index 0000000..bc7c23b --- /dev/null +++ b/tests/test_ed25519.py @@ -0,0 +1,9 @@ +from cenclave_lib_crypto.ed25519 import sign, verify, ed25519_keygen + + +def test_ed25519(): + message: bytes = b"Hello World!" + pk, seed, _ = ed25519_keygen() + + sig: bytes = sign(message, seed) + assert verify(message, sig, pk) == message diff --git a/tests/test_seal.py b/tests/test_seal.py new file mode 100644 index 0000000..92cfc7a --- /dev/null +++ b/tests/test_seal.py @@ -0,0 +1,12 @@ +from cenclave_lib_crypto.x25519 import x25519_keygen +from cenclave_lib_crypto.seal_box import seal, unseal + + +def test_seal_box(): + message: bytes = b"Hello World!" + pk, sk = x25519_keygen() + + ciphertext: bytes = seal(message, pk) + cleartext: bytes = unseal(ciphertext, sk) + + assert message == cleartext diff --git a/tests/test_x25519.py b/tests/test_x25519.py new file mode 100644 index 0000000..4bd787f --- /dev/null +++ b/tests/test_x25519.py @@ -0,0 +1,8 @@ +from cenclave_lib_crypto.x25519 import x25519_keygen, x25519 + + +def test_x25519(): + pk1, sk1 = x25519_keygen() + pk2, sk2 = x25519_keygen() + + assert x25519(sk2, pk1) == x25519(sk1, pk2) diff --git a/tests/test_xsalsa20_poly1305.py b/tests/test_xsalsa20_poly1305.py new file mode 100644 index 0000000..2442498 --- /dev/null +++ b/tests/test_xsalsa20_poly1305.py @@ -0,0 +1,59 @@ +from pathlib import Path +from cenclave_lib_crypto.xsalsa20_poly1305 import ( + encrypt, + decrypt, + encrypt_directory, + decrypt_directory, + random_key, + KEY_LENGTH, + NONCE_LENGTH, +) + + +def test_xsalsa20_poly1305(): + message: bytes = b"Hello World!" + key: bytes = random_key() + + assert len(key) == KEY_LENGTH + + ciphertext: bytes = encrypt(message, key) + cleartext: bytes = decrypt(ciphertext, key) + + assert message == cleartext + + +def test_xsalsa20_poly1305_directory(tmp_path): + plaindir = tmp_path / "plain" + plaindir.mkdir() + dir1 = plaindir / "dir1" + dir1.mkdir() + encdir = tmp_path / "encrypted" + enc_dir1 = encdir / "dir1" + + f1 = dir1 / "file1.txt" + f2 = dir1 / "file2.txt" + + f1.write_bytes(b"Hello from file1!") + f2.write_bytes(b"Hello from file2!") + + key: bytes = random_key() + + nonce_map = encrypt_directory(dir1, "*.txt", key, None, [], [], enc_dir1) + enc_f1 = enc_dir1 / "file1.txt.enc" + enc_f2 = enc_dir1 / "file2.txt.enc" + expected_f1 = enc_dir1 / "file1.txt" + expected_f2 = enc_dir1 / "file2.txt" + assert enc_f1.read_bytes()[:NONCE_LENGTH] == nonce_map[expected_f1.name] + assert enc_f2.read_bytes()[:NONCE_LENGTH] == nonce_map[expected_f2.name] + + # test to encrypt with specific nonce_map + enc_dir1_test = encdir / "test-dir1" + expected_enc_f1 = enc_dir1_test / "file1.txt.enc" + expected_enc_f2 = enc_dir1_test / "file2.txt.enc" + encrypt_directory(dir1, "*.txt", key, nonce_map, [], [], enc_dir1_test) + assert enc_f1.read_bytes() == expected_enc_f1.read_bytes() + assert enc_f2.read_bytes() == expected_enc_f2.read_bytes() + + decrypt_directory(enc_dir1, key) + assert expected_f1.read_bytes() == f1.read_bytes() + assert expected_f2.read_bytes() == f2.read_bytes()