From fbe42e3f80dea41ef6eb3439e0ab3d653f33fc06 Mon Sep 17 00:00:00 2001 From: Manuthor <32013169+Manuthor@users.noreply.github.com> Date: Mon, 27 Jun 2022 16:17:16 +0200 Subject: [PATCH] Feature/add attribute rotation to pyo3 (#16) * [pyo3 + JS/bindgen + FFI] Add CoverCrypt attributes rotation mechanism --- .gitignore | 1 + .gitlab-ci.yml | 16 +++ CHANGELOG.md | 9 ++ Cargo.lock | 26 ++--- Cargo.toml | 2 +- benches/benches.rs | 2 +- src/api.rs | 103 +++++++++-------- src/error.rs | 2 + src/interfaces/ffi/generate_cc_keys.rs | 82 ++++++++++++- src/interfaces/pyo3/generate_cc_keys.rs | 56 ++++++++- src/interfaces/pyo3/mod.rs | 6 +- src/interfaces/pyo3/tests/demo.py | 109 ++++++++++-------- src/interfaces/pyo3/tests/test.sh | 2 +- src/interfaces/statics.rs | 90 ++++++++++++++- .../wasm_bindgen/generate_cc_keys.rs | 45 ++++++-- src/interfaces/wasm_bindgen/tests.rs | 64 +++++++++- src/policies/policy.rs | 46 +++++--- 17 files changed, 512 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index 7f54bff9..2b13edcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target* .cargo_check non_regression_vector.json +*nix* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2af0e988..9748c50d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,9 @@ clippy: stage: prebuild cache: {} script: + # no feature activated + - cargo clippy --all-targets -- -D warnings + # all features activated - cargo clippy-all # Security check @@ -122,3 +125,16 @@ build_osx: - target/release/*.so - target/*.h expire_in: 3 mos + +build_android: + <<: *base_compile + stage: build + image: gitlab.cosmian.com:5000/core/ci-rust-android:latest + before_script: + - rustup target add i686-linux-android x86_64-linux-android armv7-linux-androideabi aarch64-linux-android + script: + - cargo ndk -t x86 -t x86_64 -t armeabi-v7a -t arm64-v8a -o jniLibs build --release --features ffi --lib + artifacts: + paths: + - jniLibs + expire_in: 3 mos diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c05d24..7194d550 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. --- +## [3.0.1] - 2022-06-21 +### Added +- [pyo3 + JS/bindgen + FFI] Add CoverCrypt attributes rotation mechanism +### Changed +### Fixed +- Fix access policy to partitions +### Removed +--- + ## [3.0.0] - 2022-06-14 ### Added - Add Gitlab CI .gitlab-ci.yml diff --git a/Cargo.lock b/Cargo.lock index ca2e4592..0e6cd64c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,7 +264,7 @@ dependencies = [ [[package]] name = "cover_crypt" -version = "3.0.0" +version = "3.0.1" dependencies = [ "cosmian_crypto_base", "criterion", @@ -861,11 +861,11 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1200,13 +1200,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.93" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04066589568b72ec65f42d65a1a52436e954b168773148893c020269563decf2" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1302,16 +1302,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-ident" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] -name = "unicode-xid" -version = "0.2.3" +name = "unicode-width" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unindent" diff --git a/Cargo.toml b/Cargo.toml index b3b0913b..dd338186 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cover_crypt" -version = "3.0.0" +version = "3.0.1" edition = "2021" authors = [ "Théophile Brezot ", diff --git a/benches/benches.rs b/benches/benches.rs index 47ba3f5b..344ba9f1 100644 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -314,7 +314,7 @@ unsafe fn unwrap_ffi_error(val: i32) -> Result<(), Error> { let mut message_bytes_len = message_bytes_key.len() as c_int; get_last_error(message_bytes_ptr, &mut message_bytes_len); let cstr = CStr::from_ptr(message_bytes_ptr); - return Err(Error::Other(format!("FFI ERROR: {}", cstr.to_str()?))); + Err(Error::Other(format!("FFI ERROR: {}", cstr.to_str()?))) } else { Ok(()) } diff --git a/src/api.rs b/src/api.rs index 0d169007..1a3d5c29 100644 --- a/src/api.rs +++ b/src/api.rs @@ -269,47 +269,42 @@ fn access_policy_to_partitions( access_policy: &AccessPolicy, policy: &Policy, ) -> Result, Error> { - let combinations = to_attribute_combinations(access_policy, policy)?; - let mut set: HashSet = HashSet::with_capacity(combinations.len()); - for combination in combinations { - set.insert(Partition::from_attributes(combination)?); + let attr_combinations = to_attribute_combinations(access_policy, policy)?; + let mut set = HashSet::with_capacity(attr_combinations.len()); + for attr_combination in &attr_combinations { + for partition in to_partitions(attr_combination, policy)? { + let is_unique = set.insert(partition); + if !is_unique { + return Err(Error::ExistingCombination(format!("{attr_combination:?}"))); + } + } } Ok(set) } -/// Returns the list of partitions that can be built using the values of -/// each attribute in the given access policy. This corresponds to an OR -/// expression of AND expressions. +/// Returns the list of attribute combinations that can be built using the +/// values of each attribute in the given access policy. This corresponds to +/// an OR expression of AND expressions. /// /// - `policy` : global policy fn to_attribute_combinations( access_policy: &AccessPolicy, policy: &Policy, -) -> Result>, Error> { +) -> Result>, Error> { match access_policy { AccessPolicy::Attr(attr) => { let (attribute_names, is_hierarchical) = policy .as_map() .get(&attr.axis()) .ok_or_else(|| Error::UnknownPartition(attr.axis()))?; - let mut res = policy - .attribute_values(attr)? - .iter() - .map(|&value| vec![value]) - .collect::>>(); + let mut res = vec![vec![attr.clone()]]; if *is_hierarchical { // add attribute values for all attributes below the given one for name in attribute_names.iter() { if *name == attr.name() { break; } - res.extend( - policy - .attribute_values(&Attribute::new(&attr.axis(), name))? - .iter() - .map(|&value| vec![value]) - .collect::>>(), - ); + res.push(vec![Attribute::new(&attr.axis(), name)]); } } Ok(res) @@ -474,39 +469,51 @@ mod tests { } #[test] - fn test_to_attribute_combinations() -> Result<(), Error> { + fn test_access_policy_to_partition() -> Result<(), Error> { + // + // create policy let mut policy = policy()?; - policy.rotate(&Attribute::new("Department", "FIN"))?; - let access_policy = (AccessPolicy::new("Department", "HR") - | AccessPolicy::new("Department", "FIN")) - & AccessPolicy::new("Security Level", "Confidential"); - let combinations = to_attribute_combinations(&access_policy, &policy)?; - let mut partitions_: HashSet = HashSet::with_capacity(combinations.len()); - for combination in combinations { - partitions_.insert(Partition::from_attributes(combination)?); - } - // combine attribute values to verify - let mut map: HashMap> = HashMap::new(); - let mut dpt_axis_attributes = - policy.attribute_values(&Attribute::new("Department", "FIN"))?; - dpt_axis_attributes.extend(policy.attribute_values(&Attribute::new("Department", "HR"))?); - map.insert("Department".to_owned(), dpt_axis_attributes); - let mut lvl_axis_attributes = - policy.attribute_values(&Attribute::new("Security Level", "Confidential"))?; - lvl_axis_attributes - .extend(policy.attribute_values(&Attribute::new("Security Level", "Protected"))?); - map.insert("Security Level".to_owned(), lvl_axis_attributes); - - let axes: Vec = policy.as_map().keys().cloned().collect(); - - let combinations = combine_attribute_values(0, axes.as_slice(), &map)?; - let mut partitions: HashSet = HashSet::with_capacity(combinations.len()); - for combination in combinations { - partitions.insert(Partition::from_attributes(combination)?); + // + // create access policy + let access_policy = AccessPolicy::new("Department", "HR") + | (AccessPolicy::new("Department", "FIN") + & AccessPolicy::new("Security Level", "Confidential")); + + // + // create partitions from access policy + let partitions = access_policy_to_partitions(&access_policy, &policy)?; + + // + // manually create the partitions + let mut partitions_ = HashSet::new(); + // add the partitions associated with the HR department: combine with + // all attributes of the Security Level axis + let hr_value = policy.attribute_current_value(&Attribute::new("Department", "HR"))?; + let (security_levels, _) = policy.as_map().get("Security Level").unwrap(); + for attr_name in security_levels { + let attr_value = + policy.attribute_current_value(&Attribute::new("Security Level", attr_name))?; + let mut partition = vec![hr_value, attr_value]; + partition.sort_unstable(); + partitions_.insert(Partition::from_attributes(partition)?); } + // add the other attribute combination: FIN && Confidential + let fin_value = policy.attribute_current_value(&Attribute::new("Department", "FIN"))?; + let conf_value = + policy.attribute_current_value(&Attribute::new("Security Level", "Confidential"))?; + let mut partition = vec![fin_value, conf_value]; + partition.sort_unstable(); + partitions_.insert(Partition::from_attributes(partition)?); + // since this is a hyerachical axis, add the lower values: here only protected + let prot_value = + policy.attribute_current_value(&Attribute::new("Security Level", "Protected"))?; + let mut partition = vec![fin_value, prot_value]; + partition.sort_unstable(); + partitions_.insert(Partition::from_attributes(partition)?); + assert_eq!(partitions, partitions_); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 4c412c56..bf7407cb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,6 +39,8 @@ pub enum Error { ExistingAttribute(String, String), #[error("policy {0} already exists")] ExistingPolicy(String), + #[error("Combination {0} already exists")] + ExistingCombination(String), #[error("invalid size")] InvalidSize(String), #[error("Empty private key")] diff --git a/src/interfaces/ffi/generate_cc_keys.rs b/src/interfaces/ffi/generate_cc_keys.rs index 22b3cd85..9846941e 100644 --- a/src/interfaces/ffi/generate_cc_keys.rs +++ b/src/interfaces/ffi/generate_cc_keys.rs @@ -9,7 +9,7 @@ use crate::{ api::{CoverCrypt, PrivateKey}, ffi_bail, ffi_not_null, ffi_unwrap, interfaces::ffi::error::{set_last_error, FfiError}, - policies::{AccessPolicy, Policy}, + policies::{AccessPolicy, Attribute, Policy}, }; #[no_mangle] @@ -161,7 +161,7 @@ pub unsafe extern "C" fn h_generate_user_private_key( let policy: Policy = ffi_unwrap!(serde_json::from_str(&policy)); // - // Generate master keys + // Generate user private key let user_key = ffi_unwrap!( CoverCrypt::::default().generate_user_private_key( &master_private_key, @@ -191,3 +191,81 @@ pub unsafe extern "C" fn h_generate_user_private_key( 0 } + +#[no_mangle] +/// Generate the user private key matching the given access policy +/// +/// - `new_policy_ptr`: Output buffer containing new policy +/// - `new_policy_len`: Size of the output buffer +/// - `attributes_ptr`: Attributes to rotate (JSON) +/// - `policy_ptr`: Policy to use to generate the keys (JSON) +/// # Safety +pub unsafe extern "C" fn h_rotate_attributes( + new_policy_ptr: *mut c_char, + new_policy_len: *mut c_int, + attributes_ptr: *const c_char, + policy_ptr: *const c_char, +) -> c_int { + // + // Checks inputs + ffi_not_null!( + new_policy_ptr, + "New policy pointer should point to pre-allocated memory" + ); + if *new_policy_len == 0 { + ffi_bail!("The new policy buffer should not be empty"); + } + ffi_not_null!(attributes_ptr, "Attributes pointer should not be null"); + ffi_not_null!(policy_ptr, "Policy pointer should not be null"); + + // + // Attributes + let attributes = match CStr::from_ptr(attributes_ptr).to_str() { + Ok(msg) => msg.to_owned(), + Err(e) => { + set_last_error(FfiError::Generic(format!( + "CoverCrypt attributes rotation: invalid Attributes: {e}" + ))); + return 1; + } + }; + let attributes: Vec = ffi_unwrap!(serde_json::from_str(&attributes)); + + // + // Policy + let policy = match CStr::from_ptr(policy_ptr).to_str() { + Ok(msg) => msg.to_owned(), + Err(e) => { + set_last_error(FfiError::Generic(format!( + "CoverCrypt keys generation: invalid Policy: {e}" + ))); + return 1; + } + }; + let mut policy: Policy = ffi_unwrap!(serde_json::from_str(&policy)); + + // + // Rotate attributes of the current policy + for attr in &attributes { + ffi_unwrap!(policy.rotate(attr)); + } + + // + // Serialize new policy + let new_policy_string = policy.to_string(); + + // + // Prepare output + let allocated = *new_policy_len; + let len = new_policy_string.len(); + if (allocated as usize) < len { + ffi_bail!( + "The pre-allocated output policy buffer is too small; need {len} bytes, allocated {allocated}" + ); + } + std::slice::from_raw_parts_mut(new_policy_ptr as *mut u8, len) + .copy_from_slice(new_policy_string.as_bytes()); + *new_policy_len = len as c_int; + + 0 +} diff --git a/src/interfaces/pyo3/generate_cc_keys.rs b/src/interfaces/pyo3/generate_cc_keys.rs index d49e59a0..bc9524a3 100755 --- a/src/interfaces/pyo3/generate_cc_keys.rs +++ b/src/interfaces/pyo3/generate_cc_keys.rs @@ -3,9 +3,12 @@ use pyo3::{exceptions::PyTypeError, prelude::*}; use crate::{ api::{CoverCrypt, PrivateKey}, - policies::{AccessPolicy, Policy}, + policies::{AccessPolicy, Attribute, Policy, PolicyAxis}, }; +/// 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()) @@ -22,6 +25,11 @@ pub fn generate_master_keys(policy_bytes: Vec) -> PyResult<(Vec, Vec )) } +/// Generate a user private key. +/// +/// - `master_private_key_bytes` : master secret key +/// - `access_policy_str` : user access policy +/// - `policy_bytes` : global policy #[pyfunction] pub fn generate_user_private_key( master_private_key_bytes: Vec, @@ -42,3 +50,49 @@ pub fn generate_user_private_key( Ok(user_key.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.is_hierarchical(), + ))?; + } + + 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)?; + } + serde_json::to_vec(&policy) + .map_err(|e| PyTypeError::new_err(format!("Error serializing policy: {e}"))) +} diff --git a/src/interfaces/pyo3/mod.rs b/src/interfaces/pyo3/mod.rs index 33a743b6..bca5a75f 100755 --- a/src/interfaces/pyo3/mod.rs +++ b/src/interfaces/pyo3/mod.rs @@ -1,7 +1,9 @@ use pyo3::{pymodule, types::PyModule, wrap_pyfunction, PyResult, Python}; use self::{ - generate_cc_keys::{generate_master_keys, generate_user_private_key}, + generate_cc_keys::{ + generate_master_keys, generate_policy, generate_user_private_key, rotate_attributes, + }, hybrid_cc_aes::{ decrypt, decrypt_hybrid_block, decrypt_hybrid_header, encrypt, encrypt_hybrid_block, encrypt_hybrid_header, get_encrypted_header_size, @@ -20,6 +22,8 @@ impl From for pyo3::PyErr { fn cover_crypt(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(generate_master_keys, m)?)?; m.add_function(wrap_pyfunction!(generate_user_private_key, m)?)?; + m.add_function(wrap_pyfunction!(generate_policy, m)?)?; + m.add_function(wrap_pyfunction!(rotate_attributes, m)?)?; m.add_function(wrap_pyfunction!(get_encrypted_header_size, m)?)?; m.add_function(wrap_pyfunction!(encrypt_hybrid_header, m)?)?; m.add_function(wrap_pyfunction!(decrypt_hybrid_header, m)?)?; diff --git a/src/interfaces/pyo3/tests/demo.py b/src/interfaces/pyo3/tests/demo.py index dfc531ed..7dfcc201 100644 --- a/src/interfaces/pyo3/tests/demo.py +++ b/src/interfaces/pyo3/tests/demo.py @@ -2,62 +2,35 @@ import cover_crypt -# CoverCrypt Policy with 2 axes: -policy_json = { - "last_attribute_value": 10, - "max_attribute_value": 100, - "store": { - "Security Level": [ - [ - "Protected", - "Low Secret", - "Medium Secret", - "High Secret", - "Top Secret" - ], True +# Declare 2 CoverCrypt policy axis: +policy_axis_json = [ + { + "name": "Security Level", + "attributes": [ + "Protected", + "Low Secret", + "Medium Secret", + "High Secret", + "Top Secret" ], - "Department": [ - [ - "R&D", - "HR", - "MKG", - "FIN" - ], False - ] + "hierarchical": True }, - "attribute_to_int": { - "Security Level::Low Secret": [ - 2 + { + "name": "Department", + "attributes": [ + "R&D", + "HR", + "MKG", + "FIN" ], - "Department::MKG": [ - 8 - ], - "Security Level::Medium Secret": [ - 3 - ], - "Security Level::Top Secret": [ - 5 - ], - "Security Level::Protected": [ - 1 - ], - "Department::FIN": [ - 10, - 9 - ], - "Department::HR": [ - 7 - ], - "Department::R&D": [ - 6 - ], - "Security Level::High Secret": [ - 4 - ] + "hierarchical": False } -} +] -policy = bytes(json.dumps(policy_json), 'utf-8') +policy_axis = bytes(json.dumps(policy_axis_json), 'utf-8') + +policy = cover_crypt.generate_policy( + policy_axis_bytes=policy_axis, max_attribute_value=100) master_keys = cover_crypt.generate_master_keys(policy) @@ -111,3 +84,37 @@ cleartext = cover_crypt.decrypt(top_secret_mkg_fin_user, low_secret_fin_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 = 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 = cover_crypt.generate_master_keys(new_policy) +new_low_secret_mkg_data = cover_crypt.encrypt(metadata, new_policy, bytes(json.dumps( + ['Security Level::Low Secret', 'Department::MKG']), 'utf8'), master_keys[1], plaintext_bytes) + +# The medium secret user cannot decrypt the new message until its key is refreshed +try: + cleartext = cover_crypt.decrypt( + medium_secret_mkg_user, new_low_secret_mkg_data) +except Exception as ex: + print(f"As expected, user cannot decrypt this message: {ex}") + +# Refresh medium secret key +new_medium_secret_mkg_user = cover_crypt.generate_user_private_key( + master_keys[0], "Security Level::Medium Secret && Department::MKG", new_policy) + +# New messages can now be decrypted +cleartext = cover_crypt.decrypt( + new_medium_secret_mkg_user, new_low_secret_mkg_data) +assert(str(bytes(cleartext), "utf-8") == plaintext) diff --git a/src/interfaces/pyo3/tests/test.sh b/src/interfaces/pyo3/tests/test.sh index 817b0c09..08b8c952 100644 --- a/src/interfaces/pyo3/tests/test.sh +++ b/src/interfaces/pyo3/tests/test.sh @@ -12,5 +12,5 @@ init(){ rm -f target/wheels/*.whl maturin build --cargo-extra-args="--release --features python" -pip install target/wheels/*.whl +pip install --force-reinstall target/wheels/*.whl python3 src/interfaces/pyo3/tests/demo.py diff --git a/src/interfaces/statics.rs b/src/interfaces/statics.rs index 9b1a238f..6fdfe0e1 100644 --- a/src/interfaces/statics.rs +++ b/src/interfaces/statics.rs @@ -213,7 +213,7 @@ mod tests { use serde_json::Value; use super::*; - use crate::policies::{ap, Attribute, PolicyAxis}; + use crate::policies::{ap, AccessPolicy, Attribute, PolicyAxis}; #[derive(Serialize, Deserialize)] struct NonRegressionTestVector { @@ -345,4 +345,92 @@ mod tests { ) .is_err()); } + + fn policy() -> Result { + let sec_level = PolicyAxis::new( + "Security Level", + &["Protected", "Confidential", "Top Secret"], + true, + ); + let department = PolicyAxis::new("Department", &["R&D", "HR", "MKG", "FIN"], false); + let mut policy = Policy::new(100); + policy.add_axis(&sec_level)?; + policy.add_axis(&department)?; + Ok(policy) + } + + #[test] + fn test_single_attribute_in_access_policy() -> Result<(), Error> { + // + // Declare policy + let policy = policy()?; + + // + // Setup CoverCrypt + let cc = CoverCrypt::::default(); + let (master_private_key, _master_public_key) = cc.generate_master_keys(&policy)?; + + // + // New user private key + let access_policy = AccessPolicy::from_boolean_expression("Security Level::Top Secret")?; + let _user_key = + cc.generate_user_private_key(&master_private_key, &access_policy, &policy)?; + + Ok(()) + } + + #[test] + fn test_rotate_then_encrypt() -> Result<(), Error> { + // + // Declare policy + let mut policy = policy()?; + + // + // Setup CoverCrypt + let cc = CoverCrypt::::default(); + let (master_private_key, master_public_key) = cc.generate_master_keys(&policy)?; + + // + // New user private key + let access_policy = + AccessPolicy::from_boolean_expression("Security Level::Top Secret && Department::FIN")?; + let user_key = + cc.generate_user_private_key(&master_private_key, &access_policy, &policy)?; + + // + // Encrypt + let encrypted_header = encrypt_hybrid_header::( + &policy, + &master_public_key, + &[Attribute::from(("Security Level", "Top Secret"))], + None, + )?; + + let _cleartext_header = decrypt_hybrid_header::( + &user_key, + &encrypted_header.header_bytes, + )?; + + // + // Rotate argument (must refresh master keys) + policy.rotate(&Attribute::from(("Security Level", "Top Secret")))?; + let (_master_private_key, master_public_key) = cc.generate_master_keys(&policy)?; + + // + // Encrypt with new attribute + let encrypted_header = encrypt_hybrid_header::( + &policy, + &master_public_key, + &[Attribute::from(("Security Level", "Top Secret"))], + None, + )?; + + assert!(decrypt_hybrid_header::( + &user_key, + &encrypted_header.header_bytes, + ) + .is_err()); + + Ok(()) + } } diff --git a/src/interfaces/wasm_bindgen/generate_cc_keys.rs b/src/interfaces/wasm_bindgen/generate_cc_keys.rs index adaa1b81..37d5b177 100644 --- a/src/interfaces/wasm_bindgen/generate_cc_keys.rs +++ b/src/interfaces/wasm_bindgen/generate_cc_keys.rs @@ -4,11 +4,12 @@ // Wait for `wasm-bindgen` issue 2774: https://github.com/rustwasm/wasm-bindgen/issues/2774 use cosmian_crypto_base::asymmetric::ristretto::X25519Crypto; +use js_sys::Uint8Array; use wasm_bindgen::prelude::*; use crate::{ api::{CoverCrypt, PrivateKey}, - policies::{AccessPolicy, Policy}, + policies::{AccessPolicy, Attribute, Policy}, }; /// Generate the master authority keys for supplied Policy @@ -16,9 +17,7 @@ use crate::{ /// - `policy_bytes` : Policy to use to generate the keys (serialized from /// JSON) #[wasm_bindgen] -pub fn webassembly_generate_master_keys( - policy_bytes: js_sys::Uint8Array, -) -> Result { +pub fn webassembly_generate_master_keys(policy_bytes: Uint8Array) -> Result { let policy: Policy = serde_json::from_slice(policy_bytes.to_vec().as_slice()) .map_err(|e| JsValue::from_str(&format!("Error deserializing policy:{e}")))?; @@ -42,7 +41,7 @@ pub fn webassembly_generate_master_keys( master_keys_bytes.extend_from_slice(&u32::to_be_bytes(master_private_key_bytes.len() as u32)); master_keys_bytes.extend_from_slice(&master_private_key_bytes); master_keys_bytes.extend_from_slice(&master_public_key_bytes); - Ok(js_sys::Uint8Array::from(&master_keys_bytes[..])) + Ok(Uint8Array::from(&master_keys_bytes[..])) } /// Generate a user private key. @@ -53,10 +52,10 @@ pub fn webassembly_generate_master_keys( /// - `policy_bytes` : global policy (serialized from JSON) #[wasm_bindgen] pub fn webassembly_generate_user_private_key( - master_private_key_bytes: js_sys::Uint8Array, + master_private_key_bytes: Uint8Array, access_policy_str: &str, - policy_bytes: js_sys::Uint8Array, -) -> Result { + policy_bytes: Uint8Array, +) -> Result { let master_private_key: PrivateKey = PrivateKey::try_from_bytes(master_private_key_bytes.to_vec().as_slice()) .map_err(|e| JsValue::from_str(&format!("Error deserializing private key: {e}")))?; @@ -72,5 +71,33 @@ pub fn webassembly_generate_user_private_key( let user_key_bytes = user_key .try_to_bytes() .map_err(|e| JsValue::from_str(&format!("Error serializing user key: {e}")))?; - Ok(js_sys::Uint8Array::from(user_key_bytes.as_slice())) + Ok(Uint8Array::from(user_key_bytes.as_slice())) +} + +/// Rotate attributes, changing their underlying values with that of an unused +/// slot +/// +/// - `attributes_bytes` : user access policy (boolean expression as +/// string) +/// - `policy_bytes` : global policy (serialized from JSON) +#[wasm_bindgen] +pub fn webassembly_rotate_attributes( + attributes_bytes: Uint8Array, + policy_bytes: Uint8Array, +) -> Result { + let attributes: Vec = + serde_json::from_slice(attributes_bytes.to_vec().as_slice()) + .map_err(|e| JsValue::from_str(&format!("Error deserializing attributes: {e}")))?; + let mut policy: Policy = serde_json::from_slice(policy_bytes.to_vec().as_slice()) + .map_err(|e| JsValue::from_str(&format!("Error deserializing policy: {e}")))?; + + // + // Rotate attributes of the current policy + for attr in &attributes { + policy + .rotate(attr) + .map_err(|e| JsValue::from_str(&format!("Error rotating attribute: {e}")))?; + } + + Ok(policy.to_string()) } diff --git a/src/interfaces/wasm_bindgen/tests.rs b/src/interfaces/wasm_bindgen/tests.rs index 06d74a3c..b6be91e4 100644 --- a/src/interfaces/wasm_bindgen/tests.rs +++ b/src/interfaces/wasm_bindgen/tests.rs @@ -12,6 +12,7 @@ use wasm_bindgen_test::*; use super::generate_cc_keys::{ webassembly_generate_master_keys, webassembly_generate_user_private_key, + webassembly_rotate_attributes, }; use crate::{ api::{self, CoverCrypt, PrivateKey, PublicKey}, @@ -160,10 +161,71 @@ fn test_generate_keys() { // // Generate user private key - webassembly_generate_user_private_key( + let user_private_key_bytes = webassembly_generate_user_private_key( Uint8Array::from(private_key_bytes), "Department::FIN && Security Level::Top Secret", Uint8Array::from(serialized_policy.as_slice()), ) + .unwrap() + .to_vec(); + let user_private_key = + PrivateKey::::try_from_bytes(&user_private_key_bytes).unwrap(); + + let attributes = vec![Attribute::new("Security Level", "Confidential")]; + let serialized_attributes = serde_json::to_vec(&attributes).unwrap(); + + let new_policy = webassembly_rotate_attributes( + Uint8Array::from(serialized_attributes.as_slice()), + Uint8Array::from(serialized_policy.as_slice()), + ) .unwrap(); + + // + // Generate master keys + let master_keys = + webassembly_generate_master_keys(Uint8Array::from(new_policy.as_bytes())).unwrap(); + let master_keys_vec = master_keys.to_vec(); + let private_key_size = u32::from_be_bytes(master_keys_vec[0..4].try_into().unwrap()); + let private_key_bytes = &master_keys_vec[4..4 + private_key_size as usize]; + + // + // Check deserialization + PrivateKey::::try_from_bytes(private_key_bytes).unwrap(); + let master_public_key = PublicKey::::try_from_bytes( + &master_keys_vec[4 + private_key_size as usize..], + ) + .unwrap(); + + // + // Encrypt / decrypt + // + let meta_data = Metadata { + uid: vec![1, 2, 3, 4, 5, 6, 7, 8, 9], + additional_data: Some(vec![10, 11, 12, 13, 14]), + }; + + let encrypted_header = + encrypt_header(&meta_data, &policy, &attributes, &master_public_key).unwrap(); + + // + // Try to decrypt with a non-refreshed private key (it fails) + // + assert!(decrypt_header(&encrypted_header, &user_private_key).is_err()); + + // + // Refresh user private key + let user_private_key_bytes = webassembly_generate_user_private_key( + Uint8Array::from(private_key_bytes), + "Security Level::Confidential", + Uint8Array::from(serialized_policy.as_slice()), + ) + .unwrap() + .to_vec(); + let user_private_key = + PrivateKey::::try_from_bytes(&user_private_key_bytes).unwrap(); + + // + // Decrypt with the refreshed private key (it now works) + // + decrypt_header(&encrypted_header, &user_private_key).unwrap(); } diff --git a/src/policies/policy.rs b/src/policies/policy.rs index 0e43a11b..2896034b 100644 --- a/src/policies/policy.rs +++ b/src/policies/policy.rs @@ -1,18 +1,19 @@ #![allow(clippy::module_name_repetitions)] -use crate::error::Error; -use serde::{Deserialize, Serialize}; use std::{ collections::{BinaryHeap, HashMap}, fmt::{Debug, Display}, }; +use serde::{Deserialize, Serialize}; + use super::attribute::Attribute; +use crate::error::Error; // Define a policy axis by its name and its underlying attribute names // If `hierarchical` is `true`, we assume a lexicographical order based on the // attribute name -#[derive(Clone)] +#[derive(Clone, Deserialize)] pub struct PolicyAxis { name: String, attributes: Vec, @@ -29,27 +30,35 @@ impl PolicyAxis { } } - pub fn attributes(&self) -> &[String] { - &self.attributes - } - #[allow(clippy::len_without_is_empty)] #[must_use] pub fn len(&self) -> usize { self.attributes.len() } + + pub fn attributes(&self) -> &[String] { + &self.attributes + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn is_hierarchical(&self) -> bool { + self.hierarchical + } } -// A policy is a set of fixed policy axes, defining an inner attribute -// element for each policy axis attribute a fixed number of revocation -// addition of attributes is allowed +/// A policy is a set of fixed policy axes, defining an inner attribute +/// element for each policy axis attribute a fixed number of revocation +/// addition of attributes is allowed #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Policy { pub(crate) last_attribute_value: u32, pub(crate) max_attribute_value: u32, - // store the policies by name + /// store the policies by name pub(crate) store: HashMap, bool)>, - // mapping between (policy_name, policy_attribute) -> integer + /// mapping between attribute -> integer pub(crate) attribute_to_int: HashMap>, } @@ -130,16 +139,15 @@ impl Policy { /// Rotate an attribute, changing its underlying value with that of an /// unused slot pub fn rotate(&mut self, attr: &Attribute) -> Result<(), Error> { - if self.last_attribute_value + 1 > self.max_attribute_value { - return Err(Error::CapacityOverflow); - } - if let Some(uint) = self.attribute_to_int.get_mut(attr) { + if self.last_attribute_value == self.max_attribute_value { + Err(Error::CapacityOverflow) + } else if let Some(heap) = self.attribute_to_int.get_mut(attr) { self.last_attribute_value += 1; - uint.push(self.last_attribute_value); + heap.push(self.last_attribute_value); + Ok(()) } else { - return Err(Error::AttributeNotFound(format!("{:?}", attr))); + Err(Error::AttributeNotFound(format!("{:?}", attr))) } - Ok(()) } /// Returns the list of Attributes of this Policy