Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1ac2f38
Relock cargo dependencies
cygnusv Apr 15, 2025
5eaf563
Modify encrypt/decrypt_with_shared_secret to allow to test it determi…
cygnusv Apr 15, 2025
4033a38
cargo-fixn' stuff
cygnusv Apr 15, 2025
f5749f4
Use conditional compilation to set deterministic encryption for speci…
cygnusv Apr 16, 2025
6c91098
Tests and utils to generate test vectors for encrypt_with_shared_secret
cygnusv Apr 16, 2025
5126436
Utility function to produce test vectors as files
cygnusv Apr 16, 2025
3be51bc
Don't store session shared secret as part of the test vector
cygnusv Apr 16, 2025
48fc6a4
New util function `create_session_shared_secret_from_seed`
cygnusv Apr 16, 2025
0195f2c
Add serde_json dependency
cygnusv Apr 16, 2025
e0365d6
Fix some imports
cygnusv Apr 16, 2025
cdaa05e
Serialize test vectors as JSON strings
cygnusv Apr 16, 2025
7b44d2a
Implement deterministic encryption via supplied nonces
cygnusv Apr 21, 2025
43c7f51
cargo-fmt'n stuff
cygnusv Apr 21, 2025
1c28d96
Separate utility executable that produces test vectors as files
cygnusv Apr 22, 2025
1aaa2df
Relocate test vector specific logic to separate module
cygnusv Apr 22, 2025
1d8814a
Use serde-encoded-bytes to produce test vectors files in hex format
cygnusv Apr 22, 2025
6f3f733
Add usage docs and output a single test vector file
cygnusv Apr 22, 2025
ee1fc6a
Make deterministic_encryption feature flag NOT a default
cygnusv Apr 22, 2025
a2ff422
Fix incorrect feature flag in deterministic encryption test
cygnusv Apr 23, 2025
b350af2
Apply deterministic feature flag to nonce generation, instead of encr…
cygnusv Apr 23, 2025
216deee
Remove duplicate of create_session_shared_secret_from_seed function
cygnusv Apr 23, 2025
3ebd016
Add session shared secret to test vectors
cygnusv Apr 23, 2025
93457d2
Add intermediate static secrets to test vectors
cygnusv Apr 24, 2025
8a8992d
Cleaning
cygnusv Apr 24, 2025
9e85d8b
First test vector file for encrypt_with_shared_secret
cygnusv Apr 24, 2025
eab5a03
Change Cargo.lock version to 3 to fix CI errors
cygnusv Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
691 changes: 447 additions & 244 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion nucypher-core-wasm/tests/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ fn fleet_state_checksum_to_bytes() {
let fleet_state_checksum = make_fleet_state_checksum();

assert!(
fleet_state_checksum.to_bytes().len() > 0,
!fleet_state_checksum.to_bytes().is_empty(),
"FleetStateChecksum does not serialize to bytes"
);
}
Expand Down
12 changes: 12 additions & 0 deletions nucypher-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ categories = ["cryptography", "no-std"]
umbral-pre = { version = "0.11.0", features = ["serde"] }
ferveo = { package = "ferveo-pre-release", version = "0.3.0" }
serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = "1.0"
serde-encoded-bytes = "0.1"
generic-array = { version = "0.14", features = ["zeroize"] }
sha3 = "0.10"
rmp-serde = "1"
Expand All @@ -26,3 +28,13 @@ zeroize = { version = "1.6.0", features = ["derive"] }
rand_core = "0.6.4"
rand_chacha = "0.3.1"
rand = "0.8.5"

[features]
default = []
deterministic_encryption = []
test_vectors = ["deterministic_encryption"]

[[bin]]
name = "generate-test-vectors"
path = "src/bin/generate_test_vectors.rs"
required-features = ["deterministic_encryption", "test_vectors"]
8 changes: 4 additions & 4 deletions nucypher-core/src/access_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl AuthenticatedData {
}
}

impl<'a> ProtocolObjectInner<'a> for AuthenticatedData {
impl ProtocolObjectInner<'_> for AuthenticatedData {
fn version() -> (u16, u16) {
(1, 0)
}
Expand All @@ -65,7 +65,7 @@ impl<'a> ProtocolObjectInner<'a> for AuthenticatedData {
}
}

impl<'a> ProtocolObject<'a> for AuthenticatedData {}
impl ProtocolObject<'_> for AuthenticatedData {}

/// Encrypt data based on conditions and dkg public key.
pub fn encrypt_for_dkg(
Expand Down Expand Up @@ -118,7 +118,7 @@ impl AccessControlPolicy {
}
}

impl<'a> ProtocolObjectInner<'a> for AccessControlPolicy {
impl ProtocolObjectInner<'_> for AccessControlPolicy {
fn version() -> (u16, u16) {
(1, 0)
}
Expand All @@ -140,7 +140,7 @@ impl<'a> ProtocolObjectInner<'a> for AccessControlPolicy {
}
}

impl<'a> ProtocolObject<'a> for AccessControlPolicy {}
impl ProtocolObject<'_> for AccessControlPolicy {}

#[cfg(test)]
mod tests {
Expand Down
20 changes: 20 additions & 0 deletions nucypher-core/src/bin/generate_test_vectors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::fs;
use std::path::Path;
use nucypher_core::test_vectors::{TestVector, generate_test_vectors};

// Usage: cargo run --bin generate-test-vectors --features test_vectors deterministic_encryption
fn main() {
// Generate test vectors
let test_vectors: Vec<TestVector> = generate_test_vectors();

// Create output directory if it doesn't exist
let output_dir = Path::new("test_vectors");
fs::create_dir_all(output_dir).expect("Failed to create output directory");

// Save all test vectors to a single file
let filename = "test_vectors/encrypt_with_shared_secret.json";
let json = serde_json::to_string_pretty(&test_vectors).expect("Failed to serialize test vectors");
fs::write(filename, json).expect("Failed to write test vectors to file");

println!("Generated {} test vectors in '{}'", test_vectors.len(), filename);
}
146 changes: 120 additions & 26 deletions nucypher-core/src/dkg.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit};
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use ferveo::api::{CiphertextHeader, FerveoVariant};
use generic_array::typenum::Unsigned;
use serde::{Deserialize, Serialize};
use rand_core::{CryptoRng, RngCore};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
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::dkg::session::{SessionSecretFactory, SessionSharedSecret, SessionStaticKey};
use crate::versioning::{
messagepack_deserialize, messagepack_serialize, DeserializationError, ProtocolObject,
ProtocolObjectInner,
Expand Down Expand Up @@ -64,16 +66,40 @@ impl fmt::Display for DecryptionError {

type NonceSize = <ChaCha20Poly1305 as AeadCore>::NonceSize;

fn encrypt_with_shared_secret(
#[cfg(not(feature = "deterministic_encryption"))]
pub fn generate_encryption_nonce() -> Nonce {
use chacha20poly1305::aead::OsRng;
ChaCha20Poly1305::generate_nonce(&mut OsRng)
}

#[cfg(feature = "deterministic_encryption")]
pub fn generate_encryption_nonce() -> Nonce {
use rand::rngs::StdRng;
use rand::SeedableRng;
let rng = <StdRng as SeedableRng>::from_seed([0u8; 32]); // TODO: Note that this seed is currently fixed for all tests
ChaCha20Poly1305::generate_nonce(rng)
}

/// Encrypts data using a shared secret with the default OS RNG.
pub fn encrypt_with_shared_secret(
shared_secret: &SessionSharedSecret,
plaintext: &[u8],
) -> Result<Box<[u8]>, EncryptionError> {
let nonce = generate_encryption_nonce();
encrypt_with_shared_secret_and_nonce(shared_secret, &nonce, plaintext)
}

/// Encrypts data using a shared secret with a custom nonce.
fn encrypt_with_shared_secret_and_nonce(
shared_secret: &SessionSharedSecret,
nonce: &Nonce,
plaintext: &[u8],
) -> Result<Box<[u8]>, 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())
.encrypt(nonce, plaintext.as_ref())
.map_err(|_err| EncryptionError::PlaintextTooLarge)?;
result.extend(ciphertext);
Ok(result.into_boxed_slice())
Expand Down Expand Up @@ -210,7 +236,7 @@ pub mod session {
}
}

impl<'a> ProtocolObjectInner<'a> for SessionStaticKey {
impl ProtocolObjectInner<'_> for SessionStaticKey {
fn version() -> (u16, u16) {
(2, 0)
}
Expand All @@ -235,7 +261,7 @@ pub mod session {
}
}

impl<'a> ProtocolObject<'a> for SessionStaticKey {}
impl ProtocolObject<'_> for SessionStaticKey {}

/// A session secret key.
#[derive(ZeroizeOnDrop)]
Expand Down Expand Up @@ -391,7 +417,7 @@ impl ThresholdDecryptionRequest {
}
}

impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionRequest {
impl ProtocolObjectInner<'_> for ThresholdDecryptionRequest {
fn version() -> (u16, u16) {
(4, 0)
}
Expand All @@ -413,7 +439,7 @@ impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionRequest {
}
}

impl<'a> ProtocolObject<'a> for ThresholdDecryptionRequest {}
impl ProtocolObject<'_> for ThresholdDecryptionRequest {}

/// An encrypted request for an Ursula to derive a decryption share.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -456,7 +482,7 @@ impl EncryptedThresholdDecryptionRequest {
}
}

impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionRequest {
impl ProtocolObjectInner<'_> for EncryptedThresholdDecryptionRequest {
fn version() -> (u16, u16) {
(2, 0)
}
Expand All @@ -478,7 +504,7 @@ impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionRequest {
}
}

impl<'a> ProtocolObject<'a> for EncryptedThresholdDecryptionRequest {}
impl ProtocolObject<'_> for EncryptedThresholdDecryptionRequest {}

/// A response from Ursula with a derived decryption share.
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -509,7 +535,7 @@ impl ThresholdDecryptionResponse {
}
}

impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionResponse {
impl ProtocolObjectInner<'_> for ThresholdDecryptionResponse {
fn version() -> (u16, u16) {
(2, 0)
}
Expand All @@ -531,7 +557,7 @@ impl<'a> ProtocolObjectInner<'a> for ThresholdDecryptionResponse {
}
}

impl<'a> ProtocolObject<'a> for ThresholdDecryptionResponse {}
impl ProtocolObject<'_> for ThresholdDecryptionResponse {}

/// An encrypted response from Ursula with a derived decryption share.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -567,7 +593,7 @@ impl EncryptedThresholdDecryptionResponse {
}
}

impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionResponse {
impl ProtocolObjectInner<'_> for EncryptedThresholdDecryptionResponse {
fn version() -> (u16, u16) {
(2, 0)
}
Expand All @@ -589,25 +615,42 @@ impl<'a> ProtocolObjectInner<'a> for EncryptedThresholdDecryptionResponse {
}
}

impl<'a> ProtocolObject<'a> for EncryptedThresholdDecryptionResponse {}
impl ProtocolObject<'_> for EncryptedThresholdDecryptionResponse {}

#[cfg(test)]
mod tests {
use crate::dkg::session::{
SessionSecretFactory, SessionSharedSecret, SessionStaticKey, SessionStaticSecret,
};
use crate::dkg::{
decrypt_with_shared_secret, encrypt_with_shared_secret, DecryptionError,
EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, NonceSize,
ThresholdDecryptionRequest, ThresholdDecryptionResponse,
};
#[cfg(feature = "test_vectors")]
use crate::test_vectors::{TestVector, create_session_shared_secret_from_seed, generate_test_vectors};
use crate::{AuthenticatedData, Conditions};
use alloc::boxed::Box;
#[cfg(feature = "test_vectors")]
use alloc::format;
#[cfg(feature = "test_vectors")]
use alloc::string::String;
use alloc::vec;
use core::clone::Clone;
use ferveo::api::{encrypt as ferveo_encrypt, DkgPublicKey, FerveoVariant, SecretBox};
use generic_array::typenum::Unsigned;
use rand::rngs::StdRng;
use rand::SeedableRng;
use rand_core::RngCore;
use serde::{Deserialize, Serialize};
use serde_json;
use x25519_dalek::{PublicKey, StaticSecret};

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::{ProtocolObject, ProtocolObjectInner};
use crate::{
AuthenticatedData, EncryptedThresholdDecryptionRequest,
EncryptedThresholdDecryptionResponse, SessionSecretFactory, SessionStaticKey,
ThresholdDecryptionRequest, ThresholdDecryptionResponse,
use crate::conditions::Context;
use crate::versioning::{
messagepack_deserialize, messagepack_serialize, DeserializationError, ProtocolObject,
ProtocolObjectInner,
};

#[test]
Expand Down Expand Up @@ -832,4 +875,55 @@ mod tests {
.decrypt(&random_shared_secret)
.is_err());
}

#[test]
#[cfg(feature = "deterministic_encryption")]
fn test_encryption_deterministic() {
use crate::dkg::session::SessionSharedSecret;
use rand::rngs::StdRng;
use rand_core::SeedableRng;
use x25519_dalek::{PublicKey, StaticSecret};

// Create a test session_shared_secret and test plaintext
let mut rng0 = <StdRng as SeedableRng>::from_seed([0u8; 32]);
let static_secret_a = StaticSecret::random_from_rng(&mut rng0);
let static_secret_b = StaticSecret::random_from_rng(&mut rng0);
let public_key_b = PublicKey::from(&static_secret_b);
let shared_secret = static_secret_a.diffie_hellman(&public_key_b);
let session_shared_secret = SessionSharedSecret::new(shared_secret);

let plaintext = b"test data";

// Use a seeded RNG for deterministic testing on encryption
let ciphertext1 = encrypt_with_shared_secret(&session_shared_secret, plaintext).unwrap();

// Reset the RNG with the same seed
let ciphertext2 = encrypt_with_shared_secret(&session_shared_secret, plaintext).unwrap();

// The ciphertexts will be identical because we used the same seed
assert_eq!(ciphertext1, ciphertext2);
}

#[test]
#[cfg(feature = "test_vectors")]
fn test_encryption_vectors() {
let test_vectors = generate_test_vectors();

// Verify each test vector
for vector in test_vectors {
let session_shared_secret = create_session_shared_secret_from_seed(vector.seed);
assert_eq!(session_shared_secret.as_ref(), vector.session_shared_secret);

// Verify decryption works
let decrypted = decrypt_with_shared_secret(&session_shared_secret, &vector.ciphertext)
.expect("Decryption failed");
assert_eq!(decrypted.as_ref(), vector.plaintext.as_slice());

// Verify encryption is deterministic
let new_ciphertext =
encrypt_with_shared_secret(&session_shared_secret, &vector.plaintext)
.expect("Encryption failed");
assert_eq!(new_ciphertext, vector.ciphertext);
}
}
}
2 changes: 1 addition & 1 deletion nucypher-core/src/fleet_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl FleetStateChecksum {
// so this may lead to unnecessary fleet state update.
// But, unlike ProtocolObject::to_bytes(), payload serialization
// is not standardized, so it is better not to rely on it.
digest.chain(&node.to_bytes())
digest.chain(node.to_bytes())
})
.finalize();

Expand Down
8 changes: 4 additions & 4 deletions nucypher-core/src/key_frag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl AuthorizedKeyFrag {
}
}

impl<'a> ProtocolObjectInner<'a> for AuthorizedKeyFrag {
impl ProtocolObjectInner<'_> for AuthorizedKeyFrag {
fn brand() -> [u8; 4] {
*b"AKFr"
}
Expand All @@ -74,7 +74,7 @@ impl<'a> ProtocolObjectInner<'a> for AuthorizedKeyFrag {
}
}

impl<'a> ProtocolObject<'a> for AuthorizedKeyFrag {}
impl ProtocolObject<'_> for AuthorizedKeyFrag {}

#[allow(clippy::enum_variant_names)]
#[derive(Debug)]
Expand Down Expand Up @@ -144,7 +144,7 @@ impl EncryptedKeyFrag {
}
}

impl<'a> ProtocolObjectInner<'a> for EncryptedKeyFrag {
impl ProtocolObjectInner<'_> for EncryptedKeyFrag {
fn brand() -> [u8; 4] {
*b"EKFr"
}
Expand All @@ -166,4 +166,4 @@ impl<'a> ProtocolObjectInner<'a> for EncryptedKeyFrag {
}
}

impl<'a> ProtocolObject<'a> for EncryptedKeyFrag {}
impl ProtocolObject<'_> for EncryptedKeyFrag {}
Loading
Loading