diff --git a/CHANGELOG.md b/CHANGELOG.md index 005fcdb4..e83aac8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.12.0] - Unreleased + +### Changed + +- Modified `ThresholdDecryptionResponse` to use `CiphertextHeader` and `AccessControlPolicy` to utilize encapsulation now provided by `ferveo`. ([#74]) + +### Added + +- Added `ThresholdMessageKit` which is the representation of data encrypted via `ferveo` that utilizes data encapsulation and ephemeral symmetric key. ([#74]) +- Added `AccessControlPolicy` which contains access metadata (conditions, public key, authorization etc.) which forms part of the `ThresholdMessageKit`. ([#74]) +- Added `AuthenticatedData` which forms part of the `AccessControlPolicy` and is needed to ensure that the aad is consistent during encryption process and during decryption process. ([#74]) +- Added `encrypt_for_dkg` method for generation of `ferveo` `Ciphertext` and `AuthenticatedData`. ([#74]) + + +[#74]: https://github.com/nucypher/nucypher-core/pull/74 + + ## [0.11.0] - 2023-08-01 ### Changed diff --git a/Cargo.lock b/Cargo.lock index f2654251..b85818c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,8 +518,7 @@ dependencies = [ [[package]] name = "ferveo-common-pre-release" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b88f694e924a2878d4abf89b79df44cdee2e0670875aba885bb769ccd71be3" +source = "git+https://github.com/derekpierre/nucypher-ferveo.git?branch=acp#c3fe68a3214b398db617e687e5244371661a77f7" dependencies = [ "ark-ec", "ark-serialize", @@ -534,8 +533,7 @@ dependencies = [ [[package]] name = "ferveo-pre-release" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34e97a82e328847d4abf9d5a4dfec06c693210fcb3e6e9feb78f225b8126464" +source = "git+https://github.com/derekpierre/nucypher-ferveo.git?branch=acp#c3fe68a3214b398db617e687e5244371661a77f7" dependencies = [ "ark-bls12-381", "ark-ec", @@ -638,8 +636,7 @@ dependencies = [ [[package]] name = "group-threshold-cryptography-pre-release" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ade4859b58171e6815b0641e23f615f5dab030ecf2376b069d652f323fa760b" +source = "git+https://github.com/derekpierre/nucypher-ferveo.git?branch=acp#c3fe68a3214b398db617e687e5244371661a77f7" dependencies = [ "ark-bls12-381", "ark-ec", @@ -869,7 +866,7 @@ checksum = "94c7128ba23c81f6471141b90f17654f89ef44a56e14b8a4dd0fddfccd655277" [[package]] name = "nucypher-core" -version = "0.10.0" +version = "0.11.0" dependencies = [ "chacha20poly1305", "ferveo-pre-release", @@ -891,19 +888,19 @@ dependencies = [ [[package]] name = "nucypher-core-python" -version = "0.10.0" +version = "0.11.0" dependencies = [ "derive_more", "ferveo-pre-release", "nucypher-core", "pyo3", - "pyo3-build-config 0.19.1", + "pyo3-build-config", "umbral-pre", ] [[package]] name = "nucypher-core-wasm" -version = "0.10.0" +version = "0.11.0" dependencies = [ "console_error_panic_hook", "derive_more", @@ -1033,7 +1030,7 @@ dependencies = [ "libc", "memoffset", "parking_lot", - "pyo3-build-config 0.18.3", + "pyo3-build-config", "pyo3-ffi", "pyo3-macros", "unindent", @@ -1049,16 +1046,6 @@ dependencies = [ "target-lexicon", ] -[[package]] -name = "pyo3-build-config" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0" -dependencies = [ - "once_cell", - "target-lexicon", -] - [[package]] name = "pyo3-ffi" version = "0.18.3" @@ -1066,7 +1053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" dependencies = [ "libc", - "pyo3-build-config 0.18.3", + "pyo3-build-config", ] [[package]] @@ -1395,8 +1382,7 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subproductdomain-pre-release" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7331c4e4ab9b7c9ed4ad2e950e8760826622c7ab110d9f1b3f350e2005017eab" +source = "git+https://github.com/derekpierre/nucypher-ferveo.git?branch=acp#c3fe68a3214b398db617e687e5244371661a77f7" dependencies = [ "anyhow", "ark-ec", diff --git a/nucypher-core-python/Cargo.toml b/nucypher-core-python/Cargo.toml index 75f9794e..c930cec9 100644 --- a/nucypher-core-python/Cargo.toml +++ b/nucypher-core-python/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] pyo3 = "0.18" nucypher-core = { path = "../nucypher-core" } umbral-pre = { version = "0.11.0", features = ["bindings-python"] } -ferveo = { version = "0.2.1", package = "ferveo-pre-release", features = ["bindings-python"] } +ferveo = { package = "ferveo-pre-release", git = "https://github.com/derekpierre/nucypher-ferveo.git", branch = "acp", features = ["bindings-python"] } derive_more = { version = "0.99", default-features = false, features = ["from", "as_ref"] } [build-dependencies] diff --git a/nucypher-core-python/nucypher_core/__init__.py b/nucypher-core-python/nucypher_core/__init__.py index cb500c48..fe122c44 100644 --- a/nucypher-core-python/nucypher_core/__init__.py +++ b/nucypher-core-python/nucypher_core/__init__.py @@ -17,6 +17,9 @@ MetadataRequest, MetadataResponse, MetadataResponsePayload, + AccessControlPolicy, + AuthenticatedData, + ThresholdMessageKit, ThresholdDecryptionRequest, ThresholdDecryptionResponse, EncryptedThresholdDecryptionRequest, @@ -25,4 +28,5 @@ SessionStaticKey, SessionStaticSecret, SessionSecretFactory, + encrypt_for_dkg, ) diff --git a/nucypher-core-python/nucypher_core/__init__.pyi b/nucypher-core-python/nucypher_core/__init__.pyi index 96093475..a9053296 100644 --- a/nucypher-core-python/nucypher_core/__init__.pyi +++ b/nucypher-core-python/nucypher_core/__init__.pyi @@ -11,9 +11,11 @@ from .umbral import ( ) from .ferveo import ( - FerveoPublicKey, Ciphertext, - FerveoVariant + CiphertextHeader, + DkgPublicKey, + FerveoPublicKey, + FerveoVariant, SharedSecret, ) @@ -429,22 +431,89 @@ class MetadataResponse: ... +@final +class AuthenticatedData: + + def __init__(self, public_key: DkgPublicKey, conditions: Optional[Conditions]): + ... + + public_key: DkgPublicKey + + conditions: Optional[Conditions] + + def aad(self) -> bytes: + ... + + @staticmethod + def from_bytes(data: bytes) -> AuthenticatedData: + ... + + def __bytes__(self) -> bytes: + ... + + +def encrypt_for_dkg(data: bytes, public_key: DkgPublicKey, conditions: Optional[Conditions]) -> Tuple[Ciphertext, AuthenticatedData]: + ... + + +@final +class AccessControlPolicy: + + def __init__(self, auth_data: AuthenticatedData, authorization: bytes): + ... + + public_key: DkgPublicKey + + conditions: Optional[Conditions] + + authorization: bytes + + def aad(self) -> bytes: + ... + + @staticmethod + def from_bytes(data: bytes) -> AccessControlPolicy: + ... + + def __bytes__(self) -> bytes: + ... + +@final +class ThresholdMessageKit: + + def __init__(self, ciphertext: Ciphertext, acp: AccessControlPolicy): + ... + + acp: AccessControlPolicy + + ciphertext_header: CiphertextHeader + + def decrypt_with_shared_secret(self, shared_secret: SharedSecret): + ... + + @staticmethod + def from_bytes(data: bytes) -> ThresholdMessageKit: + ... + + def __bytes__(self) -> bytes: + ... + + @final class ThresholdDecryptionRequest: - def __init__(self, ritual_id: int, variant: FerveoVariant, ciphertext: Ciphertext, conditions: Optional[Conditions], - context: Optional[Context]): + def __init__(self, ritual_id: int, variant: FerveoVariant, ciphertext_header: CiphertextHeader, acp: AccessControlPolicy, context: Optional[Context]): ... ritual_id: int - conditions: Optional[Conditions] + acp: AccessControlPolicy context: Optional[Context] variant: FerveoVariant - ciphertext: Ciphertext + ciphertext_header: CiphertextHeader def encrypt(self, shared_secret: SessionSharedSecret, requester_public_key: SessionStaticKey) -> EncryptedThresholdDecryptionRequest: diff --git a/nucypher-core-python/nucypher_core/ferveo.py b/nucypher-core-python/nucypher_core/ferveo.py index 3593b4b2..c8fa3fe4 100644 --- a/nucypher-core-python/nucypher_core/ferveo.py +++ b/nucypher-core-python/nucypher_core/ferveo.py @@ -35,3 +35,4 @@ ValidatorsNotSorted = _ferveo.ValidatorsNotSorted ValidatorPublicKeyMismatch = _ferveo.ValidatorPublicKeyMismatch SerializationError = _ferveo.SerializationError +CiphertextHeader = _ferveo.CiphertextHeader diff --git a/nucypher-core-python/nucypher_core/ferveo.pyi b/nucypher-core-python/nucypher_core/ferveo.pyi index 1dfab2f0..ff17fd19 100644 --- a/nucypher-core-python/nucypher_core/ferveo.pyi +++ b/nucypher-core-python/nucypher_core/ferveo.pyi @@ -119,6 +119,9 @@ class Dkg: @final class Ciphertext: + header: CiphertextHeader + payload: bytes + @staticmethod def from_bytes(data: bytes) -> Ciphertext: ... @@ -127,6 +130,16 @@ class Ciphertext: ... +@final +class CiphertextHeader: + @staticmethod + def from_bytes(data: bytes) -> CiphertextHeader: + ... + + def __bytes__(self) -> bytes: + ... + + @final class DecryptionShareSimple: @staticmethod @@ -159,7 +172,7 @@ class AggregatedTranscript: def create_decryption_share_simple( self, dkg: Dkg, - ciphertext: Ciphertext, + ciphertext_header: CiphertextHeader, aad: bytes, validator_keypair: Keypair ) -> DecryptionShareSimple: @@ -168,7 +181,7 @@ class AggregatedTranscript: def create_decryption_share_precomputed( self, dkg: Dkg, - ciphertext: Ciphertext, + ciphertext_header: CiphertextHeader, aad: bytes, validator_keypair: Keypair ) -> DecryptionSharePrecomputed: diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index b33e1ccf..dda04728 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -6,8 +6,10 @@ extern crate alloc; use alloc::collections::{BTreeMap, BTreeSet}; - -use ferveo::bindings_python::{Ciphertext, FerveoPublicKey, FerveoVariant}; +use ferveo::bindings_python::{ + Ciphertext, CiphertextHeader, DkgPublicKey, FerveoPublicKey, FerveoPythonError, FerveoVariant, + SharedSecret, +}; use pyo3::class::basic::CompareOp; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::*; @@ -734,6 +736,182 @@ impl SessionSecretFactory { } } +// +// Authenticated data. +// +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct AuthenticatedData { + backend: nucypher_core::AuthenticatedData, +} + +#[pymethods] +impl AuthenticatedData { + #[new] + pub fn new(public_key: &DkgPublicKey, conditions: Option<&Conditions>) -> Self { + Self { + backend: nucypher_core::AuthenticatedData::new( + public_key.as_ref(), + conditions + .map(|conditions| conditions.backend.clone()) + .as_ref(), + ), + } + } + + pub fn aad(&self, py: Python) -> PyObject { + let result = self.backend.aad(); + PyBytes::new(py, result.as_ref()).into() + } + + #[getter] + pub fn public_key(&self) -> DkgPublicKey { + self.backend.public_key.into() + } + + #[getter] + pub fn conditions(&self) -> Option { + self.backend + .conditions + .clone() + .map(|conditions| Conditions { + backend: conditions, + }) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::AuthenticatedData>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + +// +// Encrypt for DKG. +// +#[pyfunction] +#[pyo3(signature = (data, public_key, conditions))] +pub fn encrypt_for_dkg( + data: &[u8], + public_key: &DkgPublicKey, + conditions: Option<&Conditions>, +) -> PyResult<(Ciphertext, AuthenticatedData)> { + let (ciphertext, auth_data) = nucypher_core::encrypt_for_dkg( + data, + public_key.as_ref(), + conditions + .map(|conditions| conditions.backend.clone()) + .as_ref(), + ) + .map_err(FerveoPythonError::FerveoError)?; + Ok((ciphertext.into(), auth_data.into())) +} + +// +// Access control metadata for encrypted data. +// +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct AccessControlPolicy { + backend: nucypher_core::AccessControlPolicy, +} + +#[pymethods] +impl AccessControlPolicy { + #[new] + pub fn new(auth_data: &AuthenticatedData, authorization: &[u8]) -> Self { + Self { + backend: nucypher_core::AccessControlPolicy::new(auth_data.as_ref(), authorization), + } + } + + pub fn aad(&self, py: Python) -> PyObject { + let result = self.backend.auth_data.aad(); + PyBytes::new(py, result.as_ref()).into() + } + + #[getter] + pub fn public_key(&self) -> DkgPublicKey { + self.backend.auth_data.public_key.into() + } + + #[getter] + pub fn conditions(&self) -> Option { + self.backend + .auth_data + .conditions + .clone() + .map(|conditions| Conditions { + backend: conditions, + }) + } + + #[getter] + pub fn authorization(&self) -> &[u8] { + self.backend.authorization.as_ref() + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::AccessControlPolicy>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + +// +// ThresholdMessageKit +// +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct ThresholdMessageKit { + backend: nucypher_core::ThresholdMessageKit, +} + +#[pymethods] +impl ThresholdMessageKit { + #[new] + pub fn new(ciphertext: &Ciphertext, acp: &AccessControlPolicy) -> Self { + Self { + backend: nucypher_core::ThresholdMessageKit::new(ciphertext.as_ref(), acp.as_ref()), + } + } + + #[getter] + pub fn ciphertext_header(&self) -> PyResult { + let header = self + .backend + .ciphertext_header() + .map_err(FerveoPythonError::from)?; + Ok(CiphertextHeader::from(header)) + } + + #[getter] + pub fn acp(&self) -> AccessControlPolicy { + self.backend.acp.clone().into() + } + + pub fn decrypt_with_shared_secret(&self, shared_secret: &SharedSecret) -> PyResult> { + self.backend + .decrypt_with_shared_secret(shared_secret.as_ref()) + .map_err(|err| FerveoPythonError::FerveoError(err).into()) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::ThresholdMessageKit>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + // // Threshold Decryption Request // @@ -750,17 +928,15 @@ impl ThresholdDecryptionRequest { pub fn new( ritual_id: u32, variant: FerveoVariant, - ciphertext: &Ciphertext, - conditions: Option<&Conditions>, + ciphertext_header: &CiphertextHeader, + acp: &AccessControlPolicy, context: Option<&Context>, ) -> PyResult { Ok(Self { backend: nucypher_core::ThresholdDecryptionRequest::new( ritual_id, - ciphertext.as_ref(), - conditions - .map(|conditions| conditions.backend.clone()) - .as_ref(), + ciphertext_header.as_ref(), + acp.as_ref(), context.map(|context| context.backend.clone()).as_ref(), variant.into(), ), @@ -773,13 +949,8 @@ impl ThresholdDecryptionRequest { } #[getter] - pub fn conditions(&self) -> Option { - self.backend - .conditions - .clone() - .map(|conditions| Conditions { - backend: conditions, - }) + pub fn acp(&self) -> AccessControlPolicy { + self.backend.acp.clone().into() } #[getter] @@ -791,8 +962,8 @@ impl ThresholdDecryptionRequest { } #[getter] - pub fn ciphertext(&self) -> Ciphertext { - self.backend.ciphertext.clone().into() + pub fn ciphertext_header(&self) -> CiphertextHeader { + self.backend.ciphertext_header.clone().into() } #[getter] @@ -1423,6 +1594,10 @@ fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { core_module.add_class::()?; core_module.add_class::()?; core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_function(wrap_pyfunction!(encrypt_for_dkg, core_module)?)?; // Build the umbral module let umbral_module = PyModule::new(py, "umbral")?; diff --git a/nucypher-core-wasm/Cargo.toml b/nucypher-core-wasm/Cargo.toml index a4aed797..536bbc1b 100644 --- a/nucypher-core-wasm/Cargo.toml +++ b/nucypher-core-wasm/Cargo.toml @@ -20,7 +20,7 @@ default = ["console_error_panic_hook"] [dependencies] umbral-pre = { version = "0.11.0", features = ["bindings-wasm"] } -ferveo = { version = "0.2.1", package = "ferveo-pre-release", features = ["bindings-wasm"] } +ferveo = { package = "ferveo-pre-release", git = "https://github.com/derekpierre/nucypher-ferveo.git", branch = "acp", features = ["bindings-wasm"] } nucypher-core = { path = "../nucypher-core" } wasm-bindgen = "0.2.86" js-sys = "0.3.63" diff --git a/nucypher-core-wasm/src/lib.rs b/nucypher-core-wasm/src/lib.rs index 623a024c..09c8d8d8 100644 --- a/nucypher-core-wasm/src/lib.rs +++ b/nucypher-core-wasm/src/lib.rs @@ -12,7 +12,9 @@ use alloc::{ }; use core::fmt; -use ferveo::bindings_wasm::{Ciphertext, FerveoVariant}; +use ferveo::bindings_wasm::{ + Ciphertext, CiphertextHeader, DkgPublicKey, FerveoVariant, JsResult, SharedSecret, +}; use js_sys::Error; use umbral_pre::bindings_wasm::{ Capsule, PublicKey, RecoverableSignature, SecretKey, Signer, VerifiedCapsuleFrag, @@ -194,6 +196,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "[Address, EncryptedKeyFrag]")] pub type VerifiedRevocationOrder; + + #[wasm_bindgen(typescript_type = "[Ciphertext, AuthenticatedData]")] + pub type DkgEncryptionResult; } // @@ -648,6 +653,155 @@ impl SessionSecretFactory { } } +// +// AuthenticatedData +// + +#[wasm_bindgen] +#[derive(PartialEq, Eq, Debug, derive_more::From, derive_more::AsRef)] +pub struct AuthenticatedData(nucypher_core::AuthenticatedData); + +generate_from_bytes!(AuthenticatedData); +generate_to_bytes!(AuthenticatedData); +generate_equals!(AuthenticatedData); + +#[wasm_bindgen] +impl AuthenticatedData { + #[wasm_bindgen(constructor)] + pub fn new( + public_key: &DkgPublicKey, + conditions: &OptionConditions, + ) -> Result { + let typed_conditions = try_from_js_option::(conditions)?; + + Ok(Self(nucypher_core::AuthenticatedData::new( + public_key.as_ref(), + typed_conditions.as_ref().map(|conditions| &conditions.0), + ))) + } + + pub fn aad(&self) -> Box<[u8]> { + self.0.aad() + } + + #[wasm_bindgen(getter, js_name = publicKey)] + pub fn public_key(&self) -> DkgPublicKey { + DkgPublicKey::from(self.0.public_key) + } + + #[wasm_bindgen(getter)] + pub fn conditions(&self) -> Option { + self.0.conditions.clone().map(Conditions) + } +} + +// +// Encrypt for dkg +// +#[wasm_bindgen(js_name = "encryptForDkg")] +pub fn encrypt_for_dkg( + data: &[u8], + public_key: &DkgPublicKey, + conditions: &OptionConditions, +) -> Result { + let typed_conditions = try_from_js_option::(conditions)?; + let (ciphertext, auth_data) = nucypher_core::encrypt_for_dkg( + data, + public_key.as_ref(), + typed_conditions.as_ref().map(|conditions| &conditions.0), + ) + .map_err(map_js_err)?; + Ok(into_js_array([ + JsValue::from(Ciphertext::from(ciphertext)), + JsValue::from(AuthenticatedData::from(auth_data)), + ])) +} + +// +// AccessControlPolicy +// + +#[wasm_bindgen] +#[derive(PartialEq, Eq, Debug, derive_more::From, derive_more::AsRef)] +pub struct AccessControlPolicy(nucypher_core::AccessControlPolicy); + +generate_from_bytes!(AccessControlPolicy); +generate_to_bytes!(AccessControlPolicy); +generate_equals!(AccessControlPolicy); + +#[wasm_bindgen] +impl AccessControlPolicy { + #[wasm_bindgen(constructor)] + pub fn new( + auth_data: &AuthenticatedData, + authorization: &[u8], + ) -> Result { + Ok(Self(nucypher_core::AccessControlPolicy::new( + auth_data.as_ref(), + authorization, + ))) + } + + pub fn aad(&self) -> Box<[u8]> { + self.0.aad() + } + + #[wasm_bindgen(getter, js_name = publicKey)] + pub fn public_key(&self) -> DkgPublicKey { + DkgPublicKey::from(self.0.auth_data.public_key) + } + + #[wasm_bindgen(getter)] + pub fn authorization(&self) -> Box<[u8]> { + self.0.authorization.clone() + } + + #[wasm_bindgen(getter)] + pub fn conditions(&self) -> Option { + self.0.auth_data.conditions.clone().map(Conditions) + } +} + +// +// ThresholdMessageKit +// +#[wasm_bindgen] +#[derive(PartialEq, Eq, Debug, derive_more::From, derive_more::AsRef)] +pub struct ThresholdMessageKit(nucypher_core::ThresholdMessageKit); + +generate_from_bytes!(ThresholdMessageKit); +generate_to_bytes!(ThresholdMessageKit); +generate_equals!(ThresholdMessageKit); + +#[wasm_bindgen] +impl ThresholdMessageKit { + #[wasm_bindgen(constructor)] + pub fn new(ciphertext: &Ciphertext, acp: &AccessControlPolicy) -> Self { + Self(nucypher_core::ThresholdMessageKit::new( + ciphertext.as_ref(), + acp.as_ref(), + )) + } + + #[wasm_bindgen(getter, js_name=ciphertextHeader)] + pub fn ciphertext_header(&self) -> JsResult { + let header = self.0.ciphertext_header().map_err(map_js_err)?; + Ok(CiphertextHeader::from(header)) + } + + #[wasm_bindgen(getter)] + pub fn acp(&self) -> AccessControlPolicy { + self.0.acp.clone().into() + } + + #[wasm_bindgen(js_name = decryptWithSharedSecret)] + pub fn decrypt_with_shared_secret(&self, shared_secret: &SharedSecret) -> JsResult> { + self.0 + .decrypt_with_shared_secret(shared_secret.as_ref()) + .map_err(map_js_err) + } +} + // // Threshold Decryption Request // @@ -657,6 +811,7 @@ impl SessionSecretFactory { pub struct ThresholdDecryptionRequest(nucypher_core::ThresholdDecryptionRequest); generate_from_bytes!(ThresholdDecryptionRequest); +generate_to_bytes!(ThresholdDecryptionRequest); generate_equals!(ThresholdDecryptionRequest); #[wasm_bindgen] @@ -665,17 +820,16 @@ impl ThresholdDecryptionRequest { pub fn new( ritual_id: u32, variant: &FerveoVariant, - ciphertext: &Ciphertext, - conditions: &OptionConditions, + ciphertext_header: &CiphertextHeader, + acp: &AccessControlPolicy, context: &OptionContext, ) -> Result { - let typed_conditions = try_from_js_option::(conditions)?; let typed_context = try_from_js_option::(context)?; Ok(Self(nucypher_core::ThresholdDecryptionRequest::new( ritual_id, - ciphertext.as_ref(), - typed_conditions.as_ref().map(|conditions| &conditions.0), + ciphertext_header.as_ref(), + acp.as_ref(), typed_context.as_ref().map(|context| &context.0), variant.clone().into(), ))) @@ -691,9 +845,14 @@ impl ThresholdDecryptionRequest { self.0.variant.into() } + #[wasm_bindgen(getter, js_name = ciphertextHeader)] + pub fn ciphertext_header(&self) -> CiphertextHeader { + self.0.ciphertext_header.clone().into() + } + #[wasm_bindgen(getter)] - pub fn ciphertext(&self) -> Ciphertext { - self.0.ciphertext.clone().into() + pub fn acp(&self) -> AccessControlPolicy { + self.0.acp.clone().into() } pub fn encrypt( @@ -706,11 +865,6 @@ impl ThresholdDecryptionRequest { .encrypt(shared_secret.as_ref(), requester_public_key.as_ref()), ) } - - #[wasm_bindgen(js_name = toBytes)] - pub fn to_bytes(&self) -> Box<[u8]> { - to_bytes(self) - } } // @@ -722,6 +876,7 @@ impl ThresholdDecryptionRequest { pub struct EncryptedThresholdDecryptionRequest(nucypher_core::EncryptedThresholdDecryptionRequest); generate_from_bytes!(EncryptedThresholdDecryptionRequest); +generate_to_bytes!(EncryptedThresholdDecryptionRequest); #[wasm_bindgen] impl EncryptedThresholdDecryptionRequest { @@ -744,11 +899,6 @@ impl EncryptedThresholdDecryptionRequest { .map_err(map_js_err) .map(ThresholdDecryptionRequest) } - - #[wasm_bindgen(js_name = toBytes)] - pub fn to_bytes(&self) -> Box<[u8]> { - to_bytes(self) - } } // @@ -760,6 +910,7 @@ impl EncryptedThresholdDecryptionRequest { pub struct ThresholdDecryptionResponse(nucypher_core::ThresholdDecryptionResponse); generate_from_bytes!(ThresholdDecryptionResponse); +generate_to_bytes!(ThresholdDecryptionResponse); #[wasm_bindgen] impl ThresholdDecryptionResponse { @@ -790,11 +941,6 @@ impl ThresholdDecryptionResponse { ) -> EncryptedThresholdDecryptionResponse { EncryptedThresholdDecryptionResponse(self.0.encrypt(shared_secret.as_ref())) } - - #[wasm_bindgen(js_name = toBytes)] - pub fn to_bytes(&self) -> Box<[u8]> { - to_bytes(self) - } } // @@ -808,6 +954,7 @@ pub struct EncryptedThresholdDecryptionResponse( ); generate_from_bytes!(EncryptedThresholdDecryptionResponse); +generate_to_bytes!(EncryptedThresholdDecryptionResponse); #[wasm_bindgen] impl EncryptedThresholdDecryptionResponse { @@ -825,11 +972,6 @@ impl EncryptedThresholdDecryptionResponse { .map_err(map_js_err) .map(ThresholdDecryptionResponse) } - - #[wasm_bindgen(js_name = toBytes)] - pub fn to_bytes(&self) -> Box<[u8]> { - to_bytes(self) - } } // diff --git a/nucypher-core-wasm/tests/wasm.rs b/nucypher-core-wasm/tests/wasm.rs index b44e967e..459acbfb 100644 --- a/nucypher-core-wasm/tests/wasm.rs +++ b/nucypher-core-wasm/tests/wasm.rs @@ -1,5 +1,3 @@ -use nucypher_core_wasm::*; - use ferveo::bindings_wasm::{ferveo_encrypt, DkgPublicKey, FerveoVariant, Keypair}; use umbral_pre::bindings_wasm::{ generate_kfrags, reencrypt, Capsule, RecoverableSignature, SecretKey, Signer, @@ -9,6 +7,8 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; +use nucypher_core_wasm::*; + // // Test utilities // @@ -699,14 +699,23 @@ fn threshold_decryption_request() { let context: JsValue = Some(Context::new("{'user': 'context'}")).into(); let dkg_pk = DkgPublicKey::random(); + + let auth_data = + AuthenticatedData::new(&dkg_pk, &conditions_js.unchecked_into::()) + .unwrap(); + + let authorization = b"we_dont_need_no_stinking_badges"; + let acp = AccessControlPolicy::new(&auth_data, authorization).unwrap(); + let message = "my-message".as_bytes(); - let ciphertext = ferveo_encrypt(message, conditions.as_bytes(), &dkg_pk).unwrap(); + let ciphertext = ferveo_encrypt(message, &acp.aad(), &dkg_pk).unwrap(); + let ciphertext_header = ciphertext.header().unwrap(); let request = ThresholdDecryptionRequest::new( ritual_id, &FerveoVariant::simple(), - &ciphertext, - &conditions_js.unchecked_into::(), + &ciphertext_header, + &acp, &context.unchecked_into::(), ) .unwrap(); @@ -787,3 +796,111 @@ fn threshold_decryption_response() { .decrypt(&random_shared_secret) .is_err()); } + +#[wasm_bindgen_test] +fn authenticated_data() { + let dkg_pk = DkgPublicKey::random(); + + let conditions = "{'some': 'condition'}"; + let conditions_js: JsValue = Some(Conditions::new(conditions)).into(); + + let auth_data = + AuthenticatedData::new(&dkg_pk, &conditions_js.unchecked_into::()) + .unwrap(); + + assert_eq!( + auth_data.public_key().to_bytes().unwrap(), + dkg_pk.to_bytes().unwrap() + ); + assert_eq!(auth_data.conditions().unwrap().to_string(), conditions); + + let mut expected_aad = dkg_pk.to_bytes().unwrap().to_vec(); + expected_aad.extend(conditions.as_bytes()); + + assert_eq!(auth_data.aad(), expected_aad.into_boxed_slice()); + + // mimic serialization/deserialization over the wire + let serialized_auth_data = auth_data.to_bytes(); + let deserialized_auth_data = AuthenticatedData::from_bytes(&serialized_auth_data).unwrap(); + assert_eq!( + deserialized_auth_data.public_key().to_bytes().unwrap(), + dkg_pk.to_bytes().unwrap() + ); + assert_eq!( + deserialized_auth_data.conditions().unwrap().to_string(), + conditions, + ); +} + +#[wasm_bindgen_test] +fn access_control_policy() { + let dkg_pk = DkgPublicKey::random(); + + let conditions = "{'some': 'condition'}"; + let conditions_js: JsValue = Some(Conditions::new(conditions)).into(); + + let auth_data = + AuthenticatedData::new(&dkg_pk, &conditions_js.unchecked_into::()) + .unwrap(); + + let authorization = b"we_dont_need_no_stinking_badges"; + let acp = AccessControlPolicy::new(&auth_data, authorization).unwrap(); + + assert_eq!( + dkg_pk.to_bytes().unwrap(), + acp.public_key().to_bytes().unwrap() + ); + assert_eq!( + authorization.to_vec().into_boxed_slice(), + acp.authorization() + ); + assert_eq!(conditions, acp.conditions().unwrap().to_string()); + + // mimic serialization/deserialization over the wire + let serialized_acp = acp.to_bytes(); + let deserialized_acp = AccessControlPolicy::from_bytes(&serialized_acp).unwrap(); + assert_eq!( + dkg_pk.to_bytes().unwrap(), + deserialized_acp.public_key().to_bytes().unwrap() + ); + assert_eq!( + authorization.to_vec().into_boxed_slice(), + deserialized_acp.authorization() + ); + assert_eq!( + conditions, + deserialized_acp.conditions().unwrap().to_string() + ); + + // check aad; expected acp and auth_data acps to be the same + assert_eq!(deserialized_acp.aad(), auth_data.aad()); +} + +#[wasm_bindgen_test] +fn threshold_message_kit() { + let conditions = "{'some': 'condition'}"; + let conditions_js: JsValue = Some(Conditions::new(conditions)).into(); + + let dkg_pk = DkgPublicKey::random(); + + let auth_data = + AuthenticatedData::new(&dkg_pk, &conditions_js.unchecked_into::()) + .unwrap(); + + let authorization = b"we_dont_need_no_stinking_badges"; + let acp = AccessControlPolicy::new(&auth_data, authorization).unwrap(); + + let data = "The Tyranny of Merit".as_bytes(); + let ciphertext = ferveo_encrypt(data, &acp.aad(), &dkg_pk).unwrap(); + + let tmk = ThresholdMessageKit::new(&ciphertext, &acp); + + // mimic serialization/deserialization over the wire + let serialized_tmk = tmk.to_bytes(); + let deserialized_tmk = ThresholdMessageKit::from_bytes(&serialized_tmk).unwrap(); + assert_eq!( + ciphertext.header().unwrap(), + deserialized_tmk.ciphertext_header().unwrap() + ); + assert_eq!(acp, deserialized_tmk.acp()); +} diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index 63abfba8..9d92fb37 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -11,7 +11,7 @@ categories = ["cryptography", "no-std"] [dependencies] umbral-pre = { version = "0.11.0", features = ["serde"] } -ferveo = { version = "0.2.1", package = "ferveo-pre-release" } +ferveo = { package = "ferveo-pre-release", git = "https://github.com/derekpierre/nucypher-ferveo.git", branch = "acp" } serde = { version = "1", default-features = false, features = ["derive"] } generic-array = { version = "0.14", features = ["zeroize"] } sha3 = "0.10" diff --git a/nucypher-core/src/access_control.rs b/nucypher-core/src/access_control.rs new file mode 100644 index 00000000..989ac521 --- /dev/null +++ b/nucypher-core/src/access_control.rs @@ -0,0 +1,208 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use ferveo::api::{encrypt, Ciphertext, DkgPublicKey, SecretBox}; +use ferveo::Error; +use serde::{Deserialize, Serialize}; +use umbral_pre::serde_bytes; + +use crate::conditions::Conditions; +use crate::versioning::{ + messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, +}; + +/// Authenticated data for encrypted data. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthenticatedData { + /// The public key for the encrypted data + pub public_key: DkgPublicKey, + + /// The conditions associated with the encrypted data + pub conditions: Option, +} + +impl AuthenticatedData { + /// Creates a new access control policy. + pub fn new(public_key: &DkgPublicKey, conditions: Option<&Conditions>) -> Self { + AuthenticatedData { + public_key: *public_key, + conditions: conditions.cloned(), + } + } + + /// Return the aad. + pub fn aad(&self) -> Box<[u8]> { + let public_key_bytes = self.public_key.to_bytes().unwrap(); + let condition_bytes = self.conditions.as_ref().unwrap().as_ref().as_bytes(); + let mut result = Vec::with_capacity(public_key_bytes.len() + condition_bytes.len()); + result.extend(public_key_bytes); + result.extend(condition_bytes); + result.into_boxed_slice() + } +} + +impl PartialEq for AuthenticatedData { + fn eq(&self, other: &Self) -> bool { + self.public_key == other.public_key && self.conditions == other.conditions + } +} + +impl Eq for AuthenticatedData {} + +impl<'a> ProtocolObjectInner<'a> for AuthenticatedData { + fn version() -> (u16, u16) { + (1, 0) + } + + fn brand() -> [u8; 4] { + *b"AuDa" + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for AuthenticatedData {} + +/// Encrypt data based on conditions and dkg public key. +pub fn encrypt_for_dkg( + data: &[u8], + public_key: &DkgPublicKey, + conditions: Option<&Conditions>, +) -> Result<(Ciphertext, AuthenticatedData), Error> { + let auth_data = AuthenticatedData::new(public_key, conditions); + let ciphertext = encrypt( + SecretBox::new(data.to_vec()), + auth_data.aad().as_ref(), + public_key, + )?; + Ok((ciphertext, auth_data)) +} + +/// Access control policy data for encrypted data. +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)] +pub struct AccessControlPolicy { + /// The authenticated data for the access control policy + pub auth_data: AuthenticatedData, + + /// The authorization data for the authenticated data + #[serde(with = "serde_bytes::as_base64")] + pub authorization: Box<[u8]>, +} + +impl AccessControlPolicy { + /// Creates a new access control policy. + pub fn new(auth_data: &AuthenticatedData, authorization: &[u8]) -> Self { + AccessControlPolicy { + auth_data: auth_data.clone(), + authorization: authorization.to_vec().into(), + } + } + + /// Return the aad. + pub fn aad(&self) -> Box<[u8]> { + self.auth_data.aad() + } + + /// Return the DKG public key + pub fn public_key(&self) -> DkgPublicKey { + self.auth_data.public_key + } + + /// Return the conditions + pub fn conditions(&self) -> Option { + self.auth_data.conditions.clone() + } +} + +impl<'a> ProtocolObjectInner<'a> for AccessControlPolicy { + fn version() -> (u16, u16) { + (1, 0) + } + + fn brand() -> [u8; 4] { + *b"ACPo" + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for AccessControlPolicy {} + +#[cfg(test)] +mod tests { + use ferveo::api::DkgPublicKey; + + use crate::access_control::{AccessControlPolicy, AuthenticatedData}; + use crate::conditions::Conditions; + use crate::versioning::ProtocolObject; + + #[test] + fn authenticated_data() { + let dkg_pk = DkgPublicKey::random(); + let conditions = Conditions::new("abcd"); + + let auth_data = AuthenticatedData::new(&dkg_pk, Some(&conditions)); + + // check aad for auth data; expected to be dkg public key + conditions + let mut expected_aad = dkg_pk.to_bytes().unwrap().to_vec(); + expected_aad.extend(conditions.as_ref().as_bytes()); + let auth_data_aad = auth_data.aad(); + assert_eq!(expected_aad.into_boxed_slice(), auth_data_aad); + + assert_eq!(auth_data.public_key, dkg_pk); + assert_eq!(auth_data.conditions, Some(conditions)); + + let auth_data_2 = AuthenticatedData::new(&dkg_pk, Some(&Conditions::new("abcd"))); + assert_eq!(auth_data, auth_data_2); + + // mimic serialization/deserialization over the wire + let serialized_auth_data = auth_data.to_bytes(); + let deserialized_auth_data = AuthenticatedData::from_bytes(&serialized_auth_data).unwrap(); + assert_eq!(auth_data.public_key, deserialized_auth_data.public_key); + assert_eq!(auth_data.conditions, deserialized_auth_data.conditions); + } + + #[test] + fn access_control_policy() { + let dkg_pk = DkgPublicKey::random(); + let conditions = Conditions::new("abcd"); + + let auth_data = AuthenticatedData::new(&dkg_pk, Some(&conditions)); + let authorization = b"we_dont_need_no_stinking_badges"; + let acp = AccessControlPolicy::new(&auth_data, authorization); + + // check that aad for auth_data and acp are the same + assert_eq!(auth_data.aad(), acp.aad()); + + // mimic serialization/deserialization over the wire + let serialized_acp = acp.to_bytes(); + let deserialized_acp = AccessControlPolicy::from_bytes(&serialized_acp).unwrap(); + assert_eq!(auth_data.public_key, deserialized_acp.public_key()); + assert_eq!(auth_data.conditions, deserialized_acp.conditions()); + assert_eq!( + authorization.to_vec().into_boxed_slice(), + deserialized_acp.authorization + ); + } +} diff --git a/nucypher-core/src/conditions.rs b/nucypher-core/src/conditions.rs index f41c6ee0..9191a23d 100644 --- a/nucypher-core/src/conditions.rs +++ b/nucypher-core/src/conditions.rs @@ -3,7 +3,7 @@ use core::fmt; use serde::{Deserialize, Serialize}; -/// Reencryption conditions. +/// Access control conditions. #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct Conditions(String); diff --git a/nucypher-core/src/dkg.rs b/nucypher-core/src/dkg.rs index cd4f86df..ebc49244 100644 --- a/nucypher-core/src/dkg.rs +++ b/nucypher-core/src/dkg.rs @@ -1,23 +1,22 @@ use alloc::boxed::Box; use alloc::string::String; use core::fmt; -use ferveo::api::{Ciphertext, FerveoVariant}; -use generic_array::typenum::Unsigned; +use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng}; +use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use ferveo::api::{CiphertextHeader, FerveoVariant}; +use generic_array::typenum::Unsigned; use serde::{Deserialize, Serialize}; use umbral_pre::serde_bytes; // TODO should this be in umbral? +use crate::access_control::AccessControlPolicy; +use crate::conditions::Context; +use crate::dkg::session::{SessionSharedSecret, SessionStaticKey}; use crate::versioning::{ messagepack_deserialize, messagepack_serialize, DeserializationError, ProtocolObject, ProtocolObjectInner, }; -use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng}; -use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; - -use crate::conditions::{Conditions, Context}; -use crate::dkg::session::{SessionSharedSecret, SessionStaticKey}; - /// Errors during encryption. #[derive(Debug)] pub enum EncryptionError { @@ -105,6 +104,7 @@ pub mod session { use alloc::boxed::Box; use alloc::string::String; use core::fmt; + use generic_array::{ typenum::{Unsigned, U32}, GenericArray, @@ -115,10 +115,9 @@ pub mod session { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use umbral_pre::serde_bytes; use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; - - use crate::secret_box::{kdf, SecretBox}; use zeroize::ZeroizeOnDrop; + use crate::secret_box::{kdf, SecretBox}; use crate::versioning::{ messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, }; @@ -355,9 +354,9 @@ pub struct ThresholdDecryptionRequest { /// The ID of the ritual. pub ritual_id: u32, /// The ciphertext to generate a decryption share for. - pub ciphertext: Ciphertext, - /// A blob of bytes containing decryption conditions for this message. - pub conditions: Option, + pub ciphertext_header: CiphertextHeader, + /// The associated access control metadata. + pub acp: AccessControlPolicy, /// A blob of bytes containing context required to evaluate conditions. pub context: Option, /// The ferveo variant to use for the decryption share derivation. @@ -368,15 +367,15 @@ impl ThresholdDecryptionRequest { /// Creates a new decryption request. pub fn new( ritual_id: u32, - ciphertext: &Ciphertext, - conditions: Option<&Conditions>, + ciphertext_header: &CiphertextHeader, + acp: &AccessControlPolicy, context: Option<&Context>, variant: FerveoVariant, ) -> Self { Self { ritual_id, - ciphertext: ciphertext.clone(), - conditions: conditions.cloned(), + ciphertext_header: ciphertext_header.clone(), + acp: acp.clone(), context: context.cloned(), variant, } @@ -394,7 +393,7 @@ impl ThresholdDecryptionRequest { impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionRequest { fn version() -> (u16, u16) { - (2, 0) + (4, 0) } fn brand() -> [u8; 4] { @@ -595,21 +594,21 @@ impl<'a> ProtocolObject<'a> for EncryptedThresholdDecryptionResponse {} #[cfg(test)] mod tests { use ferveo::api::{encrypt as ferveo_encrypt, DkgPublicKey, FerveoVariant, SecretBox}; - - use crate::{ - Conditions, Context, EncryptedThresholdDecryptionRequest, - EncryptedThresholdDecryptionResponse, ProtocolObject, SessionSecretFactory, - SessionStaticKey, ThresholdDecryptionRequest, ThresholdDecryptionResponse, - }; - use generic_array::typenum::Unsigned; use rand_core::RngCore; + use crate::access_control::AccessControlPolicy; + use crate::conditions::{Conditions, Context}; use crate::dkg::session::SessionStaticSecret; use crate::dkg::{ decrypt_with_shared_secret, encrypt_with_shared_secret, DecryptionError, NonceSize, }; - use crate::versioning::ProtocolObjectInner; + use crate::versioning::{ProtocolObject, ProtocolObjectInner}; + use crate::{ + AuthenticatedData, EncryptedThresholdDecryptionRequest, + EncryptedThresholdDecryptionResponse, SessionSecretFactory, SessionStaticKey, + ThresholdDecryptionRequest, ThresholdDecryptionResponse, + }; #[test] fn decryption_with_shared_secret() { @@ -731,10 +730,17 @@ mod tests { let aad = "my-add".as_bytes(); let ciphertext = ferveo_encrypt(SecretBox::new(message), aad, &dkg_pk).unwrap(); + let auth_data = AuthenticatedData::new(&dkg_pk, Some(&Conditions::new("abcd"))); + + let authorization = b"self_authorization"; + let acp = AccessControlPolicy::new(&auth_data, authorization); + + let ciphertext_header = ciphertext.header().unwrap(); + let request = ThresholdDecryptionRequest::new( ritual_id, - &ciphertext, - Some(&Conditions::new("abcd")), + &ciphertext_header, + &acp, Some(&Context::new("efgh")), variant, ); diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index 19255742..da0fb1a6 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -7,6 +7,7 @@ extern crate alloc; +mod access_control; mod address; mod conditions; mod dkg; @@ -19,12 +20,15 @@ mod reencryption; mod retrieval_kit; mod revocation_order; mod secret_box; +mod threshold_message_kit; mod treasure_map; mod versioning; /// Error returned by various `verify()` methods in the crate. pub struct VerificationError; +pub use access_control::{encrypt_for_dkg, AccessControlPolicy, AuthenticatedData}; + pub use address::Address; pub use conditions::{Conditions, Context}; pub use dkg::{ @@ -42,6 +46,7 @@ pub use node_metadata::{ pub use reencryption::{ReencryptionRequest, ReencryptionResponse}; pub use retrieval_kit::RetrievalKit; pub use revocation_order::RevocationOrder; +pub use threshold_message_kit::ThresholdMessageKit; pub use treasure_map::{EncryptedTreasureMap, TreasureMap}; pub use versioning::ProtocolObject; diff --git a/nucypher-core/src/threshold_message_kit.rs b/nucypher-core/src/threshold_message_kit.rs new file mode 100644 index 00000000..3ed6b53b --- /dev/null +++ b/nucypher-core/src/threshold_message_kit.rs @@ -0,0 +1,107 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use ferveo::api::{Ciphertext, CiphertextHeader, SharedSecret}; +use ferveo::Error; +use serde::{Deserialize, Serialize}; + +use crate::access_control::AccessControlPolicy; +use crate::versioning::{ + messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, +}; + +/// Access control metadata for encrypted data. +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] +pub struct ThresholdMessageKit { + /// The key encapsulation ciphertext + pub ciphertext: Ciphertext, + + /// The associated access control metadata. + pub acp: AccessControlPolicy, +} + +impl ThresholdMessageKit { + /// Creates a new threshold message kit. + pub fn new(ciphertext: &Ciphertext, acp: &AccessControlPolicy) -> Self { + ThresholdMessageKit { + ciphertext: ciphertext.clone(), + acp: acp.clone(), + } + } + + /// Returns ciphertext header. + pub fn ciphertext_header(&self) -> Result { + self.ciphertext.header() + } + + /// Decrypts encrypted data. + pub fn decrypt_with_shared_secret( + &self, + shared_secret: &SharedSecret, + ) -> Result, Error> { + ferveo::api::decrypt_with_shared_secret( + &self.ciphertext, + self.acp.aad().as_ref(), + shared_secret, + ) + } +} + +impl<'a> ProtocolObjectInner<'a> for ThresholdMessageKit { + fn version() -> (u16, u16) { + (1, 0) + } + + fn brand() -> [u8; 4] { + *b"TMKi" + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for ThresholdMessageKit {} + +#[cfg(test)] +mod tests { + use ferveo::api::{encrypt as ferveo_encrypt, DkgPublicKey, SecretBox}; + + use crate::access_control::{AccessControlPolicy, AuthenticatedData}; + use crate::conditions::Conditions; + use crate::threshold_message_kit::ThresholdMessageKit; + use crate::versioning::ProtocolObject; + + #[test] + fn threshold_message_kit() { + let dkg_pk = DkgPublicKey::random(); + let data = "The Tyranny of Merit".as_bytes().to_vec(); + + let authorization = b"we_dont_need_no_stinking_badges"; + let acp = AccessControlPolicy::new( + &AuthenticatedData::new(&dkg_pk, Some(&Conditions::new("abcd"))), + authorization, + ); + + let ciphertext = ferveo_encrypt(SecretBox::new(data), &acp.aad(), &dkg_pk).unwrap(); + let tmk = ThresholdMessageKit::new(&ciphertext, &acp); + + // mimic serialization/deserialization over the wire + let serialized_tmk = tmk.to_bytes(); + let deserialized_tmk = ThresholdMessageKit::from_bytes(&serialized_tmk).unwrap(); + assert_eq!( + ciphertext.header().unwrap(), + deserialized_tmk.ciphertext_header().unwrap() + ); + assert_eq!(acp, deserialized_tmk.acp); + } +}