diff --git a/.cargo/config.toml b/.cargo/config.toml index ddff440..05d9c7e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ -[build] +[target.wasm32-wasip1] +runner = "wasmtime" + +[target.'cfg(not(target_family = "wasm")))'] rustflags = ["-C", "target-cpu=native"] diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index 9dac301..1828384 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -19,10 +19,13 @@ jobs: # for available targets. os: [ ubuntu-latest, # x86-64 - ubuntu-24.04-arm, # aarch64 - macos-latest, # aarch64 + ubuntu-24.04-arm, # arm64 + macos-13, # x86_64 - windows-latest # x86_64 + macos-latest, # arm64 + + windows-latest, # x86_64 + windows-11-arm # arm64 ] steps: @@ -35,5 +38,13 @@ jobs: - name: Build and Test on ${{ matrix.os }} run: cargo test --profile test-release - - name: Run Example on ${{ matrix.os }} - run: cargo run --example kw_pir --profile optimized + - name: Build ChalametPIR Client Crate for wasm32 target + run: | + rustup target add wasm32-unknown-unknown + cargo build -p chalametpir_client --target wasm32-unknown-unknown --features wasm --no-default-features --profile test-release + + - name: Run ChalametPIR Common Crate Tests on wasm32 target + run: | + rustup target add wasm32-wasip1 + cargo install wasmtime-cli --locked + cargo test -p chalametpir_common --target wasm32-wasip1 --features wasm --no-default-features --profile test-release diff --git a/Cargo.toml b/Cargo.toml index 3f14c29..8b8fd6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,48 +1,11 @@ -[package] -name = "chalamet_pir" -version = "0.6.0" -edition = "2024" -resolver = "2" -rust-version = "1.85.0" -authors = ["Anjan Roy "] -description = "Simple, Stateful, Single-Server Private Information Retrieval for Key-Value Databases" -readme = "README.md" -repository = "https://github.com/itzmeanjan/ChalametPIR.git" -license = "MPL-2.0" -keywords = [ - "priv-info-retrieval", - "lwe-pir", - "frodo-pir", - "chalamet-pir", - "gpu", +[workspace] +members = [ + "chalametpir_client", + "chalametpir_common", + "chalametpir_server", + "integrations", ] -categories = ["cryptography", "data-structures", "concurrency"] - -[dependencies] -turboshake = "=0.4.1" -rayon = "=1.10.0" -rand = "=0.9.1" -rand_chacha = "=0.9.0" -vulkano = { version = "=0.35.1", optional = true } -vulkano-shaders = { version = "=0.35.0", optional = true } - -[dev-dependencies] -test-case = "=3.3.1" -divan = "=0.1.21" -unicode-xid = "=0.2.6" - -[[bench]] -name = "offline_phase" -harness = false - -[[bench]] -name = "online_phase" -harness = false -required-features = ["mutate_internal_client_state"] - -[features] -mutate_internal_client_state = [] -gpu = ["dep:vulkano", "dep:vulkano-shaders"] +resolver = "3" [profile.optimized] inherits = "release" diff --git a/README.md b/README.md index db2f9d0..5a8dd04 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,14 @@ ChalametPIR allows a client to retrieve a specific value from a key-value databa The protocol has two participants: **Server:** +Implemented by [chalametpir_server](./chalametpir_server) crate. + * **`setup`:** Initializes the server with a seed, a key-value database, generating a public matrix, a hint matrix, and a Binary Fuse Filter (3-wise XOR or 4-wise XOR, configurable at compile time). It returns serialized representations of the hint matrix and filter parameters. This phase can be completed offline and is completely client-agnostic. But it is very compute-intensive, which is why this library allows you to offload expensive matrix multiplication and transposition to a GPU, gated behind the opt-in `gpu` feature. For large key-value databases (e.g., with >= $2^{18}$ entries), I recommend enabling the `gpu` feature, as it can significantly reduce the cost of the server-setup phase. * **`respond`:** Processes a client's encrypted query, returning an encrypted response vector. **Client:** +Implemented by [chalametpir_client](./chalametpir_client) crate. PIR clients can run in-browser, by enabling `wasm` (Web Assembly) feature. + * **`setup`:** Initializes the client using the seed, serialized hint matrix and filter parameters received from the server. * **`query`:** Generates an encrypted PIR query for a given key, which can be sent to server. * **`process_response`:** Decrypts the server's response and extracts the requested value. @@ -82,7 +86,7 @@ rustc 1.85.1 (e71f9a9a9 2025-01-27) If you plan to offload server-setup to GPU, you need to install Vulkan drivers and library for your target setup. I followed https://linux.how2shout.com/how-to-install-vulkan-on-ubuntu-24-04-or-22-04-lts-linux on Ubuntu 24.04 LTS, with Nvidia GPUs - it was easy to setup. ## Testing -The `chalamet_pir` library includes comprehensive tests to ensure functional correctness. +The ChalametPIR library includes comprehensive tests to ensure functional correctness. - **Property -based Tests:** Verify individual components: matrix operations (multiplication, addition), Binary Fuse Filter construction (3-wise and 4-wise XOR, including bits-per-entry (BPE) validation), and serialization/deserialization of `Matrix` and `BinaryFuseFilter`. - **Integration Tests:** Cover end-to-end PIR protocol functionality: key-value database encoding/decoding (parameterized by database size, key/value lengths, and filter arity), and client-server interaction to verify correct value retrieval without key disclosure (tested with both 3-wise and 4-wise XOR filters). @@ -96,6 +100,12 @@ cargo test --profile test-release # For testing if offloading to GPU works as expected. cargo test --features gpu --profile test-release + +# Testing chalametpir-common lib crate on web assembly target, using `wasmtime`. +# Note, chalametpir-server lib crate is not wasm friendly. +rustup target add wasm32-unknown-unknown wasm32-wasip1 +cargo install wasmtime-cli --locked +cargo test -p chalametpir_common --target wasm32-wasip1 --features wasm --no-default-features --profile test-release ``` @@ -105,9 +115,8 @@ Performance benchmarks are included to evaluate the efficiency of the PIR scheme To run the benchmarks, execute the following command from the root of the project: ```bash -# For benchmarking the online phase of the PIR, -# you need to enable feature `mutate_internal_client_state`. -cargo bench --features mutate_internal_client_state --profile optimized +# Run all benchmarks. +cargo bench --profile optimized # For benchmarking only the server-setup phase, offloaded to the GPU. cargo bench --features gpu --profile optimized --bench offline_phase -q server_setup @@ -129,135 +138,37 @@ cargo bench --features gpu --profile optimized --bench offline_phase -q server_s > More about AWS EC2 instances @ https://aws.amazon.com/ec2/instance-types. ## Usage -First, add this library crate as a dependency in your Cargo.toml file. - -```toml -[dependencies] -chalamet_pir = "=0.6.0" -# Or, if you want to offload server-setup to a GPU. -# chalamet_pir = { version = "=0.6.0", features = ["gpu"] } -rand = "=0.9.0" -rand_chacha = "=0.9.0" -``` -Then, let's code a very simple keyword PIR scheme: - -```rust -use chalamet_pir::{client::Client, server::Server, SEED_BYTE_LEN}; -use rand::prelude::*; -use rand_chacha::ChaCha8Rng; -use std::collections::HashMap; - -fn main() { - // Example database (replace with your own) - let mut db: HashMap<&[u8], &[u8]> = HashMap::new(); - db.insert(b"apple", b"red"); - db.insert(b"banana", b"yellow"); - - // Server setup (offline phase) - let mut rng = ChaCha8Rng::from_os_rng(); - let mut seed_μ = [0u8; SEED_BYTE_LEN]; // You'll want to generate a cryptographically secure random seed - rng.fill_bytes(&mut seed_μ); - - let (server, hint_bytes, filter_param_bytes) = Server::setup::<3>(&seed_μ, db.clone()).expect("Server setup failed"); - - // Client setup (offline phase) - let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); - - // Client query (online phase) - let key = b"banana"; - if let Ok(query) = client.query(key) { - // Send `query` to the server - - // Server response (online phase) - let response = server.respond(&query).expect("Server failed to respond"); - - // Client processes the response (online phase) - if let Ok(value) = client.process_response(key, &response) { - println!("Retrieved value: '{}'", String::from_utf8_lossy(&value)); // Should print "yellow" - } else { - println!("Failed to retrieve value."); - } - } else { - println!("Failed to generate query."); - } -} -``` +- For understanding how PIR server library crate `chalametpir_server` can be used, read [this](./chalametpir_server/README.md). +- While for using PIR client library crate `chalametpir_client`, read [this](./chalametpir_client/README.md). + +The constant parameter `ARITY` (3 or 4) in `Server::setup` controls the type of Binary Fuse Filter used to encode the KV database, which affects size of the query vector and the encoded database dimensions, stored in-memory server-side. -The constant parameter `ARITY` (3 or 4) in `Server::setup` controls the type of Binary Fuse Filter used to encode the KV database, which affects size of the query vector and the encoded database dimensions, stored in-memory server-side. This implementation should allow you to run PIR queries on a KV database with at max 2^42 (~4 trillion) number of entries. +> [!IMPORTANT] +> This implementation should allow you to run PIR queries on a KV database with at max 2^42 (~4 trillion) number of entries. There doesn't exist any limit on how large each key or value needs to be. Keys and values can be of variable length. If values have variable length, database encoder routine pads it to the maximum value byte length present in that key-value database. -I maintain one example [program](./examples/kw_pir.rs) which demonstrates usage of the ChalametPIR API. +I maintain two example binaries, implementing PIR server and client execution flow. ```bash -cargo run --example kw_pir --profile optimized +# First, issue following command on one terminal window. +$ cargo run --example server --profile optimized + +PIR Server listening @ 127.0.0.1:8080 +New connection from PIR client @ 127.0.0.1:43322 +Sent setup data to PIR client @ 127.0.0.1:43322 +Received query of length 200B, from PIR client @ 127.0.0.1:43322 +Sent response of length 104B, to PIR client @ 127.0.0.1:43322 +... ``` ```bash -# Using 3-wise XOR Binary Fuse Filter -ChalametPIR: -Number of entries in Key-Value Database : 65536 -Size of each key : 8.0B -Size of each value : 4.0B -Arity of Binary Fuse Filter : 3 -Seed size : 32.0B -Hint size : 207.9KB -Filter parameters size : 68.0B -Query size : 304.0KB -Response size : 128.0B - -✅ '64187' maps to 'b', in 274.995µs -⚠️ Random key '112599' is not present in DB -⚠️ Random key '108662' is not present in DB -⚠️ Random key '79395' is not present in DB -⚠️ Random key '72638' is not present in DB -⚠️ Random key '123690' is not present in DB -⚠️ Random key '69344' is not present in DB -⚠️ Random key '69155' is not present in DB -✅ '5918' maps to 'J', in 165.606µs -⚠️ Random key '128484' is not present in DB -⚠️ Random key '79290' is not present in DB -⚠️ Random key '104015' is not present in DB -⚠️ Random key '111256' is not present in DB -⚠️ Random key '124342' is not present in DB -⚠️ Random key '74982' is not present in DB -⚠️ Random key '93082' is not present in DB -✅ '32800' maps to 'b', in 233.29µs -✅ '20236' maps to 'Q', in 233.531µs -✅ '47334' maps to 'p', in 223.548µs -✅ '12225' maps to 'U', in 209.217µs - -# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - -# Using 4-wise XOR Binary Fuse Filter -ChalametPIR: -Number of entries in Key-Value Database : 65536 -Size of each key : 8.0B -Size of each value : 4.0B -Arity of Binary Fuse Filter : 4 -Seed size : 32.0B -Hint size : 207.9KB -Filter parameters size : 68.0B -Query size : 292.0KB -Response size : 128.0B - -✅ '13239' maps to 'T', in 241.21µs -⚠️ Random key '112983' is not present in DB -⚠️ Random key '89821' is not present in DB -✅ '63385' maps to 'I', in 188.06µs -⚠️ Random key '123914' is not present in DB -⚠️ Random key '119919' is not present in DB -⚠️ Random key '72903' is not present in DB -⚠️ Random key '93634' is not present in DB -⚠️ Random key '68582' is not present in DB -✅ '55692' maps to 'n', in 359.112µs -⚠️ Random key '68191' is not present in DB -⚠️ Random key '92762' is not present in DB -✅ '997' maps to 'v', in 302.626µs -⚠️ Random key '123011' is not present in DB -✅ '37638' maps to 'F', in 240.428µs -⚠️ Random key '75802' is not present in DB -⚠️ Random key '80496' is not present in DB -✅ '42586' maps to 'T', in 224.29µs -✅ '25911' maps to 'u', in 250.494µs -✅ '15478' maps to 'S', in 257.656µs +# And then run this command on another terminal window. +$ cargo run --example client --profile optimized + +Connected to PIR server @ 127.0.0.1:8080 +Received setup data from PIR server +Generated query for key: [98, 97, 110, 97, 110, 97] +Sent query of length 200B +Received response of length 104B +Retrieved value: 'yellow' ``` diff --git a/chalametpir_client/Cargo.toml b/chalametpir_client/Cargo.toml new file mode 100644 index 0000000..2686cfb --- /dev/null +++ b/chalametpir_client/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "chalametpir_client" +version = "0.7.0" +edition = "2024" +resolver = "3" +rust-version = "1.85.0" +authors = ["Anjan Roy "] +description = "Client Implementation of ChalametPIR: Simple, Stateful, Single-Server Private Information Retrieval for Key-Value Databases" +readme = "README.md" +repository = "https://github.com/itzmeanjan/ChalametPIR.git" +license = "MPL-2.0" +keywords = [ + "priv-info-retrieval", + "lwe-pir", + "frodo-pir", + "chalamet-pir", + "pir-server", + "key-value-databases", +] +categories = ["cryptography", "data-structures", "concurrency"] + +[dependencies] +chalametpir_common = { path = "../chalametpir_common", version = "=0.7.0", default-features = false } + +[dev-dependencies] +tokio = { version = "=1.45.0", features = ["full"] } + +[features] +wasm = ["chalametpir_common/wasm"] +default = ["chalametpir_common/default"] +mutate_internal_client_state = [] diff --git a/chalametpir_client/README.md b/chalametpir_client/README.md new file mode 100644 index 0000000..d6b6018 --- /dev/null +++ b/chalametpir_client/README.md @@ -0,0 +1,56 @@ +# ChalametPIR Client + +Client Implementation of ChalametPIR: Simple, Stateful, Single-Server Private Information Retrieval for Key-Value Databases. + +This crate provides the client-side implementation for the ChalametPIR protocol. It includes functionality for: + +- Setting up the PIR client with parameters received from the server. +- Generating private information retrieval (PIR) queries for specific keys. +- Processing responses received from the server to recover the desired value. + +Key components: + +- `Client`: The main struct for interacting with the PIR client. It handles query generation and response processing. +- `Query`: Represents a PIR query, containing the secret vector needed to recover the value from the server's response. + +## Usage Example + +Add these dependencies to your `Cargo.toml`: + +```toml +chalametpir_client = "=0.7.0" +``` + +```rust +use chalametpir_client::{Client, SEED_BYTE_LEN}; + +fn main() { + // Assume seed, hint_bytes and filter_param_bytes are received from the PIR server + let seed_μ = [0u8; SEED_BYTE_LEN]; + let hint_bytes = vec![0u8; 0]; + let filter_param_bytes = vec![0u8; 0]; + + match Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes) { + Ok(mut client) => { + let key = b"example_key"; + if let Ok(query) = client.query(key) { + println!("Generated query for key: {:?}", key); + // Send query to PIR server + let response = vec![0u8; 0]; + if let Ok(value) = client.process_response(key, &response) { + println!("Received response {:?}", response); + } + } + } + Err(err) => { + println!("Client setup failed: {}", err); + } + }; +} +``` + +> [!IMPORTANT] +> ChalametPIR clients can run in-browser, by enabling `wasm` feature. + +> [!NOTE] +> More documentation on ChalametPIR [here](../README.md). diff --git a/chalametpir_client/examples/client.rs b/chalametpir_client/examples/client.rs new file mode 100644 index 0000000..bc0ce77 --- /dev/null +++ b/chalametpir_client/examples/client.rs @@ -0,0 +1,76 @@ +use std::error::Error; + +use chalametpir_client::{Client, SEED_BYTE_LEN}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +const SERVER_IP: &str = "127.0.0.1"; +const SERVER_PORT: u16 = 8080; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server_address = format!("{}:{}", SERVER_IP, SERVER_PORT); + + let mut stream = TcpStream::connect(&server_address).await.expect("Failed to connect to PIR server"); + println!("Connected to PIR server @ {}", &server_address); + + // Receive seed from PIR server + let mut seed_μ = [0u8; SEED_BYTE_LEN]; + stream.read_exact(&mut seed_μ).await?; + + // Receive hint from PIR server + let mut hint_len_buf = [0u8; 4]; + stream.read_exact(&mut hint_len_buf).await?; + + let hint_len = u32::from_le_bytes(hint_len_buf) as usize; + + let mut hint_bytes = vec![0u8; hint_len]; + stream.read_exact(&mut hint_bytes).await?; + + // Receive Binary Fuse Filter parameters from PIR server + let mut filter_len_buf = [0u8; 4]; + stream.read_exact(&mut filter_len_buf).await?; + + let filter_len = u32::from_le_bytes(filter_len_buf) as usize; + + let mut filter_param_bytes = vec![0u8; filter_len]; + stream.read_exact(&mut filter_param_bytes).await?; + + println!("Received setup data from PIR server"); + + let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("PIR client setup failed"); + + let key = b"banana"; + if let Ok(query) = client.query(key) { + println!("Generated query for key: {:?}", key); + + // Send query to PIR server + let query_len = query.len() as u32; + stream.write_all(&query_len.to_le_bytes()).await?; + stream.write_all(&query).await?; + + println!("Sent query of length {}B", query_len); + + // Receive response from PIR server + let mut response_len_buf = [0u8; 4]; + stream.read_exact(&mut response_len_buf).await?; + + let response_len = u32::from_le_bytes(response_len_buf) as usize; + + let mut response = vec![0u8; response_len]; + stream.read_exact(&mut response).await?; + + println!("Received response of length {}B", response_len); + + if let Ok(value) = client.process_response(key, &response) { + println!("Retrieved value: '{}'", String::from_utf8_lossy(&value)); + } else { + println!("Failed to retrieve value."); + } + } else { + println!("Failed to generate query."); + } + + Ok(()) +} diff --git a/src/client.rs b/chalametpir_client/src/client.rs similarity index 98% rename from src/client.rs rename to chalametpir_client/src/client.rs index 9db66e2..b097891 100644 --- a/src/client.rs +++ b/chalametpir_client/src/client.rs @@ -1,12 +1,10 @@ -use crate::{ - ChalametPIRError, - pir_internals::{ - binary_fuse_filter::{self, BinaryFuseFilter}, - branch_opt_util, - matrix::Matrix, - params::{HASHED_KEY_BYTE_LEN, LWE_DIMENSION, SEED_BYTE_LEN}, - serialization, - }, +use chalametpir_common::{ + binary_fuse_filter::{self, BinaryFuseFilter}, + branch_opt_util, + error::ChalametPIRError, + matrix::Matrix, + params::{HASHED_KEY_BYTE_LEN, LWE_DIMENSION, SEED_BYTE_LEN}, + serialization, }; use std::collections::HashMap; diff --git a/chalametpir_client/src/lib.rs b/chalametpir_client/src/lib.rs new file mode 100644 index 0000000..e508f76 --- /dev/null +++ b/chalametpir_client/src/lib.rs @@ -0,0 +1,65 @@ +//! ChalametPIR: A Rust library implementation of the Chalamet **P**rivate **I**nformation **R**etrieval (PIR) protocol, described in . +//! +//! This crate provides a Rust library implementation of the ChalametPIR Client, enabling efficient and private lookup of value associated with a key, from encoded key-value database, stored PIR server-side. +//! It leverages Binary Fuse Filters for efficient indexing and storage of key-value database and LWE-based encryption for data confidentiality. +//! +//! +//! ## Features +//! +//! * **Secure Private Information Retrieval:** Allows PIR clients to retrieve value from a PIR server without disclosing corresponding key. Server learns neither the value nor the queried key. +//! * **Error Handling:** Comprehensive error handling to catch and report issues during setup, query generation, and response processing. +//! +//! ## Usage +//! +//! This crate is designed to be used in conjunction with other crates which provides communication mechanism between PIR clients and server. +//! See examples. You'll typically interact with the `Client` struct to setup PIR client using server provided seed, hint and filter params. Also for +//! creating PIR queries and processing response received from PIR server. There is also a `Query` struct, which generally holds +//! the LWE secret vector for a specific queried key and uses it to decode server response. +//! +//! +//! Add this crate as dependency to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! chalametpir_client = "=0.7.0" +//! +//! # Or, if you want to run the client on wasm environments. +//! # chalametpir_client = { version = "=0.7.0", features = "wasm", default-features = false} +//! ``` +//! +//! Then, you can use it in your code: +//! +//! ```rust +//! use chalametpir_client::{Client, SEED_BYTE_LEN}; +//! +//! fn main() { +//! // Assume seed, hint_bytes and filter_param_bytes are received from the PIR server +//! let seed_μ = [0u8; SEED_BYTE_LEN]; +//! let hint_bytes = vec![0u8; 0]; +//! let filter_param_bytes = vec![0u8; 0]; +//! +//! match Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes) { +//! Ok(mut client) => { +//! let key = b"example_key"; +//! if let Ok(query) = client.query(key) { +//! println!("Generated query for key: {:?}", key); +//! // Send query to PIR server +//! let response = vec![0u8; 0]; +//! if let Ok(value) = client.process_response(key, &response) { +//! println!("Received response {:?}", response); +//! } +//! } +//! } +//! Err(err) => { +//! println!("Client setup failed: {}", err); +//! } +//! }; +//! } +//! ``` +//! +//! For more see README in ChalametPIR repository @ . + +mod client; + +pub use chalametpir_common::{error::ChalametPIRError, params::SEED_BYTE_LEN}; +pub use client::{Client, Query}; diff --git a/chalametpir_common/Cargo.toml b/chalametpir_common/Cargo.toml new file mode 100644 index 0000000..d71cce7 --- /dev/null +++ b/chalametpir_common/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "chalametpir_common" +version = "0.7.0" +edition = "2024" +resolver = "3" +rust-version = "1.85.0" +authors = ["Anjan Roy "] +description = "Common Utilities for ChalametPIR: Private Information Retrieval for Key-Value Databases" +readme = "../README.md" +repository = "https://github.com/itzmeanjan/ChalametPIR.git" +license = "MPL-2.0" +keywords = [ + "priv-info-retrieval", + "lwe-pir", + "frodo-pir", + "chalamet-pir", + "gpu", +] +categories = ["cryptography", "data-structures", "concurrency"] + +[dependencies] +turboshake = "=0.4.1" +rand = { version = "=0.9.1", optional = true } +rand_chacha = { version = "=0.9.0", optional = true } +rayon = "=1.10.0" +tinyrand = { version = "=0.5.0", optional = true } + +[dev-dependencies] +test-case = "=3.3.1" + +[features] +wasm = ["dep:tinyrand"] +default = ["dep:rand", "dep:rand_chacha"] diff --git a/chalametpir_common/README.md b/chalametpir_common/README.md new file mode 100644 index 0000000..ccdc720 --- /dev/null +++ b/chalametpir_common/README.md @@ -0,0 +1,19 @@ +# ChalametPIR Common + +Common Utilities for ChalametPIR: Private Information Retrieval for Key-Value Databases. + +This crate provides common utilities and data structures used by both the client and server implementations of the ChalametPIR protocol. It includes: + +- Matrix operations: A `Matrix` struct for efficient matrix manipulation, including multiplication and addition. +- Binary Fuse Filter: Implementation of Binary Fuse Filters for encoding key-value databases. +- Error handling: A unified `ChalametPIRError` enum for reporting errors across the client and server. +- Parameters: Constants and parameters used in the ChalametPIR protocol. + +> [!NOTE] +> This crate is not supposed to be used by you on its own, rather it is a common dependency of both `chalametpir_server` and `chalametpir_client` crates. + +> [!IMPORTANT] +> This crate is Web Assembly environment friendly. So you can use it in wasm family of targets, by enabling `wasm` feature. + +> [!NOTE] +> More documentation on ChalametPIR [here](../README.md). diff --git a/src/pir_internals/binary_fuse_filter.rs b/chalametpir_common/src/binary_fuse_filter.rs similarity index 97% rename from src/pir_internals/binary_fuse_filter.rs rename to chalametpir_common/src/binary_fuse_filter.rs index d6d7c75..7b8a6f3 100644 --- a/src/pir_internals/binary_fuse_filter.rs +++ b/chalametpir_common/src/binary_fuse_filter.rs @@ -1,10 +1,16 @@ -use super::{error::ChalametPIRError, params}; -use crate::pir_internals::branch_opt_util; -use rand::prelude::*; -use rand_chacha::ChaCha20Rng; use std::collections::HashMap; + +use super::{branch_opt_util, error::ChalametPIRError, params}; use turboshake::TurboShake128; +#[cfg(feature = "wasm")] +use tinyrand::{Rand, StdRand}; + +#[cfg(not(feature = "wasm"))] +use rand::prelude::*; +#[cfg(not(feature = "wasm"))] +use rand_chacha::ChaCha20Rng; + #[derive(Clone, Debug)] pub struct BinaryFuseFilter { pub seed: [u8; 32], @@ -87,10 +93,17 @@ impl BinaryFuseFilter { let mut ultimate_size = 0; let mut seed = [0u8; 32]; + + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha20Rng::from_os_rng(); for _ in 0..max_attempt_count { + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); for (idx, val) in start_pos.iter_mut().enumerate() { *val = (((idx as u64) * (db_size as u64)) >> block_bits) as usize; @@ -289,10 +302,17 @@ impl BinaryFuseFilter { let mut ultimate_size = 0; let mut seed = [0u8; 32]; + + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha20Rng::from_os_rng(); for _ in 0..max_attempt_count { + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); for (idx, val) in start_pos.iter_mut().enumerate().take(start_pos_len) { *val = (((idx as u64) * (db_size as u64)) >> block_bits) as usize; @@ -435,7 +455,6 @@ impl BinaryFuseFilter { )) } - #[cfg(test)] pub fn bits_per_entry(&self) -> f64 { ((self.num_fingerprints as f64) * (self.mat_elem_bit_len as f64)) / (self.filter_size as f64) } diff --git a/chalametpir_common/src/branch_opt_util.rs b/chalametpir_common/src/branch_opt_util.rs new file mode 100644 index 0000000..39e9ce6 --- /dev/null +++ b/chalametpir_common/src/branch_opt_util.rs @@ -0,0 +1,22 @@ +/// Rust equivalent for C++'s compiler hint on which branch is more/ less likely https://en.cppreference.com/w/cpp/language/attributes/likely. +/// Collects inspiration from https://users.rust-lang.org/t/compiler-hint-for-unlikely-likely-for-if-branches/62102/4. + +#[inline] +#[cold] +pub fn cold() {} + +#[inline] +pub fn likely(b: bool) -> bool { + if !b { + cold() + } + b +} + +#[inline] +pub fn unlikely(b: bool) -> bool { + if b { + cold() + } + b +} diff --git a/src/pir_internals/error.rs b/chalametpir_common/src/error.rs similarity index 100% rename from src/pir_internals/error.rs rename to chalametpir_common/src/error.rs diff --git a/chalametpir_common/src/lib.rs b/chalametpir_common/src/lib.rs new file mode 100644 index 0000000..2bcbc9c --- /dev/null +++ b/chalametpir_common/src/lib.rs @@ -0,0 +1,7 @@ +pub mod binary_fuse_filter; +pub mod branch_opt_util; +pub mod error; +pub mod matrix; +pub mod params; +pub mod serialization; +pub mod utils; diff --git a/src/pir_internals/matrix.rs b/chalametpir_common/src/matrix.rs similarity index 92% rename from src/pir_internals/matrix.rs rename to chalametpir_common/src/matrix.rs index 915b57b..4c8e418 100644 --- a/src/pir_internals/matrix.rs +++ b/chalametpir_common/src/matrix.rs @@ -1,22 +1,28 @@ -use crate::pir_internals::{ +use std::{ + collections::HashMap, + ops::{Add, Index, IndexMut, Mul}, +}; + +use crate::{ binary_fuse_filter, branch_opt_util, + error::ChalametPIRError, params::{HASHED_KEY_BYTE_LEN, MAX_CIPHER_TEXT_BIT_LEN, MIN_CIPHER_TEXT_BIT_LEN, SEED_BYTE_LEN}, serialization, }; -use rand::prelude::*; -use rand_chacha::ChaCha8Rng; + use rayon::prelude::*; -use std::{ - collections::HashMap, - ops::{Add, Index, IndexMut, Mul}, -}; use turboshake::TurboShake128; +#[cfg(not(feature = "wasm"))] +use rand::prelude::*; +#[cfg(not(feature = "wasm"))] +use rand_chacha::ChaCha8Rng; +#[cfg(feature = "wasm")] +use tinyrand::{Rand, StdRand}; + #[cfg(test)] use std::ops::Neg; -use super::error::ChalametPIRError; - #[derive(Clone, Debug, PartialEq)] pub struct Matrix { rows: u32, @@ -90,7 +96,7 @@ impl Matrix { } pub fn row_wise_compress(self, mat_elem_bit_len: usize) -> Result { - if branch_opt_util::unlikely(!(mat_elem_bit_len >= MIN_CIPHER_TEXT_BIT_LEN && mat_elem_bit_len <= MAX_CIPHER_TEXT_BIT_LEN)) { + if branch_opt_util::unlikely(!(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN).contains(&mat_elem_bit_len)) { return Err(ChalametPIRError::ImpossibleEncodedDBMatrixElementBitLength); } @@ -571,19 +577,32 @@ impl Matrix { const TERNARY_INTERVAL_SIZE: u32 = (u32::MAX - 2) / 3; const TERNARY_REJECTION_SAMPLING_MAX: u32 = TERNARY_INTERVAL_SIZE * 3; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); + let mut vec = Matrix::new(rows, cols)?; let num_elems = vec.num_elems(); let mut elem_idx = 0; while branch_opt_util::likely(elem_idx < num_elems) { + #[cfg(not(feature = "wasm"))] let mut val = rng.random::(); + #[cfg(feature = "wasm")] + let mut val = rng.next_u32(); + #[cfg(not(feature = "wasm"))] while branch_opt_util::unlikely(val > TERNARY_REJECTION_SAMPLING_MAX) { val = rng.random::(); } + #[cfg(feature = "wasm")] + while branch_opt_util::unlikely(val > TERNARY_REJECTION_SAMPLING_MAX) { + val = rng.next_u32(); + } + let ternary = if val <= TERNARY_INTERVAL_SIZE { 0 } else if val > TERNARY_INTERVAL_SIZE && val <= 2 * TERNARY_INTERVAL_SIZE { @@ -1092,55 +1111,27 @@ impl Neg for &Matrix { } #[cfg(test)] -pub mod test { - use crate::{ - SEED_BYTE_LEN, - pir_internals::{ - binary_fuse_filter::BinaryFuseFilter, - error::ChalametPIRError, - matrix::Matrix, - params::{MAX_CIPHER_TEXT_BIT_LEN, MIN_CIPHER_TEXT_BIT_LEN, SERVER_SETUP_MAX_ATTEMPT_COUNT}, - }, - }; - use rand::prelude::*; - use rand_chacha::ChaCha8Rng; +mod test { use std::collections::HashMap; - use test_case::test_case; - /// Generates a random key-value database with the requested number of key-value pairs. - /// - /// # Arguments - /// - /// * `num_kv_pairs` - The number of key-value pairs to generate. - /// - /// # Returns - /// - /// * `HashMap, Vec>` - A HashMap containing the generated key-value pairs. - /// The keys and values are randomly generated byte arrays with lengths between fixed minimum and maximum values. - pub fn generate_random_kv_database(num_kv_pairs: usize) -> HashMap, Vec> { - const MIN_KEY_BYTE_LEN: usize = 16; - const MAX_KEY_BYTE_LEN: usize = 32; - const MIN_VALUE_BYTE_LEN: usize = 1; - const MAX_VALUE_BYTE_LEN: usize = 512; - - let mut kv = HashMap::with_capacity(num_kv_pairs); - let mut rng = ChaCha8Rng::from_os_rng(); - - for _ in 0..num_kv_pairs { - let key_byte_len = rng.random_range(MIN_KEY_BYTE_LEN..=MAX_KEY_BYTE_LEN); - let value_byte_len = rng.random_range(MIN_VALUE_BYTE_LEN..=MAX_VALUE_BYTE_LEN); - - let mut key = vec![0u8; key_byte_len]; - let mut value = vec![0u8; value_byte_len]; + use crate::{ + binary_fuse_filter::BinaryFuseFilter, + error::ChalametPIRError, + matrix::Matrix, + params::SEED_BYTE_LEN, + params::{MAX_CIPHER_TEXT_BIT_LEN, MIN_CIPHER_TEXT_BIT_LEN, SERVER_SETUP_MAX_ATTEMPT_COUNT}, + utils::generate_random_kv_database, + }; - rng.fill_bytes(&mut key); - rng.fill_bytes(&mut value); + use test_case::test_case; - kv.insert(key, value); - } + #[cfg(feature = "wasm")] + use tinyrand::{Rand, RandRange, StdRand}; - kv - } + #[cfg(not(feature = "wasm"))] + use rand::prelude::*; + #[cfg(not(feature = "wasm"))] + use rand_chacha::ChaCha8Rng; #[test] fn encode_kv_database_using_3_wise_xor_filter_and_recover_values() { @@ -1149,13 +1140,23 @@ pub mod test { const MIN_NUM_KV_PAIRS: usize = 1usize << 8; const MAX_NUM_KV_PAIRS: usize = 1usize << 16; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); const NUM_TEST_ITERATIONS: usize = 100; let mut test_iter = 0; while test_iter < NUM_TEST_ITERATIONS { + #[cfg(feature = "wasm")] + let num_kv_pairs_in_db = rng.next_range(MIN_NUM_KV_PAIRS..(MAX_NUM_KV_PAIRS + 1)); + #[cfg(not(feature = "wasm"))] let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + + #[cfg(feature = "wasm")] + let mat_elem_bit_len = rng.next_range(MIN_CIPHER_TEXT_BIT_LEN..(MAX_CIPHER_TEXT_BIT_LEN + 1)); + #[cfg(not(feature = "wasm"))] let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); let kv_db = generate_random_kv_database(num_kv_pairs_in_db); @@ -1188,13 +1189,23 @@ pub mod test { const MIN_NUM_KV_PAIRS: usize = 1usize << 8; const MAX_NUM_KV_PAIRS: usize = 1usize << 16; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); const NUM_TEST_ITERATIONS: usize = 100; let mut test_iter = 0; while test_iter < NUM_TEST_ITERATIONS { + #[cfg(feature = "wasm")] + let num_kv_pairs_in_db = rng.next_range(MIN_NUM_KV_PAIRS..(MAX_NUM_KV_PAIRS + 1)); + #[cfg(not(feature = "wasm"))] let num_kv_pairs_in_db = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + + #[cfg(feature = "wasm")] + let mat_elem_bit_len = rng.next_range(MIN_CIPHER_TEXT_BIT_LEN..(MAX_CIPHER_TEXT_BIT_LEN + 1)); + #[cfg(not(feature = "wasm"))] let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); let kv_db = generate_random_kv_database(num_kv_pairs_in_db); @@ -1267,15 +1278,29 @@ pub mod test { const MIN_MATRIX_DIM: u32 = 1; const MAX_MATRIX_DIM: u32 = 1024; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); let mut seed = [0u8; SEED_BYTE_LEN]; + + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); let mut current_attempt_count = 0; while current_attempt_count < NUM_ATTEMPT_MATRIX_MULTIPLICATIONS { + #[cfg(not(feature = "wasm"))] let num_rows = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_rows = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); + + #[cfg(not(feature = "wasm"))] let num_cols = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_cols = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); let matrix_a = Matrix::generate_from_seed(num_rows, num_cols, &seed).expect("Matrix must be generated from seed"); let matrix_i = Matrix::identity(num_cols).expect("Identity matrix must be created"); @@ -1297,19 +1322,37 @@ pub mod test { const MIN_ROW_VECTOR_DIM: u32 = 1; const MAX_ROW_VECTOR_DIM: u32 = 1024; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); let mut seed = [0u8; SEED_BYTE_LEN]; + + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); let mut current_attempt_count = 0; while current_attempt_count < NUM_ATTEMPT_VECTOR_MATRIX_MULTIPLICATIONS { let vec_num_rows = 1; + #[cfg(not(feature = "wasm"))] let vec_num_cols = rng.random_range(MIN_ROW_VECTOR_DIM..=MAX_ROW_VECTOR_DIM); + #[cfg(feature = "wasm")] + let vec_num_cols = rng.next_range(MIN_ROW_VECTOR_DIM..(MAX_ROW_VECTOR_DIM + 1)); + let mat_num_rows = vec_num_cols; + #[cfg(not(feature = "wasm"))] let mat_num_cols = rng.random_range(MIN_ROW_VECTOR_DIM..=MAX_ROW_VECTOR_DIM); + #[cfg(feature = "wasm")] + let mat_num_cols = rng.next_range(MIN_ROW_VECTOR_DIM..(MAX_ROW_VECTOR_DIM + 1)); + let mat_num_elems = (mat_num_rows * mat_num_cols) as usize; + #[cfg(not(feature = "wasm"))] let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); + #[cfg(feature = "wasm")] + let mat_elem_bit_len = rng.next_range(MIN_CIPHER_TEXT_BIT_LEN..(MAX_CIPHER_TEXT_BIT_LEN + 1)); let row_vector = Matrix::generate_from_seed(vec_num_rows, vec_num_cols, &seed).expect("Row vector must be generated from seed"); let all_ones = Matrix::from_values(mat_num_rows, mat_num_cols, vec![1; mat_num_elems]).expect("Matrix of ones must be created"); @@ -1338,15 +1381,29 @@ pub mod test { const MIN_MATRIX_DIM: u32 = 1; const MAX_MATRIX_DIM: u32 = 1024; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); let mut seed = [0u8; SEED_BYTE_LEN]; + + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); let mut current_attempt_count = 0; while current_attempt_count < NUM_ATTEMPT_MATRIX_ADDITIONS { + #[cfg(not(feature = "wasm"))] let num_rows = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_rows = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); + + #[cfg(not(feature = "wasm"))] let num_cols = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_cols = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); let matrix_a = Matrix::generate_from_seed(num_rows, num_cols, &seed).expect("Matrix must be generated from seed"); let matrix_neg_a = (-&matrix_a).expect("Must be able to negate matrix"); @@ -1394,15 +1451,29 @@ pub mod test { const MIN_MATRIX_DIM: u32 = 1; const MAX_MATRIX_DIM: u32 = 1024; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); let mut seed = [0u8; SEED_BYTE_LEN]; + + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut seed); + #[cfg(feature = "wasm")] + seed.fill_with(|| rng.next_u32() as u8); let mut current_attempt_count = 0; while current_attempt_count < NUM_ATTEMPT_MATRIX_SERIALIZATIONS { + #[cfg(not(feature = "wasm"))] let num_rows = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_rows = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); + + #[cfg(not(feature = "wasm"))] let num_cols = rng.random_range(MIN_MATRIX_DIM..=MAX_MATRIX_DIM); + #[cfg(feature = "wasm")] + let num_cols = rng.next_range(MIN_MATRIX_DIM..(MAX_MATRIX_DIM + 1)); let matrix_a = Matrix::generate_from_seed(num_rows, num_cols, &seed).expect("Matrix must be generated from seed"); let matrix_a_bytes = matrix_a.to_bytes(); @@ -1453,14 +1524,24 @@ pub mod test { const MIN_NUM_KV_PAIRS: usize = 1_000; const MAX_NUM_KV_PAIRS: usize = 10_000; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); const NUM_TEST_ITERATIONS: usize = 100; let mut test_iter = 0; while test_iter < NUM_TEST_ITERATIONS { + #[cfg(not(feature = "wasm"))] let num_kv_pairs = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + #[cfg(feature = "wasm")] + let num_kv_pairs = rng.next_range(MIN_NUM_KV_PAIRS..(MAX_NUM_KV_PAIRS + 1)); + + #[cfg(not(feature = "wasm"))] let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); + #[cfg(feature = "wasm")] + let mat_elem_bit_len = rng.next_range(MIN_CIPHER_TEXT_BIT_LEN..(MAX_CIPHER_TEXT_BIT_LEN + 1)); let kv_db = generate_random_kv_database(num_kv_pairs); let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); @@ -1486,14 +1567,24 @@ pub mod test { const MIN_NUM_KV_PAIRS: usize = 1_000; const MAX_NUM_KV_PAIRS: usize = 10_000; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); const NUM_TEST_ITERATIONS: usize = 100; let mut test_iter = 0; while test_iter < NUM_TEST_ITERATIONS { + #[cfg(not(feature = "wasm"))] let num_kv_pairs = rng.random_range(MIN_NUM_KV_PAIRS..=MAX_NUM_KV_PAIRS); + #[cfg(feature = "wasm")] + let num_kv_pairs = rng.next_range(MIN_NUM_KV_PAIRS..(MAX_NUM_KV_PAIRS + 1)); + + #[cfg(not(feature = "wasm"))] let mat_elem_bit_len = rng.random_range(MIN_CIPHER_TEXT_BIT_LEN..=MAX_CIPHER_TEXT_BIT_LEN); + #[cfg(feature = "wasm")] + let mat_elem_bit_len = rng.next_range(MIN_CIPHER_TEXT_BIT_LEN..(MAX_CIPHER_TEXT_BIT_LEN + 1)); let kv_db = generate_random_kv_database(num_kv_pairs); let kv_db_as_ref = kv_db.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); diff --git a/src/pir_internals/params.rs b/chalametpir_common/src/params.rs similarity index 100% rename from src/pir_internals/params.rs rename to chalametpir_common/src/params.rs diff --git a/src/pir_internals/serialization.rs b/chalametpir_common/src/serialization.rs similarity index 94% rename from src/pir_internals/serialization.rs rename to chalametpir_common/src/serialization.rs index 2686fda..7bc6794 100644 --- a/src/pir_internals/serialization.rs +++ b/chalametpir_common/src/serialization.rs @@ -1,4 +1,4 @@ -use super::{branch_opt_util, error::ChalametPIRError, params}; +use crate::{branch_opt_util, error::ChalametPIRError, params}; use std::cmp::min; use turboshake::TurboShake128; @@ -227,13 +227,19 @@ pub fn u64_to_le_bytes(word: u64, bytes: &mut [u8]) { #[cfg(test)] mod test { - use crate::pir_internals::{ + use crate::{ params, serialization::{decode_kv_from_row, encode_kv_as_row}, }; + use turboshake::TurboShake128; + + #[cfg(feature = "wasm")] + use tinyrand::{Rand, StdRand}; + + #[cfg(not(feature = "wasm"))] use rand::prelude::*; + #[cfg(not(feature = "wasm"))] use rand_chacha::ChaCha8Rng; - use turboshake::TurboShake128; #[test] fn encode_kv_as_row_and_recover() { @@ -246,6 +252,9 @@ mod test { const MIN_MAT_ELEM_BIT_LEN: usize = 7; const MAX_MAT_ELEM_BIT_LEN: usize = 11; + #[cfg(feature = "wasm")] + let mut rng = StdRand::default(); + #[cfg(not(feature = "wasm"))] let mut rng = ChaCha8Rng::from_os_rng(); for key_byte_len in MIN_KEY_BYTE_LEN..=MAX_KEY_BYTE_LEN { @@ -254,8 +263,15 @@ mod test { let mut key = vec![0u8; key_byte_len]; let mut value = vec![0u8; value_byte_len]; + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut key); + #[cfg(feature = "wasm")] + key.fill_with(|| rng.next_u32() as u8); + + #[cfg(not(feature = "wasm"))] rng.fill_bytes(&mut value); + #[cfg(feature = "wasm")] + value.fill_with(|| rng.next_u32() as u8); let hashed_key = { let mut hasher = TurboShake128::default(); diff --git a/chalametpir_common/src/utils.rs b/chalametpir_common/src/utils.rs new file mode 100644 index 0000000..4b3f465 --- /dev/null +++ b/chalametpir_common/src/utils.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +#[cfg(feature = "wasm")] +use tinyrand::{Rand, RandRange, StdRand}; + +#[cfg(not(feature = "wasm"))] +use rand::prelude::*; +#[cfg(not(feature = "wasm"))] +use rand_chacha::ChaCha8Rng; + +/// Generates a random key-value database with the requested number of key-value pairs. +/// +/// # Arguments +/// +/// * `num_kv_pairs` - The number of key-value pairs to generate. +/// +/// # Returns +/// +/// * `HashMap, Vec>` - A HashMap containing the generated key-value pairs. +/// The keys and values are randomly generated byte arrays with lengths between fixed minimum and maximum values. +#[cfg(not(feature = "wasm"))] +pub fn generate_random_kv_database(num_kv_pairs: usize) -> HashMap, Vec> { + const MIN_KEY_BYTE_LEN: usize = 16; + const MAX_KEY_BYTE_LEN: usize = 32; + const MIN_VALUE_BYTE_LEN: usize = 1; + const MAX_VALUE_BYTE_LEN: usize = 512; + + let mut kv = HashMap::with_capacity(num_kv_pairs); + let mut rng = ChaCha8Rng::from_os_rng(); + + for _ in 0..num_kv_pairs { + let key_byte_len = rng.random_range(MIN_KEY_BYTE_LEN..=MAX_KEY_BYTE_LEN); + let value_byte_len = rng.random_range(MIN_VALUE_BYTE_LEN..=MAX_VALUE_BYTE_LEN); + + let mut key = vec![0u8; key_byte_len]; + let mut value = vec![0u8; value_byte_len]; + + rng.fill_bytes(&mut key); + rng.fill_bytes(&mut value); + + kv.insert(key, value); + } + + kv +} + +/// Generates a random key-value database with the requested number of key-value pairs. +/// +/// # Arguments +/// +/// * `num_kv_pairs` - The number of key-value pairs to generate. +/// +/// # Returns +/// +/// * `HashMap, Vec>` - A HashMap containing the generated key-value pairs. +/// The keys and values are randomly generated byte arrays with lengths between fixed minimum and maximum values. +#[cfg(feature = "wasm")] +pub fn generate_random_kv_database(num_kv_pairs: usize) -> HashMap, Vec> { + const MIN_KEY_BYTE_LEN: usize = 16; + const MAX_KEY_BYTE_LEN: usize = 32; + const MIN_VALUE_BYTE_LEN: usize = 1; + const MAX_VALUE_BYTE_LEN: usize = 512; + + let mut kv = HashMap::with_capacity(num_kv_pairs); + let mut rng = StdRand::default(); + + for _ in 0..num_kv_pairs { + let key_byte_len = rng.next_range(MIN_KEY_BYTE_LEN..(MAX_KEY_BYTE_LEN + 1)); + let value_byte_len = rng.next_range(MIN_VALUE_BYTE_LEN..(MAX_VALUE_BYTE_LEN + 1)); + + let key = (0..key_byte_len).map(|_| rng.next_u32() as u8).collect::>(); + let value = (0..value_byte_len).map(|_| rng.next_u32() as u8).collect::>(); + + kv.insert(key, value); + } + + kv +} diff --git a/chalametpir_server/Cargo.toml b/chalametpir_server/Cargo.toml new file mode 100644 index 0000000..01bf2ed --- /dev/null +++ b/chalametpir_server/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "chalametpir_server" +version = "0.7.0" +edition = "2024" +resolver = "3" +rust-version = "1.85.0" +authors = ["Anjan Roy "] +description = "Server Implementation of ChalametPIR: Simple, Stateful, Single-Server Private Information Retrieval for Key-Value Databases" +readme = "README.md" +repository = "https://github.com/itzmeanjan/ChalametPIR.git" +license = "MPL-2.0" +keywords = [ + "priv-info-retrieval", + "lwe-pir", + "frodo-pir", + "chalamet-pir", + "gpu", + "pir-server", + "key-value-databases", +] +categories = ["cryptography", "data-structures", "concurrency"] + +[dependencies] +chalametpir_common = { path = "../chalametpir_common", version = "=0.7.0" } +vulkano = { version = "=0.35.1", optional = true } +vulkano-shaders = { version = "=0.35.0", optional = true } + +[dev-dependencies] +rand = "=0.9.1" +rand_chacha = "=0.9.0" +tokio = { version = "=1.45.0", features = ["full"] } + +[features] +gpu = ["dep:vulkano", "dep:vulkano-shaders"] diff --git a/chalametpir_server/README.md b/chalametpir_server/README.md new file mode 100644 index 0000000..b45d3dd --- /dev/null +++ b/chalametpir_server/README.md @@ -0,0 +1,68 @@ +# ChalametPIR Server + +Server Implementation of ChalametPIR: Simple, Stateful, Single-Server Private Information Retrieval for Key-Value Databases. + +This crate provides the server-side implementation for the ChalametPIR protocol. It includes functionality for: + +- Setting up the PIR server with a key-value database. +- Responding to PIR queries from clients s.t. the server itself doesn't learn what the client looked up. + +Key components: + +- `Server`: The main struct for handling PIR requests. It contains the encoded database and methods for responding to client queries. + +## Usage Example + +Add these dependencies to your `Cargo.toml`: + +```toml +rand = "=0.9.1" +rand_chacha = "=0.9.0" +chalametpir_server = "=0.7.0" +``` + +```rust +use std::collections::HashMap; + +use chalametpir_server::{SEED_BYTE_LEN, Server}; + +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; + +fn main() { + // Can be either 3 or 4, denoting usage of 3-wise or 4-wise xor binary fuse filter for PIR server setup. + const ARITY: u32 = 3; + + let mut rng = ChaCha8Rng::from_os_rng(); + let mut seed_μ = [0u8; SEED_BYTE_LEN]; + rng.fill_bytes(&mut seed_μ); + + let mut db: HashMap<&[u8], &[u8]> = HashMap::new(); + db.insert(b"key1", b"value1"); + db.insert(b"key2", b"value2"); + + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, db).expect("Server setup failed"); + + // Start handling client PIR queries + loop { + // First send seed, hint and filter params to PIR client + // so that it can set itself up. + + // Assume query_bytes is received from the client + let query_bytes = vec![0u8; 0]; + + if let Ok(response) = server.respond(&query_bytes) { + // Send the response to the client... + println!("Generated response of size: {} bytes", response.len()); + } + + break; + } +} +``` + +> [!IMPORTANT] +> This crate allows you to offload compute-heavy server-setup phase to a GPU, if you enable `gpu` feature. + +> [!NOTE] +> More documentation on ChalametPIR [here](../README.md). diff --git a/chalametpir_server/examples/server.rs b/chalametpir_server/examples/server.rs new file mode 100644 index 0000000..1484d13 --- /dev/null +++ b/chalametpir_server/examples/server.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; +use std::{collections::HashMap, error::Error}; + +use chalametpir_server::{SEED_BYTE_LEN, Server}; + +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +const ARITY_OF_BINARY_FUSE_FILTER: u32 = 3; +const HOST_IP: &str = "127.0.0.1"; +const HOST_PORT: u16 = 8080; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server_address = format!("{}:{}", HOST_IP, HOST_PORT); + + let listener = TcpListener::bind(&server_address).await.expect("Failed to setup TCP listener for PIR server"); + println!("PIR Server listening @ {}", &server_address); + + let mut rng = ChaCha8Rng::from_os_rng(); + let mut seed_μ = [0u8; SEED_BYTE_LEN]; + rng.fill_bytes(&mut seed_μ); + + let mut db: HashMap<&[u8], &[u8]> = HashMap::new(); + db.insert(b"apple", b"red"); + db.insert(b"banana", b"yellow"); + db.insert(b"grape", b"purple"); + db.insert(b"orange", b"orange"); + db.insert(b"lemon", b"yellow"); + db.insert(b"blueberry", b"blue"); + db.insert(b"kiwi", b"brown"); + db.insert(b"watermelon", b"green"); + db.insert(b"strawberry", b"red"); + db.insert(b"peach", b"pink"); + db.insert(b"pineapple", b"yellow"); + db.insert(b"cherry", b"red"); + db.insert(b"avocado", b"green"); + db.insert(b"plum", b"purple"); + db.insert(b"cantaloupe", b"orange"); + + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, db).expect("PIR server setup failed"); + + let arced_server = Arc::new(server); + let arced_hint = Arc::new(hint_bytes); + let arced_filter_param = Arc::new(filter_param_bytes); + + loop { + let (mut stream, _) = listener.accept().await?; + let peer_address = stream.peer_addr().unwrap(); + println!("New connection from PIR client @ {}", peer_address); + + // Cheap cloning, because they are Arced ! + let cloned_server = arced_server.clone(); + let cloned_hint = arced_hint.clone(); + let cloned_filter_param = arced_filter_param.clone(); + + tokio::spawn(async move { + // Send seed to PIR client + stream.write_all(&seed_μ).await.unwrap(); + + // Send hint to PIR client + let hint_len = cloned_hint.len() as u32; + stream.write_all(&hint_len.to_le_bytes()).await.unwrap(); + stream.write_all(&cloned_hint).await.unwrap(); + + // Send Binary Fuse Filter parameters to PIR client + let filter_len = cloned_filter_param.len() as u32; + stream.write_all(&filter_len.to_le_bytes()).await.unwrap(); + stream.write_all(&cloned_filter_param).await.unwrap(); + + println!("Sent setup data to PIR client @ {}", peer_address); + + // Receive query from PIR client + let mut query_len_buf = [0u8; 4]; + stream.read_exact(&mut query_len_buf).await.unwrap(); + + let query_len = u32::from_le_bytes(query_len_buf) as usize; + + let mut query = vec![0u8; query_len]; + stream.read_exact(&mut query).await.unwrap(); + + println!("Received query of length {}B, from PIR client @ {}", query_len, peer_address); + let response = cloned_server.respond(&query).expect("PIR server failed to respond"); + + // Send response to PIR client + let response_len = response.len() as u32; + stream.write_all(&response_len.to_le_bytes()).await.unwrap(); + stream.write_all(&response).await.unwrap(); + + println!("Sent response of length {}B, to PIR client @ {}", response_len, peer_address); + }); + } +} diff --git a/shaders/mat_transpose.glsl b/chalametpir_server/shaders/mat_transpose.glsl similarity index 100% rename from shaders/mat_transpose.glsl rename to chalametpir_server/shaders/mat_transpose.glsl diff --git a/shaders/mat_x_mat.glsl b/chalametpir_server/shaders/mat_x_mat.glsl similarity index 100% rename from shaders/mat_x_mat.glsl rename to chalametpir_server/shaders/mat_x_mat.glsl diff --git a/src/pir_internals/gpu.rs b/chalametpir_server/src/gpu/gpu_utils.rs similarity index 99% rename from src/pir_internals/gpu.rs rename to chalametpir_server/src/gpu/gpu_utils.rs index f0629f8..b4e3d44 100644 --- a/src/pir_internals/gpu.rs +++ b/chalametpir_server/src/gpu/gpu_utils.rs @@ -1,20 +1,15 @@ pub use std::sync::Arc; pub use vulkano::{ - buffer::Subbuffer, - command_buffer::allocator::StandardCommandBufferAllocator, - device::{Device, Queue}, - memory::allocator::StandardMemoryAllocator, -}; - -use super::{mat_transpose_shader, mat_x_mat_shader, matrix::Matrix}; -use crate::ChalametPIRError; -use vulkano::{ VulkanLibrary, + buffer::Subbuffer, buffer::{Buffer, BufferCreateInfo, BufferUsage}, + command_buffer::allocator::StandardCommandBufferAllocator, command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, CopyBufferInfo, PrimaryCommandBufferAbstract}, descriptor_set::{DescriptorSet, WriteDescriptorSet, allocator::StandardDescriptorSetAllocator}, + device::{Device, Queue}, device::{DeviceCreateInfo, DeviceExtensions, QueueCreateInfo, QueueFlags, physical::PhysicalDeviceType}, instance::{Instance, InstanceCreateFlags, InstanceCreateInfo}, + memory::allocator::StandardMemoryAllocator, memory::allocator::{AllocationCreateInfo, MemoryTypeFilter}, pipeline::{ ComputePipeline, Pipeline, PipelineBindPoint, PipelineLayout, PipelineShaderStageCreateInfo, compute::ComputePipelineCreateInfo, @@ -23,6 +18,10 @@ use vulkano::{ sync::GpuFuture, }; +use super::{mat_transpose_shader, mat_x_mat_shader}; +use crate::ChalametPIRError; +use chalametpir_common::matrix::Matrix; + pub fn setup_gpu() -> Result<(Arc, Arc, Arc, Arc), ChalametPIRError> { let library = VulkanLibrary::new().map_err(|_| ChalametPIRError::VulkanLibraryNotFound)?; let instance = Instance::new( diff --git a/src/pir_internals/mat_transpose_shader.rs b/chalametpir_server/src/gpu/mat_transpose_shader.rs similarity index 100% rename from src/pir_internals/mat_transpose_shader.rs rename to chalametpir_server/src/gpu/mat_transpose_shader.rs diff --git a/src/pir_internals/mat_x_mat_shader.rs b/chalametpir_server/src/gpu/mat_x_mat_shader.rs similarity index 100% rename from src/pir_internals/mat_x_mat_shader.rs rename to chalametpir_server/src/gpu/mat_x_mat_shader.rs diff --git a/chalametpir_server/src/gpu/mod.rs b/chalametpir_server/src/gpu/mod.rs new file mode 100644 index 0000000..c10759f --- /dev/null +++ b/chalametpir_server/src/gpu/mod.rs @@ -0,0 +1,3 @@ +pub mod gpu_utils; +pub mod mat_transpose_shader; +pub mod mat_x_mat_shader; diff --git a/chalametpir_server/src/lib.rs b/chalametpir_server/src/lib.rs new file mode 100644 index 0000000..23039ed --- /dev/null +++ b/chalametpir_server/src/lib.rs @@ -0,0 +1,81 @@ +//! ChalametPIR: A Rust library implementation of the Chalamet **P**rivate **I**nformation **R**etrieval (PIR) protocol, described in . +//! +//! This crate provides a Rust library implementation of the ChalametPIR Server, enabling efficient and private retrieval of value associated with a key, from encoded key-value database, stored server-side. +//! It leverages Binary Fuse Filters for efficient indexing and storage of key-value database and LWE-based encryption for data confidentiality. +//! +//! +//! ## Features +//! +//! * **Secure Private Information Retrieval:** Allows clients to retrieve value from a PIR server without disclosing corresponding key. Server learns neither the value nor the queried key. +//! * **Error Handling:** Comprehensive error handling to catch and report issues during setup and responding to client queries. +//! * **Flexibility:** Supports both 3-wise and 4-wise XOR Binary Fuse Filters, allowing a choice between trade-offs in client/server computation and communication costs. +//! * **Efficient:** It supports offloading parts of the server-setup phase to a GPU, using Vulkan Compute API, which can drastically reduce time taken to setup PIR server, for large key-value databases. Look for `gpu` feature. +//! +//! ## Usage +//! +//! This crate is designed to be used in conjunction with other crates which provides communication mechanism between PIR clients and server. +//! See examples. You'll typically interact with the `Server` struct to setup PIR server from a key-value database and respond to PIR client queries. +//! +//! Add this crate as dependency to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! chalametpir_server = "=0.7.0" +//! # Or, if you want to offload server-setup to GPU. +//! # chalametpir_server = { version = "=0.7.0", features = ["gpu"] } +//! +//! rand = "=0.9.1" +//! rand_chacha = "=0.9.0" +//! ``` +//! +//! Then, you can use it in your code: +//! +//! ```rust +//! use std::collections::HashMap; +//! +//! use chalametpir_server::{SEED_BYTE_LEN, Server}; +//! +//! use rand::prelude::*; +//! use rand_chacha::ChaCha8Rng; +//! +//! fn main() { +//! // Can be either 3 or 4, denoting usage of 3-wise or 4-wise xor binary fuse filter for PIR server setup. +//! const ARITY: u32 = 3; +//! +//! let mut rng = ChaCha8Rng::from_os_rng(); +//! let mut seed_μ = [0u8; SEED_BYTE_LEN]; +//! rng.fill_bytes(&mut seed_μ); +//! +//! let mut db: HashMap<&[u8], &[u8]> = HashMap::new(); +//! db.insert(b"key1", b"value1"); +//! db.insert(b"key2", b"value2"); +//! +//! let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, db).expect("Server setup failed"); +//! +//! // Start handling client PIR queries +//! loop { +//! // First send seed, hint and filter params to PIR client +//! // so that it can set itself up. +//! +//! // Assume query_bytes is received from the client +//! let query_bytes = vec![0u8; 0]; +//! +//! if let Ok(response) = server.respond(&query_bytes) { +//! // Send the response to the client... +//! println!("Generated response of size: {} bytes", response.len()); +//! } +//! +//! break; +//! } +//! } +//! ``` +//! +//! For more see README in ChalametPIR repository @ . + +#[cfg(feature = "gpu")] +mod gpu; + +mod server; + +pub use chalametpir_common::{error::ChalametPIRError, params::SEED_BYTE_LEN}; +pub use server::Server; diff --git a/src/server.rs b/chalametpir_server/src/server.rs similarity index 93% rename from src/server.rs rename to chalametpir_server/src/server.rs index 89067fc..e8c0e59 100644 --- a/src/server.rs +++ b/chalametpir_server/src/server.rs @@ -1,15 +1,14 @@ -#[cfg(feature = "gpu")] -use crate::pir_internals::gpu; -use crate::{ - ChalametPIRError, - pir_internals::{ - branch_opt_util, - matrix::Matrix, - params::{LWE_DIMENSION, SEED_BYTE_LEN, SERVER_SETUP_MAX_ATTEMPT_COUNT}, - }, +use chalametpir_common::{ + branch_opt_util, + error::ChalametPIRError, + matrix::Matrix, + params::{LWE_DIMENSION, SEED_BYTE_LEN, SERVER_SETUP_MAX_ATTEMPT_COUNT}, }; use std::collections::HashMap; +#[cfg(feature = "gpu")] +use crate::gpu::gpu_utils; + /// Represents the server in the Keyword Private Information Retrieval (PIR) scheme ChalametPIR. /// /// The server stores an encoded database matrix, in transposed form, and then row-wise compressed, to optimize query response time. @@ -115,7 +114,7 @@ impl Server { let pub_mat_a = unsafe { Matrix::generate_from_seed(pub_mat_a_num_rows, pub_mat_a_num_cols, seed_μ).unwrap_unchecked() }; - let (device, queue, mem_alloc, cmd_buf_alloc) = gpu::setup_gpu()?; + let (device, queue, mem_alloc, cmd_buf_alloc) = gpu_utils::setup_gpu()?; let hint_mat_m_num_rows = pub_mat_a_num_rows; let hint_mat_m_num_cols = parsed_db_mat_d.num_cols(); @@ -125,12 +124,12 @@ impl Server { let parsed_db_mat_d_byte_len = parsed_db_mat_d.num_bytes() as u64; let parsed_db_mat_d_wg_count = [parsed_db_mat_d.num_rows().div_ceil(8), parsed_db_mat_d.num_cols().div_ceil(8), 1]; - let pub_mat_a_buf = gpu::transfer_mat_to_device(queue.clone(), mem_alloc.clone(), cmd_buf_alloc.clone(), pub_mat_a)?; - let parsed_db_mat_d_buf = gpu::transfer_mat_to_device(queue.clone(), mem_alloc.clone(), cmd_buf_alloc.clone(), parsed_db_mat_d.clone())?; - let hint_mat_m_buf = gpu::get_empty_host_readable_buffer(mem_alloc.clone(), hint_mat_m_byte_len)?; - let transposed_parsed_db_mat_d_buf = gpu::get_empty_host_readable_buffer(mem_alloc.clone(), parsed_db_mat_d_byte_len)?; + let pub_mat_a_buf = gpu_utils::transfer_mat_to_device(queue.clone(), mem_alloc.clone(), cmd_buf_alloc.clone(), pub_mat_a)?; + let parsed_db_mat_d_buf = gpu_utils::transfer_mat_to_device(queue.clone(), mem_alloc.clone(), cmd_buf_alloc.clone(), parsed_db_mat_d.clone())?; + let hint_mat_m_buf = gpu_utils::get_empty_host_readable_buffer(mem_alloc.clone(), hint_mat_m_byte_len)?; + let transposed_parsed_db_mat_d_buf = gpu_utils::get_empty_host_readable_buffer(mem_alloc.clone(), parsed_db_mat_d_byte_len)?; - gpu::mat_x_mat( + gpu_utils::mat_x_mat( device.clone(), queue.clone(), cmd_buf_alloc.clone(), @@ -140,7 +139,7 @@ impl Server { hint_mat_m_wg_count, )?; - gpu::mat_transpose( + gpu_utils::mat_transpose( device.clone(), queue.clone(), cmd_buf_alloc.clone(), diff --git a/examples/kw_pir.rs b/examples/kw_pir.rs deleted file mode 100644 index 903b90d..0000000 --- a/examples/kw_pir.rs +++ /dev/null @@ -1,136 +0,0 @@ -use chalamet_pir::{SEED_BYTE_LEN, client::Client, server::Server}; -use rand::prelude::*; -use rand_chacha::ChaCha8Rng; -use std::collections::HashMap; -use std::time::Instant; -use unicode_xid::UnicodeXID; - -/// Generates a toy Key-Value database with a specified number of entries. -/// Each key is a usize, and each value is a randomly chosen Unicode character -/// that is either ASCII graphic, alphanumeric, and a valid Unicode identifier start. -/// -/// # Arguments -/// -/// * `rng` - A mutable reference to a ChaCha8Rng random number generator. This is used to generate random values. -/// -/// # Returns -/// -/// A HashMap containing the generated Key-Value pairs. -fn make_toy_kv_db(rng: &mut ChaCha8Rng) -> HashMap { - const NUM_DB_ENTRIES: usize = u16::MAX as usize + 1; - - (0..NUM_DB_ENTRIES) - .map(|db_entry_index| { - let c = loop { - let mut buf = [0u8; 4]; - rng.fill_bytes(&mut buf); - - let s = String::from_utf8_lossy(&buf); - if let Some(c) = s.chars().next() { - if (c.is_ascii_graphic() || c.is_alphanumeric()) && c.is_xid_start() { - break c; - } - } - }; - - (db_entry_index, c) - }) - .collect::>() -} - -fn format_bytes(bytes: usize) -> String { - let suffixes = ["B", "KB", "MB", "GB"]; - let mut index = 0; - let mut size = bytes as f64; - - while size >= 1024.0 && index < 3 { - size /= 1024.0; - index += 1; - } - - format!("{:.1}{}", size, suffixes[index]) -} - -fn main() { - const ARITY: u32 = 3; - - let mut rng = ChaCha8Rng::from_os_rng(); - - // Make a sample Key-Value database. - let kv_db = make_toy_kv_db(&mut rng); - let kv_db_as_bytes = kv_db - .iter() - .map(|(k, v)| (k.to_le_bytes(), v.encode_utf8(&mut [0u8; 4]).as_bytes().to_vec())) - .collect::>>(); - let kv_db_as_ref = kv_db_as_bytes.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - - let key_byte_len = std::mem::size_of_val(kv_db.keys().next().unwrap()); - let value_byte_len = std::mem::size_of_val(kv_db.values().next().unwrap()); - - println!("ChalametPIR:"); - println!("Number of entries in Key-Value Database : {}", kv_db.len()); - println!("Size of each key : {}", format_bytes(key_byte_len)); - println!("Size of each value : {}", format_bytes(value_byte_len)); - println!("Arity of Binary Fuse Filter : {}", ARITY); - - // Sample seed for producing public LWE matrix A. - let mut seed_μ = [0u8; SEED_BYTE_LEN]; - rng.fill_bytes(&mut seed_μ); - - // Setup PIR server, for given KV database. - let (server_handle, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_db_as_ref.clone()).expect("Server setup failed"); - - println!("Seed size : {}", format_bytes(seed_μ.len())); - println!("Hint size : {}", format_bytes(hint_bytes.len())); - println!("Filter parameters size : {}", format_bytes(filter_param_bytes.len())); - - // Setup a PIR client, given seed, hint bytes and filter param bytes, received from server. - let mut client_handle = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); - - // Sample n -many random valid/ invalid keys and attempt to query them using PIR scheme. - // See if valid keys can be retrieved successfully. And absent keys can't be retrieved. - - let total_num_keys_to_be_queried = 20; - let mut num_keys_quried = 0; - while num_keys_quried < total_num_keys_to_be_queried { - let random_key = rng.random_range(0..kv_db.len() * 2); - let is_random_key_in_db = kv_db.contains_key(&random_key); - - let key_as_bytes = random_key.to_le_bytes(); - if let Ok(query) = client_handle.query(&key_as_bytes) { - if num_keys_quried == 0 { - println!("Query size : {}", format_bytes(query.len())); - } - - let respond_begin = Instant::now(); - if let Ok(response) = server_handle.respond(query.as_slice()) { - let respond_end = Instant::now(); - - if num_keys_quried == 0 { - println!("Response size : {}\n", format_bytes(response.len())); - } - - if let Ok(received_value_bytes) = client_handle.process_response(key_as_bytes.as_slice(), response.as_slice()) { - assert!(is_random_key_in_db); - let &expected_value = kv_db.get(&random_key).expect("Key must be present in the DB!"); - - let received_value = String::from_utf8_lossy(received_value_bytes.as_slice()).chars().next().unwrap(); - if received_value == expected_value { - println!("✅ '{}' maps to '{}', in {:?}", random_key, received_value, (respond_end - respond_begin)); - } else { - println!("🚫 Didn't receive expected value for key '{}'!", random_key); - } - } else { - assert!(!is_random_key_in_db); - println!("⚠️ Random key '{}' is not present in DB", random_key); - } - } else { - println!("⛔ Failed to receive a response for queried key '{}'", random_key); - } - } else { - println!("⛔ Failed to prepare a query for key '{}'", random_key); - } - - num_keys_quried += 1; - } -} diff --git a/integrations/Cargo.toml b/integrations/Cargo.toml new file mode 100644 index 0000000..03c9bd5 --- /dev/null +++ b/integrations/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "chalametpir_integrations" +version = "0.7.0" +edition = "2024" +resolver = "3" +rust-version = "1.85.0" +authors = ["Anjan Roy "] +description = "Integration tests, benchmarks and examples for ChalametPIR" +repository = "https://github.com/itzmeanjan/ChalametPIR.git" +license = "MPL-2.0" +publish = false + +[dev-dependencies] +test-case = "=3.3.1" +divan = "=0.1.21" +rand = "=0.9.1" +rand_chacha = "=0.9.0" +unicode-xid = "=0.2.6" +chalametpir_common = { path = "../chalametpir_common" } +chalametpir_server = { path = "../chalametpir_server" } +chalametpir_client = { path = "../chalametpir_client", features = [ + "mutate_internal_client_state", +] } + +[[bench]] +name = "offline_phase" +harness = false + +[[bench]] +name = "online_phase" +harness = false +required-features = ["chalametpir_client/mutate_internal_client_state"] diff --git a/benches/offline_phase.rs b/integrations/benches/offline_phase.rs similarity index 80% rename from benches/offline_phase.rs rename to integrations/benches/offline_phase.rs index dc84fcb..8107763 100644 --- a/benches/offline_phase.rs +++ b/integrations/benches/offline_phase.rs @@ -1,8 +1,11 @@ -use chalamet_pir::{client, server}; +use std::{collections::HashMap, time::Duration}; + +use chalametpir_client::Client; +use chalametpir_server::Server; + use divan; use rand::prelude::*; use rand_chacha::ChaCha8Rng; -use std::{collections::HashMap, time::Duration}; fn main() { divan::main(); @@ -60,12 +63,12 @@ fn server_setup(bencher: divan::Bencher, db_config: &DBConfig) let kv = generate_random_kv_database(&mut rng, db_config.db_entry_count, db_config.key_byte_len, db_config.value_byte_len); let kv_as_ref = kv.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - let mut seed_μ = [0u8; chalamet_pir::SEED_BYTE_LEN]; + let mut seed_μ = [0u8; chalametpir_server::SEED_BYTE_LEN]; rng.fill_bytes(&mut seed_μ); bencher .with_inputs(|| (kv_as_ref.clone(), seed_μ)) - .bench_values(|(kv, seed)| server::Server::setup::(divan::black_box(&seed), divan::black_box(kv))); + .bench_values(|(kv, seed)| Server::setup::(divan::black_box(&seed), divan::black_box(kv))); } #[divan::bench(args = ARGS, consts = ARITIES, max_time = Duration::from_secs(300), skip_ext_time = true)] @@ -75,9 +78,9 @@ fn client_setup(bencher: divan::Bencher, db_config: &DBConfig) let kv = generate_random_kv_database(&mut rng, db_config.db_entry_count, db_config.key_byte_len, db_config.value_byte_len); let kv_as_ref = kv.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - let mut seed_μ = [0u8; chalamet_pir::SEED_BYTE_LEN]; + let mut seed_μ = [0u8; chalametpir_server::SEED_BYTE_LEN]; rng.fill_bytes(&mut seed_μ); - let (_, hint_bytes, filter_param_bytes) = server::Server::setup::(&seed_μ, kv_as_ref).expect("Server setup failed"); - bencher.bench(|| client::Client::setup(divan::black_box(&seed_μ), divan::black_box(&hint_bytes), divan::black_box(&filter_param_bytes))); + let (_, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_as_ref).expect("Server setup failed"); + bencher.bench(|| Client::setup(divan::black_box(&seed_μ), divan::black_box(&hint_bytes), divan::black_box(&filter_param_bytes))); } diff --git a/benches/online_phase.rs b/integrations/benches/online_phase.rs similarity index 81% rename from benches/online_phase.rs rename to integrations/benches/online_phase.rs index 61206be..0474f77 100644 --- a/benches/online_phase.rs +++ b/integrations/benches/online_phase.rs @@ -1,8 +1,11 @@ -use chalamet_pir::{client, server}; +use std::{collections::HashMap, time::Duration}; + +use chalametpir_client::Client; +use chalametpir_server::Server; + use divan; use rand::prelude::*; use rand_chacha::ChaCha8Rng; -use std::{collections::HashMap, time::Duration}; fn main() { divan::main(); @@ -60,11 +63,11 @@ fn client_query(bencher: divan::Bencher, db_config: &DBConfig) let kv = generate_random_kv_database(&mut rng, db_config.db_entry_count, db_config.key_byte_len, db_config.value_byte_len); let kv_as_ref = kv.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - let mut seed_μ = [0u8; chalamet_pir::SEED_BYTE_LEN]; + let mut seed_μ = [0u8; chalametpir_server::SEED_BYTE_LEN]; rng.fill_bytes(&mut seed_μ); - let (_, hint_bytes, filter_param_bytes) = server::Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); - let client = client::Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); + let (_, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); + let client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); let (&key, _) = kv_as_ref.iter().last().unwrap(); @@ -81,11 +84,11 @@ fn server_respond(bencher: divan::Bencher, db_config: &DBConfi let kv = generate_random_kv_database(&mut rng, db_config.db_entry_count, db_config.key_byte_len, db_config.value_byte_len); let kv_as_ref = kv.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - let mut seed_μ = [0u8; chalamet_pir::SEED_BYTE_LEN]; + let mut seed_μ = [0u8; chalametpir_server::SEED_BYTE_LEN]; rng.fill_bytes(&mut seed_μ); - let (server, hint_bytes, filter_param_bytes) = server::Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); - let mut client = client::Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); + let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); let (&key, _) = kv_as_ref.iter().last().unwrap(); let query_bytes = client.query(key).unwrap(); @@ -100,11 +103,11 @@ fn client_process_response(bencher: divan::Bencher, db_config: let kv = generate_random_kv_database(&mut rng, db_config.db_entry_count, db_config.key_byte_len, db_config.value_byte_len); let kv_as_ref = kv.iter().map(|(k, v)| (k.as_slice(), v.as_slice())).collect::>(); - let mut seed_μ = [0u8; chalamet_pir::SEED_BYTE_LEN]; + let mut seed_μ = [0u8; chalametpir_server::SEED_BYTE_LEN]; rng.fill_bytes(&mut seed_μ); - let (server, hint_bytes, filter_param_bytes) = server::Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); - let mut client = client::Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); + let (server, hint_bytes, filter_param_bytes) = Server::setup::(&seed_μ, kv_as_ref.clone()).unwrap(); + let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).unwrap(); let (&key, _) = kv_as_ref.iter().last().unwrap(); let query_bytes = client.query(key).unwrap(); diff --git a/integrations/src/lib.rs b/integrations/src/lib.rs new file mode 100644 index 0000000..511ee9c --- /dev/null +++ b/integrations/src/lib.rs @@ -0,0 +1 @@ +mod test_pir; diff --git a/src/test_pir.rs b/integrations/src/test_pir.rs similarity index 97% rename from src/test_pir.rs rename to integrations/src/test_pir.rs index d8e2978..ee31df4 100644 --- a/src/test_pir.rs +++ b/integrations/src/test_pir.rs @@ -1,11 +1,13 @@ #![cfg(test)] -use crate::ChalametPIRError; -use crate::pir_internals::matrix::test::generate_random_kv_database; -use crate::{client::Client, server::Server}; +use std::collections::HashMap; + +use chalametpir_client::Client; +use chalametpir_common::utils::generate_random_kv_database; +use chalametpir_server::{ChalametPIRError, Server}; + use rand::prelude::*; use rand_chacha::ChaCha8Rng; -use std::collections::HashMap; #[test] fn test_keyword_pir_with_3_wise_xor_filter() { diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9c57822..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! ChalametPIR: A Rust library implementation of the Chalamet **P**rivate **I**nformation **R**etrieval (PIR) protocol, described in . -//! -//! This crate provides a Rust library implementation of the ChalametPIR protocol, enabling efficient and private retrieval of value associated with a key, from encoded key-value database, stored server-side. -//! It leverages Binary Fuse Filters for efficient indexing and storage of key-value database and LWE-based encryption for data confidentiality. -//! -//! ## Features -//! -//! * **Secure Private Information Retrieval:** Allows clients to retrieve value from a PIR server without disclosing corresponding key. Server learns neither the value nor the queried key. -//! * **Error Handling:** Comprehensive error handling to catch and report issues during setup, query generation, and response processing. -//! * **Flexibility:** Supports both 3-wise and 4-wise XOR Binary Fuse Filters, allowing a choice between trade-offs in client/server computation and communication costs. -//! * **Efficient:** It supports offloading parts of the server-setup phase to a GPU, using Vulkan Compute API, which can drastically reduce time taken to setup PIR server, for large key-value databases. -//! -//! ## Usage -//! -//! This crate is designed to be used in conjunction with other crates which provides communication mechanism between clients and server. -//! You'll typically interact with the `Client` and `Server` structs to perform/ handle queries and process responses. -//! -//! Add ChalametPIR as dependency to your `Cargo.toml`: -//! -//! ```toml -//! [dependencies] -//! chalametpir = "=0.6.0" -//! # Or, if you want to offload server-setup to GPU. -//! # chalamet_pir = { version = "=0.6.0", features = ["gpu"] } -//! rand = "=0.9.0" -//! rand_chacha = "=0.9.0" -//! ``` -//! -//! Then, you can use it in your code: -//! -//! ```rust -//! use chalamet_pir::{client::Client, server::Server, SEED_BYTE_LEN}; -//! use rand::prelude::*; -//! use rand_chacha::ChaCha8Rng; -//! use std::collections::HashMap; -//! -//! // Example database (replace with your own) -//! let mut db: HashMap<&[u8], &[u8]> = HashMap::new(); -//! db.insert(b"apple", b"red"); -//! db.insert(b"banana", b"yellow"); -//! -//! // Server setup (offline phase) -//! let mut rng = ChaCha8Rng::from_os_rng(); -//! let mut seed_μ = [0u8; SEED_BYTE_LEN]; // You'll want to generate a cryptographically secure random seed -//! rng.fill_bytes(&mut seed_μ); -//! -//! let (server, hint_bytes, filter_param_bytes) = Server::setup::<3>(&seed_μ, db.clone()).expect("Server setup failed"); -//! -//! // Client setup (offline phase) -//! let mut client = Client::setup(&seed_μ, &hint_bytes, &filter_param_bytes).expect("Client setup failed"); -//! -//! // Client query (online phase) -//! let key = b"banana"; -//! if let Ok(query) = client.query(key) { -//! // Send `query` to the server -//! -//! // Server response (online phase) -//! let response = server.respond(&query).expect("Server failed to respond"); -//! -//! // Client processes the response (online phase) -//! if let Ok(value) = client.process_response(key, &response) { -//! assert_eq!(value, b"yellow"); -//! println!("Retrieved value: '{}'", String::from_utf8_lossy(&value)); // Should print "yellow" -//! } else { -//! assert!(false); -//! println!("Failed to retrieve value."); -//! } -//! } else { -//! println!("Failed to generate query."); -//! } -//! ``` -//! -//! ## Modules -//! -//! * `server`: Contains the `Server` struct and associated methods for setting up a PIR server from a key-value database and responding to client queries. -//! * `client`: Contains the `Client` struct and associated methods for generating PIR queries and decoding server responses. -//! -//! For more see README in ChalametPIR repository @ . - -pub use pir_internals::error::ChalametPIRError; -pub use pir_internals::params::SEED_BYTE_LEN; -pub mod client; -pub mod server; - -mod pir_internals; - -mod test_pir; diff --git a/src/pir_internals/branch_opt_util.rs b/src/pir_internals/branch_opt_util.rs deleted file mode 100644 index e2d2a91..0000000 --- a/src/pir_internals/branch_opt_util.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Rust equivalent for C++'s compiler hint on which branch is more/ less likely https://en.cppreference.com/w/cpp/language/attributes/likely. -// Collects inspiration from https://users.rust-lang.org/t/compiler-hint-for-unlikely-likely-for-if-branches/62102/4. - -#[inline] -#[cold] -pub fn cold() {} - -#[inline] -pub fn likely(b: bool) -> bool { - if !b { - cold() - } - b -} - -#[inline] -pub fn unlikely(b: bool) -> bool { - if b { - cold() - } - b -} diff --git a/src/pir_internals/mod.rs b/src/pir_internals/mod.rs deleted file mode 100644 index edad805..0000000 --- a/src/pir_internals/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod binary_fuse_filter; -pub mod branch_opt_util; -pub mod error; -pub mod matrix; -pub mod params; -pub mod serialization; - -#[cfg(feature = "gpu")] -pub mod gpu; -#[cfg(feature = "gpu")] -pub mod mat_transpose_shader; -#[cfg(feature = "gpu")] -pub mod mat_x_mat_shader;