Skip to content

cedoor/squid

Repository files navigation

🦑 Squid

An ergonomic Rust wrapper for Poulpy, making Fully Homomorphic Encryption accessible without sacrificing control.

License CI Status

Poulpy is a low-level, modular toolkit exposing the full machinery of lattice-based homomorphic encryption. That power comes with sharp edges: manual scratch arenas, explicit lifecycle transitions, trait-heavy APIs. squid wraps Poulpy with a smaller, opinionated surface so you can write FHE programs without managing every byte of workspace memory or tracking which representation a ciphertext currently lives in.

Current scope: squid wraps Poulpy's bin_fhe::bdd_arithmetic layer: gate-level FHE on encrypted unsigned integers (u8, u16, u32). This is the only fully exposed end-to-end capability in poulpy-schemes today. The API will expand as Poulpy adds more scheme-level implementations.

Usage

Quick start

use squid::{Context, Params};

fn main() {
    // Demo preset — not a vetted security level (see Params::unsecure docs)
    let mut ctx = Context::new(Params::unsecure());

    // Generate keys (secret key + evaluation key)
    let (sk, ek) = ctx.keygen();

    // Encrypt two 32-bit integers
    let a = ctx.encrypt::<u32>(255, &sk);
    let b = ctx.encrypt::<u32>(30, &sk);

    // Homomorphic addition: computes (a + b) under encryption
    let c = ctx.add(&a, &b, &ek);

    // Decrypt the result
    let result: u32 = ctx.decrypt(&c, &sk);
    assert_eq!(result, 255_u32.wrapping_add(30));
    println!("255 + 30 = {result}");
}

Serialize / deserialize an evaluation key

The evaluation key is public material needed for every homomorphic op. Persist it once and reload it on the server that runs the circuits. The blob is versioned and tied to the Params used at keygen — loading under different parameters returns an io::Error.

use squid::{Context, EvaluationKey, Params};

let mut ctx = Context::new(Params::unsecure());
let (_sk, ek) = ctx.keygen();

// Serialize to a versioned little-endian blob.
let blob: Vec<u8> = ctx.serialize_evaluation_key(&ek).unwrap();
std::fs::write("ek.bin", &blob).unwrap();

// Reload later, under the same Params, into a fresh Context.
let mut ctx = Context::new(Params::unsecure());
let bytes = std::fs::read("ek.bin").unwrap();
let ek: EvaluationKey = ctx.deserialize_evaluation_key(&bytes).unwrap();

Secret keys do not expose binary I/O — persist KeygenSeeds instead.

Serialize / deserialize a ciphertext

Ciphertexts are the wire format for sending encrypted values between parties. The blob records the plaintext bit width and GLWE layout, so mismatched parameters or a wrong T are rejected before any ciphertext bytes are read.

use squid::{Ciphertext, Context, Params};

let mut ctx = Context::new(Params::unsecure());
let (sk, _ek) = ctx.keygen();

let ct = ctx.encrypt::<u32>(42, &sk);
let blob: Vec<u8> = ctx.serialize_ciphertext(&ct).unwrap();

// Reload with the same T and the same Params.
let ct: Ciphertext<u32> = ctx.deserialize_ciphertext(&blob).unwrap();
assert_eq!(ctx.decrypt::<u32>(&ct, &sk), 42);

Deterministic key generation from seeds

Poulpy does not expose a stable wire format for secret keys. To reproduce the same (SecretKey, EvaluationKey) pair across runs or machines, persist the three 32-byte ChaCha8 seeds returned by keygen_with_seeds and rebuild with keygen_from_seeds. Same Params, same backend, same keys.

use squid::{Context, KeygenSeeds, Params};

let mut ctx = Context::new(Params::unsecure());

// OS-random seeds (kept so we can replay keygen).
let (sk, ek, seeds) = ctx.keygen_with_seeds();

// Persist the seeds at the app level — `KeygenSeeds` redacts its Debug output.
let KeygenSeeds { lattice, bdd_mask, bdd_noise } = seeds;
// std::fs::write("seeds.bin", [lattice, bdd_mask, bdd_noise].concat()).unwrap();

// Later: regenerate the same keys deterministically.
let mut ctx = Context::new(Params::unsecure());
let (sk2, ek2) = ctx.keygen_from_seeds(seeds);

// If you only need encrypt/decrypt (no homomorphic ops), the lattice seed alone
// is enough to rebuild the SecretKey — no EvaluationKey produced.
let sk_only = ctx.secret_key_from_lattice_seed(seeds.lattice);

Treat the seeds as secret: anyone holding them can reconstruct sk.

Operations

All operations currently require T = u32 (the only width with compiled BDD circuits in Poulpy). Encrypt and decrypt work for u8, u16, and u32.

Method Description
ctx.add(a, b, ek) Wrapping addition
ctx.sub(a, b, ek) Wrapping subtraction
ctx.and(a, b, ek) Bitwise AND
ctx.or(a, b, ek) Bitwise OR
ctx.xor(a, b, ek) Bitwise XOR
ctx.sll(a, b, ek) Logical left shift
ctx.srl(a, b, ek) Logical right shift
ctx.sra(a, b, ek) Arithmetic right shift
ctx.slt(a, b, ek) Signed less-than
ctx.sltu(a, b, ek) Unsigned less-than

Backends

Feature Backend Notes
(default) FFT64Ref Portable
backend-avx FFT64Avx x86-64, AVX2+FMA (~3–5× vs ref)
RUSTFLAGS="-C target-cpu=native" cargo build --release --features backend-avx

The public API is identical regardless of which backend is selected.

Roadmap

Milestone 1 — Working Foundation: #1

  • Write README with installation, quick start example: #2
  • Set up GitHub Actions (cargo test, cargo clippy, cargo fmt check): #3
  • Release first alpha version: #5
  • Add at least one runnable example in examples/: #7
  • Add tests for all existing ops: #4
  • Add rustdoc comments to all public items: #6
  • Faster tests via fixtures or deterministic keygen: #19

Milestone 2 — Full bin_fhe Coverage: #2

  • Wrap Poulpy's blind selection / retrieval primitives: #8
  • Multi-threaded evaluation: #9
  • Sub-word operations: #10
  • Identity / noise refresh: #11
  • NTT backend: #12
  • Key serialization: #13
  • Revert encrypt workaround once upstream poulpy bug is fixed: #24

Milestone 3 — Developer Experience & Optimization: #3

  • WASM crate: #14
  • Params validation with clear error messages: #15
  • Realistic examples: #16
  • Benchmarks: #17
  • Vetted Params presets: #18
  • Refactor context.rs: #20
  • Add CHANGELOG file: #26
  • #22 — closed: Context no longer keeps a persistent max-sized arena; scratch is allocated per operation from Poulpy’s *_tmp_bytes (supersedes the issue’s “split keygen vs runtime” split).

Design goals

  • Hide scratch management. Callers never allocate or thread scratch buffers.
  • Hide lifecycle transitions. The Standard → Prepared → BDD-eval pipeline is handled internally; users see one coherent Ciphertext<T> type.
  • Explicitly non-production defaults. Params::unsecure() matches Poulpy's bdd_arithmetic example for demos; treat it as unaudited unless you analyse parameters yourself.
  • No magic. Every abstraction is traceable to the underlying Poulpy call. No hidden global state; scratch is sized with Poulpy’s *_tmp_bytes at each operation.
  • Safe defaults. Every user-facing choice has a default that works without configuration. Alternatives are documented with their trade-offs and the conditions under which they should be preferred.

About

A ergonomic Rust wrapper for Poulpy.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages