diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1e811d3e..09892bde 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,4 @@ +--- image: gitlab.cosmian.com:5000/core/ci-rust:latest variables: @@ -67,7 +68,7 @@ is_publishable: .base_compile: &base_compile stage: build cache: - key: "${CI_COMMIT_REF_SLUG}" + key: ${CI_COMMIT_REF_SLUG} policy: pull paths: - $CARGO_HOME @@ -144,6 +145,19 @@ build_android: - jniLibs expire_in: 3 mos +build_python: + stage: build + image: + name: ghcr.io/pyo3/maturin + # remove the image custom entrypoint because it is not supported by gitlab runners + entrypoint: [''] + script: + - maturin build --release --features python + artifacts: + paths: + - target/wheels/*.whl + expire_in: 3 mos + test_cloudproof_java: image: openjdk:8 stage: test @@ -159,12 +173,8 @@ test_cloudproof_java: test_python: stage: test script: - - maturin build --release --features python - - bash src/interfaces/pyo3/tests/test.sh - artifacts: - paths: - - target/wheels/*.whl - expire_in: 3 mos + - pip install --force-reinstall target/wheels/cover_crypt*.whl + - python3 src/interfaces/pyo3/tests/test_cover_crypt.py test_cloudproof_js: image: node:16 @@ -181,13 +191,13 @@ test_cloudproof_js: pack_all_artifacts: stage: pack rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/' + - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ before_script: - apt update && apt install -y zip script: - zip -r ${CI_PROJECT_NAME}-${CI_COMMIT_TAG}-bin.zip pkg target jniLibs artifacts: - name: "cosmian_${CI_PROJECT_NAME}_${CI_COMMIT_TAG}" + name: cosmian_${CI_PROJECT_NAME}_${CI_COMMIT_TAG} paths: - ${CI_PROJECT_NAME}-${CI_COMMIT_TAG}-bin.zip expire_in: 3 mos @@ -196,7 +206,7 @@ npm_publish: image: gitlab.cosmian.com:5000/core/ci-npm:latest stage: publish rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/' + - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ script: - echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" > ~/.npmrc - wasm-pack build --target web --release --features wasm_bindgen @@ -206,7 +216,7 @@ npm_publish: cargo_publish: stage: publish rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/' + - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ script: - echo "[registry]" > ~/.cargo/credentials - echo "token = \"$CRATES_IO\"" >> ~/.cargo/credentials @@ -217,11 +227,19 @@ cargo_publish: - cargo publish --token $CRATES_IO - rm -rf /tmp/${CI_PROJECT_NAME} +python_publish: + stage: publish + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ + script: + - pip install twine + - twine upload -u "${PYPI_USERNAME}" -p "${PYPI_PASSWORD}" target/wheels/cover_crypt-${CI_COMMIT_TAG}*.whl + # Finally, run benchmarks at once benchmarks: stage: publish rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/' + - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ before_script: - apt update && apt install -y gnuplot script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d15e678..4614de5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. ### Changed - improve serialization +- new python interfaces based on objects rather than functions covering a broader range of functionalities ### Fixed @@ -57,6 +58,7 @@ All notable changes to this project will be documented in this file. --- --- + ## [6.0.8] - 2022-10-17 ### Added @@ -68,14 +70,17 @@ All notable changes to this project will be documented in this file. ### Fixed ### Removed + --- --- + ## [6.0.7] - 2022-10-14 ### Added - expose boolean Access Policy parsing in WASM + ### Fixed ### Removed diff --git a/Cargo.lock b/Cargo.lock index 04c9500c..414dd479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,7 +180,7 @@ dependencies = [ [[package]] name = "cosmian_cover_crypt" -version = "7.0.1" +version = "8.0.0" dependencies = [ "abe_policy", "cosmian_crypto_core", @@ -202,8 +202,9 @@ dependencies = [ [[package]] name = "cosmian_crypto_core" -version = "4.0.1" -source = "git+https://github.com/Cosmian/crypto_core?branch=develop#945c06d86c7c5cfa8d63245b7a32eb84b04923eb" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5490966c09f4f783327dc7b5dea4c4a7d164fcdfb5fc4a277f3677473017ca" dependencies = [ "aes-gcm", "curve25519-dalek", @@ -228,8 +229,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.3.6" -source = "git+https://github.com/bheisler/criterion.rs?branch=version-0.4#935c6327e152e44f2b9178797682b9b99b5123a5" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" dependencies = [ "anes", "atty", @@ -251,8 +253,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.4.5" -source = "git+https://github.com/bheisler/criterion.rs?branch=version-0.4#935c6327e152e44f2b9178797682b9b99b5123a5" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", @@ -479,6 +482,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -549,9 +561,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" @@ -564,13 +576,14 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.16.6" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0220c44442c9b239dd4357aa856ac468a4f5e1f0df19ddb89b2522952eb4c6ca" +checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543" dependencies = [ "cfg-if", "indoc", "libc", + "memoffset", "parking_lot", "pyo3-build-config", "pyo3-ffi", @@ -580,9 +593,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.6" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c819d397859445928609d0ec5afc2da5204e0d0f73d6bf9e153b04e83c9cdc2" +checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8" dependencies = [ "once_cell", "target-lexicon", @@ -590,9 +603,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.6" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca882703ab55f54702d7bfe1189b41b0af10272389f04cae38fe4cd56c65f75f" +checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc" dependencies = [ "libc", "pyo3-build-config", @@ -600,9 +613,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.6" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568749402955ad7be7bad9a09b8593851cd36e549ac90bfd44079cea500f3f21" +checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -612,9 +625,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.6" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611f64e82d98f447787e82b8e7b0ebc681e1eb78fc1252668b2c605ffb4e1eb8" +checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f" dependencies = [ "proc-macro2", "quote", @@ -669,18 +682,18 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "ryu" @@ -775,9 +788,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" +checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" [[package]] name = "textwrap" diff --git a/Cargo.toml b/Cargo.toml index 146d8bc3..e76ba8f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,58 +1,51 @@ [package] name = "cosmian_cover_crypt" -version = "7.0.1" -edition = "2021" +version = "8.0.0" authors = [ "Théophile Brezot ", "Bruno Grieder ", ] -description = "Key Policy attribute encryption based on subset cover" documentation = "https://docs.rs/cosmian_cover_crypt/" +edition = "2021" license = "MIT/Apache-2.0" repository = "https://github.com/Cosmian/cover_crypt" +description = "Key Policy attribute encryption based on subset cover" [lib] -name = "cosmian_cover_crypt" crate-type = ["rlib", "cdylib", "staticlib"] +name = "cosmian_cover_crypt" # The cdylib is only interesting if the `--features ffi` flag is set on build # This does not seem to be actionable conditionally https://github.com/rust-lang/cargo/issues/4881 [[bench]] -name = "benches" harness = false +name = "benches" [profile.bench] debug = true [features] ffi = ["lazy_static"] -wasm_bindgen = ["js-sys", "wasm-bindgen"] -python = ["pyo3"] full_bench = [] +python = ["pyo3"] +wasm_bindgen = ["js-sys", "wasm-bindgen"] [dependencies] abe_policy = "1.0" -cosmian_crypto_core = { git = "https://github.com/Cosmian/crypto_core", branch = "develop"} +cosmian_crypto_core = "5.0.0" hex = "0.4" +js-sys = { version = "0.3", optional = true } +lazy_static = { version = "1.4", optional = true } leb128 = "0.2" +pyo3 = { version = "0.17.3", features = ["extension-module", "abi3", "abi3-py37"], optional = true } rand_core = { version = "0.6", features = ["getrandom"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha3 = "0.10" thiserror = "1.0" +wasm-bindgen = { version = "0.2", features = ["serde-serialize"], optional = true } zeroize = "1.5" -# Optional ones -js-sys = { version = "0.3", optional = true } -lazy_static = { version = "1.4", optional = true } -pyo3 = { version = "0.16.3", features = ["extension-module"], optional = true } -wasm-bindgen = { version = "0.2", features = [ - "serde-serialize", -], optional = true } [dev-dependencies] -wasm-bindgen-test = { version = "0.3" } -# Criterion used on branch "version-0.4" -# serde_cbor is unmaintained: https://rustsec.org/advisories/RUSTSEC-2021-0127 -criterion = { git = "https://github.com/bheisler/criterion.rs", branch = "version-0.4", features = [ - "html_reports", -], default_features = false } +criterion = { version = "0.4", features = ["html_reports"], default_features = false } +wasm-bindgen-test = "0.3" diff --git a/README.md b/README.md index 2b569677..1c3c04c3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CoverCrypt   [![Build Status]][actions] [![Latest Version]][crates.io] -[Build Status]: https://img.shields.io/github/workflow/status/Cosmian/cosmian_cover_crypt/CI%20checks/main +[build status]: https://img.shields.io/github/workflow/status/Cosmian/cosmian_cover_crypt/CI%20checks/main [actions]: https://github.com/Cosmian/cosmian_cover_crypt/actions?query=branch%3Amain -[Latest Version]: https://img.shields.io/crates/v/cosmian_cover_crypt.svg +[latest version]: https://img.shields.io/crates/v/cosmian_cover_crypt.svg [crates.io]: https://crates.io/crates/cosmian_cover_crypt Implementation of the [CoverCrypt](bib/CoverCrypt.pdf) algorithm which allows @@ -14,7 +14,7 @@ policies over these attributes. The following code sample introduces the CoverCrypt functionalities. It can be run from `examples/runme.rs` using `cargo run --example runme`. -``` rust +```rust use abe_policy::{AccessPolicy, Attribute, Policy, PolicyAxis}; use cosmian_cover_crypt::{ interfaces::statics::{CoverCryptX25519Aes256, EncryptedHeader}, @@ -113,33 +113,39 @@ assert!(encrypted_header.decrypt(&cover_crypt, &usk, None).is_err()); # Building and testing To build the core only, run: -``` bash + +```bash cargo build --release ``` To build the FFI interface: -``` bash + +```bash cargo build --release --features interfaces ``` To build everything (including the FFI): -``` bash + +```bash cargo build --release --all-features ``` The latter will build a shared library. On Linux, one can verify that the FFI symbols are present using: -``` bash + +```bash objdump -T target/release/libcosmian_cover_crypt.so ``` The code contains numerous tests that you can run using: -``` bash + +```bash cargo test --release --all-features ``` Benchmarks can be run using (one can pass any feature flag): -``` bash + +```bash cargo bench ``` @@ -147,22 +153,21 @@ cargo bench Go to the [build](build/glibc-2.17/) directory for an example on how to build for GLIBC 2.17 -### Building for Pyo3 +### Build and tests for Pyo3 ```bash -maturin develop --cargo-extra-args="--release --features python +./src/interfaces/pyo3/tests/test.sh ``` - ## Features and Benchmarks - In CoverCrypt, messages are encrypted using a symmetric scheme. The right management is performed by a novel asymmetric scheme which is used to encapsulate a symmetric key. This encapsulation is stored in an object called encrypted header, along with the symmetric ciphertext. This design brings several advantages: + - the central authority has a unique key to protect (the master secret key); - encapsulation can be performed without the need to store any sensitive information (public cryptography); @@ -175,13 +180,15 @@ i7-10750H CPU @ 3.20GHz. Asymmetric keys must be generated beforehand. This is the role of a central authority, which is in charge of: + - generating and updating the master keys according to the right policy; - generate and update user secret keys. The CoverCrypt APIs exposes everything that is needed: -- `CoverCrypt::setup` : generate master keys -- `CoverCrypt::join` : create a user secret key for the given rights -- `CoverCrypt::update` : update the master keys for the given policy + +- `CoverCrypt::setup` : generate master keys +- `CoverCrypt::join` : create a user secret key for the given rights +- `CoverCrypt::update` : update the master keys for the given policy - `CoverCrypt::refresh` : refresh a user secret key from the master secret key The key generations may be long if the policy contains many rights or if there diff --git a/pyproject.toml b/pyproject.toml index abae8955..a2da4a41 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,17 @@ [build-system] -requires = ["maturin>=0.12,<0.13"] +requires = ["maturin>=0.13,<0.14"] build-backend = "maturin" +[tool.maturin] +# https://github.com/pypa/manylinux#manylinux +compatibility = "manylinux_2_17" + [project] name = "cover_crypt" -requires-python = ">=3.6" +requires-python = ">=3.7" classifiers = [ - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = ["cffi"] diff --git a/src/error.rs b/src/error.rs index e4e91f83..fb4eeeae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,7 +30,7 @@ pub enum Error { #[error("could not decode number of attributes in encrypted message")] DecodingAttributeNumber, #[error( - "Unable to decrypt the header size. User decryption key has not the right policy to \ + "Unable to decrypt the header. User decryption key has not the right policy to \ decrypt this input." )] InsufficientAccessPolicy, diff --git a/src/interfaces/pyo3/generate_cc_keys.rs b/src/interfaces/pyo3/generate_cc_keys.rs deleted file mode 100644 index df9e2dc5..00000000 --- a/src/interfaces/pyo3/generate_cc_keys.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::{ - api::CoverCrypt, - interfaces::statics::{CoverCryptX25519Aes256, MasterSecretKey}, -}; -use abe_policy::{AccessPolicy, Attribute, Policy, PolicyAxis}; -use cosmian_crypto_core::bytes_ser_de::Serializable; -use pyo3::{exceptions::PyTypeError, prelude::*}; - -/// Generate the master authority keys for supplied Policy -/// -/// - `policy_bytes` : Policy to use to generate the keys (JSON serialized) -#[pyfunction] -pub fn generate_master_keys(policy_bytes: Vec) -> PyResult<(Vec, Vec)> { - let policy: Policy = serde_json::from_slice(policy_bytes.as_slice()) - .map_err(|e| PyTypeError::new_err(format!("Policy deserialization failed: {e}")))?; - - let (msk, mpk) = CoverCryptX25519Aes256::default().generate_master_keys(&policy)?; - - Ok((msk.try_to_bytes()?, mpk.try_to_bytes()?)) -} - -/// Generate a user secret key. -/// -/// - `msk_bytes` : master secret key -/// - `access_policy_str` : user access policy -/// - `policy_bytes` : global policy -#[pyfunction] -pub fn generate_user_secret_key( - msk_bytes: Vec, - access_policy_str: String, - policy_bytes: Vec, -) -> PyResult> { - let msk = MasterSecretKey::try_from_bytes(&msk_bytes)?; - let policy = serde_json::from_slice(&policy_bytes) - .map_err(|e| PyTypeError::new_err(format!("Policy deserialization failed: {e}")))?; - let access_policy = AccessPolicy::from_boolean_expression(&access_policy_str) - .map_err(|e| PyTypeError::new_err(format!("Access policy creation failed: {e}")))?; - - let usk = CoverCryptX25519Aes256::default().generate_user_secret_key( - &msk, - &access_policy, - &policy, - )?; - - Ok(usk.try_to_bytes()?) -} - -/// Generate ABE policy from axis given in serialized JSON -/// -/// - `policy_axis_bytes`: as many axis as needed -/// - `max_attribute_value`: maximum number of attributes that can be used in -/// policy -#[pyfunction] -pub fn generate_policy(policy_axis_bytes: Vec, max_attribute_value: u32) -> PyResult> { - let policy_axis: Vec = serde_json::from_slice(&policy_axis_bytes) - .map_err(|e| PyTypeError::new_err(format!("Policy Axis deserialization failed: {e}")))?; - let mut policy = Policy::new(max_attribute_value); - for axis in &policy_axis { - let attrs = axis - .attributes - .iter() - .map(std::ops::Deref::deref) - .collect::>(); - policy - .add_axis(&PolicyAxis::new(&axis.name, &attrs, axis.hierarchical)) - .map_err(|e| PyTypeError::new_err(format!("Error adding axes: {e}")))?; - } - - let policy_bytes = serde_json::to_vec(&policy) - .map_err(|e| PyTypeError::new_err(format!("Error serializing policy: {e}")))?; - - Ok(policy_bytes) -} - -/// Rotate attributes: changing its underlying value with that of an unused slot -/// -/// Returns the new policy with refreshed attributes -#[pyfunction] -pub fn rotate_attributes(attributes_bytes: Vec, policy_bytes: Vec) -> PyResult> { - let attributes: Vec = serde_json::from_slice(&attributes_bytes) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing attributes: {e}")))?; - let mut policy: Policy = serde_json::from_slice(&policy_bytes) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing policy: {e}")))?; - - for attr in &attributes { - policy - .rotate(attr) - .map_err(|e| PyTypeError::new_err(format!("Rotation failed: {e}")))?; - } - serde_json::to_vec(&policy) - .map_err(|e| PyTypeError::new_err(format!("Error serializing policy: {e}"))) -} diff --git a/src/interfaces/pyo3/hybrid_cc_aes.rs b/src/interfaces/pyo3/hybrid_cc_aes.rs deleted file mode 100644 index 04412ceb..00000000 --- a/src/interfaces/pyo3/hybrid_cc_aes.rs +++ /dev/null @@ -1,273 +0,0 @@ -// needed to remove wasm_bindgen warnings -#![allow(non_upper_case_globals)] -// Wait for `wasm-bindgen` issue 2774: https://github.com/rustwasm/wasm-bindgen/issues/2774 - -use crate::{ - api::CoverCrypt, - interfaces::statics::{ - CoverCryptX25519Aes256, EncryptedHeader, PublicKey, SymmetricKey, UserSecretKey, - }, -}; -use abe_policy::AccessPolicy; -use cosmian_crypto_core::{ - bytes_ser_de::{Deserializer, Serializable, Serializer}, - KeyTrait, -}; -use pyo3::{exceptions::PyTypeError, pyfunction, PyResult}; - -/// Generate an encrypted header. A header contains the following elements: -/// -/// - `encapsulation_size` : the size of the symmetric key encapsulation (u32) -/// - `encapsulation` : symmetric key encapsulation using CoverCrypt -/// - `encrypted_metadata` : Optional metadata encrypted using the DEM -/// -/// Parameters: -/// -/// - `policy_bytes` : serialized global policy -/// - `attributes_bytes` : serialized access policy -/// - `public_key_bytes` : CoverCrypt public key -/// - `additional_data` : additional data to encrypt with the header -/// - `authenticated_data` : authenticated data to use in symmetric encryption -#[pyfunction] -pub fn encrypt_hybrid_header( - policy_bytes: Vec, - access_policy: String, - public_key_bytes: Vec, - additional_data: Vec, - authenticated_data: Vec, -) -> PyResult<(Vec, Vec)> { - // - // Deserialize inputs - let policy = serde_json::from_slice(&policy_bytes) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing policy: {e}")))?; - let access_policy = AccessPolicy::from_boolean_expression(&access_policy) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing attributes: {e}")))?; - let public_key = PublicKey::try_from_bytes(&public_key_bytes)?; - - let additional_data = if additional_data.is_empty() { - None - } else { - Some(additional_data) - }; - - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - // - // Encrypt - let (symmetric_key, encrypted_header) = EncryptedHeader::generate( - &CoverCryptX25519Aes256::default(), - &policy, - &public_key, - &access_policy, - additional_data.as_deref(), - authenticated_data.as_deref(), - )?; - - Ok(( - symmetric_key.to_bytes().to_vec(), - encrypted_header.try_to_bytes()?, - )) -} - -/// Decrypt the given header bytes using a user decryption key. -/// -/// - `usk_bytes` : serialized user secret key -/// - `encrypted_header_bytes` : encrypted header bytes -/// - `authenticated_data` : authenticated data to use in symmetric -/// decryption -#[pyfunction] -pub fn decrypt_hybrid_header( - usk_bytes: Vec, - encrypted_header_bytes: Vec, - authenticated_data: Vec, -) -> PyResult<(Vec, Vec)> { - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - // - // Finally decrypt symmetric key using given user decryption key - let cleartext_header = EncryptedHeader::try_from_bytes(&encrypted_header_bytes)?.decrypt( - &CoverCryptX25519Aes256::default(), - &UserSecretKey::try_from_bytes(&usk_bytes)?, - authenticated_data.as_deref(), - )?; - - Ok(( - cleartext_header.symmetric_key.to_bytes().to_vec(), - cleartext_header.additional_data, - )) -} - -/// Encrypt data symmetrically in a block. -/// -/// - `symmetric_key` : symmetric key -/// - `plaintext_bytes` : plaintext to encrypt -/// - `authenticated_data` : associated data to be passed to the DEM scheme -#[pyfunction] -pub fn encrypt_symmetric_block( - symmetric_key: Vec, - plaintext: Vec, - authenticated_data: Vec, -) -> PyResult> { - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - // - // Parse symmetric key - let symmetric_key = SymmetricKey::try_from_bytes(&symmetric_key) - .map_err(|e| PyTypeError::new_err(format!("Deserialize symmetric key failed: {e}")))?; - - // - // Encrypt block - Ok(CoverCryptX25519Aes256::default().encrypt( - &symmetric_key, - &plaintext, - authenticated_data.as_deref(), - )?) -} - -/// Symmetrically Decrypt encrypted data in a block. -/// -/// - `symmetric_key` : symmetric key -/// - `ciphertext` : ciphertext -/// - `authenticated_data` : associated data to be passed to the DEM scheme -#[pyfunction] -pub fn decrypt_symmetric_block( - symmetric_key: Vec, - ciphertext: Vec, - authenticated_data: Vec, -) -> PyResult> { - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - // - // Parse symmetric key - let symmetric_key = SymmetricKey::try_from_bytes(&symmetric_key) - .map_err(|e| PyTypeError::new_err(format!("Deserialize symmetric key failed: {e}")))?; - - // - // Decrypt block - Ok(CoverCryptX25519Aes256::default().decrypt( - &symmetric_key, - &ciphertext, - authenticated_data.as_deref(), - )?) -} - -/// Hybrid encryption. Concatenates the encrypted header and the symmetric -/// ciphertext. -/// -/// - `policy_bytes` : policy -/// - `attributes_bytes` : attributes -/// - `pk_bytes` : CoverCrypt public key -/// - `plaintext` : plaintext to encrypt using the DEM -/// - `additional_data` : additional data to symmetrically encrypt in the -/// header -/// - `authenticated_data` : authenticated data to use in symmetric encryptions -#[pyfunction] -pub fn encrypt( - policy_bytes: Vec, - access_policy: String, - pk: Vec, - plaintext: Vec, - additional_data: Vec, - authenticated_data: Vec, -) -> PyResult> { - let additional_data = if additional_data.is_empty() { - None - } else { - Some(additional_data) - }; - - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - let policy = serde_json::from_slice(&policy_bytes) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing policy: {e}")))?; - let access_policy = AccessPolicy::from_boolean_expression(&access_policy) - .map_err(|e| PyTypeError::new_err(format!("Error deserializing attributes: {e}")))?; - let pk = PublicKey::try_from_bytes(&pk)?; - - // instantiate CoverCrypt - let cover_crypt = CoverCryptX25519Aes256::default(); - - // generate encrypted header - let (symmetric_key, encrypted_header) = EncryptedHeader::generate( - &cover_crypt, - &policy, - &pk, - &access_policy, - additional_data.as_deref(), - authenticated_data.as_deref(), - )?; - - // encrypt the plaintext - let ciphertext = - cover_crypt.encrypt(&symmetric_key, &plaintext, authenticated_data.as_deref())?; - - // concatenate the encrypted header and the ciphertext - let mut ser = Serializer::with_capacity(encrypted_header.length() + ciphertext.len()); - encrypted_header.write(&mut ser)?; - ser.write_array(&ciphertext) - .map_err(|e| PyTypeError::new_err(format!("Error serializing ciphertext: {e}")))?; - Ok(ser.finalize()) -} - -/// Hybrid decryption. -/// -/// - `usk_bytes` : serialized user secret key -/// - `encrypted_bytes` : encrypted header || symmetric ciphertext -/// - `authenticated_data` : authenticated data to use in symmetric decryptions -#[pyfunction] -pub fn decrypt( - usk_bytes: Vec, - encrypted_bytes: Vec, - authenticated_data: Vec, -) -> PyResult> { - let mut de = Deserializer::new(encrypted_bytes.as_slice()); - // this will read the exact header size - let header = EncryptedHeader::read(&mut de)?; - // the rest is the symmetric ciphertext - let ciphertext = de.finalize(); - - let authenticated_data = if authenticated_data.is_empty() { - None - } else { - Some(authenticated_data) - }; - - // Instantiate CoverCrypt - let cover_crypt = CoverCryptX25519Aes256::default(); - - // Decrypt header - let cleartext_header = header.decrypt( - &cover_crypt, - &UserSecretKey::try_from_bytes(&usk_bytes)?, - authenticated_data.as_deref(), - )?; - - // Decrypt plaintext - cover_crypt - .decrypt( - &cleartext_header.symmetric_key, - ciphertext.as_slice(), - authenticated_data.as_deref(), - ) - .map_err(|e| PyTypeError::new_err(e.to_string())) -} diff --git a/src/interfaces/pyo3/mod.rs b/src/interfaces/pyo3/mod.rs index 2844d04d..9f9af390 100644 --- a/src/interfaces/pyo3/mod.rs +++ b/src/interfaces/pyo3/mod.rs @@ -1,37 +1,28 @@ -use pyo3::{pymodule, types::PyModule, wrap_pyfunction, PyResult, Python}; - -use self::{ - generate_cc_keys::{ - generate_master_keys, generate_policy, generate_user_secret_key, rotate_attributes, - }, - hybrid_cc_aes::{ - decrypt, decrypt_hybrid_header, decrypt_symmetric_block, encrypt, encrypt_hybrid_header, - encrypt_symmetric_block, - }, -}; use crate::error::Error; +use pyo3::{pymodule, types::PyModule, PyResult, Python}; + +pub mod py_abe_policy; +pub mod py_cover_crypt; + +use py_abe_policy::{Attribute, Policy, PolicyAxis}; +use py_cover_crypt::{CoverCrypt, MasterSecretKey, PublicKey, SymmetricKey, UserSecretKey}; impl From for pyo3::PyErr { fn from(e: Error) -> Self { - pyo3::exceptions::PyTypeError::new_err(format!("{e}")) + pyo3::exceptions::PyException::new_err(format!("{e}")) } } /// A Python module implemented in Rust. #[pymodule] fn cosmian_cover_crypt(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(generate_master_keys, m)?)?; - m.add_function(wrap_pyfunction!(generate_user_secret_key, m)?)?; - m.add_function(wrap_pyfunction!(generate_policy, m)?)?; - m.add_function(wrap_pyfunction!(rotate_attributes, m)?)?; - m.add_function(wrap_pyfunction!(encrypt_hybrid_header, m)?)?; - m.add_function(wrap_pyfunction!(decrypt_hybrid_header, m)?)?; - m.add_function(wrap_pyfunction!(encrypt_symmetric_block, m)?)?; - m.add_function(wrap_pyfunction!(decrypt_symmetric_block, m)?)?; - m.add_function(wrap_pyfunction!(encrypt, m)?)?; - m.add_function(wrap_pyfunction!(decrypt, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } - -pub mod generate_cc_keys; -pub mod hybrid_cc_aes; diff --git a/src/interfaces/pyo3/py_abe_policy.rs b/src/interfaces/pyo3/py_abe_policy.rs new file mode 100644 index 00000000..3e8a8d67 --- /dev/null +++ b/src/interfaces/pyo3/py_abe_policy.rs @@ -0,0 +1,159 @@ +use abe_policy::{Attribute as AttributeRust, Policy as PolicyRust, PolicyAxis as PolicyAxisRust}; +use pyo3::{ + exceptions::{PyException, PyTypeError}, + prelude::*, + types::PyType, +}; + +// Pyo3 doc on classes +// https://pyo3.rs/v0.16.2/class.html + +/// An attribute in a policy group is characterized by the axis policy name +/// and its unique name within this axis. +#[pyclass] +pub struct Attribute(AttributeRust); + +#[pymethods] +impl Attribute { + /// Creates a Policy Attribute. + /// + /// - `axis` : policy axis the attributes belongs to + /// - `name` : unique attribute name within this axis + #[new] + pub fn new(axis: &str, name: &str) -> Self { + Self(AttributeRust::new(axis, name)) + } + + /// Returns a string representation of the Attribute + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + format!("{}", self.0) + } + + /// Creates a Policy Attribute from a string representation + #[classmethod] + pub fn from_string(_cls: &PyType, string: &str) -> PyResult { + match AttributeRust::try_from(string) { + Ok(inner) => Ok(Self(inner)), + Err(e) => Err(PyException::new_err(e.to_string())), + } + } +} + +/// Defines an unique policy axis by its name and its underlying attribute names. +/// +/// If the axis is defined as hierarchical, we assume a lexicographical order +/// on the attribute name. +#[pyclass] +pub struct PolicyAxis(PolicyAxisRust); + +#[pymethods] +impl PolicyAxis { + /// Generates a new policy axis with the given name and attribute names. + /// If `hierarchical` is set to `true`, the axis is defined as hierarchical. + /// + /// - `name` : axis name + /// - `attributes` : name of the attributes on this axis + /// - `hierarchical`: set the axis to be hierarchical + #[new] + fn new(name: &str, attributes: Vec<&str>, hierarchical: bool) -> Self { + Self(PolicyAxisRust::new( + name, + attributes.as_slice(), + hierarchical, + )) + } + + /// Returns the number of attributes belonging to this axis. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Return `true` if the attribute list is empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return a string representation of the Policy Axis + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + format!( + "{}: {:?}, hierarchical: {}", + &self.0.name, &self.0.attributes, &self.0.hierarchical + ) + } +} + +#[pyclass] +pub struct Policy(pub(super) PolicyRust); + +#[pymethods] +impl Policy { + /// Generates a new policy object with the given number of attribute + /// creations (revocation + addition) allowed. + /// Default maximum of attribute creations is u32::MAX + #[new] + #[args(max_attribute_creations = "4294967295")] + fn new(max_attribute_creations: u32) -> Self { + Self(PolicyRust::new(max_attribute_creations)) + } + + /// Adds the given policy axis to the policy. + pub fn add_axis(&mut self, axis: &PolicyAxis) -> PyResult<()> { + self.0 + .add_axis(&axis.0) + .map_err(|e| PyException::new_err(e.to_string())) + } + + /// Rotates an attribute, changing its underlying value with an unused + /// value. + pub fn rotate(&mut self, attr: &Attribute) -> PyResult<()> { + self.0 + .rotate(&attr.0) + .map_err(|e| PyException::new_err(e.to_string())) + } + + /// Returns the list of Attributes of this Policy. + pub fn attributes(&self) -> Vec { + self.0.attributes().into_iter().map(Attribute).collect() + } + + /// Returns the list of all attributes values given to this Attribute + /// over the time after rotations. The current value is returned first + pub fn attribute_values(&self, attribute: &Attribute) -> PyResult> { + self.0 + .attribute_values(&attribute.0) + .map_err(|e| PyException::new_err(e.to_string())) + } + + /// Retrieves the current value of an attribute. + pub fn attribute_current_value(&self, attribute: &Attribute) -> PyResult { + self.0 + .attribute_current_value(&attribute.0) + .map_err(|e| PyException::new_err(e.to_string())) + } + + /// Returns a string representation of the Policy + #[allow(clippy::inherent_to_string)] + pub fn to_string(&self) -> String { + format!("{}", &self.0) + } + + /// Performs deep copy of the Policy + pub fn deep_copy(&self) -> Self { + Self(self.0.clone()) + } + + /// JSON serialization + pub fn to_json(&self) -> PyResult { + serde_json::to_string(&self.0).map_err(|e| PyException::new_err(e.to_string())) + } + + /// JSON deserialization + #[classmethod] + pub fn from_json(_cls: &PyType, policy_json: &str) -> PyResult { + let policy: PolicyRust = serde_json::from_str(policy_json) + .map_err(|e| PyTypeError::new_err(format!("Error deserializing attributes: {e}")))?; + Ok(Self(policy)) + } +} diff --git a/src/interfaces/pyo3/py_cover_crypt.rs b/src/interfaces/pyo3/py_cover_crypt.rs new file mode 100644 index 00000000..9d67b597 --- /dev/null +++ b/src/interfaces/pyo3/py_cover_crypt.rs @@ -0,0 +1,376 @@ +use abe_policy::AccessPolicy; +use cosmian_crypto_core::{ + bytes_ser_de::{Deserializer, Serializable, Serializer}, + symmetric_crypto::SymKey, + KeyTrait, +}; +use pyo3::{ + exceptions::{PyException, PyTypeError}, + prelude::*, + types::PyType, + PyErr, +}; + +use crate::{ + api::CoverCrypt as CoverCryptRust, + interfaces::{ + pyo3::py_abe_policy::Policy, + statics::{ + CoverCryptX25519Aes256, EncryptedHeader, MasterSecretKey as MasterSecretKeyRust, + PublicKey as PublicKeyRust, SymmetricKey as SymmetricKeyRust, + UserSecretKey as UserSecretKeyRust, + }, + }, +}; + +// Pyo3 doc on classes +// https://pyo3.rs/v0.16.2/class.html + +#[pyclass] +pub struct MasterSecretKey(MasterSecretKeyRust); + +#[pymethods] +impl MasterSecretKey { + /// Converts key to bytes + pub fn to_bytes(&self) -> PyResult> { + self.0.try_to_bytes().map_err(PyErr::from) + } + + /// Reads key from bytes + #[classmethod] + pub fn from_bytes(_cls: &PyType, key_bytes: Vec) -> PyResult { + match MasterSecretKeyRust::try_from_bytes(&key_bytes) { + Ok(key) => Ok(Self(key)), + Err(e) => Err(PyErr::from(e)), + } + } +} + +#[pyclass] +pub struct PublicKey(PublicKeyRust); + +#[pymethods] +impl PublicKey { + /// Converts key to bytes + pub fn to_bytes(&self) -> PyResult> { + self.0.try_to_bytes().map_err(PyErr::from) + } + + /// Reads key from bytes + #[classmethod] + pub fn from_bytes(_cls: &PyType, key_bytes: Vec) -> PyResult { + match PublicKeyRust::try_from_bytes(&key_bytes) { + Ok(key) => Ok(Self(key)), + Err(e) => Err(PyErr::from(e)), + } + } +} + +#[pyclass] +pub struct UserSecretKey(UserSecretKeyRust); + +#[pymethods] +impl UserSecretKey { + /// Converts key to bytes + pub fn to_bytes(&self) -> PyResult> { + self.0.try_to_bytes().map_err(PyErr::from) + } + + /// Reads key from bytes + #[classmethod] + pub fn from_bytes(_cls: &PyType, key_bytes: Vec) -> PyResult { + match UserSecretKeyRust::try_from_bytes(&key_bytes) { + Ok(key) => Ok(Self(key)), + Err(e) => Err(PyErr::from(e)), + } + } +} + +#[pyclass] +pub struct SymmetricKey(SymmetricKeyRust); + +#[pymethods] +impl SymmetricKey { + /// Converts key to bytes + pub fn to_bytes(&self) -> PyResult> { + Ok(self.0.as_bytes().to_vec()) + } + + /// Reads key from bytes + #[classmethod] + pub fn from_bytes(_cls: &PyType, key_bytes: Vec) -> PyResult { + match SymmetricKeyRust::try_from_bytes(&key_bytes) { + Ok(key) => Ok(Self(key)), + Err(e) => Err(PyException::new_err(e.to_string())), + } + } +} + +#[pyclass] +pub struct CoverCrypt(CoverCryptX25519Aes256); + +#[pymethods] +impl CoverCrypt { + #[new] + fn new() -> Self { + Self(CoverCryptX25519Aes256::default()) + } + + /// Generate the master authority keys for supplied Policy + /// + /// - `policy` : Policy to use to generate the keys + pub fn generate_master_keys(&self, policy: &Policy) -> PyResult<(MasterSecretKey, PublicKey)> { + match self.0.generate_master_keys(&policy.0) { + Ok((msk, pk)) => Ok((MasterSecretKey(msk), PublicKey(pk))), + Err(e) => Err(PyErr::from(e)), + } + } + + /// Update the master keys according to this new policy. + /// + /// When a partition exists in the new policy but not in the master keys, + /// a new key pair is added to the master keys for that partition. + /// When a partition exists on the master keys, but not in the new policy, + /// it is removed from the master keys. + /// + /// - `policy` : Policy to use to generate the keys + /// - `msk` : master secret key + /// - `mpk` : master public key + pub fn update_master_keys( + &self, + policy: &Policy, + msk: &mut MasterSecretKey, + pk: &mut PublicKey, + ) -> PyResult<()> { + self.0 + .update_master_keys(&policy.0, &mut msk.0, &mut pk.0) + .map_err(PyErr::from) + } + + /// Generate a user secret key. + /// + /// A new user secret key does NOT include to old (i.e. rotated) partitions + /// + /// - `msk` : master secret key + /// - `access_policy_str` : user access policy + /// - `policy` : global policy + pub fn generate_user_secret_key( + &self, + msk: &MasterSecretKey, + access_policy_str: &str, + policy: &Policy, + ) -> PyResult { + let access_policy = AccessPolicy::from_boolean_expression(access_policy_str) + .map_err(|e| PyTypeError::new_err(format!("Access policy creation failed: {e}")))?; + + match self + .0 + .generate_user_secret_key(&msk.0, &access_policy, &policy.0) + { + Ok(usk) => Ok(UserSecretKey(usk)), + Err(e) => Err(PyErr::from(e)), + } + } + + /// Refreshes the user key according to the given master key and access policy. + /// + /// The user key will be granted access to the current partitions, as determined by its access policy. + /// If `preserve_old_partitions_access` is set, the user access to rotated partitions will be preserved + /// + /// - `usk` : the user key to refresh + /// - `access_policy` : the access policy of the user key + /// - `msk` : master secret key + /// - `policy` : global policy of the master secret key + /// - `keep_old_accesses` : whether access to old partitions (i.e. before rotation) should be kept + pub fn refresh_user_secret_key( + &self, + usk: &mut UserSecretKey, + access_policy_str: &str, + msk: &MasterSecretKey, + policy: &Policy, + keep_old_accesses: bool, + ) -> PyResult<()> { + let access_policy = AccessPolicy::from_boolean_expression(access_policy_str) + .map_err(|e| PyTypeError::new_err(format!("Access policy creation failed: {e}")))?; + + self.0 + .refresh_user_secret_key( + &mut usk.0, + &access_policy, + &msk.0, + &policy.0, + keep_old_accesses, + ) + .map_err(PyErr::from) + } + + /// Encrypts data symmetrically in a block. + /// + /// - `symmetric_key` : symmetric key + /// - `plaintext` : plaintext to encrypt + /// - `authenticated_data` : associated data to be passed to the DEM scheme + pub fn encrypt_symmetric_block( + &self, + symmetric_key: &SymmetricKey, + plaintext: Vec, + authenticated_data: Option>, + ) -> PyResult> { + self.0 + .encrypt(&symmetric_key.0, &plaintext, authenticated_data.as_deref()) + .map_err(PyErr::from) + } + + /// Symmetrically Decrypts encrypted data in a block. + /// + /// - `symmetric_key` : symmetric key + /// - `ciphertext` : ciphertext + /// - `authenticated_data` : associated data to be passed to the DEM scheme + pub fn decrypt_symmetric_block( + &self, + symmetric_key: &SymmetricKey, + ciphertext: Vec, + authenticated_data: Option>, + ) -> PyResult> { + self.0 + .decrypt(&symmetric_key.0, &ciphertext, authenticated_data.as_deref()) + .map_err(PyErr::from) + } + + /// Generates an encrypted header. A header contains the following elements: + /// + /// - `encapsulation_size` : the size of the symmetric key encapsulation (u32) + /// - `encapsulation` : symmetric key encapsulation using CoverCrypt + /// - `encrypted_metadata` : Optional metadata encrypted using the DEM + /// + /// Parameters: + /// + /// - `policy` : global policy + /// - `access_policy_str` : access policy + /// - `public_key` : CoverCrypt public key + /// - `additional_data` : additional data to encrypt with the header + /// - `authenticated_data` : authenticated data to use in symmetric encryption + pub fn encrypt_header( + &self, + policy: &Policy, + access_policy_str: &str, + public_key: &PublicKey, + additional_data: Option>, + authenticated_data: Option>, + ) -> PyResult<(SymmetricKey, Vec)> { + // Deserialize inputs + let access_policy = AccessPolicy::from_boolean_expression(access_policy_str) + .map_err(|e| PyTypeError::new_err(format!("Access policy creation failed: {e}")))?; + + // Encrypt + let (symmetric_key, encrypted_header) = EncryptedHeader::generate( + &self.0, + &policy.0, + &public_key.0, + &access_policy, + additional_data.as_deref(), + authenticated_data.as_deref(), + )?; + + Ok(( + SymmetricKey(symmetric_key), + encrypted_header.try_to_bytes()?, + )) + } + + /// Decrypts the given header bytes using a user decryption key. + /// + /// - `usk` : user secret key + /// - `encrypted_header_bytes` : encrypted header bytes + /// - `authenticated_data` : authenticated data to use in symmetric decryption + pub fn decrypt_header( + &self, + usk: &UserSecretKey, + encrypted_header_bytes: Vec, + authenticated_data: Option>, + ) -> PyResult<(SymmetricKey, Vec)> { + // Finally decrypt symmetric key using given user decryption key + let cleartext_header = EncryptedHeader::try_from_bytes(&encrypted_header_bytes)?.decrypt( + &self.0, + &usk.0, + authenticated_data.as_deref(), + )?; + + Ok(( + SymmetricKey(cleartext_header.symmetric_key), + cleartext_header.additional_data, + )) + } + + /// Hybrid encryption. Concatenates the encrypted header and the symmetric + /// ciphertext. + /// + /// - `policy` : global policy + /// - `access_policy_str` : access policy + /// - `pk` : CoverCrypt public key + /// - `plaintext` : plaintext to encrypt using the DEM + /// - `additional_data` : additional data to symmetrically encrypt in the header + /// - `authenticated_data` : authenticated data to use in symmetric encryptions + pub fn encrypt( + &self, + policy: &Policy, + access_policy_str: &str, + pk: &PublicKey, + plaintext: Vec, + additional_data: Option>, + authenticated_data: Option>, + ) -> PyResult> { + let access_policy = AccessPolicy::from_boolean_expression(access_policy_str) + .map_err(|e| PyTypeError::new_err(format!("Access policy creation failed: {e}")))?; + + // generates encrypted header + let (symmetric_key, encrypted_header) = EncryptedHeader::generate( + &self.0, + &policy.0, + &pk.0, + &access_policy, + additional_data.as_deref(), + authenticated_data.as_deref(), + )?; + + // encrypts the plaintext + let ciphertext = + self.0 + .encrypt(&symmetric_key, &plaintext, authenticated_data.as_deref())?; + + // concatenates the encrypted header and the ciphertext + let mut ser = Serializer::with_capacity(encrypted_header.length() + ciphertext.len()); + encrypted_header.write(&mut ser)?; + ser.write_array(&ciphertext) + .map_err(|e| PyTypeError::new_err(format!("Error serializing ciphertext: {e}")))?; + Ok(ser.finalize()) + } + + /// Hybrid decryption. + /// + /// - `usk` : user secret key + /// - `encrypted_bytes` : encrypted header || symmetric ciphertext + /// - `authenticated_data` : authenticated data to use in symmetric decryptions + pub fn decrypt( + &self, + usk: &UserSecretKey, + encrypted_bytes: Vec, + authenticated_data: Option>, + ) -> PyResult> { + let mut de = Deserializer::new(encrypted_bytes.as_slice()); + // this will read the exact header size + let header = EncryptedHeader::read(&mut de)?; + // the rest is the symmetric ciphertext + let ciphertext = de.finalize(); + + // decrypts the header + let cleartext_header = header.decrypt(&self.0, &usk.0, authenticated_data.as_deref())?; + + self.0 + .decrypt( + &cleartext_header.symmetric_key, + ciphertext.as_slice(), + authenticated_data.as_deref(), + ) + .map_err(PyErr::from) + } +} diff --git a/src/interfaces/pyo3/tests/demo.py b/src/interfaces/pyo3/tests/demo.py deleted file mode 100644 index 2def968b..00000000 --- a/src/interfaces/pyo3/tests/demo.py +++ /dev/null @@ -1,130 +0,0 @@ -import json -import cosmian_cover_crypt - - -# Declare 2 CoverCrypt policy axis: -policy_axis_json = [ - { - "name": "Security Level", - "attributes": [ - "Protected", - "Low Secret", - "Medium Secret", - "High Secret", - "Top Secret" - ], - "hierarchical": True - }, - { - "name": "Department", - "attributes": [ - "R&D", - "HR", - "MKG", - "FIN" - ], - "hierarchical": False - } -] - -policy_axis = bytes(json.dumps(policy_axis_json), 'utf-8') - -policy = cosmian_cover_crypt.generate_policy( - policy_axis_bytes=policy_axis, max_attribute_value=100) - -master_keys = cosmian_cover_crypt.generate_master_keys(policy) - -top_secret_mkg_fin_user = cosmian_cover_crypt.generate_user_secret_key( - master_keys[0], "Security Level::Top Secret && (Department::MKG || Department::FIN)", policy) - -medium_secret_mkg_user = cosmian_cover_crypt.generate_user_secret_key( - master_keys[0], "Security Level::Medium Secret && Department::MKG", policy) - - -# Encryption -plaintext = "My secret data" -plaintext_bytes = bytes(plaintext, 'utf-8') -additional_data = [0, 0, 0, 0, 0, 0, 0, 1]; -authenticated_data = []; - -# Encrypt with different ABE policies -low_secret_mkg_data = cosmian_cover_crypt.encrypt(policy, "Security Level::Low Secret && Department::MKG", - master_keys[1], - plaintext_bytes, - additional_data, - authenticated_data) -top_secret_mkg_data = cosmian_cover_crypt.encrypt(policy, "Security Level::Top Secret && Department::MKG", - master_keys[1], plaintext_bytes, - additional_data, authenticated_data) -low_secret_fin_data = cosmian_cover_crypt.encrypt(policy, "Security Level::Low Secret && Department::FIN", - master_keys[1], plaintext_bytes, - additional_data, authenticated_data) - -# The medium secret marketing user can successfully decrypt a low security marketing message: -cleartext = cosmian_cover_crypt.decrypt(medium_secret_mkg_user, low_secret_mkg_data, - authenticated_data) -assert(str(bytes(cleartext), "utf-8") == plaintext) - -# .. however it can neither decrypt a marketing message with higher security: -try: - cleartext = cosmian_cover_crypt.decrypt( - medium_secret_mkg_user, top_secret_mkg_data, authenticated_data) -except Exception as ex: - print(f"As expected, user cannot decrypt this message: {ex}") - -try: - cleartext = cosmian_cover_crypt.decrypt( - medium_secret_mkg_user, low_secret_fin_data, authenticated_data) -except Exception as ex: - print(f"As expected, user cannot decrypt this message: {ex}") - -# The "top secret-marketing-financial" user can decrypt messages from the marketing -# department OR the financial department that have a security level of Top Secret or below - -# As expected, the top secret marketing financial user can successfully decrypt all messages -cleartext = cosmian_cover_crypt.decrypt(top_secret_mkg_fin_user, low_secret_mkg_data, - authenticated_data) -assert(str(bytes(cleartext), "utf-8") == plaintext) - -cleartext = cosmian_cover_crypt.decrypt(top_secret_mkg_fin_user, top_secret_mkg_data, - authenticated_data) -assert(str(bytes(cleartext), "utf-8") == plaintext) - -cleartext = cosmian_cover_crypt.decrypt(top_secret_mkg_fin_user, low_secret_fin_data, - authenticated_data) -assert(str(bytes(cleartext), "utf-8") == plaintext) - -# Rotation of Policy attributes -# At anytime, Policy attributes can be rotated. -# When that happens future encryption of data for a "rotated" attribute cannot -# be decrypted with user decryption keys which are not "refreshed" for that -# attribute. Let us rotate the Security Level Low Secret -new_policy = cosmian_cover_crypt.rotate_attributes(bytes(json.dumps( - ['Security Level::Low Secret']), 'utf8'), policy) -# # Printing the policy before and after the rotation of the attribute. -# print("Before the rotation of attribute Security Level::Low Secret") -# print(json.loads(str(bytes(policy), "utf-8"))) -# print("After attributes rotation") -# print(json.loads(str(bytes(new_policy), "utf-8"))) - -# Master keys MUST be refreshed -master_keys = cosmian_cover_crypt.generate_master_keys(new_policy) -new_low_secret_mkg_data = cosmian_cover_crypt.encrypt(new_policy, "Security Level::Low Secret && Department::MKG", - master_keys[1], plaintext_bytes, - additional_data, authenticated_data) - -# The medium secret user cannot decrypt the new message until its key is refreshed -try: - cleartext = cosmian_cover_crypt.decrypt( - medium_secret_mkg_user, new_low_secret_mkg_data, authenticated_data) -except Exception as ex: - print(f"As expected, user cannot decrypt this message: {ex}") - -# Refresh medium secret key -new_medium_secret_mkg_user = cosmian_cover_crypt.generate_user_secret_key( - master_keys[0], "Security Level::Medium Secret && Department::MKG", new_policy) - -# New messages can now be decrypted -cleartext = cosmian_cover_crypt.decrypt( - new_medium_secret_mkg_user, new_low_secret_mkg_data, authenticated_data) -assert(str(bytes(cleartext), "utf-8") == plaintext) diff --git a/src/interfaces/pyo3/tests/requirements.txt b/src/interfaces/pyo3/tests/requirements.txt index ea199435..c2ac67d0 100644 --- a/src/interfaces/pyo3/tests/requirements.txt +++ b/src/interfaces/pyo3/tests/requirements.txt @@ -1,2 +1 @@ -maturin -notebook +maturin>=0.13.7 diff --git a/src/interfaces/pyo3/tests/test.sh b/src/interfaces/pyo3/tests/test.sh index d92c4949..cb325f77 100755 --- a/src/interfaces/pyo3/tests/test.sh +++ b/src/interfaces/pyo3/tests/test.sh @@ -2,15 +2,13 @@ set -euEx -init(){ - virtualenv env - source env/bin/activate - pip install maturin -} +# Clean previous build +rm -f target/wheels/*.whl -# init +# Build for manylinux (glibc 2.17) +# Alternatively you can build for your glibc only by setting `compatibility = "off"` in pyproject.toml and running +# maturin build --release --features python +docker run --rm -v $(pwd):/io ghcr.io/pyo3/maturin build --release --features python -rm -f target/wheels/*.whl -maturin build --release --features python pip install --force-reinstall target/wheels/*.whl -python3 src/interfaces/pyo3/tests/demo.py +python3 src/interfaces/pyo3/tests/test_cover_crypt.py diff --git a/src/interfaces/pyo3/tests/test_cover_crypt.py b/src/interfaces/pyo3/tests/test_cover_crypt.py new file mode 100644 index 00000000..42ea9bb4 --- /dev/null +++ b/src/interfaces/pyo3/tests/test_cover_crypt.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +import unittest +from cosmian_cover_crypt import ( + Attribute, + Policy, + PolicyAxis, + CoverCrypt, + SymmetricKey, + MasterSecretKey, + PublicKey, + UserSecretKey, +) + + +class TestPolicy(unittest.TestCase): + def test_attribute(self) -> None: + att = Attribute('Country', 'France') + self.assertEqual(att.to_string(), 'Country::France') + self.assertEqual( + Attribute.from_string('Country::France').to_string(), 'Country::France' + ) + + def test_policy_creation_rotation(self) -> None: + country_axis = PolicyAxis( + 'Country', ['France', 'UK', 'Spain', 'Germany'], False + ) + self.assertEqual( + country_axis.to_string(), + 'Country: ["France", "UK", "Spain", "Germany"], hierarchical: false', + ) + secrecy_axis = PolicyAxis('Secrecy', ['Low', 'Medium', 'High'], True) + self.assertEqual( + secrecy_axis.to_string(), + 'Secrecy: ["Low", "Medium", "High"], hierarchical: true', + ) + policy = Policy() + policy.add_axis(country_axis) + policy.add_axis(secrecy_axis) + # test attributes + attributes = policy.attributes() + self.assertEqual(len(attributes), 4 + 3) + # rotate + france_attribute = Attribute('Country', 'France') + france_value = policy.attribute_current_value(france_attribute) + self.assertEqual(france_value, 1) + policy.rotate(france_attribute) + new_france_value = policy.attribute_current_value(france_attribute) + self.assertEqual(new_france_value, 8) + self.assertEqual(policy.attribute_values(france_attribute), [8, 1]) + + def test_policy_cloning_serialization(self) -> None: + country_axis = PolicyAxis( + 'Country', ['France', 'UK', 'Spain', 'Germany'], False + ) + secrecy_axis = PolicyAxis('Secrecy', ['Low', 'Medium', 'High'], True) + policy = Policy() + policy.add_axis(country_axis) + policy.add_axis(secrecy_axis) + + copy_policy = policy.deep_copy() + self.assertIsInstance(copy_policy, Policy) + + json_str = policy.to_json() + self.assertEqual(json_str, copy_policy.to_json()) + + deserialized_policy = Policy.from_json(json_str) + self.assertIsInstance(deserialized_policy, Policy) + + with self.assertRaises(Exception): + Policy.from_json('wrong data format') + + +class TestKeyGeneration(unittest.TestCase): + def setUp(self) -> None: + country_axis = PolicyAxis( + 'Country', ['France', 'UK', 'Spain', 'Germany'], False + ) + secrecy_axis = PolicyAxis('Secrecy', ['Low', 'Medium', 'High'], True) + self.policy = Policy() + self.policy.add_axis(country_axis) + self.policy.add_axis(secrecy_axis) + + self.cc = CoverCrypt() + self.msk, self.pk = self.cc.generate_master_keys(self.policy) + + def test_master_key_serialization(self) -> None: + msk_bytes = self.msk.to_bytes() + self.assertIsInstance(MasterSecretKey.from_bytes(msk_bytes), MasterSecretKey) + + with self.assertRaises(Exception): + MasterSecretKey.from_bytes(b'wrong data') + + pk_bytes = self.pk.to_bytes() + self.assertIsInstance(PublicKey.from_bytes(pk_bytes), PublicKey) + + with self.assertRaises(Exception): + PublicKey.from_bytes(b'wrong data') + + def test_user_key_serialization(self) -> None: + usk = self.cc.generate_user_secret_key( + self.msk, + 'Secrecy::High && (Country::France || Country::Spain)', + self.policy, + ) + + usk_bytes = usk.to_bytes() + self.assertIsInstance(UserSecretKey.from_bytes(usk_bytes), UserSecretKey) + + with self.assertRaises(Exception): + UserSecretKey.from_bytes(b'wrong data') + + def test_sym_key_serialization(self) -> None: + sym_key, _ = self.cc.encrypt_header( + self.policy, + 'Secrecy::High && Country::Germany', + self.pk, + None, + None, + ) + sym_key_bytes = sym_key.to_bytes() + self.assertIsInstance(SymmetricKey.from_bytes(sym_key_bytes), SymmetricKey) + + with self.assertRaises(Exception): + SymmetricKey.from_bytes(b'wrong data') + + +class TestEncryption(unittest.TestCase): + def setUp(self) -> None: + country_axis = PolicyAxis( + 'Country', ['France', 'UK', 'Spain', 'Germany'], False + ) + secrecy_axis = PolicyAxis('Secrecy', ['Low', 'Medium', 'High'], True) + self.policy = Policy() + self.policy.add_axis(country_axis) + self.policy.add_axis(secrecy_axis) + + self.cc = CoverCrypt() + self.msk, self.pk = self.cc.generate_master_keys(self.policy) + + self.plaintext = b'My secret data' + self.additional_data = [0, 0, 0, 0, 0, 0, 0, 1] + self.authenticated_data = None + + def test_simple_encryption_decryption(self) -> None: + + ciphertext = self.cc.encrypt( + self.policy, + 'Secrecy::High && Country::France', + self.pk, + self.plaintext, + self.additional_data, + self.authenticated_data, + ) + + sec_high_fr_sp_user = self.cc.generate_user_secret_key( + self.msk, + 'Secrecy::High && (Country::France || Country::Spain)', + self.policy, + ) + + # Successful decryption + cleartext = self.cc.decrypt( + sec_high_fr_sp_user, ciphertext, self.authenticated_data + ) + self.assertEqual(bytes(cleartext), self.plaintext) + + # Wrong key + sec_low_fr_sp_user = self.cc.generate_user_secret_key( + self.msk, 'Secrecy::Low && (Country::France || Country::Spain)', self.policy + ) + + with self.assertRaises(Exception): + cleartext = self.cc.decrypt( + sec_low_fr_sp_user, ciphertext, self.authenticated_data + ) + + def test_policy_rotation_encryption_decryption(self) -> None: + + ciphertext = self.cc.encrypt( + self.policy, + 'Secrecy::High && Country::France', + self.pk, + self.plaintext, + self.additional_data, + self.authenticated_data, + ) + + sec_high_fr_sp_user = self.cc.generate_user_secret_key( + self.msk, + 'Secrecy::High && (Country::France || Country::Spain)', + self.policy, + ) + + france_attribute = Attribute('Country', 'France') + # new_policy = deepcopy(self.policy) + self.policy.rotate(france_attribute) + + self.cc.update_master_keys(self.policy, self.msk, self.pk) + new_plaintext = b'My secret data 2' + new_ciphertext = self.cc.encrypt( + self.policy, + 'Secrecy::High && Country::France', + self.pk, + new_plaintext, + self.additional_data, + self.authenticated_data, + ) + + # user cannot decrypt the new message until its key is refreshed + with self.assertRaises(Exception): + cleartext = self.cc.decrypt( + sec_high_fr_sp_user, new_ciphertext, self.authenticated_data + ) + + # new user can still decrypt old message with keep_old_accesses + self.cc.refresh_user_secret_key( + sec_high_fr_sp_user, + 'Secrecy::High && (Country::France || Country::Spain)', + self.msk, + self.policy, + keep_old_accesses=True, + ) + + cleartext = self.cc.decrypt( + sec_high_fr_sp_user, ciphertext, self.authenticated_data + ) + self.assertEqual(bytes(cleartext), self.plaintext) + + # new user key can no longer decrypt the old message + self.cc.refresh_user_secret_key( + sec_high_fr_sp_user, + 'Secrecy::High && (Country::France || Country::Spain)', + self.msk, + self.policy, + keep_old_accesses=False, + ) + with self.assertRaises(Exception): + cleartext = self.cc.decrypt( + sec_high_fr_sp_user, ciphertext, self.authenticated_data + ) + + cleartext = self.cc.decrypt( + sec_high_fr_sp_user, new_ciphertext, self.authenticated_data + ) + self.assertEqual(bytes(cleartext), new_plaintext) + + def test_decomposed_encryption_decryption(self) -> None: + """Test individually the header and the symmetric encryption/decryption""" + sym_key, enc_header = self.cc.encrypt_header( + self.policy, + 'Secrecy::Medium && Country::UK', + self.pk, + self.additional_data, + self.authenticated_data, + ) + + ciphertext = self.cc.encrypt_symmetric_block( + sym_key, self.plaintext, self.authenticated_data + ) + + sec_med_uk_user = self.cc.generate_user_secret_key( + self.msk, 'Secrecy::Medium && Country::UK', self.policy + ) + + decrypted_sym_key, decrypted_metadata = self.cc.decrypt_header( + sec_med_uk_user, enc_header, self.authenticated_data + ) + self.assertEqual(decrypted_metadata, self.additional_data) + + decrypted_data = self.cc.decrypt_symmetric_block( + decrypted_sym_key, ciphertext, self.authenticated_data + ) + self.assertEqual(bytes(decrypted_data), self.plaintext) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/interfaces/statics.rs b/src/interfaces/statics.rs index fc6326cb..11bc4e72 100644 --- a/src/interfaces/statics.rs +++ b/src/interfaces/statics.rs @@ -569,7 +569,7 @@ mod tests { // // New user secret key - let top_secret_fin_usk = cover_crypt.generate_user_secret_key( + let mut top_secret_fin_usk = cover_crypt.generate_user_secret_key( &msk, &AccessPolicy::from_boolean_expression( "Security Level::Top Secret && Department::FIN", @@ -607,10 +607,26 @@ mod tests { None, )?; + // Decryption fails without refreshing the user key assert!(encrypted_header .decrypt(&cover_crypt, &top_secret_fin_usk, None) .is_err()); + cover_crypt.refresh_user_secret_key( + &mut top_secret_fin_usk, + &AccessPolicy::from_boolean_expression( + "Security Level::Top Secret && Department::FIN", + )?, + &msk, + &policy, + false, + )?; + + // The refreshed key can decrypt the header + assert!(encrypted_header + .decrypt(&cover_crypt, &top_secret_fin_usk, None) + .is_ok()); + Ok(()) } }