diff --git a/.github/workflows/python/build/linux/action.yml b/.github/workflows/python/build/linux/action.yml index 4b64a4f3..7295d0dd 100644 --- a/.github/workflows/python/build/linux/action.yml +++ b/.github/workflows/python/build/linux/action.yml @@ -31,61 +31,13 @@ runs: working-directory: "./python" shell: bash - - name: Building 3.14 wheel + - name: Building wheels working-directory: ./python shell: bash run: | - maturin build -i 3.14 --release --target=x86_64-unknown-linux-gnu - maturin build -i 3.14 --release --target=i686-unknown-linux-gnu - maturin build -i 3.14 --release --target=aarch64-unknown-linux-gnu - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Building 3.13 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.13 --release --target=x86_64-unknown-linux-gnu - maturin build -i 3.13 --release --target=i686-unknown-linux-gnu - maturin build -i 3.13 --release --target=aarch64-unknown-linux-gnu - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Building 3.12 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.12 --release --target=x86_64-unknown-linux-gnu - maturin build -i 3.12 --release --target=i686-unknown-linux-gnu - maturin build -i 3.12 --release --target=aarch64-unknown-linux-gnu - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Building 3.11 Wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.11 --release --target=x86_64-unknown-linux-gnu - maturin build -i 3.11 --release --target=i686-unknown-linux-gnu - maturin build -i 3.11 --release --target=aarch64-unknown-linux-gnu - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Building 3.10 Wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.10 --release --target=x86_64-unknown-linux-gnu - maturin build -i 3.10 --release --target=i686-unknown-linux-gnu - maturin build -i 3.10 --release --target=aarch64-unknown-linux-gnu + maturin build --release --target=x86_64-unknown-linux-gnu + maturin build --release --target=i686-unknown-linux-gnu + maturin build --release --target=aarch64-unknown-linux-gnu - name: Place Artifacts shell: bash diff --git a/.github/workflows/python/build/macos/action.yml b/.github/workflows/python/build/macos/action.yml index 821c3ced..2b759b9b 100644 --- a/.github/workflows/python/build/macos/action.yml +++ b/.github/workflows/python/build/macos/action.yml @@ -22,66 +22,14 @@ runs: shell: bash run: pip install maturin --disable-pip-version-check - - name: Building 3.14 Wheel + - name: Building wheels working-directory: ./python shell: bash run: | source $HOME/.cargo/env - maturin build --release -i 3.14 --target=x86_64-apple-darwin - maturin build --release -i 3.14 --target=aarch64-apple-darwin - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Building 3.13 Wheel - working-directory: ./python - shell: bash - run: | - source $HOME/.cargo/env - - maturin build --release -i 3.13 --target=x86_64-apple-darwin - maturin build --release -i 3.13 --target=aarch64-apple-darwin - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Building 3.12 Wheel - working-directory: ./python - shell: bash - run: | - source $HOME/.cargo/env - - maturin build --release -i 3.12 --target=x86_64-apple-darwin - maturin build --release -i 3.12 --target=aarch64-apple-darwin - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Building 3.11 Wheel - working-directory: ./python - shell: bash - run: | - source $HOME/.cargo/env - - maturin build --release -i 3.11 --target=x86_64-apple-darwin - maturin build --release -i 3.11 --target=aarch64-apple-darwin - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Building 3.10 Wheel - working-directory: ./python - shell: bash - run: | - source $HOME/.cargo/env - - maturin build --release -i 3.10 --target=x86_64-apple-darwin - maturin build --release -i 3.10 --target=aarch64-apple-darwin + maturin build --release --target=x86_64-apple-darwin + maturin build --release --target=aarch64-apple-darwin - name: Place Artifacts shell: bash diff --git a/.github/workflows/python/build/windows/action.yml b/.github/workflows/python/build/windows/action.yml index 5abfcb3b..6be6cbd4 100644 --- a/.github/workflows/python/build/windows/action.yml +++ b/.github/workflows/python/build/windows/action.yml @@ -5,6 +5,7 @@ runs: - uses: actions/setup-python@v5 with: python-version: "3.14" + architecture: "x64" - name: Setting up PATH environment variable shell: bash @@ -22,115 +23,13 @@ runs: shell: bash run: pip install maturin - - uses: actions/setup-python@v5 - with: - python-version: "3.14" - architecture: "x86" - - - name: Building i686 3.14 wheel - working-directory: ./python - shell: bash - run: maturin build -i 3.14 --release --target=i686-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.14" - architecture: "x64" - - - name: Building x86_64, aarch64 3.14 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.14 --release --target=x86_64-pc-windows-msvc - maturin build -i 3.14 --release --target=aarch64-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - architecture: "x86" - - - name: Building i686 3.13 wheel - working-directory: ./python - shell: bash - run: maturin build -i 3.13 --release --target=i686-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - architecture: "x64" - - - name: Building x86_64, aarch64 3.13 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.13 --release --target=x86_64-pc-windows-msvc - maturin build -i 3.13 --release --target=aarch64-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - architecture: "x86" - - - name: Building i686 3.12 wheel - working-directory: ./python - shell: bash - run: maturin build -i 3.12 --release --target=i686-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - architecture: "x64" - - - name: Building x86_64, aarch64 3.12 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.12 --release --target=x86_64-pc-windows-msvc - maturin build -i 3.12 --release --target=aarch64-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - architecture: "x86" - - - name: Building i686 3.11 wheel - working-directory: ./python - shell: bash - run: maturin build -i 3.11 --release --target=i686-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - architecture: "x64" - - - name: Building x86_64, aarch64 3.11 wheel - working-directory: ./python - shell: bash - run: | - maturin build -i 3.11 --release --target=x86_64-pc-windows-msvc - maturin build -i 3.11 --release --target=aarch64-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - architecture: "x86" - - - name: Building i686 3.10 wheel - working-directory: ./python - shell: bash - run: maturin build -i 3.10 --release --target=i686-pc-windows-msvc - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - architecture: "x64" - - - name: Building x86_64, aarch64 3.10 wheel + - name: Building wheels working-directory: ./python shell: bash run: | - maturin build -i 3.10 --release --target=x86_64-pc-windows-msvc - maturin build -i 3.10 --release --target=aarch64-pc-windows-msvc + maturin build --release --target=x86_64-pc-windows-msvc + maturin build --release --target=i686-pc-windows-msvc + maturin build --release --target=aarch64-pc-windows-msvc - name: Place Artifacts shell: bash diff --git a/Cargo.lock b/Cargo.lock index 71924549..347668f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,10 +666,7 @@ dependencies = [ name = "devolutions-crypto-python" version = "0.9.3" dependencies = [ - "base64 0.22.1", - "devolutions-crypto", - "pyo3", - "zeroize", + "uniffi", ] [[package]] @@ -934,15 +931,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -1042,15 +1030,6 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "minicov" version = "0.3.8" @@ -1197,12 +1176,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1240,77 +1213,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pyo3" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" -dependencies = [ - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" -dependencies = [ - "python3-dll-a", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "python3-dll-a" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80ba7540edb18890d444c5aa8e1f1f99b1bdf26fb26ae383135325f4a36042b" -dependencies = [ - "cc", -] - [[package]] name = "quote" version = "1.0.45" @@ -1674,12 +1576,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - [[package]] name = "tempfile" version = "3.27.0" @@ -1948,12 +1844,6 @@ dependencies = [ "weedle2", ] -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "universal-hash" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index cc5b52e8..ee0bf256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ members = [ "uniffi/uniffi-bindgen", "uniffi/devolutions-crypto-uniffi", ] +default-members = [ + ".", + "python", + ] [workspace.dependencies] uniffi = "0.31.1" diff --git a/python/Cargo.toml b/python/Cargo.toml index 78872874..317820f7 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -3,14 +3,9 @@ name = "devolutions-crypto-python" version.workspace = true edition = "2021" -[lib] -name = "devolutions_crypto_python" -crate-type = ["cdylib"] +[[bin]] +name = "uniffi-bindgen" +path = "src/uniffi-bindgen.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -devolutions-crypto = { path = "../" } -zeroize = "1" -base64 = "0.22" -pyo3 = { version = "0.27", features = ["extension-module", "generate-import-lib"] } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +uniffi = { workspace = true, features = ["cli"] } diff --git a/python/PYPI_README.md b/python/PYPI_README.md index fb16a421..6c8e9591 100644 --- a/python/PYPI_README.md +++ b/python/PYPI_README.md @@ -20,7 +20,7 @@ pip install devolutions-crypto - **Password Hashing**: Secure password hashing with Argon2 and PBKDF2 - **Digital Signatures**: Ed25519 signatures for data authentication - **Key Derivation**: Argon2 and PBKDF2 key derivation functions -- **Type Safety**: Full type hints and IDE support via stub files +- **Type Safety**: Full type hints and IDE support ## Quick Start @@ -49,6 +49,7 @@ assert decrypted == plaintext * [Password Hashing](#password-hashing) * [Digital Signatures](#digital-signatures) * [Key Derivation](#key-derivation) +* [Password-Based Encryption](#password-based-encryption) ### Symmetric Encryption @@ -83,16 +84,16 @@ plaintext = b"Secret message" aad = b"user_id:12345" # Context data (not encrypted, but authenticated) # Encrypt with AAD -ciphertext = devolutions_crypto.encrypt(plaintext, key, aad=aad) +ciphertext = devolutions_crypto.encrypt_with_aad(plaintext, key, aad) # Decrypt with AAD (must match encryption AAD) -decrypted = devolutions_crypto.decrypt(ciphertext, key, aad=aad) +decrypted = devolutions_crypto.decrypt_with_aad(ciphertext, key, aad) assert decrypted == plaintext # Decryption fails with wrong or missing AAD try: - devolutions_crypto.decrypt(ciphertext, key, aad=b"wrong_context") -except devolutions_crypto.DevolutionsCryptoException: + devolutions_crypto.decrypt_with_aad(ciphertext, key, b"wrong_context") +except devolutions_crypto.DevolutionsCryptoError: print("Authentication failed - AAD mismatch") ``` @@ -141,17 +142,14 @@ assert decrypted == message ### Password Hashing -Securely hash and verify passwords using PBKDF2: +Securely hash and verify passwords. The default uses Argon2id: ```python import devolutions_crypto -# Hash a password (this is slow by design - use high iterations) +# Hash a password (this is slow by design) password = b"my_secure_password123!" -password_hash = devolutions_crypto.hash_password( - password, - version=0 -) +password_hash = devolutions_crypto.hash_password(password) # Verify the password is_valid = devolutions_crypto.verify_password(password, password_hash) @@ -175,7 +173,7 @@ import devolutions_crypto signing_keypair = devolutions_crypto.generate_signing_keypair() # Extract the public key -public_key = devolutions_crypto.get_signing_public_key(signing_keypair) +public_key = signing_keypair.get_public_key() ``` #### Signing Data @@ -185,7 +183,7 @@ import devolutions_crypto # Sign some data data = b"This is an important message" -signature = devolutions_crypto.sign(data, signing_keypair) +signature = devolutions_crypto.sign(data, signing_keypair.get_private_key()) ``` #### Verifying Signatures @@ -234,12 +232,52 @@ ciphertext = devolutions_crypto.encrypt(plaintext, derived_key) ```python import devolutions_crypto -# Derive a key using Argon2 (requires Argon2Parameters) +# Derive a key using Argon2id password = b"user_password" -# parameters should be serialized Argon2Parameters +parameters = devolutions_crypto.Argon2ParametersBuilder().build() # default Argon2id parameters derived_key = devolutions_crypto.derive_key_argon2(password, parameters) ``` +### Password-Based Encryption + +Encrypt data directly with a password. The key is derived with Argon2id and the +derivation parameters (including the random salt) are stored in the returned +blob, so decryption only needs the password. + +```python +import devolutions_crypto + +password = b"my_secure_password" +plaintext = b"secret data" + +# Encrypt with a password +blob = devolutions_crypto.derive_encrypt_with_password(plaintext, password) + +# Decrypt with the same password +decrypted = devolutions_crypto.derive_decrypt_with_password(blob, password) +assert decrypted == plaintext +``` + +#### With Additional Authenticated Data (AAD) + +```python +import devolutions_crypto + +password = b"my_secure_password" +plaintext = b"secret data" +aad = b"context" + +blob = devolutions_crypto.derive_encrypt_with_password_and_aad(plaintext, password, aad) +decrypted = devolutions_crypto.derive_decrypt_with_password_and_aad(blob, password, aad) +assert decrypted == plaintext + +# Decryption fails with wrong or missing AAD +try: + devolutions_crypto.derive_decrypt_with_password(blob, password) +except devolutions_crypto.DevolutionsCryptoError: + print("Authentication failed - AAD required") +``` + ## Supported Python Versions - Python 3.10+ @@ -265,7 +303,7 @@ Pre-built wheels are available for: ## Exception Handling -All functions may raise `DevolutionsCryptoException` on errors: +All functions may raise `DevolutionsCryptoError` on errors: ```python import devolutions_crypto @@ -273,7 +311,7 @@ import devolutions_crypto try: # Invalid key size result = devolutions_crypto.encrypt(b"data", b"short_key") -except devolutions_crypto.DevolutionsCryptoException as e: +except devolutions_crypto.DevolutionsCryptoError as e: print(f"Encryption error: {e}") ``` diff --git a/python/devolutions_crypto.pyi b/python/devolutions_crypto.pyi deleted file mode 100644 index 93c20d61..00000000 --- a/python/devolutions_crypto.pyi +++ /dev/null @@ -1,544 +0,0 @@ -""" -Devolutions Crypto Python Bindings - -A high-performance cryptography library providing symmetric encryption, asymmetric encryption, -password hashing, digital signatures, and key derivation functions. -""" - -from typing import Optional - -class DevolutionsCryptoException(Exception): - """Base exception class for all Devolutions Crypto errors.""" - ... - -class Keypair: - """ - A container for asymmetric encryption keypair. - - Attributes: - public_key: The public key as bytes - private_key: The private key as bytes - """ - public_key: bytes - private_key: bytes - -def encrypt( - data: bytes, - key: bytes, - aad: Optional[bytes] = None, - version: int = 0 -) -> bytes: - """ - Encrypt data using symmetric encryption (AES-256-GCM). - - Args: - data: The plaintext data to encrypt - key: The encryption key (32 bytes for AES-256) - aad: Optional Additional Authenticated Data for AEAD - version: Ciphertext version (default: 0) - - Returns: - The encrypted ciphertext as bytes - - Raises: - DevolutionsCryptoException: If encryption fails or invalid parameters provided - - Example: - >>> key = b'0' * 32 # 32-byte key - >>> plaintext = b'Hello, World!' - >>> ciphertext = encrypt(plaintext, key) - """ - ... - -def decrypt( - data: bytes, - key: bytes, - aad: Optional[bytes] = None -) -> bytes: - """ - Decrypt data that was encrypted with symmetric encryption. - - Args: - data: The ciphertext to decrypt - key: The decryption key - aad: Optional Additional Authenticated Data (must match encryption AAD) - - Returns: - The decrypted plaintext as bytes - - Raises: - DevolutionsCryptoException: If decryption fails, authentication fails, or invalid ciphertext - - Example: - >>> plaintext = decrypt(ciphertext, key) - """ - ... - -def encrypt_asymmetric( - data: bytes, - key: bytes, - aad: Optional[bytes] = None, - version: int = 0 -) -> bytes: - """ - Encrypt data using asymmetric encryption (X25519 + AES-256-GCM). - - Args: - data: The plaintext data to encrypt - key: The recipient's public key - aad: Optional Additional Authenticated Data for AEAD - version: Ciphertext version (default: 0) - - Returns: - The encrypted ciphertext as bytes - - Raises: - DevolutionsCryptoException: If encryption fails or invalid public key provided - - Example: - >>> keypair = generate_keypair() - >>> ciphertext = encrypt_asymmetric(b'secret', keypair.public_key) - """ - ... - -def decrypt_asymmetric( - data: bytes, - key: bytes, - aad: Optional[bytes] = None -) -> bytes: - """ - Decrypt data that was encrypted with asymmetric encryption. - - Args: - data: The ciphertext to decrypt - key: The recipient's private key - aad: Optional Additional Authenticated Data (must match encryption AAD) - - Returns: - The decrypted plaintext as bytes - - Raises: - DevolutionsCryptoException: If decryption fails or invalid private key provided - - Example: - >>> plaintext = decrypt_asymmetric(ciphertext, keypair.private_key) - """ - ... - -def hash_password( - password: bytes, - version: int = 0 -) -> bytes: - """ - Hash a password using a secure password hashing algorithm. - - Uses Argon2id (V2) by default. - Use ``version=1`` for PBKDF2-SHA256. - - Args: - password: The password to hash - version: Password hash version (default: 0 = Latest = Argon2id V2) - - Returns: - The password hash as bytes (contains salt and parameters) - - Raises: - DevolutionsCryptoException: If hashing fails or invalid parameters provided - - Example: - >>> password = b'my_secure_password' - >>> hash_value = hash_password(password) - >>> assert verify_password(password, hash_value) - """ - ... - -def hash_password_with_params( - password: bytes, - params: bytes -) -> bytes: - """ - Hash a password using serialized DerivationParameters. - - Args: - password: The password to hash - params: Serialized DerivationParameters bytes (Argon2id or PBKDF2) - - Returns: - The password hash as bytes - - Raises: - DevolutionsCryptoException: If hashing fails or invalid parameters - - Example: - >>> result = derive_secret_key_argon2(b'seed') - >>> hash_value = hash_password_with_params(b'my_secure_password', result.parameters) - >>> assert verify_password(b'my_secure_password', hash_value) - """ - ... - -def verify_password( - password: bytes, - hash: bytes -) -> bool: - """ - Verify a password against a previously generated hash. - - Args: - password: The password to verify - hash: The hash to verify against (generated by hash_password) - - Returns: - True if the password matches the hash, False otherwise - - Raises: - DevolutionsCryptoException: If the hash format is invalid - - Example: - >>> is_valid = verify_password(b'my_secure_password', hash_value) - """ - ... - -def derive_key_pbkdf2( - key: bytes, - salt: Optional[bytes] = None, - iterations: int = 600000, - length: int = 32 -) -> bytes: - """ - Derive a cryptographic key from input material using PBKDF2. - - Args: - key: The input key material - salt: Optional salt (default: empty bytes) - iterations: Number of iterations (default: 600000, higher is more secure) - length: Length of the derived key in bytes (default: 32) - - Returns: - The derived key as bytes - - Raises: - DevolutionsCryptoException: If key derivation fails - - Example: - >>> derived = derive_key_pbkdf2(b'password', b'salt', iterations=600000, length=32) - """ - ... - -def derive_key_argon2( - key: bytes, - parameters: bytes -) -> bytes: - """ - Derive a cryptographic key from input material using Argon2. - - Args: - key: The input key material - parameters: Argon2 parameters (serialized) - - Returns: - The derived key as bytes - - Raises: - DevolutionsCryptoException: If key derivation fails or invalid parameters - - Example: - >>> derived = derive_key_argon2(b'password', parameters) - """ - ... - -def get_argon2_derivation_parameters(parameters: bytes | None = None) -> bytes: - """ - Build serialized ``DerivationParameters`` from the given Argon2 parameters without - performing any key derivation. - - Args: - parameters: Serialized Argon2Parameters bytes. Uses library defaults when ``None``. - - Returns: - Serialized DerivationParameters bytes suitable for :func:`hash_password_with_params`. - - Raises: - DevolutionsCryptoException: If the parameters are invalid. - - Example: - >>> dp = get_argon2_derivation_parameters() - >>> hashed = hash_password_with_params(b'my password', dp) - """ - ... - -def get_pbkdf2_derivation_parameters(iterations: int = 600000) -> bytes: - """ - Build serialized ``DerivationParameters`` for PBKDF2 with the given iteration count, - without performing any key derivation. - - Args: - iterations: Number of PBKDF2 iterations (default: 600,000). - - Returns: - Serialized DerivationParameters bytes suitable for :func:`hash_password_with_params`. - - Raises: - DevolutionsCryptoException: If parameter generation fails. - - Example: - >>> dp = get_pbkdf2_derivation_parameters(iterations=600000) - >>> hashed = hash_password_with_params(b'my password', dp) - """ - ... - -def generate_keypair(version: int = 0) -> Keypair: - """ - Generate a new asymmetric encryption keypair (X25519). - - Args: - version: Key version (default: 0) - - Returns: - A Keypair object containing public_key and private_key attributes - - Raises: - DevolutionsCryptoException: If keypair generation fails or invalid version - - Example: - >>> keypair = generate_keypair() - >>> public_key = keypair.public_key - >>> private_key = keypair.private_key - """ - ... - -def generate_signing_keypair(version: int = 0) -> bytes: - """ - Generate a new signing keypair (Ed25519). - - Args: - version: Key version (default: 0) - - Returns: - The signing keypair as bytes (contains both private and public key) - - Raises: - DevolutionsCryptoException: If keypair generation fails or invalid version - - Example: - >>> signing_keypair = generate_signing_keypair() - """ - ... - -def generate_secret_key(version: int = 0) -> bytes: - """ - Generate a random secret key for symmetric encryption. - - Args: - version: Key version (default: 0) - - Returns: - The serialized secret key as bytes (header + 32 raw key bytes) - - Raises: - DevolutionsCryptoException: If key generation fails or invalid version - - Example: - >>> secret_key = generate_secret_key() - >>> ciphertext = encrypt_with_secret_key(b'data', secret_key) - """ - ... - -def encrypt_with_secret_key( - data: bytes, - key: bytes, - aad: Optional[bytes] = None, - version: int = 0 -) -> bytes: - """ - Encrypt data using a SecretKey. - - Args: - data: The plaintext data to encrypt - key: The serialized SecretKey (generated by generate_secret_key) - aad: Optional Additional Authenticated Data for AEAD - version: Ciphertext version (default: 0) - - Returns: - The encrypted ciphertext as bytes - - Raises: - DevolutionsCryptoException: If encryption fails or invalid key provided - - Example: - >>> secret_key = generate_secret_key() - >>> ciphertext = encrypt_with_secret_key(b'Hello', secret_key) - """ - ... - -def decrypt_with_secret_key( - data: bytes, - key: bytes, - aad: Optional[bytes] = None -) -> bytes: - """ - Decrypt data that was encrypted with a SecretKey. - - Args: - data: The ciphertext to decrypt - key: The serialized SecretKey used for encryption - aad: Optional Additional Authenticated Data (must match encryption AAD) - - Returns: - The decrypted plaintext as bytes - - Raises: - DevolutionsCryptoException: If decryption fails, authentication fails, or invalid key - - Example: - >>> plaintext = decrypt_with_secret_key(ciphertext, secret_key) - """ - ... - -def derive_encrypt_with_password( - data: bytes, - password: bytes, - aad: Optional[bytes] = None, - key_derivation_version: int = 0, - ciphertext_version: int = 0 -) -> bytes: - """ - Derive a key from a password and encrypt data in a single blob. - - The output contains the key derivation parameters and the ciphertext, - allowing decryption with only the password. - - Args: - data: The plaintext data to encrypt - password: The password to derive the encryption key from - aad: Optional Additional Authenticated Data for AEAD - key_derivation_version: KDF version (0 = latest/Argon2, 1 = PBKDF2) - ciphertext_version: Ciphertext version (0 = latest) - - Returns: - The DeriveEncrypt blob as bytes (KDF params + ciphertext) - - Raises: - DevolutionsCryptoException: If encryption or key derivation fails - - Example: - >>> blob = derive_encrypt_with_password(b'secret data', b'my password') - """ - ... - -def derive_decrypt_with_password( - data: bytes, - password: bytes, - aad: Optional[bytes] = None -) -> bytes: - """ - Decrypt a DeriveEncrypt blob using the original password. - - Re-derives the encryption key from the embedded KDF parameters and password, - then decrypts the embedded ciphertext. - - Args: - data: The DeriveEncrypt blob (produced by derive_encrypt_with_password) - password: The password used during encryption - aad: Optional Additional Authenticated Data (must match encryption AAD) - - Returns: - The decrypted plaintext as bytes - - Raises: - DevolutionsCryptoException: If decryption fails, wrong password, or invalid blob - - Example: - >>> plaintext = derive_decrypt_with_password(blob, b'my password') - """ - ... - -def get_signing_public_key(keypair: bytes) -> bytes: - """ - Extract the public key from a signing keypair. - - Args: - keypair: The signing keypair (generated by generate_signing_keypair) - - Returns: - The public key as bytes - - Raises: - DevolutionsCryptoException: If the keypair format is invalid - - Example: - >>> public_key = get_signing_public_key(signing_keypair) - """ - ... - -def sign( - data: bytes, - keypair: bytes, - version: int = 0 -) -> bytes: - """ - Sign data using a signing keypair (Ed25519). - - Args: - data: The data to sign - keypair: The signing keypair (generated by generate_signing_keypair) - version: Signature version (default: 0) - - Returns: - The signature as bytes - - Raises: - DevolutionsCryptoException: If signing fails or invalid keypair/version - - Example: - >>> signing_keypair = generate_signing_keypair() - >>> signature = sign(b'message', signing_keypair) - """ - ... - -def verify_signature( - data: bytes, - public_key: bytes, - signature: bytes -) -> bool: - """ - Verify a signature against data using a public key. - - Args: - data: The data that was signed - public_key: The signer's public key - signature: The signature to verify - - Returns: - True if the signature is valid, False otherwise - - Raises: - DevolutionsCryptoException: If the signature or public key format is invalid - - Example: - >>> public_key = get_signing_public_key(signing_keypair) - >>> is_valid = verify_signature(b'message', public_key, signature) - """ - ... - -__all__ = [ - 'DevolutionsCryptoException', - 'Keypair', - 'encrypt', - 'decrypt', - 'encrypt_asymmetric', - 'decrypt_asymmetric', - 'hash_password', - 'verify_password', - 'derive_key_pbkdf2', - 'derive_key_argon2', - 'generate_keypair', - 'generate_signing_keypair', - 'get_signing_public_key', - 'sign', - 'verify_signature', - 'generate_secret_key', - 'encrypt_with_secret_key', - 'decrypt_with_secret_key', - 'derive_encrypt_with_password', - 'derive_decrypt_with_password', -] diff --git a/python/pyproject.toml b/python/pyproject.toml index ab782e32..97d5f207 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -29,6 +29,6 @@ requires = ["maturin>=1.10"] build-backend = "maturin" [tool.maturin] -bindings = "pyo3" +bindings = "uniffi" module-name = "devolutions_crypto" -include = ["devolutions_crypto.pyi"] \ No newline at end of file +manifest-path = "../uniffi/devolutions-crypto-uniffi/Cargo.toml" \ No newline at end of file diff --git a/python/src/lib.rs b/python/src/lib.rs deleted file mode 100644 index 9cc26759..00000000 --- a/python/src/lib.rs +++ /dev/null @@ -1,465 +0,0 @@ -use pyo3::create_exception; -use pyo3::prelude::*; -use pyo3::types::{PyBool, PyBytes}; - -use std::convert::TryFrom; - -use devolutions_crypto::utils; -use devolutions_crypto::Error; -use devolutions_crypto::{ciphertext, ciphertext::Ciphertext}; -use devolutions_crypto::{derive_encrypt, derive_encrypt::KdfEncryptedData}; -use devolutions_crypto::{ - key, - key::{PrivateKey, PublicKey, SecretKey}, -}; -use devolutions_crypto::{signature, signature::Signature}; -use devolutions_crypto::{ - signing_key, - signing_key::{SigningKeyPair, SigningPublicKey}, -}; -use devolutions_crypto::{Argon2, Argon2Parameters, Pbkdf2}; -use devolutions_crypto::{ - CiphertextVersion, KeyDerivationVersion, KeyVersion, SignatureVersion, SigningKeyVersion, -}; - -enum DevolutionsCryptoError { - DevolutionsCrypto(Error), - Python(PyErr), -} - -create_exception!( - devolutions_crypto, - DevolutionsCryptoException, - pyo3::exceptions::PyException -); - -#[pyclass] -pub struct Keypair { - #[pyo3(get)] - pub public_key: Py, - #[pyo3(get)] - pub private_key: Py, -} - -type Result = std::result::Result; - -#[pyfunction] -#[pyo3(name = "encrypt")] -#[pyo3(signature = (data, key, aad=None, version=0))] -fn encrypt( - py: Python, - data: &[u8], - key: &[u8], - aad: Option<&[u8]>, - version: u16, -) -> Result> { - let version = match CiphertextVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let ciphertext: Vec = match aad { - Some(aad) => ciphertext::encrypt_with_aad(data, key, aad, version)?.into(), - None => ciphertext::encrypt(data, key, version)?.into(), - }; - - Ok(PyBytes::new(py, &ciphertext).into()) -} - -#[pyfunction] -#[pyo3(name = "encrypt_asymmetric")] -#[pyo3(signature = (data, key, aad=None, version=0))] -fn encrypt_asymmetric( - py: Python, - data: &[u8], - key: &[u8], - aad: Option<&[u8]>, - version: u16, -) -> Result> { - let version = match CiphertextVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let key = PublicKey::try_from(key)?; - - let ciphertext: Vec = match aad { - Some(aad) => ciphertext::encrypt_asymmetric_with_aad(data, &key, aad, version)?.into(), - None => ciphertext::encrypt_asymmetric(data, &key, version)?.into(), - }; - - Ok(PyBytes::new(py, &ciphertext).into()) -} - -#[pyfunction] -#[pyo3(name = "hash_password")] -#[pyo3(signature = (password, version=0))] -fn hash_password(py: Python, password: &[u8], version: u16) -> Result> { - let version = match devolutions_crypto::password_hash::PasswordHashVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let hash: Vec = devolutions_crypto::password_hash::hash_password(password, version)?.into(); - Ok(PyBytes::new(py, &hash).into()) -} - -#[pyfunction] -#[pyo3(name = "hash_password_with_params")] -fn hash_password_with_params(py: Python, password: &[u8], params: &[u8]) -> Result> { - let dp = devolutions_crypto::key_derivation::DerivationParameters::try_from(params)?; - let hash: Vec = - devolutions_crypto::password_hash::hash_password_with_parameters(password, dp)?.into(); - Ok(PyBytes::new(py, &hash).into()) -} - -#[pyfunction] -#[pyo3(name = "verify_password")] -fn verify_password(py: Python, password: &[u8], hash: &[u8]) -> Result> { - let res = devolutions_crypto::password_hash::PasswordHash::try_from(hash)?; - - Ok(PyBool::new(py, res.verify_password(password)) - .to_owned() - .into()) -} - -#[pyfunction] -#[pyo3(name = "decrypt")] -#[pyo3(signature = (data, key, aad=None))] -fn decrypt(py: Python, data: &[u8], key: &[u8], aad: Option<&[u8]>) -> Result> { - let ciphertext: Ciphertext = ciphertext::Ciphertext::try_from(data)?; - let plaintext: Vec = match aad { - Some(aad) => ciphertext.decrypt_with_aad(key, aad)?.into(), - None => ciphertext.decrypt(key)?.into(), - }; - - Ok(PyBytes::new(py, &plaintext).into()) -} - -#[pyfunction] -#[pyo3(name = "decrypt_asymmetric")] -#[pyo3(signature = (data, key, aad=None))] -fn decrypt_asymmetric( - py: Python, - data: &[u8], - key: &[u8], - aad: Option<&[u8]>, -) -> Result> { - let ciphertext: Ciphertext = ciphertext::Ciphertext::try_from(data)?; - let key: PrivateKey = PrivateKey::try_from(key)?; - let plaintext: Vec = match aad { - Some(aad) => ciphertext.decrypt_asymmetric_with_aad(&key, aad)?.into(), - None => ciphertext.decrypt_asymmetric(&key)?.into(), - }; - - Ok(PyBytes::new(py, &plaintext).into()) -} - -#[pyfunction] -#[pyo3(name = "derive_key_pbkdf2")] -#[pyo3(signature = (key, salt=None, iterations=600000, length=32))] -fn derive_key_pbkdf2( - py: Python, - key: &[u8], - salt: Option>, - iterations: u32, - length: usize, -) -> Result> { - let salt = salt.unwrap_or_else(|| vec![0u8; 0]); - - let key = utils::derive_key_pbkdf2(key, &salt, iterations, length); - Ok(PyBytes::new(py, &key).into()) -} - -#[pyfunction] -#[pyo3(name = "derive_key_argon2")] -fn derive_key_argon2(py: Python, key: &[u8], parameters: &[u8]) -> Result> { - let parameters = Argon2Parameters::try_from(parameters)?; - - let key = utils::derive_key_argon2(key, ¶meters)?; - Ok(PyBytes::new(py, &key).into()) -} - -#[pyfunction] -#[pyo3(name = "get_argon2_derivation_parameters")] -#[pyo3(signature = (parameters=None))] -fn get_argon2_derivation_parameters(py: Python, parameters: Option<&[u8]>) -> Result> { - let params = match parameters { - Some(p) => Argon2Parameters::try_from(p)?, - None => Argon2Parameters::default(), - }; - let dp: Vec = devolutions_crypto::key_derivation::Argon2::with_params(params) - .parameters() - .into(); - Ok(PyBytes::new(py, &dp).into()) -} - -#[pyfunction] -#[pyo3(name = "get_pbkdf2_derivation_parameters")] -#[pyo3(signature = (iterations=600000))] -fn get_pbkdf2_derivation_parameters(py: Python, iterations: u32) -> Result> { - let dp: Vec = devolutions_crypto::key_derivation::Pbkdf2::with_params(iterations) - .parameters()? - .into(); - Ok(PyBytes::new(py, &dp).into()) -} - -#[pyfunction] -#[pyo3(name = "sign")] -#[pyo3(signature = (data, keypair, version=0))] -fn sign(py: Python, data: &[u8], keypair: &[u8], version: u16) -> Result> { - let version = match SignatureVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let keypair = SigningKeyPair::try_from(keypair)?; - - let signature: Vec = signature::sign(data, &keypair, version).into(); - Ok(PyBytes::new(py, &signature).into()) -} - -#[pyfunction] -#[pyo3(name = "verify_signature")] -fn verify_signature( - py: Python, - data: &[u8], - public_key: &[u8], - signature: &[u8], -) -> Result> { - let public_key = SigningPublicKey::try_from(public_key)?; - let signature = Signature::try_from(signature)?; - - Ok(PyBool::new(py, signature.verify(data, &public_key)) - .to_owned() - .into()) -} - -#[pyfunction] -#[pyo3(name = "generate_keypair")] -#[pyo3(signature = (version=0))] -fn generate_keypair(py: Python, version: u16) -> Result { - let version = match KeyVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let kp = key::generate_keypair(version); - - let private_key: Vec = kp.private_key.into(); - let public_key: Vec = kp.public_key.into(); - - let keypair = Keypair { - private_key: PyBytes::new(py, &private_key).into(), - public_key: PyBytes::new(py, &public_key).into(), - }; - - Ok(keypair) -} - -#[pyfunction] -#[pyo3(name = "generate_secret_key")] -#[pyo3(signature = (version=0))] -fn generate_secret_key(py: Python, version: u16) -> Result> { - let version = match KeyVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let key = key::generate_secret_key(version); - let bytes: Vec = key.into(); - Ok(PyBytes::new(py, &bytes).into()) -} - -#[pyfunction] -#[pyo3(name = "encrypt_with_secret_key")] -#[pyo3(signature = (data, key, aad=None, version=0))] -fn encrypt_with_secret_key( - py: Python, - data: &[u8], - key: &[u8], - aad: Option<&[u8]>, - version: u16, -) -> Result> { - let version = match CiphertextVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let key = SecretKey::try_from(key)?; - let aad = aad.unwrap_or(&[]); - let ct: Vec = ciphertext::encrypt_with_secret_key_and_aad(data, &key, aad, version)?.into(); - Ok(PyBytes::new(py, &ct).into()) -} - -#[pyfunction] -#[pyo3(name = "decrypt_with_secret_key")] -#[pyo3(signature = (data, key, aad=None))] -fn decrypt_with_secret_key( - py: Python, - data: &[u8], - key: &[u8], - aad: Option<&[u8]>, -) -> Result> { - let key = SecretKey::try_from(key)?; - let aad = aad.unwrap_or(&[]); - let ct = ciphertext::Ciphertext::try_from(data)?; - let plaintext = ct.decrypt_with_secret_key_and_aad(&key, aad)?; - Ok(PyBytes::new(py, &plaintext).into()) -} - -#[pyfunction] -#[pyo3(name = "generate_signing_keypair")] -#[pyo3(signature = (version=0))] -fn generate_signing_keypair(py: Python, version: u16) -> Result> { - let version = match SigningKeyVersion::try_from(version) { - Ok(v) => v, - Err(_) => { - let error: DevolutionsCryptoError = Error::UnknownVersion.into(); - return Err(error); - } - }; - - let kp = signing_key::generate_signing_keypair(version); - - let kp: Vec = kp.into(); - - Ok(PyBytes::new(py, &kp).into()) -} - -#[pyfunction] -#[pyo3(name = "derive_encrypt_with_password")] -#[pyo3(signature = (data, password, aad=None, key_derivation_version=0, ciphertext_version=0))] -fn derive_encrypt_with_password( - py: Python, - data: &[u8], - password: &[u8], - aad: Option<&[u8]>, - key_derivation_version: u16, - ciphertext_version: u16, -) -> Result> { - let kdf_version = match KeyDerivationVersion::try_from(key_derivation_version) { - Ok(v) => v, - Err(_) => return Err(Error::UnknownVersion.into()), - }; - let ct_version = match CiphertextVersion::try_from(ciphertext_version) { - Ok(v) => v, - Err(_) => return Err(Error::UnknownVersion.into()), - }; - let aad = aad.unwrap_or(&[]); - - let params = match kdf_version { - KeyDerivationVersion::Latest | KeyDerivationVersion::V2 => Argon2::new().parameters(), - KeyDerivationVersion::V1 => Pbkdf2::new() - .parameters() - .expect("default PKBDF2 parameters shouldn't fail"), - }; - let result: Vec = - derive_encrypt::encrypt_with_password_and_aad(data, password, aad, params, ct_version)? - .into(); - Ok(PyBytes::new(py, &result).into()) -} - -#[pyfunction] -#[pyo3(name = "derive_decrypt_with_password")] -#[pyo3(signature = (data, password, aad=None))] -fn derive_decrypt_with_password( - py: Python, - data: &[u8], - password: &[u8], - aad: Option<&[u8]>, -) -> Result> { - let aad = aad.unwrap_or(&[]); - let blob = KdfEncryptedData::try_from(data)?; - let plaintext = blob.decrypt_with_password_and_aad(password, aad)?; - Ok(PyBytes::new(py, &plaintext).into()) -} - -#[pyfunction] -#[pyo3(name = "get_signing_public_key")] -fn get_signing_public_key(py: Python, keypair: &[u8]) -> Result> { - let keypair = SigningKeyPair::try_from(keypair)?; - - let public_key: Vec = keypair.get_public_key().into(); - - Ok(PyBytes::new(py, &public_key).into()) -} - -#[pymodule] -#[pyo3(name = "devolutions_crypto")] -fn devolutions_crypto_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(encrypt, m)?)?; - m.add_function(wrap_pyfunction!(decrypt, m)?)?; - m.add_function(wrap_pyfunction!(encrypt_asymmetric, m)?)?; - m.add_function(wrap_pyfunction!(decrypt_asymmetric, m)?)?; - m.add_function(wrap_pyfunction!(hash_password, m)?)?; - m.add_function(wrap_pyfunction!(hash_password_with_params, m)?)?; - m.add_function(wrap_pyfunction!(verify_password, m)?)?; - m.add_function(wrap_pyfunction!(derive_key_pbkdf2, m)?)?; - m.add_function(wrap_pyfunction!(derive_key_argon2, m)?)?; - m.add_function(wrap_pyfunction!(get_argon2_derivation_parameters, m)?)?; - m.add_function(wrap_pyfunction!(get_pbkdf2_derivation_parameters, m)?)?; - m.add_function(wrap_pyfunction!(sign, m)?)?; - m.add_function(wrap_pyfunction!(verify_signature, m)?)?; - m.add_function(wrap_pyfunction!(generate_keypair, m)?)?; - m.add_function(wrap_pyfunction!(generate_signing_keypair, m)?)?; - m.add_function(wrap_pyfunction!(get_signing_public_key, m)?)?; - m.add_function(wrap_pyfunction!(generate_secret_key, m)?)?; - m.add_function(wrap_pyfunction!(encrypt_with_secret_key, m)?)?; - m.add_function(wrap_pyfunction!(decrypt_with_secret_key, m)?)?; - m.add_function(wrap_pyfunction!(derive_encrypt_with_password, m)?)?; - m.add_function(wrap_pyfunction!(derive_decrypt_with_password, m)?)?; - m.add_class::()?; - m.add( - "DevolutionsCryptoException", - m.py().get_type::(), - )?; - - Ok(()) -} - -impl From for PyErr { - fn from(error: DevolutionsCryptoError) -> Self { - match error { - DevolutionsCryptoError::DevolutionsCrypto(error) => { - let description: String = error.to_string(); - let name: &str = error.into(); - DevolutionsCryptoException::new_err((name, description)) - } - DevolutionsCryptoError::Python(error) => error, - } - } -} - -impl From for DevolutionsCryptoError { - fn from(error: Error) -> Self { - Self::DevolutionsCrypto(error) - } -} - -impl From for DevolutionsCryptoError { - fn from(error: PyErr) -> Self { - Self::Python(error) - } -} diff --git a/python/src/uniffi-bindgen.rs b/python/src/uniffi-bindgen.rs new file mode 100644 index 00000000..0f592eaf --- /dev/null +++ b/python/src/uniffi-bindgen.rs @@ -0,0 +1,7 @@ +#[cfg(not(target_arch = "wasm32"))] +fn main() { + uniffi::uniffi_bindgen_main() +} + +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/wrappers/python/tests/asymmetric.py b/wrappers/python/tests/asymmetric.py index fd0e8467..47c20070 100644 --- a/wrappers/python/tests/asymmetric.py +++ b/wrappers/python/tests/asymmetric.py @@ -16,15 +16,15 @@ def test_asymmetric_with_aad(self): plaintext = b'Test plaintext' aad = b"Test AAD" - ciphertext = devolutions_crypto.encrypt_asymmetric(plaintext, keypair.public_key, aad) + ciphertext = devolutions_crypto.encrypt_asymmetric_with_aad(plaintext, keypair.public_key, aad) - self.assertEqual(devolutions_crypto.decrypt_asymmetric(ciphertext, keypair.private_key, aad), plaintext) + self.assertEqual(devolutions_crypto.decrypt_asymmetric_with_aad(ciphertext, keypair.private_key, aad), plaintext) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): devolutions_crypto.decrypt_asymmetric(ciphertext, keypair.private_key) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): - devolutions_crypto.decrypt_asymmetric(ciphertext, keypair.private_key, aad = b"Wrong AAD") + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.decrypt_asymmetric_with_aad(ciphertext, keypair.private_key, b"Wrong AAD") if __name__ == "__main__": diff --git a/wrappers/python/tests/conformity.py b/wrappers/python/tests/conformity.py index 05b4de70..6111e9ab 100644 --- a/wrappers/python/tests/conformity.py +++ b/wrappers/python/tests/conformity.py @@ -4,7 +4,7 @@ class TestComformity(unittest.TestCase): def test_derive_pbkdf2(self): - self.assertEqual(devolutions_crypto.derive_key_pbkdf2(b'testpassword'), b64decode(b'wdU+cxAOpTFddVhTQlKQTSzmVjZqPAXVx1cRrAqTGek=')) + self.assertEqual(devolutions_crypto.derive_key_pbkdf2(b'testpassword', None), b64decode(b'wdU+cxAOpTFddVhTQlKQTSzmVjZqPAXVx1cRrAqTGek=')) self.assertEqual(devolutions_crypto.derive_key_pbkdf2(b'testPa$$', None, 100), b64decode(b'ev/GiJLvOgIkkWrnIrHSi2fdZE5qJBIrW+DLeMLIXK4=')) self.assertEqual(devolutions_crypto.derive_key_pbkdf2(b'testPa$$', b64decode(b'tdTt5wgeqQYLvkiXKkFirqy2hMbzadBtL+jekVeNCRA='), 100), b64decode(b'ZaYRZeQiIPJ+Jl511AgHZjv4/HbCFq4eUP9yNa3gowI=')) @@ -13,13 +13,13 @@ def test_decrypt_v1(self): ciphertext = b64decode(b'DQwCAAAAAQCK1twEut+TeJfFbTWCRgHjyS6bOPOZUEQAeBtSFFRl2jHggM/34n68zIZWGbsZHkufVzU6mTN5N2Dx9bTplrycv5eNVevT4P9FdVHJ751D+A==') self.assertEqual(devolutions_crypto.decrypt(ciphertext, key), b'test Ciph3rtext~') - + def test_decrypt_v1_with_aad(self): key = b64decode(b'ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=') aad = b"this is some public data" ciphertext = b64decode(b'DQwCAAEAAQCeKfbTqYjfVCEPEiAJjiypBstPmZz0AnpliZKoR+WXTKdj2f/4ops0++dDBVZ+XdyE1KfqxViWVc9djy/HSCcPR4nDehtNI69heGCIFudXfQ==') - self.assertEqual(devolutions_crypto.decrypt(ciphertext, key, aad=aad), b'test Ciph3rtext~') + self.assertEqual(devolutions_crypto.decrypt_with_aad(ciphertext, key, aad), b'test Ciph3rtext~') def test_decrypt_v2(self): key = b64decode(b'ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=') @@ -32,20 +32,20 @@ def test_decrypt_v2_with_aad(self): aad = b"this is some public data" ciphertext = b64decode(b'DQwCAAEAAgA9bh989dao0Pvaz1NpJTI5m7M4br2qVjZtFwXXoXZOlkCjtqU/uif4pbNCcpEodzeP4YG1QvfKVQ==') - self.assertEqual(devolutions_crypto.decrypt(ciphertext, key, aad=aad), b'test Ciph3rtext~') + self.assertEqual(devolutions_crypto.decrypt_with_aad(ciphertext, key, aad), b'test Ciph3rtext~') def test_asymmetric_v2(self): private_key = b64decode(b'DQwBAAEAAQAAwQ3oJvU6bq2iZlJwAzvbmqJczNrFoeWPeIyJP9SSbQ==') ciphertext = b64decode(b'DQwCAAIAAgCIG9L2MTiumytn7H/p5I3aGVdhV3WUL4i8nIeMWIJ1YRbNQ6lEiQDAyfYhbs6gg1cD7+5Ft2Q5cm7ArsGfiFYWnscm1y7a8tAGfjFFTonzrg==') - + self.assertEqual(devolutions_crypto.decrypt_asymmetric(ciphertext, private_key), b"testdata") def test_asymmetric_v2_with_aad(self): private_key = b64decode(b'DQwBAAEAAQC9qf9UY1ovL/48ALGHL9SLVpVozbdjYsw0EPerUl3zYA==') ciphertext = b64decode(b'DQwCAAIAAgB1u62xYeyppWf83QdWwbwGUt5QuiAFZr+hIiFEvMRbXiNCE3RMBNbmgQkLr/vME0BeQa+uUTXZARvJcyNXHyAE4tSdw6o/psU/kw/Z/FbsPw==') aad = b"this is some public data" - - self.assertEqual(devolutions_crypto.decrypt_asymmetric(ciphertext, private_key, aad=aad), b"testdata") + + self.assertEqual(devolutions_crypto.decrypt_asymmetric_with_aad(ciphertext, private_key, aad), b"testdata") def test_signature(self): signature = b64decode(b'DQwGAAAAAQD82uRk4sFC8vEni6pDNw/vOdN1IEDg9cAVfprWJZ/JBls9Gi61cUt5u6uBJtseNGZFT7qKLvp4NUZrAOL8FH0K') diff --git a/wrappers/python/tests/derive_encrypt.py b/wrappers/python/tests/derive_encrypt.py new file mode 100644 index 00000000..d7af4ac8 --- /dev/null +++ b/wrappers/python/tests/derive_encrypt.py @@ -0,0 +1,59 @@ +import unittest +import devolutions_crypto + + +class TestDeriveEncrypt(unittest.TestCase): + def test_roundtrip_with_password(self): + plaintext = b"hello world" + password = b"mypassword" + + blob = devolutions_crypto.derive_encrypt_with_password(plaintext, password) + + self.assertEqual(devolutions_crypto.derive_decrypt_with_password(blob, password), plaintext) + + def test_blob_differs_from_plaintext(self): + plaintext = b"sensitive data" + password = b"password123" + + blob = devolutions_crypto.derive_encrypt_with_password(plaintext, password) + + self.assertNotEqual(blob, plaintext) + + def test_each_encryption_uses_random_salt(self): + plaintext = b"same data" + password = b"same password" + + blob1 = devolutions_crypto.derive_encrypt_with_password(plaintext, password) + blob2 = devolutions_crypto.derive_encrypt_with_password(plaintext, password) + + self.assertNotEqual(blob1, blob2) + + def test_wrong_password_fails(self): + blob = devolutions_crypto.derive_encrypt_with_password(b"secret", b"correct-password") + + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.derive_decrypt_with_password(blob, b"wrong-password") + + def test_roundtrip_with_aad(self): + plaintext = b"authenticated data" + password = b"mypassword" + aad = b"context" + + blob = devolutions_crypto.derive_encrypt_with_password_and_aad(plaintext, password, aad) + + self.assertEqual( + devolutions_crypto.derive_decrypt_with_password_and_aad(blob, password, aad), + plaintext, + ) + + # Wrong aad fails to decrypt. + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.derive_decrypt_with_password_and_aad(blob, password, b"wrong-context") + + # An aad-encrypted blob cannot be decrypted without the aad. + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.derive_decrypt_with_password(blob, password) + + +if __name__ == "__main__": + unittest.main() diff --git a/wrappers/python/tests/password_hash.py b/wrappers/python/tests/password_hash.py index 16b461f5..3b7ef46d 100644 --- a/wrappers/python/tests/password_hash.py +++ b/wrappers/python/tests/password_hash.py @@ -12,7 +12,7 @@ def test_hash_password_default(self): def test_hash_password_v1_pbkdf2(self): """Explicit V1 uses PBKDF2-SHA256.""" password = b'my_secure_password' - hash_value = devolutions_crypto.hash_password(password, version=1) + hash_value = devolutions_crypto.hash_password(password, version=devolutions_crypto.PasswordHashVersion.V1) self.assertTrue(devolutions_crypto.verify_password(password, hash_value)) def test_verify_wrong_password(self): @@ -32,7 +32,9 @@ def test_hash_is_non_deterministic(self): def test_hash_password_with_argon2_params(self): """hash_password_with_params works with default Argon2id parameters.""" password = b'my_secure_password' - params = devolutions_crypto.get_argon2_derivation_parameters() + params = devolutions_crypto.get_argon2_derivation_parameters( + devolutions_crypto.Argon2ParametersBuilder().build() + ) hash_value = devolutions_crypto.hash_password_with_params(password, params) self.assertTrue(devolutions_crypto.verify_password(password, hash_value)) self.assertFalse(devolutions_crypto.verify_password(b'wrong_password', hash_value)) @@ -47,17 +49,12 @@ def test_hash_password_with_pbkdf2_params(self): def test_verify_invalid_hash_raises(self): """verify_password raises on invalid/truncated hash bytes.""" - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): devolutions_crypto.verify_password(b'password', b'not_a_valid_hash') - def test_hash_password_unknown_version_raises(self): - """hash_password raises on an unknown version number.""" - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): - devolutions_crypto.hash_password(b'password', version=999) - def test_hash_password_with_params_invalid_params_raises(self): """hash_password_with_params raises on invalid DerivationParameters bytes.""" - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): devolutions_crypto.hash_password_with_params(b'password', b'invalid_params') diff --git a/wrappers/python/tests/signature.py b/wrappers/python/tests/signature.py index aaabe28c..55ec30f6 100644 --- a/wrappers/python/tests/signature.py +++ b/wrappers/python/tests/signature.py @@ -6,9 +6,9 @@ class TestSignature(unittest.TestCase): def test_signature(self): data = b"this is some test data" keypair = devolutions_crypto.generate_signing_keypair() - public = devolutions_crypto.get_signing_public_key(keypair) + public = keypair.get_public_key() - signature = devolutions_crypto.sign(data, keypair) + signature = devolutions_crypto.sign(data, keypair.get_private_key()) self.assertTrue(devolutions_crypto.verify_signature(data, public, signature)) self.assertFalse(devolutions_crypto.verify_signature(b"this data is wrong", public, signature)) diff --git a/wrappers/python/tests/symmetric.py b/wrappers/python/tests/symmetric.py index d6595485..aec5401f 100644 --- a/wrappers/python/tests/symmetric.py +++ b/wrappers/python/tests/symmetric.py @@ -11,21 +11,21 @@ def test_symmetric(self): ciphertext = devolutions_crypto.encrypt(plaintext, key) self.assertEqual(devolutions_crypto.decrypt(ciphertext, key), plaintext) - + def test_symmetric_with_aad(self): key = os.urandom(32) plaintext = b'Test plaintext' aad = b"Test AAD" - ciphertext = devolutions_crypto.encrypt(plaintext, key, aad) + ciphertext = devolutions_crypto.encrypt_with_aad(plaintext, key, aad) - self.assertEqual(devolutions_crypto.decrypt(ciphertext, key, aad), plaintext) + self.assertEqual(devolutions_crypto.decrypt_with_aad(ciphertext, key, aad), plaintext) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): devolutions_crypto.decrypt(ciphertext, key) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): - devolutions_crypto.decrypt(ciphertext, key, aad = b"Wrong AAD") + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.decrypt_with_aad(ciphertext, key, b"Wrong AAD") def test_symmetric_with_secret_key(self): key = devolutions_crypto.generate_secret_key() @@ -40,15 +40,15 @@ def test_symmetric_with_secret_key_and_aad(self): plaintext = b'Test plaintext' aad = b"Test AAD" - ciphertext = devolutions_crypto.encrypt_with_secret_key(plaintext, key, aad) + ciphertext = devolutions_crypto.encrypt_with_secret_key_and_aad(plaintext, key, aad) - self.assertEqual(devolutions_crypto.decrypt_with_secret_key(ciphertext, key, aad), plaintext) + self.assertEqual(devolutions_crypto.decrypt_with_secret_key_and_aad(ciphertext, key, aad), plaintext) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): devolutions_crypto.decrypt_with_secret_key(ciphertext, key) - with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): - devolutions_crypto.decrypt_with_secret_key(ciphertext, key, aad = b"Wrong AAD") + with self.assertRaises(devolutions_crypto.DevolutionsCryptoError): + devolutions_crypto.decrypt_with_secret_key_and_aad(ciphertext, key, b"Wrong AAD") if __name__ == "__main__":