diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ba2ac980 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 + hooks: + - id: fmt + - id: cargo-check + - id: clippy + args: ["--all", "--all-features", "--", "-D", "warnings"] diff --git a/CHANGELOG.md b/CHANGELOG.md index b99ca19c..ab5987bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,20 +3,28 @@ 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.9.0] - Unreleased ### Added - Re-exported `ferveo` Python and WASM bindings. ([#58]) +- Added `SessionSharedSecret`, `SessionStaticKey`, `SessionStaticSecret`, `SessionSecretFactory` as wrappers for underlying Curve 25519 key functionality. ([#54]) +- Added Rust `pre-commit` hooks for repos. ([#54]) +- Added `secret_box` functionality. ([#54]) + -### Changed +### Changed - Replaced opaque types with native `ferveo` types. ([#53]) +- Removed `E2EThresholdDecryptionRequest` type and bindings. ([#54]) +- Modified `EncryptedThresholdDecryptionRequest`/`EncryptedThresholdDecryptionResponse` to use Curve 25519 keys instead of Umbral keys for encryption/decryption. ([#54]) +- Modified `ThresholdDecryptionResponse`/`EncryptedThresholdDecryptionResponse` to include `ritual_id` member in struct. ([#54]) +- Ritual ID for `ThresholdDecryption[Request/Response]` / `EncryptedThresholdDecryption[Request/Response]` is now u32 instead of u16. ([#54]) [#53]: https://github.com/nucypher/nucypher-core/pull/53 [#58]: https://github.com/nucypher/nucypher-core/pull/58 +[#54]: https://github.com/nucypher/nucypher-core/pull/54 ## [0.8.0] - 2023-05-23 @@ -38,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `ThresholdDecryptionRequest`/`ThresholdDecryptionResponse` types and bindings. ([#48])` +- Add `ThresholdDecryptionRequest`/`ThresholdDecryptionResponse` types and bindings. ([#48]) - Add `ferveo_public_key` field to `NodeMetadataPayload`. ([#48]) diff --git a/Cargo.lock b/Cargo.lock index 231d33d5..02abd3da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,21 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +dependencies = [ + "cfg-if", + "fiat-crypto", + "packed_simd_2", + "platforms", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.13.4" @@ -549,6 +564,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "fnv" version = "1.0.7" @@ -786,6 +807,12 @@ version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "lock_api" version = "0.4.9" @@ -831,14 +858,22 @@ checksum = "94c7128ba23c81f6471141b90f17654f89ef44a56e14b8a4dd0fddfccd655277" name = "nucypher-core" version = "0.8.0" dependencies = [ + "chacha20poly1305", "ferveo-pre-release", "generic-array", "hex", + "hkdf", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "rmp-serde", "serde", "serde_with 1.14.0", + "sha2", "sha3", "umbral-pre", + "x25519-dalek", + "zeroize", ] [[package]] @@ -866,6 +901,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-derive", "wasm-bindgen-test", + "x25519-dalek", ] [[package]] @@ -910,6 +946,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -939,6 +985,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "poly1305" version = "0.8.0" @@ -1777,6 +1829,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "x25519-dalek" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/nucypher-core-python/nucypher_core/__init__.py b/nucypher-core-python/nucypher_core/__init__.py index 501c856f..cb500c48 100644 --- a/nucypher-core-python/nucypher_core/__init__.py +++ b/nucypher-core-python/nucypher_core/__init__.py @@ -18,8 +18,11 @@ MetadataResponse, MetadataResponsePayload, ThresholdDecryptionRequest, - E2EThresholdDecryptionRequest, ThresholdDecryptionResponse, EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, + SessionSharedSecret, + SessionStaticKey, + SessionStaticSecret, + SessionSecretFactory, ) diff --git a/nucypher-core-python/nucypher_core/__init__.pyi b/nucypher-core-python/nucypher_core/__init__.pyi index fb2d3332..fce1d08b 100644 --- a/nucypher-core-python/nucypher_core/__init__.pyi +++ b/nucypher-core-python/nucypher_core/__init__.pyi @@ -423,7 +423,7 @@ class ThresholdDecryptionRequest: ciphertext: Ciphertext - def encrypt(self, request_encrypting_key: PublicKey, response_encrypting_key: PublicKey) -> EncryptedThresholdDecryptionRequest: + def encrypt(self, shared_secret: SessionSharedSecret, requester_public_key: SessionStaticKey) -> EncryptedThresholdDecryptionRequest: ... @staticmethod @@ -434,27 +434,15 @@ class ThresholdDecryptionRequest: ... -class E2EThresholdDecryptionRequest: - - decryption_request: ThresholdDecryptionRequest - - response_encrypting_key: PublicKey - - @staticmethod - def from_bytes(data: bytes) -> E2EThresholdDecryptionRequest: - ... - - def __bytes__(self) -> bytes: - ... - - class EncryptedThresholdDecryptionRequest: ritual_id: int + requester_public_key: SessionStaticKey + def decrypt( self, - sk: SecretKey - ) -> E2EThresholdDecryptionRequest: + shared_secret: SessionSharedSecret + ) -> ThresholdDecryptionRequest: ... @staticmethod @@ -467,12 +455,14 @@ class EncryptedThresholdDecryptionRequest: class ThresholdDecryptionResponse: - def __init__(self, decryption_share: bytes): + def __init__(self, ritual_id: int, decryption_share: bytes): ... decryption_share: bytes - def encrypt(self, encrypting_key: PublicKey) -> EncryptedThresholdDecryptionResponse: + ritual_id: int + + def encrypt(self, shared_secret: SessionSharedSecret) -> EncryptedThresholdDecryptionResponse: ... @staticmethod @@ -485,9 +475,11 @@ class ThresholdDecryptionResponse: class EncryptedThresholdDecryptionResponse: + ritual_id: int + def decrypt( self, - sk: SecretKey + shared_secret: SessionSharedSecret ) -> ThresholdDecryptionResponse: ... @@ -497,3 +489,48 @@ class EncryptedThresholdDecryptionResponse: def __bytes__(self) -> bytes: ... + + +class SessionSharedSecret: + ... + + +class SessionStaticKey: + + @staticmethod + def from_bytes(data: bytes) -> SessionStaticKey: + ... + + def __bytes__(self) -> bytes: + ... + + +class SessionStaticSecret: + + @staticmethod + def random() -> SessionStaticSecret: + ... + + def public_key(self) -> SessionStaticKey: + ... + + def derive_shared_secret(self, their_public_key: SessionStaticKey) -> SessionSharedSecret: + ... + + +class SessionSecretFactory: + + @staticmethod + def random() -> SessionSecretFactory: + ... + + @staticmethod + def seed_size() -> int: + ... + + @staticmethod + def from_secure_randomness(seed: bytes) -> SessionSecretFactory: + ... + + def make_key(self, label: bytes) -> SessionStaticSecret: + ... diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index 7aab285d..5aa09c55 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -632,6 +632,116 @@ impl ReencryptionResponse { } } +// +// Session Keys +// + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SessionSharedSecret { + backend: nucypher_core::SessionSharedSecret, +} + +#[pyclass(module = "nucypher_core")] +#[derive(Clone, PartialEq, Eq, derive_more::From, derive_more::AsRef)] +pub struct SessionStaticKey { + backend: nucypher_core::SessionStaticKey, +} + +#[pymethods] +impl SessionStaticKey { + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::SessionStaticKey>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + richcmp(self, other, op) + } + + fn __hash__(&self) -> PyResult { + hash("SessionStaticKey", self) + } + + fn __str__(&self) -> PyResult { + Ok(format!("{}", self.backend)) + } +} + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SessionStaticSecret { + backend: nucypher_core::SessionStaticSecret, +} + +#[pymethods] +impl SessionStaticSecret { + #[staticmethod] + pub fn random() -> PyResult { + Ok(Self { + backend: nucypher_core::SessionStaticSecret::random(), + }) + } + + pub fn public_key(&self) -> SessionStaticKey { + SessionStaticKey { + backend: self.backend.public_key(), + } + } + + pub fn derive_shared_secret(&self, their_public_key: &SessionStaticKey) -> SessionSharedSecret { + SessionSharedSecret { + backend: self.backend.derive_shared_secret(their_public_key.as_ref()), + } + } + + fn __str__(&self) -> String { + self.backend.to_string() + } +} + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SessionSecretFactory { + backend: nucypher_core::SessionSecretFactory, +} + +#[pymethods] +impl SessionSecretFactory { + #[staticmethod] + pub fn random() -> PyResult { + Ok(Self { + backend: nucypher_core::SessionSecretFactory::random(), + }) + } + + #[staticmethod] + pub fn seed_size() -> usize { + nucypher_core::SessionSecretFactory::seed_size() + } + + #[staticmethod] + pub fn from_secure_randomness(seed: &[u8]) -> PyResult { + let factory = nucypher_core::SessionSecretFactory::from_secure_randomness(seed) + .map_err(|err| PyValueError::new_err(format!("{}", err)))?; + Ok(Self { backend: factory }) + } + + pub fn make_key(&self, label: &[u8]) -> SessionStaticSecret { + SessionStaticSecret { + backend: self.backend.make_key(label), + } + } + + fn __str__(&self) -> String { + self.backend.to_string() + } +} + // // Threshold Decryption Request // @@ -646,7 +756,7 @@ pub struct ThresholdDecryptionRequest { impl ThresholdDecryptionRequest { #[new] pub fn new( - ritual_id: u16, + ritual_id: u32, variant: u8, ciphertext: &Ciphertext, conditions: Option<&Conditions>, @@ -676,7 +786,7 @@ impl ThresholdDecryptionRequest { } #[getter] - pub fn ritual_id(&self) -> u16 { + pub fn ritual_id(&self) -> u32 { self.backend.ritual_id } @@ -713,14 +823,14 @@ impl ThresholdDecryptionRequest { pub fn encrypt( &self, - request_encrypting_key: &PublicKey, - response_encrypting_key: &PublicKey, + shared_secret: &SessionSharedSecret, + requester_public_key: &SessionStaticKey, ) -> EncryptedThresholdDecryptionRequest { + let encrypted_request = self + .backend + .encrypt(shared_secret.as_ref(), requester_public_key.as_ref()); EncryptedThresholdDecryptionRequest { - backend: self.backend.encrypt( - request_encrypting_key.as_ref(), - response_encrypting_key.as_ref(), - ), + backend: encrypted_request, } } @@ -734,37 +844,6 @@ impl ThresholdDecryptionRequest { } } -// -// E2EThresholdDecryptionRequest -// -#[pyclass(module = "nucypher_core")] -#[derive(derive_more::From, derive_more::AsRef)] -pub struct E2EThresholdDecryptionRequest { - backend: nucypher_core::E2EThresholdDecryptionRequest, -} - -#[pymethods] -impl E2EThresholdDecryptionRequest { - #[getter] - pub fn decryption_request(&self) -> ThresholdDecryptionRequest { - self.backend.decryption_request.clone().into() - } - - #[getter] - pub fn response_encrypting_key(&self) -> PublicKey { - self.backend.response_encrypting_key.into() - } - - #[staticmethod] - pub fn from_bytes(data: &[u8]) -> PyResult { - from_bytes::<_, nucypher_core::E2EThresholdDecryptionRequest>(data) - } - - fn __bytes__(&self) -> PyObject { - to_bytes(self) - } -} - // // EncryptedThresholdDecryptionRequest // @@ -778,14 +857,22 @@ pub struct EncryptedThresholdDecryptionRequest { #[pymethods] impl EncryptedThresholdDecryptionRequest { #[getter] - pub fn ritual_id(&self) -> u16 { + pub fn ritual_id(&self) -> u32 { self.backend.ritual_id } - pub fn decrypt(&self, sk: &SecretKey) -> PyResult { + #[getter] + pub fn requester_public_key(&self) -> SessionStaticKey { + self.backend.requester_public_key.into() + } + + pub fn decrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> PyResult { self.backend - .decrypt(sk.as_ref()) - .map(E2EThresholdDecryptionRequest::from) + .decrypt(shared_secret.as_ref()) + .map(ThresholdDecryptionRequest::from) .map_err(|err| PyValueError::new_err(format!("{}", err))) } @@ -812,20 +899,28 @@ pub struct ThresholdDecryptionResponse { #[pymethods] impl ThresholdDecryptionResponse { #[new] - pub fn new(decryption_share: &[u8]) -> Self { + pub fn new(ritual_id: u32, decryption_share: &[u8]) -> Self { ThresholdDecryptionResponse { - backend: nucypher_core::ThresholdDecryptionResponse::new(decryption_share), + backend: nucypher_core::ThresholdDecryptionResponse::new(ritual_id, decryption_share), } } + #[getter] + pub fn ritual_id(&self) -> u32 { + self.backend.ritual_id + } + #[getter] pub fn decryption_share(&self) -> &[u8] { self.backend.decryption_share.as_ref() } - pub fn encrypt(&self, encrypting_key: &PublicKey) -> EncryptedThresholdDecryptionResponse { + pub fn encrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> EncryptedThresholdDecryptionResponse { EncryptedThresholdDecryptionResponse { - backend: self.backend.encrypt(encrypting_key.as_ref()), + backend: self.backend.encrypt(shared_secret.as_ref()), } } @@ -851,9 +946,17 @@ pub struct EncryptedThresholdDecryptionResponse { #[pymethods] impl EncryptedThresholdDecryptionResponse { - pub fn decrypt(&self, sk: &SecretKey) -> PyResult { + #[getter] + pub fn ritual_id(&self) -> u32 { + self.backend.ritual_id + } + + pub fn decrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> PyResult { self.backend - .decrypt(sk.as_ref()) + .decrypt(shared_secret.as_ref()) .map(ThresholdDecryptionResponse::from) .map_err(|err| PyValueError::new_err(format!("{}", err))) } @@ -1334,10 +1437,13 @@ 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_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; // 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 1ebb0814..43716137 100644 --- a/nucypher-core-wasm/Cargo.toml +++ b/nucypher-core-wasm/Cargo.toml @@ -27,6 +27,7 @@ js-sys = "0.3.63" console_error_panic_hook = { version = "0.1", optional = true } derive_more = { version = "0.99", default-features = false, features = ["from", "as_ref"] } wasm-bindgen-derive = "0.2.1" +x25519-dalek = "2.0.0-rc.2" [dev-dependencies] console_error_panic_hook = "0.1" diff --git a/nucypher-core-wasm/src/lib.rs b/nucypher-core-wasm/src/lib.rs index fe6cb1a4..a84f4fc3 100644 --- a/nucypher-core-wasm/src/lib.rs +++ b/nucypher-core-wasm/src/lib.rs @@ -542,6 +542,104 @@ impl EncryptedTreasureMap { } } +// +// Session Keys +// + +#[wasm_bindgen] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SessionSharedSecret(nucypher_core::SessionSharedSecret); + +#[wasm_bindgen] +#[derive(PartialEq, Eq, Debug, derive_more::From, derive_more::AsRef)] +pub struct SessionStaticKey(nucypher_core::SessionStaticKey); + +#[wasm_bindgen] +impl SessionStaticKey { + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(data: &[u8]) -> Result { + from_bytes::<_, nucypher_core::SessionStaticKey>(data) + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> Box<[u8]> { + to_bytes(self) + } + + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + format!("{}", self.0) + } + + pub fn equals(&self, other: &SessionStaticKey) -> bool { + self.0 == other.0 + } +} + +#[wasm_bindgen] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SessionStaticSecret(nucypher_core::SessionStaticSecret); + +#[wasm_bindgen] +impl SessionStaticSecret { + /// Generates a secret key using the default RNG and returns it. + pub fn random() -> Self { + Self(nucypher_core::SessionStaticSecret::random()) + } + + /// Generates a secret key using the default RNG and returns it. + #[wasm_bindgen(js_name = publicKey)] + pub fn public_key(&self) -> SessionStaticKey { + SessionStaticKey(self.0.public_key()) + } + + #[wasm_bindgen(js_name = deriveSharedSecret)] + pub fn derive_shared_secret(&self, their_public_key: &SessionStaticKey) -> SessionSharedSecret { + SessionSharedSecret(self.0.derive_shared_secret(their_public_key.as_ref())) + } + + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + format!("{}", self.0) + } +} + +#[wasm_bindgen] +pub struct SessionSecretFactory(nucypher_core::SessionSecretFactory); + +#[wasm_bindgen] +impl SessionSecretFactory { + /// Generates a secret key factory using the default RNG and returns it. + pub fn random() -> Self { + Self(nucypher_core::SessionSecretFactory::random()) + } + + #[wasm_bindgen(js_name = seedSize)] + pub fn seed_size() -> usize { + nucypher_core::SessionSecretFactory::seed_size() + } + + #[wasm_bindgen(js_name = fromSecureRandomness)] + pub fn from_secure_randomness(seed: &[u8]) -> Result { + nucypher_core::SessionSecretFactory::from_secure_randomness(seed) + .map(Self) + .map_err(map_js_err) + } + + #[wasm_bindgen(js_name = makeKey)] + pub fn make_key(&self, label: &[u8]) -> SessionStaticSecret { + SessionStaticSecret(self.0.make_key(label)) + } + + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + format!("{}", self.0) + } +} + // // Threshold Decryption Request // @@ -554,7 +652,7 @@ pub struct ThresholdDecryptionRequest(nucypher_core::ThresholdDecryptionRequest) impl ThresholdDecryptionRequest { #[wasm_bindgen(constructor)] pub fn new( - id: u16, + ritual_id: u32, variant: u8, ciphertext: &Ciphertext, conditions: &OptionConditions, @@ -570,7 +668,7 @@ impl ThresholdDecryptionRequest { }; Ok(Self(nucypher_core::ThresholdDecryptionRequest::new( - id, + ritual_id, ciphertext.as_ref(), typed_conditions.as_ref().map(|conditions| &conditions.0), typed_context.as_ref().map(|context| &context.0), @@ -579,7 +677,7 @@ impl ThresholdDecryptionRequest { } #[wasm_bindgen(getter, js_name = ritualId)] - pub fn ritual_id(&self) -> u16 { + pub fn ritual_id(&self) -> u32 { self.0.ritual_id } @@ -598,13 +696,13 @@ impl ThresholdDecryptionRequest { pub fn encrypt( &self, - request_encrypting_key: &PublicKey, - response_encrypting_key: &PublicKey, + shared_secret: &SessionSharedSecret, + requester_public_key: &SessionStaticKey, ) -> EncryptedThresholdDecryptionRequest { - EncryptedThresholdDecryptionRequest(self.0.encrypt( - request_encrypting_key.as_ref(), - response_encrypting_key.as_ref(), - )) + EncryptedThresholdDecryptionRequest( + self.0 + .encrypt(shared_secret.as_ref(), requester_public_key.as_ref()), + ) } #[wasm_bindgen(js_name = fromBytes)] @@ -618,37 +716,6 @@ impl ThresholdDecryptionRequest { } } -// -// E2EThresholdDecryptionRequest -// - -#[wasm_bindgen] -#[derive(PartialEq, Debug, derive_more::From, derive_more::AsRef)] -pub struct E2EThresholdDecryptionRequest(nucypher_core::E2EThresholdDecryptionRequest); - -#[wasm_bindgen] -impl E2EThresholdDecryptionRequest { - #[wasm_bindgen(js_name = fromBytes)] - pub fn from_bytes(data: &[u8]) -> Result { - from_bytes::<_, nucypher_core::E2EThresholdDecryptionRequest>(data) - } - - #[wasm_bindgen(js_name = toBytes)] - pub fn to_bytes(&self) -> Box<[u8]> { - to_bytes(self) - } - - #[wasm_bindgen(getter, js_name = decryptionRequest)] - pub fn decryption_request(&self) -> ThresholdDecryptionRequest { - ThresholdDecryptionRequest::from(self.0.decryption_request.clone()) - } - - #[wasm_bindgen(getter, js_name = responseEncryptingKey)] - pub fn response_encrypting_key(&self) -> PublicKey { - PublicKey::from(self.0.response_encrypting_key) - } -} - // // EncryptedThresholdDecryptionRequest // @@ -660,15 +727,23 @@ pub struct EncryptedThresholdDecryptionRequest(nucypher_core::EncryptedThreshold #[wasm_bindgen] impl EncryptedThresholdDecryptionRequest { #[wasm_bindgen(getter, js_name = ritualId)] - pub fn ritual_id(&self) -> u16 { + pub fn ritual_id(&self) -> u32 { self.0.ritual_id } - pub fn decrypt(&self, sk: &SecretKey) -> Result { + #[wasm_bindgen(getter, js_name = requesterPublicKey)] + pub fn requester_public_key(&self) -> SessionStaticKey { + SessionStaticKey::from(self.0.requester_public_key) + } + + pub fn decrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> Result { self.0 - .decrypt(sk.as_ref()) + .decrypt(shared_secret.as_ref()) .map_err(map_js_err) - .map(E2EThresholdDecryptionRequest) + .map(ThresholdDecryptionRequest) } #[wasm_bindgen(js_name = fromBytes)] @@ -693,19 +768,31 @@ pub struct ThresholdDecryptionResponse(nucypher_core::ThresholdDecryptionRespons #[wasm_bindgen] impl ThresholdDecryptionResponse { #[wasm_bindgen(constructor)] - pub fn new(decryption_share: &[u8]) -> Result { + pub fn new( + ritual_id: u32, + decryption_share: &[u8], + ) -> Result { Ok(Self(nucypher_core::ThresholdDecryptionResponse::new( + ritual_id, decryption_share, ))) } + #[wasm_bindgen(getter, js_name = ritualId)] + pub fn ritual_id(&self) -> u32 { + self.0.ritual_id + } + #[wasm_bindgen(getter, js_name = decryptionShare)] pub fn decryption_share(&self) -> Box<[u8]> { self.0.decryption_share.clone() } - pub fn encrypt(&self, encrypting_key: &PublicKey) -> EncryptedThresholdDecryptionResponse { - EncryptedThresholdDecryptionResponse(self.0.encrypt(encrypting_key.as_ref())) + pub fn encrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> EncryptedThresholdDecryptionResponse { + EncryptedThresholdDecryptionResponse(self.0.encrypt(shared_secret.as_ref())) } #[wasm_bindgen(js_name = fromBytes)] @@ -731,9 +818,17 @@ pub struct EncryptedThresholdDecryptionResponse( #[wasm_bindgen] impl EncryptedThresholdDecryptionResponse { - pub fn decrypt(&self, sk: &SecretKey) -> Result { + #[wasm_bindgen(getter, js_name = ritualId)] + pub fn ritual_id(&self) -> u32 { + self.0.ritual_id + } + + pub fn decrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> Result { self.0 - .decrypt(sk.as_ref()) + .decrypt(shared_secret.as_ref()) .map_err(map_js_err) .map(ThresholdDecryptionResponse) } diff --git a/nucypher-core-wasm/tests/wasm.rs b/nucypher-core-wasm/tests/wasm.rs index db1ee189..732319ee 100644 --- a/nucypher-core-wasm/tests/wasm.rs +++ b/nucypher-core-wasm/tests/wasm.rs @@ -670,14 +670,29 @@ fn metadata_response() { // ThresholdDecryptionRequestResponse // +#[wasm_bindgen_test] +fn request_public_key() { + let secret = SessionStaticSecret::random(); + let public_key = secret.public_key(); + + assert_eq!(secret.public_key(), secret.public_key()); + + // mimic transmission public key over the wire + let serialized_public_key = public_key.to_bytes(); + let deserialized_public_key = + SessionStaticKey::from_bytes(serialized_public_key.as_ref()).unwrap(); + + assert_eq!(public_key, deserialized_public_key); + assert_eq!(serialized_public_key, deserialized_public_key.to_bytes()); +} + #[wasm_bindgen_test] fn threshold_decryption_request() { - let ritual_id: u16 = 5; - let request_secret = SecretKey::random(); - let request_encrypting_key = request_secret.public_key(); + let ritual_id: u32 = 5; + let service_secret = SessionStaticSecret::random(); + let service_public_key = service_secret.public_key(); - let response_secret = SecretKey::random(); - let response_encrypting_key = response_secret.public_key(); + let requester_secret = SessionStaticSecret::random(); let conditions = "{'some': 'condition'}"; let conditions_js: JsValue = Some(Conditions::new(conditions)).into(); @@ -696,7 +711,10 @@ fn threshold_decryption_request() { ) .unwrap(); - let encrypted_request = request.encrypt(&request_encrypting_key, &response_encrypting_key); + // requester encrypts request to send to service + let requester_shared_secret = requester_secret.derive_shared_secret(&service_public_key); + let requester_public_key = requester_secret.public_key(); + let encrypted_request = request.encrypt(&requester_shared_secret, &requester_public_key); // mimic encrypted request going over the wire let encrypted_request_bytes = encrypted_request.to_bytes(); @@ -705,54 +723,67 @@ fn threshold_decryption_request() { assert_eq!(encrypted_request_from_bytes, encrypted_request); assert_eq!(encrypted_request_from_bytes.ritual_id(), ritual_id); - - let e2e_request = encrypted_request_from_bytes - .decrypt(&request_secret) - .unwrap(); assert_eq!( - response_encrypting_key.to_compressed_bytes(), - e2e_request.response_encrypting_key().to_compressed_bytes() + encrypted_request_from_bytes.requester_public_key(), + requester_public_key ); - assert_eq!(request, e2e_request.decryption_request()); - // wrong secret key used - assert!(encrypted_request_from_bytes - .decrypt(&response_secret) - .is_err()); + // service decrypts request + let service_shared_secret = + service_secret.derive_shared_secret(&encrypted_request_from_bytes.requester_public_key()); + let decrypted_request = encrypted_request_from_bytes + .decrypt(&service_shared_secret) + .unwrap(); + assert_eq!(request, decrypted_request); - let random_secret_key = SecretKey::random(); + // wrong key used + let random_secret_key = SessionStaticSecret::random(); + let random_shared_secret = random_secret_key.derive_shared_secret(&service_public_key); assert!(encrypted_request_from_bytes - .decrypt(&random_secret_key) + .decrypt(&random_shared_secret) .is_err()); } #[wasm_bindgen_test] fn threshold_decryption_response() { - let response_secret = SecretKey::random(); - let response_encrypting_key = response_secret.public_key(); + let ritual_id = 10; + + let service_secret = SessionStaticSecret::random(); + + let requester_secret = SessionStaticSecret::random(); + let requester_public_key = requester_secret.public_key(); let decryption_share = b"The Tyranny of Merit"; - let response = ThresholdDecryptionResponse::new(decryption_share).unwrap(); + let response = ThresholdDecryptionResponse::new(ritual_id, decryption_share).unwrap(); - let encrypted_response = response.encrypt(&response_encrypting_key); - let encrypted_response_bytes = encrypted_response.to_bytes(); + // service encrypts response to send back + let service_shared_secret = service_secret.derive_shared_secret(&requester_public_key); + let encrypted_response = response.encrypt(&service_shared_secret); + assert_eq!(encrypted_response.ritual_id(), ritual_id); + // mimic serialization/deserialization over the wire + let encrypted_response_bytes = encrypted_response.to_bytes(); let encrypted_response_from_bytes = EncryptedThresholdDecryptionResponse::from_bytes(&encrypted_response_bytes).unwrap(); + // requester decrypts response + let service_public_key = service_secret.public_key(); + let requester_shared_secret = requester_secret.derive_shared_secret(&service_public_key); let decrypted_response = encrypted_response_from_bytes - .decrypt(&response_secret) + .decrypt(&requester_shared_secret) .unwrap(); assert_eq!(response, decrypted_response); + assert_eq!(response.ritual_id(), ritual_id); assert_eq!( response.decryption_share(), decrypted_response.decryption_share() ); // wrong secret key used - let random_secret_key = SecretKey::random(); + let random_secret_key = SessionStaticSecret::random(); + let random_shared_secret = random_secret_key.derive_shared_secret(&service_public_key); assert!(encrypted_response_from_bytes - .decrypt(&random_secret_key) + .decrypt(&random_shared_secret) .is_err()); } diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index d8298b86..42ac394b 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -13,8 +13,16 @@ categories = ["cryptography", "no-std"] umbral-pre = { version = "0.10.0", features = ["serde"] } ferveo = { package = "ferveo-pre-release", version = "0.1.0-alpha.8" } serde = { version = "1", default-features = false, features = ["derive"] } -generic-array = "0.14" +generic-array = { version="0.14", features = ["zeroize"] } sha3 = "0.10" rmp-serde = "1" serde_with = "1.14" hex = "0.4" +hkdf = "0.12.3" +sha2 = "0.10.6" +x25519-dalek = { version="2.0.0-rc.2", features = ["serde", "static_secrets"] } +chacha20poly1305 = "0.10.1" +zeroize = { version="1.6.0", features = ["derive"] } +rand_core = "0.6.4" +rand_chacha = "0.3.1" +rand = "0.8.5" diff --git a/nucypher-core/src/dkg.rs b/nucypher-core/src/dkg.rs index 8d411eb3..3ca231cf 100644 --- a/nucypher-core/src/dkg.rs +++ b/nucypher-core/src/dkg.rs @@ -1,17 +1,105 @@ use alloc::boxed::Box; use alloc::string::String; +use core::fmt; use ferveo::api::Ciphertext; +use generic_array::typenum::Unsigned; use serde::{Deserialize, Serialize}; -use umbral_pre::{decrypt_original, encrypt, serde_bytes, Capsule, PublicKey, SecretKey}; - -use crate::conditions::{Conditions, Context}; -use crate::key_frag::DecryptionError; +use umbral_pre::serde_bytes; // TODO should this be in umbral? use crate::versioning::{ - messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, + 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 { + /// Given plaintext is too large for the backend to handle. + PlaintextTooLarge, +} + +impl fmt::Display for EncryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PlaintextTooLarge => write!(f, "Plaintext is too large to encrypt"), + } + } +} + +/// Errors during decryption. +#[derive(Debug)] +pub enum DecryptionError { + /// Ciphertext (which should be prepended by the nonce) is shorter than the nonce length. + CiphertextTooShort, + /// The ciphertext and the attached authentication data are inconsistent. + /// This can happen if: + /// - an incorrect key is used, + /// - the ciphertext is modified or cut short, + /// - an incorrect authentication data is provided on decryption. + AuthenticationFailed, + /// Unable to create object from decrypted ciphertext + DeserializationFailed(DeserializationError), +} + +impl fmt::Display for DecryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CiphertextTooShort => write!(f, "The ciphertext must include the nonce"), + Self::AuthenticationFailed => write!( + f, + "Decryption of ciphertext failed: \ + either someone tampered with the ciphertext or \ + you are using an incorrect decryption key." + ), + Self::DeserializationFailed(err) => write!(f, "deserialization failed: {}", err), + } + } +} + +type NonceSize = ::NonceSize; + +fn encrypt_with_shared_secret( + shared_secret: &SessionSharedSecret, + plaintext: &[u8], +) -> Result, EncryptionError> { + let key = Key::from_slice(shared_secret.as_ref()); + let cipher = ChaCha20Poly1305::new(key); + let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); + let mut result = nonce.to_vec(); + let ciphertext = cipher + .encrypt(&nonce, plaintext.as_ref()) + .map_err(|_err| EncryptionError::PlaintextTooLarge)?; + result.extend(ciphertext); + Ok(result.into_boxed_slice()) +} + +fn decrypt_with_shared_secret( + shared_secret: &SessionSharedSecret, + ciphertext: &[u8], +) -> Result, DecryptionError> { + let nonce_size = ::to_usize(); + let buf_size = ciphertext.len(); + if buf_size < nonce_size { + return Err(DecryptionError::CiphertextTooShort); + } + let nonce = Nonce::from_slice(&ciphertext[..nonce_size]); + let encrypted_data = &ciphertext[nonce_size..]; + + let key = Key::from_slice(shared_secret.as_ref()); + let cipher = ChaCha20Poly1305::new(key); + let plaintext = cipher + .decrypt(nonce, encrypted_data) + .map_err(|_err| DecryptionError::AuthenticationFailed)?; + Ok(plaintext.into_boxed_slice()) +} + /// The ferveo variant to use for the decryption share derivation. #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Copy, Clone)] pub enum FerveoVariant { @@ -21,11 +109,231 @@ pub enum FerveoVariant { PRECOMPUTED, } +/// Module for session key objects. +pub mod session { + use alloc::boxed::Box; + use alloc::string::String; + use core::fmt; + use generic_array::{ + typenum::{Unsigned, U32}, + GenericArray, + }; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + use rand_core::{CryptoRng, OsRng, RngCore}; + use serde::{Deserialize, Serialize}; + use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; + + use crate::secret_box::{kdf, SecretBox}; + use zeroize::ZeroizeOnDrop; + + use crate::versioning::{ + messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, + }; + + /// A Diffie-Hellman shared secret + #[derive(ZeroizeOnDrop)] + pub struct SessionSharedSecret { + derived_bytes: [u8; 32], + } + + /// Implementation of Diffie-Hellman shared secret + impl SessionSharedSecret { + /// Create new shared secret from underlying library. + pub fn new(shared_secret: SharedSecret) -> Self { + let info = b"SESSION_SHARED_SECRET_DERIVATION/"; + let derived_key = kdf::(shared_secret.as_bytes(), Some(info)); + let derived_bytes = <[u8; 32]>::try_from(derived_key.as_secret().as_slice()).unwrap(); + Self { derived_bytes } + } + + /// View this shared secret as a byte array. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.derived_bytes + } + } + + impl AsRef<[u8]> for SessionSharedSecret { + /// View this shared secret as a byte array. + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } + } + + impl fmt::Display for SessionSharedSecret { + /// Format shared secret information. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SessionSharedSecret...") + } + } + + /// A session public key. + #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Serialize, Deserialize)] + pub struct SessionStaticKey(PublicKey); + + /// Implementation of session static key + impl SessionStaticKey { + /// Convert this public key to a byte array. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + } + + impl AsRef<[u8]> for SessionStaticKey { + /// View this public key as a byte array. + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } + } + + impl fmt::Display for SessionStaticKey { + /// Format public key information. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SessionStaticKey: {}", hex::encode(&self.as_ref()[..8])) + } + } + + impl<'a> ProtocolObjectInner<'a> for SessionStaticKey { + fn version() -> (u16, u16) { + (1, 0) + } + + fn brand() -> [u8; 4] { + *b"TSSk" + } + + 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 SessionStaticKey {} + + /// A session secret key. + #[derive(ZeroizeOnDrop)] + pub struct SessionStaticSecret(pub(crate) StaticSecret); + + impl SessionStaticSecret { + /// Perform diffie-hellman + pub fn derive_shared_secret( + &self, + their_public_key: &SessionStaticKey, + ) -> SessionSharedSecret { + let shared_secret = self.0.diffie_hellman(&their_public_key.0); + SessionSharedSecret::new(shared_secret) + } + + /// Create secret key from RNG. + pub fn random_from_rng(csprng: &mut (impl RngCore + CryptoRng)) -> Self { + let secret_key = StaticSecret::random_from_rng(csprng); + Self(secret_key) + } + + /// Create random secret key. + pub fn random() -> Self { + Self::random_from_rng(&mut OsRng) + } + + /// Returns a public key corresponding to this secret key. + pub fn public_key(&self) -> SessionStaticKey { + let public_key = PublicKey::from(&self.0); + SessionStaticKey(public_key) + } + } + + impl fmt::Display for SessionStaticSecret { + /// Format information above secret key. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SessionStaticSecret:...") + } + } + + type SessionSecretFactorySeedSize = U32; // the size of the seed material for key derivation + type SessionSecretFactoryDerivedKeySize = U32; // the size of the derived key + type SessionSecretFactorySeed = GenericArray; + + /// Error thrown when invalid random seed provided for creating key factory. + #[derive(Debug)] + pub struct InvalidSessionSecretFactorySeedLength; + + impl fmt::Display for InvalidSessionSecretFactorySeedLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid seed length") + } + } + + /// This class handles keyring material for session keys, by allowing deterministic + /// derivation of `SessionStaticSecret` objects based on labels. + #[derive(Clone, ZeroizeOnDrop, PartialEq)] + pub struct SessionSecretFactory(SecretBox); + + impl SessionSecretFactory { + /// Creates a session secret factory using the given RNG. + pub fn random_with_rng(rng: &mut (impl CryptoRng + RngCore)) -> Self { + let mut bytes = SecretBox::new(SessionSecretFactorySeed::default()); + rng.fill_bytes(bytes.as_mut_secret()); + Self(bytes) + } + + /// Creates a session secret factory using the default RNG. + pub fn random() -> Self { + Self::random_with_rng(&mut OsRng) + } + + /// Returns the seed size required by + pub fn seed_size() -> usize { + SessionSecretFactorySeedSize::to_usize() + } + + /// Creates a `SessionSecretFactory` using the given random bytes. + /// + /// **Warning:** make sure the given seed has been obtained + /// from a cryptographically secure source of randomness! + pub fn from_secure_randomness( + seed: &[u8], + ) -> Result { + if seed.len() != Self::seed_size() { + return Err(InvalidSessionSecretFactorySeedLength); + } + Ok(Self(SecretBox::new(*SessionSecretFactorySeed::from_slice( + seed, + )))) + } + + /// Creates a `SessionStaticSecret` deterministically from the given label. + pub fn make_key(&self, label: &[u8]) -> SessionStaticSecret { + let prefix = b"SESSION_KEY_DERIVATION/"; + let info = [prefix, label].concat(); + let seed = kdf::(self.0.as_secret(), Some(&info)); + let mut rng = + ChaCha20Rng::from_seed(<[u8; 32]>::try_from(seed.as_secret().as_slice()).unwrap()); + SessionStaticSecret::random_from_rng(&mut rng) + } + } + + impl fmt::Display for SessionSecretFactory { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SessionSecretFactory:...") + } + } +} + /// A request for an Ursula to derive a decryption share. #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct ThresholdDecryptionRequest { /// The ID of the ritual. - pub ritual_id: u16, + 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. @@ -39,7 +347,7 @@ pub struct ThresholdDecryptionRequest { impl ThresholdDecryptionRequest { /// Creates a new decryption request. pub fn new( - ritual_id: u16, + ritual_id: u32, ciphertext: &Ciphertext, conditions: Option<&Conditions>, context: Option<&Context>, @@ -57,20 +365,16 @@ impl ThresholdDecryptionRequest { /// Encrypts the decryption request. pub fn encrypt( &self, - request_encrypting_key: &PublicKey, - response_encrypting_key: &PublicKey, + shared_secret: &SessionSharedSecret, + requester_public_key: &SessionStaticKey, ) -> EncryptedThresholdDecryptionRequest { - EncryptedThresholdDecryptionRequest::new( - self, - request_encrypting_key, - response_encrypting_key, - ) + EncryptedThresholdDecryptionRequest::new(self, shared_secret, requester_public_key) } } impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionRequest { fn version() -> (u16, u16) { - (1, 0) + (2, 0) } fn brand() -> [u8; 4] { @@ -92,79 +396,31 @@ impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionRequest { impl<'a> ProtocolObject<'a> for ThresholdDecryptionRequest {} -/// A request for an Ursula to derive a decryption share that specifies the key to encrypt Ursula's response. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] -pub struct E2EThresholdDecryptionRequest { - /// The decryption request. - pub decryption_request: ThresholdDecryptionRequest, - /// The key to encrypt the corresponding decryption response. - pub response_encrypting_key: PublicKey, -} - -impl E2EThresholdDecryptionRequest { - /// Create E2E decryption request. - pub fn new( - decryption_request: &ThresholdDecryptionRequest, - response_encrypting_key: &PublicKey, - ) -> Self { - Self { - decryption_request: decryption_request.clone(), - response_encrypting_key: *response_encrypting_key, - } - } -} - -impl<'a> ProtocolObjectInner<'a> for E2EThresholdDecryptionRequest { - fn version() -> (u16, u16) { - (1, 0) - } - - fn brand() -> [u8; 4] { - *b"E2eR" - } - - 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 E2EThresholdDecryptionRequest {} - /// An encrypted request for an Ursula to derive a decryption share. #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct EncryptedThresholdDecryptionRequest { /// ID of the ritual - pub ritual_id: u16, - /// TODO Umbral for now - but change - capsule: Capsule, + pub ritual_id: u32, + + /// Public key of requester + pub requester_public_key: SessionStaticKey, + #[serde(with = "serde_bytes::as_base64")] + /// Encrypted request ciphertext: Box<[u8]>, } impl EncryptedThresholdDecryptionRequest { fn new( request: &ThresholdDecryptionRequest, - request_encrypting_key: &PublicKey, - response_encrypting_key: &PublicKey, + shared_secret: &SessionSharedSecret, + requester_public_key: &SessionStaticKey, ) -> Self { - let e2e_decryption_request = - E2EThresholdDecryptionRequest::new(request, response_encrypting_key); - // TODO: using Umbral for encryption to avoid introducing more crypto primitives. - let (capsule, ciphertext) = - encrypt(request_encrypting_key, &e2e_decryption_request.to_bytes()) - .expect("encryption failed - out of memory?"); - let ritual_id = request.ritual_id; + let ciphertext = encrypt_with_shared_secret(shared_secret, &request.to_bytes()) + .expect("encryption failed - out of memory?"); Self { - ritual_id, - capsule, + ritual_id: request.ritual_id, + requester_public_key: *requester_public_key, ciphertext, } } @@ -172,20 +428,18 @@ impl EncryptedThresholdDecryptionRequest { /// Decrypts the decryption request pub fn decrypt( &self, - sk: &SecretKey, - ) -> Result { - let decryption_request_bytes = decrypt_original(sk, &self.capsule, &self.ciphertext) - .map_err(DecryptionError::DecryptionFailed)?; - let decryption_request = - E2EThresholdDecryptionRequest::from_bytes(&decryption_request_bytes) - .map_err(DecryptionError::DeserializationFailed)?; + shared_secret: &SessionSharedSecret, + ) -> Result { + let decryption_request_bytes = decrypt_with_shared_secret(shared_secret, &self.ciphertext)?; + let decryption_request = ThresholdDecryptionRequest::from_bytes(&decryption_request_bytes) + .map_err(DecryptionError::DeserializationFailed)?; Ok(decryption_request) } } impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionRequest { fn version() -> (u16, u16) { - (1, 0) + (2, 0) } fn brand() -> [u8; 4] { @@ -210,6 +464,9 @@ impl<'a> ProtocolObject<'a> for EncryptedThresholdDecryptionRequest {} /// A response from Ursula with a derived decryption share. #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub struct ThresholdDecryptionResponse { + /// The ID of the ritual. + pub ritual_id: u32, + /// The decryption share to include in the response. #[serde(with = "serde_bytes::as_base64")] pub decryption_share: Box<[u8]>, @@ -217,21 +474,25 @@ pub struct ThresholdDecryptionResponse { impl ThresholdDecryptionResponse { /// Creates and a new decryption response. - pub fn new(decryption_share: &[u8]) -> Self { + pub fn new(ritual_id: u32, decryption_share: &[u8]) -> Self { ThresholdDecryptionResponse { + ritual_id, decryption_share: decryption_share.to_vec().into(), } } /// Encrypts the decryption response. - pub fn encrypt(&self, encrypting_key: &PublicKey) -> EncryptedThresholdDecryptionResponse { - EncryptedThresholdDecryptionResponse::new(encrypting_key, self) + pub fn encrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> EncryptedThresholdDecryptionResponse { + EncryptedThresholdDecryptionResponse::new(self, shared_secret) } } impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionResponse { fn version() -> (u16, u16) { - (1, 0) + (2, 0) } fn brand() -> [u8; 4] { @@ -256,31 +517,30 @@ impl<'a> ProtocolObject<'a> for ThresholdDecryptionResponse {} /// An encrypted response from Ursula with a derived decryption share. #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct EncryptedThresholdDecryptionResponse { - /// TODO Umbral for now - but change - capsule: Capsule, + /// The ID of the ritual. + pub ritual_id: u32, + #[serde(with = "serde_bytes::as_base64")] ciphertext: Box<[u8]>, } impl EncryptedThresholdDecryptionResponse { - fn new( - encrypting_key: &PublicKey, - threshold_decryption_response: &ThresholdDecryptionResponse, - ) -> Self { - // TODO: using Umbral for encryption to avoid introducing more crypto primitives. - let (capsule, ciphertext) = - encrypt(encrypting_key, &threshold_decryption_response.to_bytes()) - .expect("encryption failed - out of memory?"); + fn new(response: &ThresholdDecryptionResponse, shared_secret: &SessionSharedSecret) -> Self { + let ciphertext = encrypt_with_shared_secret(shared_secret, &response.to_bytes()) + .expect("encryption failed - out of memory?"); Self { - capsule, + ritual_id: response.ritual_id, ciphertext, } } /// Decrypts the decryption request - pub fn decrypt(&self, sk: &SecretKey) -> Result { - let decryption_response_bytes = decrypt_original(sk, &self.capsule, &self.ciphertext) - .map_err(DecryptionError::DecryptionFailed)?; + pub fn decrypt( + &self, + shared_secret: &SessionSharedSecret, + ) -> Result { + let decryption_response_bytes = + decrypt_with_shared_secret(shared_secret, &self.ciphertext)?; let decryption_response = ThresholdDecryptionResponse::from_bytes(&decryption_response_bytes) .map_err(DecryptionError::DeserializationFailed)?; @@ -290,7 +550,7 @@ impl EncryptedThresholdDecryptionResponse { impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionResponse { fn version() -> (u16, u16) { - (1, 0) + (2, 0) } fn brand() -> [u8; 4] { @@ -315,28 +575,114 @@ impl<'a> ProtocolObject<'a> for EncryptedThresholdDecryptionResponse {} #[cfg(test)] mod tests { use ferveo::api::{encrypt as ferveo_encrypt, DkgPublicKey, SecretBox}; - use umbral_pre::SecretKey; use crate::{ Conditions, Context, EncryptedThresholdDecryptionRequest, - EncryptedThresholdDecryptionResponse, FerveoVariant, ProtocolObject, + EncryptedThresholdDecryptionResponse, FerveoVariant, ProtocolObject, SessionSecretFactory, ThresholdDecryptionRequest, ThresholdDecryptionResponse, }; + use generic_array::typenum::Unsigned; + use rand_core::RngCore; + + use crate::dkg::session::SessionStaticSecret; + use crate::dkg::{ + decrypt_with_shared_secret, encrypt_with_shared_secret, DecryptionError, NonceSize, + }; + + #[test] + fn decryption_with_shared_secret() { + let service_secret = SessionStaticSecret::random(); + + let requester_secret = SessionStaticSecret::random(); + let requester_public_key = requester_secret.public_key(); + + let service_shared_secret = service_secret.derive_shared_secret(&requester_public_key); + + let ciphertext = b"1".to_vec().into_boxed_slice(); // length less than nonce size + let nonce_size = ::to_usize(); + assert!(ciphertext.len() < nonce_size); + + assert!(matches!( + decrypt_with_shared_secret(&service_shared_secret, &ciphertext).unwrap_err(), + DecryptionError::CiphertextTooShort + )); + } + + #[test] + fn request_key_factory() { + let secret_factory = SessionSecretFactory::random(); + + // ensure that shared secret derived from factory can be used correctly + let label_1 = b"label_1".to_vec().into_boxed_slice(); + let service_secret_key = secret_factory.make_key(&label_1.as_ref()); + let service_public_key = service_secret_key.public_key(); + + let label_2 = b"label_2".to_vec().into_boxed_slice(); + let requester_secret_key = secret_factory.make_key(&label_2.as_ref()); + let requester_public_key = requester_secret_key.public_key(); + + let service_shared_secret = service_secret_key.derive_shared_secret(&requester_public_key); + let requester_shared_secret = + requester_secret_key.derive_shared_secret(&service_public_key); + + let data_to_encrypt = b"The Tyranny of Merit".to_vec().into_boxed_slice(); + let ciphertext = + encrypt_with_shared_secret(&requester_shared_secret, &data_to_encrypt.as_ref()) + .unwrap(); + let decrypted_data = + decrypt_with_shared_secret(&service_shared_secret, &ciphertext).unwrap(); + assert_eq!(decrypted_data, data_to_encrypt); + + // ensure same key can be generated by the same factory using the same seed + let same_requester_secret_key = secret_factory.make_key(&label_2.as_ref()); + let same_requester_public_key = same_requester_secret_key.public_key(); + assert_eq!(requester_public_key, same_requester_public_key); + + // ensure different key generated using same seed but using different factory + let other_secret_factory = SessionSecretFactory::random(); + let not_same_requester_secret_key = other_secret_factory.make_key(&label_2.as_ref()); + let not_same_requester_public_key = not_same_requester_secret_key.public_key(); + assert_ne!(requester_public_key, not_same_requester_public_key); + + // ensure that two secret factories with the same seed generate the same keys + let mut secret_factory_seed = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut secret_factory_seed); + let seeded_secret_factory_1 = + SessionSecretFactory::from_secure_randomness(&secret_factory_seed).unwrap(); + let seeded_secret_factory_2 = + SessionSecretFactory::from_secure_randomness(&secret_factory_seed).unwrap(); + + let key_label = b"seeded_factory_key_label".to_vec().into_boxed_slice(); + let sk_1 = seeded_secret_factory_1.make_key(&key_label); + let pk_1 = sk_1.public_key(); + + let sk_2 = seeded_secret_factory_2.make_key(&key_label); + let pk_2 = sk_2.public_key(); + + assert_eq!(pk_1, pk_2); + + // test secure randomness + let bytes = [0u8; 32]; + let factory = SessionSecretFactory::from_secure_randomness(&bytes); + assert!(factory.is_ok()); + + let bytes = [0u8; 31]; + let factory = SessionSecretFactory::from_secure_randomness(&bytes); + assert!(factory.is_err()); + } + #[test] fn threshold_decryption_request() { let ritual_id = 0; - let request_secret = SecretKey::random(); - let request_encrypting_key = request_secret.public_key(); + let service_secret = SessionStaticSecret::random(); - let response_secret = SecretKey::random(); - let response_encrypting_key = response_secret.public_key(); - - let random_secret_key = SecretKey::random(); + let requester_secret = SessionStaticSecret::random(); + let requester_public_key = requester_secret.public_key(); let dkg_pk = DkgPublicKey::random(); - let message = "my_message".as_bytes().to_vec(); + let message = "The Tyranny of Merit".as_bytes().to_vec(); let aad = "my-add".as_bytes(); let ciphertext = ferveo_encrypt(SecretBox::new(message), aad, &dkg_pk).unwrap(); @@ -348,58 +694,87 @@ mod tests { FerveoVariant::SIMPLE, ); - let encrypted_request = request.encrypt(&request_encrypting_key, &response_encrypting_key); + // requester encrypts request to send to service + let service_public_key = service_secret.public_key(); + let requester_shared_secret = requester_secret.derive_shared_secret(&service_public_key); + let encrypted_request = request.encrypt(&requester_shared_secret, &requester_public_key); + // mimic serialization/deserialization over the wire let encrypted_request_bytes = encrypted_request.to_bytes(); let encrypted_request_from_bytes = EncryptedThresholdDecryptionRequest::from_bytes(&encrypted_request_bytes).unwrap(); assert_eq!(encrypted_request_from_bytes.ritual_id, ritual_id); + assert_eq!( + encrypted_request_from_bytes.requester_public_key, + requester_public_key + ); - let e2e_request = encrypted_request_from_bytes - .decrypt(&request_secret) + // service decrypts request + let service_shared_secret = + service_secret.derive_shared_secret(&encrypted_request_from_bytes.requester_public_key); + assert_eq!( + service_shared_secret.as_bytes(), + requester_shared_secret.as_bytes() + ); + let decrypted_request = encrypted_request_from_bytes + .decrypt(&service_shared_secret) .unwrap(); - assert_eq!(response_encrypting_key, e2e_request.response_encrypting_key); - assert_eq!(request, e2e_request.decryption_request); - - // wrong secret key used - assert!(encrypted_request_from_bytes - .decrypt(&response_secret) - .is_err()); + assert_eq!(decrypted_request, request); + // wrong shared key used + let random_secret_key = SessionStaticSecret::random(); + let random_shared_secret = random_secret_key.derive_shared_secret(&requester_public_key); assert!(encrypted_request_from_bytes - .decrypt(&random_secret_key) + .decrypt(&random_shared_secret) .is_err()); } #[test] fn threshold_decryption_response() { - let response_secret = SecretKey::random(); - let response_encrypting_key = response_secret.public_key(); + let ritual_id = 5; + + let service_secret = SessionStaticSecret::random(); + let requester_secret = SessionStaticSecret::random(); let decryption_share = b"The Tyranny of Merit"; - let response = ThresholdDecryptionResponse::new(decryption_share); + let response = ThresholdDecryptionResponse::new(ritual_id, decryption_share); + + // service encrypts response to send back + let requester_public_key = requester_secret.public_key(); - let encrypted_response = response.encrypt(&response_encrypting_key); + let service_shared_secret = service_secret.derive_shared_secret(&requester_public_key); + let encrypted_response = response.encrypt(&service_shared_secret); + assert_eq!(encrypted_response.ritual_id, ritual_id); + // mimic serialization/deserialization over the wire let encrypted_response_bytes = encrypted_response.to_bytes(); let encrypted_response_from_bytes = EncryptedThresholdDecryptionResponse::from_bytes(&encrypted_response_bytes).unwrap(); + // requester decrypts response + let service_public_key = service_secret.public_key(); + let requester_shared_secret = requester_secret.derive_shared_secret(&service_public_key); + assert_eq!( + requester_shared_secret.as_bytes(), + service_shared_secret.as_bytes() + ); let decrypted_response = encrypted_response_from_bytes - .decrypt(&response_secret) + .decrypt(&requester_shared_secret) .unwrap(); assert_eq!(response, decrypted_response); + assert_eq!(response.ritual_id, ritual_id); assert_eq!( response.decryption_share, decrypted_response.decryption_share ); - // wrong secret key used - let random_secret_key = SecretKey::random(); + // wrong shared key used + let random_secret_key = SessionStaticSecret::random(); + let random_shared_secret = random_secret_key.derive_shared_secret(&requester_public_key); assert!(encrypted_response_from_bytes - .decrypt(&random_secret_key) + .decrypt(&random_shared_secret) .is_err()); } } diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index 147fb698..3ed94ba7 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -18,6 +18,7 @@ mod node_metadata; mod reencryption; mod retrieval_kit; mod revocation_order; +mod secret_box; mod treasure_map; mod versioning; @@ -27,9 +28,9 @@ pub struct VerificationError; pub use address::Address; pub use conditions::{Conditions, Context}; pub use dkg::{ - E2EThresholdDecryptionRequest, EncryptedThresholdDecryptionRequest, - EncryptedThresholdDecryptionResponse, FerveoVariant, ThresholdDecryptionRequest, - ThresholdDecryptionResponse, + session::{SessionSecretFactory, SessionSharedSecret, SessionStaticKey, SessionStaticSecret}, + DecryptionError, EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, + EncryptionError, FerveoVariant, ThresholdDecryptionRequest, ThresholdDecryptionResponse, }; pub use fleet_state::FleetStateChecksum; pub use hrac::HRAC; diff --git a/nucypher-core/src/secret_box.rs b/nucypher-core/src/secret_box.rs new file mode 100644 index 00000000..64135c14 --- /dev/null +++ b/nucypher-core/src/secret_box.rs @@ -0,0 +1,92 @@ +/* +This module implements a similar API to what the crate `secrecy` provides. +So, why our own implementation? + +First `secrecy::Secret` does not put its contents in a `Box`. +Using `Box` is a general recommendation of working with secret data, +because it prevents the compiler from putting it on stack, thus avoiding possible copies on borrow. + +Now, one could use `secrecy::Secret>`. +The problem here is that `secrecy::Secret` requires its type parameter to implement `Zeroize`. +This means that for a foreign type `F` (even if it does implement `Zeroize`) +we need to define `impl Zeroize for Box`. +But the compiler does not allow impls of foreign traits on foreign types. +This means that we also need to wrap `F` in a local type, impl `Zeroize` for the wrapper, +and then for the box of the wrapper. +This is too much boilerplate. + +Additionally, `secrecy::Secret>` means that after each `expose_secret()` +we will need to deal with opening the `Box` as well. +It's an inconvenience, albeit a minor one. + +The situation may improve in the future, and `secrecy` will actually become usable. +*/ + +use alloc::boxed::Box; + +use generic_array::{ArrayLength, GenericArray}; +use hkdf::Hkdf; +use sha2::Sha256; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// A container for secret data. +/// Makes the usage of secret data explicit and easy to track, +/// prevents the secret data from being put on stack, +/// and zeroizes the contents on drop. +#[derive(Clone)] // No Debug derivation, to avoid exposing the secret data accidentally. +pub struct SecretBox(Box) +where + T: Zeroize + Clone; + +impl PartialEq> for SecretBox { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl SecretBox +where + T: Zeroize + Clone, +{ + pub(crate) fn new(val: T) -> Self { + Self(Box::new(val)) + } + + /// Returns an immutable reference to the secret data. + pub fn as_secret(&self) -> &T { + self.0.as_ref() + } + + /// Returns a mutable reference to the secret data. + pub fn as_mut_secret(&mut self) -> &mut T { + self.0.as_mut() + } +} + +impl Drop for SecretBox +where + T: Zeroize + Clone, +{ + fn drop(&mut self) { + self.0.zeroize() + } +} + +// Alternatively, it could be derived automatically, +// but there's some compilation problem in the macro. +// See https://github.com/RustCrypto/utils/issues/786 +// So we're asserting that this object is zeroized on drop, since there is a Drop impl just above. +impl ZeroizeOnDrop for SecretBox where T: Zeroize + Clone {} + +pub fn kdf>(seed: &[u8], info: Option<&[u8]>) -> SecretBox> { + let hk = Hkdf::::new(None, seed); + + let mut okm = SecretBox::new(GenericArray::::default()); + + let def_info = info.unwrap_or_default(); + + // We can only get an error here if `S` is too large, and it's known at compile-time. + hk.expand(def_info, okm.as_mut_secret()).unwrap(); + + okm +}