diff --git a/Cargo.lock b/Cargo.lock index 2f34579..88966a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aead" @@ -361,7 +361,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -409,7 +409,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -431,7 +431,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core 0.20.1", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -852,6 +852,12 @@ dependencies = [ "log", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "memoffset" version = "0.8.0" @@ -881,6 +887,7 @@ dependencies = [ "rand_core 0.6.4", "rmp-serde", "serde", + "serde_json", "serde_with 1.14.0", "sha2", "sha3", @@ -1014,9 +1021,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -1084,9 +1091,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1212,6 +1219,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.14" @@ -1251,9 +1264,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -1269,22 +1282,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1336,7 +1350,7 @@ dependencies = [ "darling 0.20.1", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -1414,9 +1428,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1446,7 +1460,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] [[package]] @@ -1550,26 +1564,27 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -1609,9 +1624,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1619,22 +1634,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-bindgen-test" @@ -1787,5 +1805,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index d96b77d..65f00b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "nucypher-core", "nucypher-core-python", diff --git a/nucypher-core-wasm/Cargo.toml b/nucypher-core-wasm/Cargo.toml index 3434ed6..6322865 100644 --- a/nucypher-core-wasm/Cargo.toml +++ b/nucypher-core-wasm/Cargo.toml @@ -22,7 +22,7 @@ default = ["console_error_panic_hook"] umbral-pre = { version = "0.11.0", features = ["bindings-wasm"] } ferveo = { package = "ferveo-pre-release", version = "0.3.0", features = ["bindings-wasm"] } nucypher-core = { path = "../nucypher-core" } -wasm-bindgen = "0.2.86" +wasm-bindgen = "0.2.88" 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"] } diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index 2043430..5f0cc37 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -26,3 +26,6 @@ zeroize = { version = "1.6.0", features = ["derive"] } rand_core = "0.6.4" rand_chacha = "0.3.1" rand = "0.8.5" + +[dev-dependencies] +serde_json = "1.0.140" diff --git a/nucypher-core/README.md b/nucypher-core/README.md index 99b0c01..34756fc 100644 --- a/nucypher-core/README.md +++ b/nucypher-core/README.md @@ -17,6 +17,19 @@ Bindings for several languages are available: * [JavaScript](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-wasm) (WASM-based) * [Python](https://github.com/nucypher/nucypher-core/tree/main/nucypher-core-python) +## Cross-Implementation Testing + +This library tests generate test vectors for ensuring compatibility between different implementations. The test vector generators automatically produce JSON files in both the Rust project and the TypeScript project. + +### Setting Custom Path for TypeScript Test Vectors + +By default, the test vector generators will look for the TypeScript project at a relative path. If your project structure is different, you can customize the TypeScript project path using an environment variable: + +```bash +# Generate shared secret test vectors with custom TypeScript project path +TS_PROJECT_TEST_VECTORS_PATH=/path/to/taco-web/packages/shared/test/fixtures/ cargo test -p nucypher-core --test generate_shared_secret_vectors +``` + [crate-image]: https://img.shields.io/crates/v/nucypher-core.svg [crate-link]: https://crates.io/crates/nucypher-core [docs-image]: https://docs.rs/nucypher-core/badge.svg diff --git a/nucypher-core/src/dkg.rs b/nucypher-core/src/dkg.rs index 43fd127..a3e6243 100644 --- a/nucypher-core/src/dkg.rs +++ b/nucypher-core/src/dkg.rs @@ -64,7 +64,10 @@ impl fmt::Display for DecryptionError { type NonceSize = ::NonceSize; -fn encrypt_with_shared_secret( +/// Encrypt data using the provided shared secret. +/// +/// The ciphertext consists of a randomly generated nonce followed by the encrypted data. +pub fn encrypt_with_shared_secret( shared_secret: &SessionSharedSecret, plaintext: &[u8], ) -> Result, EncryptionError> { @@ -79,7 +82,10 @@ fn encrypt_with_shared_secret( Ok(result.into_boxed_slice()) } -fn decrypt_with_shared_secret( +/// Decrypt data using the provided shared secret. +/// +/// The ciphertext is expected to start with a nonce, followed by the encrypted data. +pub fn decrypt_with_shared_secret( shared_secret: &SessionSharedSecret, ciphertext: &[u8], ) -> Result, DecryptionError> { @@ -138,6 +144,31 @@ pub mod session { Self { derived_bytes } } + /// Create a shared secret directly from raw bytes for testing purposes. + /// + /// This bypasses the normal key derivation process and should only be used for + /// testing with known byte vectors. + #[cfg(test)] + pub fn from_bytes(bytes: &[u8]) -> Self { + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes[0..32]); + Self { + derived_bytes: array, + } + } + + /// Create a shared secret directly from raw bytes for test vectors. + /// + /// This is a public API only intended for use in test vectors. It bypasses + /// the normal key derivation process to allow for deterministic tests. + pub fn from_test_vector(bytes: &[u8]) -> Self { + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes[0..32]); + Self { + derived_bytes: array, + } + } + /// View this shared secret as a byte array. pub fn as_bytes(&self) -> &[u8; 32] { &self.derived_bytes diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index da0fb1a..081ba5f 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -32,6 +32,7 @@ pub use access_control::{encrypt_for_dkg, AccessControlPolicy, AuthenticatedData pub use address::Address; pub use conditions::{Conditions, Context}; pub use dkg::{ + decrypt_with_shared_secret, encrypt_with_shared_secret, session::{SessionSecretFactory, SessionSharedSecret, SessionStaticKey, SessionStaticSecret}, DecryptionError, EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, EncryptionError, ThresholdDecryptionRequest, ThresholdDecryptionResponse, diff --git a/nucypher-core/tests/fixtures/shared-secret-vectors.json b/nucypher-core/tests/fixtures/shared-secret-vectors.json new file mode 100644 index 0000000..61bbdaa --- /dev/null +++ b/nucypher-core/tests/fixtures/shared-secret-vectors.json @@ -0,0 +1,50 @@ +{ + "test_vectors": [ + { + "id": "vector1", + "description": "Fixed nonce encryption with known plaintext", + "shared_secret": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + ], + "plaintext": "This is a test message", + "fixed_nonce": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "expected_ciphertext": [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 208, 43, 66, 141, 143, 213, 241, + 114, 65, 40, 4, 220, 55, 110, 74, 157, 194, 128, 148, 134, 200, 181, 72, + 212, 7, 218, 216, 247, 94, 232, 77, 109, 158, 146, 164, 44, 74 + ] + }, + { + "id": "vector2", + "description": "Fixed nonce encryption with alternative values", + "shared_secret": [ + 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, + 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 + ], + "plaintext": "", + "fixed_nonce": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "expected_ciphertext": [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 135, 38, 197, 242, 213, 184, 24, + 114, 168, 100, 147, 239, 82, 50, 170, 161 + ] + }, + { + "id": "vector3", + "description": "Rust-generated ciphertext for TypeScript compatibility check", + "shared_secret": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + ], + "plaintext": "This is a message encrypted by the Rust implementation", + "expected_ciphertext": [ + 100, 196, 241, 204, 119, 208, 40, 31, 29, 138, 199, 108, 168, 89, 32, + 208, 157, 93, 80, 94, 60, 106, 168, 38, 62, 206, 143, 135, 56, 12, 142, + 15, 156, 9, 227, 26, 97, 154, 204, 11, 179, 200, 3, 180, 203, 200, 221, + 190, 122, 118, 154, 147, 180, 170, 1, 23, 168, 86, 226, 78, 224, 176, + 218, 159, 127, 47, 55, 28, 193, 231, 42, 92, 118, 112, 139, 150, 73, + 205, 155, 90, 66, 172 + ] + } + ] +} diff --git a/nucypher-core/tests/generate_shared_secret_vectors.rs b/nucypher-core/tests/generate_shared_secret_vectors.rs new file mode 100644 index 0000000..94ab3f6 --- /dev/null +++ b/nucypher-core/tests/generate_shared_secret_vectors.rs @@ -0,0 +1,290 @@ +// Include the test_utils module at the crate root level +mod test_utils { + pub mod cross_impl_test_vectors; +} + +#[cfg(test)] +mod tests { + // Import the test utilities for TypeScript project paths + use crate::test_utils::cross_impl_test_vectors; + + // json file name + const JSON_FILE_NAME: &str = "shared-secret-vectors.json"; + + use chacha20poly1305::{Key, KeyInit, Nonce}; + use nucypher_core::{ + decrypt_with_shared_secret, encrypt_with_shared_secret, SessionSharedSecret, + }; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + use std::fs; + use std::path::Path; + + // Structure that matches the JSON test vector format + #[derive(Serialize, Deserialize)] + struct TestVector { + id: String, + description: String, + shared_secret: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + plaintext: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fixed_nonce: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + expected_ciphertext: Option>, + } + + #[derive(Serialize, Deserialize)] + struct TestVectors { + test_vectors: Vec, + } + + // Wrapper for encrypt_with_shared_secret that takes raw bytes for compatibility with test vectors + fn test_encrypt_with_shared_secret( + shared_secret: &[u8], + plaintext: &[u8], + ) -> Result, Box> { + // Create SessionSharedSecret from raw bytes + let shared_secret_obj = SessionSharedSecret::from_test_vector(shared_secret); + + // Use the actual library function + let result = encrypt_with_shared_secret(&shared_secret_obj, plaintext).map_err(|e| { + Box::::from(format!("Encryption error: {:?}", e)) + })?; + + Ok(result.to_vec()) + } + + // Wrapper for decrypt_with_shared_secret that takes raw bytes for compatibility with test vectors + fn test_decrypt_with_shared_secret( + shared_secret: &[u8], + ciphertext: &[u8], + ) -> Result, Box> { + // Create SessionSharedSecret from raw bytes + let shared_secret_obj = SessionSharedSecret::from_test_vector(shared_secret); + + // Use the actual library function + let result = decrypt_with_shared_secret(&shared_secret_obj, ciphertext).map_err(|e| { + Box::::from(format!("Decryption error: {:?}", e)) + })?; + + Ok(result.to_vec()) + } + + // Function to encrypt with fixed nonce for test vector generation + fn encrypt_with_fixed_nonce( + shared_secret: &[u8], + plaintext: &[u8], + fixed_nonce: &[u8], + ) -> Result, Box> { + use chacha20poly1305::aead::Aead; + + // Create key from shared secret bytes + let key = Key::from_slice(shared_secret); + let cipher = chacha20poly1305::ChaCha20Poly1305::new(key); + + // Use the provided fixed nonce + let nonce = Nonce::from_slice(fixed_nonce); + + // Encrypt the plaintext with the fixed nonce + let ciphertext = cipher + .encrypt(nonce, plaintext.as_ref()) + .map_err(|_| "Encryption failed: plaintext too large")?; + + // Format the result as nonce + ciphertext, matching the library format + let mut result = fixed_nonce.to_vec(); + result.extend(ciphertext); + + Ok(result) + } + + #[test] + fn generate_test_vectors() { + println!("Generating encryption test vectors for TypeScript compatibility..."); + + // Define test vectors directly as TestVector structs + let mut test_vectors = Vec::new(); + + // Vector 1: Known plaintext + fixed nonce -> expected ciphertext + let shared_secret1: Vec = (0..32).collect(); + let plaintext1 = "This is a test message"; + let fixed_nonce1: Vec = vec![0; 12]; // 12 zeros + + println!("Processing vector1 with fixed nonce"); + let mut vector1 = TestVector { + id: "vector1".to_string(), + description: "Fixed nonce encryption with known plaintext".to_string(), + shared_secret: shared_secret1.clone(), + plaintext: Some(plaintext1.to_string()), + fixed_nonce: Some(fixed_nonce1.clone()), + expected_ciphertext: None, + }; + + // Generate ciphertext with fixed nonce for vector1 + match encrypt_with_fixed_nonce(&shared_secret1, plaintext1.as_bytes(), &fixed_nonce1) { + Ok(ciphertext) => { + vector1.expected_ciphertext = Some(ciphertext); + println!(" ✓ Successfully generated ciphertext with fixed nonce"); + } + Err(e) => { + eprintln!("Error encrypting vector1: {}", e); + } + } + test_vectors.push(vector1); + + // Vector 2: Known plaintext + fixed nonce -> expected ciphertext (different values) + let shared_secret2: Vec = (0..32).rev().collect(); // Reversed range + let plaintext2 = ""; // Empty plaintext for testing empty message encryption + let fixed_nonce2: Vec = vec![1; 12]; // 12 ones + + println!("Processing vector2 with fixed nonce"); + let mut vector2 = TestVector { + id: "vector2".to_string(), + description: "Fixed nonce encryption with alternative values".to_string(), + shared_secret: shared_secret2.clone(), + plaintext: Some(plaintext2.to_string()), + fixed_nonce: Some(fixed_nonce2.clone()), + expected_ciphertext: None, + }; + + // Generate ciphertext with fixed nonce for vector2 + match encrypt_with_fixed_nonce(&shared_secret2, plaintext2.as_bytes(), &fixed_nonce2) { + Ok(ciphertext) => { + vector2.expected_ciphertext = Some(ciphertext); + println!(" ✓ Successfully generated ciphertext with fixed nonce"); + } + Err(e) => { + eprintln!("Error encrypting vector2: {}", e); + } + } + test_vectors.push(vector2); + + // Vector 3: For Rust-generated ciphertext compatibility using normal encryption + let shared_secret3: Vec = (0..32).collect(); + let plaintext3 = "This is a message encrypted by the Rust implementation"; + + println!("Creating vector3 with Rust-generated ciphertext"); + let mut vector3 = TestVector { + id: "vector3".to_string(), + description: "Rust-generated ciphertext for TypeScript compatibility check".to_string(), + shared_secret: shared_secret3.clone(), + plaintext: Some(plaintext3.to_string()), + fixed_nonce: None, + expected_ciphertext: None, + }; + + // Standard encryption with random nonce + match test_encrypt_with_shared_secret(&shared_secret3, plaintext3.as_bytes()) { + Ok(ciphertext) => { + vector3.expected_ciphertext = Some(ciphertext); + println!(" ✓ Successfully generated ciphertext for vector3"); + } + Err(e) => { + eprintln!("Error encrypting vector3: {}", e); + } + } + test_vectors.push(vector3); + + // Create the complete test vectors structure + let test_vectors_output = TestVectors { test_vectors }; + + // Format the JSON with pretty-printing + let formatted_json = serde_json::to_string_pretty(&test_vectors_output).unwrap(); + + // Path for the output file + let output_dir = Path::new("tests/fixtures"); + let output_file = output_dir.join("shared-secret-vectors.json"); + + // Create directory if it doesn't exist + fs::create_dir_all(output_dir).expect("Failed to create output directory"); + + // Save to file in the Rust project first + fs::write(&output_file, &formatted_json).expect("Unable to write test vectors file"); + println!("Test vectors saved to {:?}", output_file); + + // Write test vectors to TypeScript project + cross_impl_test_vectors::write_to_ts_project_path( + JSON_FILE_NAME, + &formatted_json, + &output_file, + ); + + // Verify vectors by decrypting + // Parse the JSON string back to a Value before passing to verify_test_vectors + let test_vectors_value: Value = serde_json::from_str(&formatted_json).unwrap(); + verify_test_vectors(&test_vectors_value); + + println!("\nInstructions for manually copying test vectors:"); + println!("1. The file has been saved to: {:?}", output_file); + println!( + "2. Copy it to: {}", + cross_impl_test_vectors::get_ts_project_path(JSON_FILE_NAME) + ); + println!("3. Run the TypeScript tests to verify compatibility"); + } + + fn verify_test_vectors(test_vectors_json: &Value) { + println!("\nVerifying test vectors..."); + let vectors = test_vectors_json["test_vectors"].as_array().unwrap(); + + for vector in vectors { + let id = vector["id"].as_str().unwrap(); + let shared_secret = vector["shared_secret"].as_array().unwrap(); + let shared_secret_bytes: Vec = shared_secret + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + println!("Verifying vector: {}", id); + + // Verify vector3 with rust-generated ciphertext + if id == "vector3" && vector["expected_ciphertext"].is_array() { + let ciphertext_json = vector["expected_ciphertext"].as_array().unwrap(); + let ciphertext: Vec = ciphertext_json + .iter() + .map(|v| v.as_u64().unwrap() as u8) + .collect(); + + let plaintext = vector["plaintext"].as_str().unwrap(); + + match test_decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + Ok(decrypted) => { + let decrypted_str = String::from_utf8_lossy(&decrypted); + assert_eq!( + decrypted_str, plaintext, + "Decryption mismatch for vector {}", + id + ); + println!(" ✓ Successfully verified rust-generated ciphertext"); + } + Err(e) => { + panic!("Failed to decrypt rust-generated ciphertext: {:?}", e); + } + } + continue; + } + + // Verify vectors with expected_ciphertext + if let Some(ciphertext_hex) = vector["expected_ciphertext"].as_str() { + let ciphertext = hex::decode(ciphertext_hex).unwrap(); + let plaintext = vector["plaintext"].as_str().unwrap().as_bytes(); + + match test_decrypt_with_shared_secret(&shared_secret_bytes, &ciphertext) { + Ok(decrypted) => { + assert_eq!( + &decrypted, plaintext, + "Decryption mismatch for vector {}", + id + ); + println!(" ✓ Successfully verified expected_ciphertext"); + } + Err(e) => { + panic!("Failed to decrypt expected_ciphertext: {:?}", e); + } + } + } + } + + println!("All test vectors verified successfully!"); + } +} diff --git a/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs b/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs new file mode 100644 index 0000000..df32328 --- /dev/null +++ b/nucypher-core/tests/test_utils/cross_impl_test_vectors.rs @@ -0,0 +1,64 @@ +// Utility functions for handling TypeScript project paths in tests +use std::path::Path; + +/// Default base path to TypeScript project test fixtures +pub const DEFAULT_TS_PROJECT_TEST_VECTORS_PATH: &str = + "../../taco-web/packages/shared/test/fixtures/"; + +/// Environment variable name for TypeScript project path +pub const TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR: &str = "TS_PROJECT_TEST_VECTORS_PATH"; + +/// Get the TypeScript project path combining the base directory with the specified file name +/// +/// Checks for the environment variable `TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR` first, +/// and falls back to the default path if not set. +pub fn get_ts_project_path(file_name: &str) -> String { + // Check for environment variable + match std::env::var(TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR) { + Ok(path) if !path.is_empty() => { + println!( + "Using custom path from {} environment variable", + TS_PROJECT_TEST_VECTORS_PATH_ENV_VAR + ); + format!("{}{}", path, file_name) + } + _ => { + println!("Using default TypeScript project path"); + format!("{}{}", DEFAULT_TS_PROJECT_TEST_VECTORS_PATH, file_name) + } + } +} + +/// Write test vectors to TypeScript project path +/// +/// Returns true if successful, false otherwise +/// +/// If writing fails, it will print manual copy instructions +/// using the provided source file path +pub fn write_to_ts_project_path(file_name: &str, content: &str, source_file_path: &Path) -> bool { + let ts_project_path = get_ts_project_path(file_name); + println!("TypeScript project path: {}", ts_project_path); + + let ts_path = Path::new(&ts_project_path); + match std::fs::write(ts_path, content) { + Ok(()) => { + println!( + "✓ Test vectors successfully copied to TypeScript project: {:?}", + ts_path + ); + true + } + Err(e) => { + println!( + "Note: Couldn't copy to TypeScript project ({:?}): {}", + ts_path, e + ); + // Add manual copy instructions + println!( + "You'll need to manually copy the file from {:?} to the TypeScript project.", + source_file_path + ); + false + } + } +}