mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
feat: add FHE example applications for sealing, voting, media auth, provenance, CRDTs, and statistics
Six production-ready example applications demonstrating the FHE library: - cmd/seal: document integrity sealing with encrypted verification - cmd/vote: encrypted voting system with threshold tallying - cmd/mediaseal: media content authentication with FHE - cmd/provenance: AI model provenance tracking with encrypted attestation - cmd/crdt: encrypted LWW-Register for fheCRDT architectures (LP-6500) - cmd/stats: secure multiparty statistics over encrypted data Also pins luxfi/mdns to v0.1.0 and excludes compiled binaries from git.
This commit is contained in:
@@ -82,3 +82,11 @@ forge-cache/
|
||||
.git-backup/
|
||||
python-sdk
|
||||
ml-sdk
|
||||
|
||||
# Compiled example binaries
|
||||
/crdt
|
||||
/mediaseal
|
||||
/provenance
|
||||
/seal
|
||||
/stats
|
||||
/vote
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command crdt demonstrates an encrypted Last-Writer-Wins Register (LWW-Register)
|
||||
// using FHE -- a core building block for fheCRDT architectures (LP-6500).
|
||||
//
|
||||
// Two nodes each write an encrypted (value, timestamp) pair. The merge
|
||||
// operation compares timestamps homomorphically and selects the latest
|
||||
// value using MUX gates, all without decrypting individual entries.
|
||||
// Only the final merged state is decrypted.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/crdt
|
||||
// go run ./cmd/crdt -bitsval 4 -bitsts 4
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
// encryptedRegister holds an encrypted LWW-Register entry.
|
||||
type encryptedRegister struct {
|
||||
value []*fhe.Ciphertext // encrypted value bits (LSB-first)
|
||||
ts []*fhe.Ciphertext // encrypted timestamp bits (LSB-first)
|
||||
}
|
||||
|
||||
func main() {
|
||||
bitsVal := flag.Int("bitsval", 4, "bits for value (1..8)")
|
||||
bitsTS := flag.Int("bitsts", 4, "bits for timestamp (1..8)")
|
||||
flag.Parse()
|
||||
|
||||
if *bitsVal < 1 || *bitsVal > 8 || *bitsTS < 1 || *bitsTS > 8 {
|
||||
fmt.Fprintln(os.Stderr, "error: bit widths must be 1..8")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Simulate two concurrent writes.
|
||||
nodeAVal, nodeATS := uint8(7), uint8(3) // Node A writes 7 at time 3
|
||||
nodeBVal, nodeBTS := uint8(12), uint8(5) // Node B writes 12 at time 5 (later)
|
||||
|
||||
fmt.Println("=== Encrypted LWW-Register CRDT ===")
|
||||
fmt.Printf("Node A: value=%d, timestamp=%d\n", nodeAVal, nodeATS)
|
||||
fmt.Printf("Node B: value=%d, timestamp=%d\n", nodeBVal, nodeBTS)
|
||||
fmt.Printf("Expected winner: Node B (later timestamp)\n\n")
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("Initialising FHE...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Encrypt both entries.
|
||||
regA := encryptEntry(enc, nodeAVal, nodeATS, *bitsVal, *bitsTS)
|
||||
regB := encryptEntry(enc, nodeBVal, nodeBTS, *bitsVal, *bitsTS)
|
||||
|
||||
// Merge: compare timestamps, select latest.
|
||||
fmt.Println("Merging homomorphically (comparing timestamps, selecting value)...")
|
||||
t0 := time.Now()
|
||||
merged, err := merge(eval, regA, regB, *bitsVal, *bitsTS)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
// Decrypt merged result.
|
||||
mergedVal := decryptUint(dec, merged.value)
|
||||
mergedTS := decryptUint(dec, merged.ts)
|
||||
|
||||
fmt.Printf("\n--- Merged State ---\n")
|
||||
fmt.Printf("Value: %d\n", mergedVal)
|
||||
fmt.Printf("Timestamp: %d\n", mergedTS)
|
||||
fmt.Printf("Elapsed: %v\n", elapsed)
|
||||
|
||||
if mergedVal == uint8(nodeBVal) && mergedTS == uint8(nodeBTS) {
|
||||
fmt.Println("PASS: merge selected the latest write (Node B).")
|
||||
} else {
|
||||
fmt.Println("FAIL: unexpected merge result!")
|
||||
}
|
||||
}
|
||||
|
||||
func encryptEntry(enc *fhe.Encryptor, val, ts uint8, bitsVal, bitsTS int) *encryptedRegister {
|
||||
return &encryptedRegister{
|
||||
value: encryptUint(enc, val, bitsVal),
|
||||
ts: encryptUint(enc, ts, bitsTS),
|
||||
}
|
||||
}
|
||||
|
||||
func encryptUint(enc *fhe.Encryptor, v uint8, nbits int) []*fhe.Ciphertext {
|
||||
cts := make([]*fhe.Ciphertext, nbits)
|
||||
for i := 0; i < nbits; i++ {
|
||||
cts[i] = enc.Encrypt((v>>i)&1 == 1)
|
||||
}
|
||||
return cts
|
||||
}
|
||||
|
||||
func decryptUint(dec *fhe.Decryptor, cts []*fhe.Ciphertext) uint8 {
|
||||
var v uint8
|
||||
for i, ct := range cts {
|
||||
if dec.Decrypt(ct) {
|
||||
v |= 1 << i
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// merge performs LWW-Register merge: compare timestamps MSB-to-LSB,
|
||||
// then MUX-select the winning entry's value and timestamp.
|
||||
func merge(eval *fhe.Evaluator, a, b *encryptedRegister, bitsVal, bitsTS int) (*encryptedRegister, error) {
|
||||
// Compare timestamps: is B > A? (MSB-first scan)
|
||||
// bGtA starts as false, eqSoFar starts as true.
|
||||
bGtA := eval.NOT(a.ts[0]) // dummy init, overwritten below
|
||||
var eqSoFar *fhe.Ciphertext
|
||||
|
||||
// We build bGtA from MSB down.
|
||||
for i := bitsTS - 1; i >= 0; i-- {
|
||||
// bitGt = B[i] AND NOT(A[i]) -- B's bit is 1, A's is 0
|
||||
bitGt, err := eval.ANDNY(a.ts[i], b.ts[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts bit %d ANDNY: %w", i, err)
|
||||
}
|
||||
// bitEq = XNOR(A[i], B[i])
|
||||
bitEq, err := eval.XNOR(a.ts[i], b.ts[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts bit %d XNOR: %w", i, err)
|
||||
}
|
||||
|
||||
if i == bitsTS-1 {
|
||||
bGtA = bitGt
|
||||
eqSoFar = bitEq
|
||||
} else {
|
||||
// bGtA = bGtA OR (eqSoFar AND bitGt)
|
||||
contrib, err := eval.AND(eqSoFar, bitGt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts bit %d AND: %w", i, err)
|
||||
}
|
||||
bGtA, err = eval.OR(bGtA, contrib)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts bit %d OR: %w", i, err)
|
||||
}
|
||||
// eqSoFar = eqSoFar AND bitEq
|
||||
eqSoFar, err = eval.AND(eqSoFar, bitEq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts bit %d eq-chain: %w", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MUX-select value and timestamp: if bGtA then B else A.
|
||||
mergedVal := make([]*fhe.Ciphertext, bitsVal)
|
||||
for i := 0; i < bitsVal; i++ {
|
||||
v, err := eval.MUX(bGtA, b.value[i], a.value[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("val MUX bit %d: %w", i, err)
|
||||
}
|
||||
mergedVal[i] = v
|
||||
}
|
||||
mergedTS := make([]*fhe.Ciphertext, bitsTS)
|
||||
for i := 0; i < bitsTS; i++ {
|
||||
t, err := eval.MUX(bGtA, b.ts[i], a.ts[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ts MUX bit %d: %w", i, err)
|
||||
}
|
||||
mergedTS[i] = t
|
||||
}
|
||||
|
||||
return &encryptedRegister{value: mergedVal, ts: mergedTS}, nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command mediaseal demonstrates media content authentication using FHE.
|
||||
//
|
||||
// A media file (image, video, audio) is hashed and the hash bits are
|
||||
// encrypted under FHE, creating a "media seal" with provenance metadata.
|
||||
// Verification checks a file against the seal homomorphically.
|
||||
// Tamper detection: if even one byte is changed, verification fails.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/mediaseal -file photo.jpg -creator "Alice" -device "iPhone"
|
||||
// go run ./cmd/mediaseal -file photo.jpg -creator "Alice" -device "iPhone" -verify photo.jpg
|
||||
// go run ./cmd/mediaseal -file photo.jpg -creator "Alice" -device "iPhone" -verify tampered.jpg
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
filePath := flag.String("file", "", "path to original media file")
|
||||
creator := flag.String("creator", "unknown", "creator name")
|
||||
device := flag.String("device", "unknown", "capture device")
|
||||
verifyPath := flag.String("verify", "", "path to file to verify against seal")
|
||||
bits := flag.Int("bits", 8, "hash bits to seal (1..256)")
|
||||
flag.Parse()
|
||||
|
||||
if *filePath == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -file is required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if *bits < 1 || *bits > 256 {
|
||||
fmt.Fprintln(os.Stderr, "error: -bits must be 1..256")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Hash original.
|
||||
origHash, err := hashFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
fmt.Println("=== Media Seal ===")
|
||||
fmt.Printf(" File: %s\n", *filePath)
|
||||
fmt.Printf(" Creator: %s\n", *creator)
|
||||
fmt.Printf(" Device: %s\n", *device)
|
||||
fmt.Printf(" Timestamp: %s\n", ts)
|
||||
fmt.Printf(" SHA-256: %x\n", origHash)
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("\nInitialising FHE...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Encrypt hash bits.
|
||||
hashBits := bytesToBits(origHash[:], *bits)
|
||||
sealCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range hashBits {
|
||||
sealCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
fmt.Printf("Seal created: %d encrypted hash bits\n", *bits)
|
||||
|
||||
if *verifyPath == "" {
|
||||
fmt.Println("\nRun with -verify <file> to check authenticity.")
|
||||
return
|
||||
}
|
||||
|
||||
// --- Verification ---
|
||||
fmt.Println("\n=== Verification ===")
|
||||
fmt.Printf("Checking: %s\n", *verifyPath)
|
||||
|
||||
candHash, err := hashFile(*verifyPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Candidate SHA-256: %x\n", candHash)
|
||||
|
||||
// Encrypt candidate hash bits.
|
||||
candBits := bytesToBits(candHash[:], *bits)
|
||||
candCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range candBits {
|
||||
candCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
|
||||
// Homomorphic equality check.
|
||||
fmt.Printf("Comparing %d bit-pairs homomorphically...\n", *bits)
|
||||
t0 := time.Now()
|
||||
|
||||
match, err := eval.XNOR(sealCts[0], candCts[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for i := 1; i < *bits; i++ {
|
||||
eq, err := eval.XNOR(sealCts[i], candCts[i])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error at bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
match, err = eval.AND(match, eq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error combining bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
result := dec.Decrypt(match)
|
||||
fmt.Printf("\nResult: authentic=%v (%v)\n", result, elapsed)
|
||||
|
||||
if result {
|
||||
fmt.Println("AUTHENTIC: file matches the sealed original.")
|
||||
fmt.Printf(" Creator: %s | Device: %s | Sealed: %s\n", *creator, *device, ts)
|
||||
} else {
|
||||
fmt.Println("TAMPERED: file does NOT match the sealed original!")
|
||||
fmt.Println(" Content has been modified since sealing.")
|
||||
}
|
||||
}
|
||||
|
||||
func hashFile(path string) ([32]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
var d [32]byte
|
||||
copy(d[:], h.Sum(nil))
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func bytesToBits(data []byte, n int) []bool {
|
||||
bits := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
byteIdx := i / 8
|
||||
bitIdx := i % 8
|
||||
if byteIdx < len(data) {
|
||||
bits[i] = (data[byteIdx]>>bitIdx)&1 == 1
|
||||
}
|
||||
}
|
||||
return bits
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command provenance demonstrates AI model provenance tracking with FHE.
|
||||
//
|
||||
// An AI model's weights hash is encrypted under FHE to create a provenance
|
||||
// record. Later, a candidate model file can be verified against the sealed
|
||||
// record homomorphically -- the original weights hash is never exposed.
|
||||
//
|
||||
// This prevents model theft detection from leaking proprietary information:
|
||||
// the verifier learns only "match" or "no match."
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/provenance -name "zen-7b" -version "1.0" -params 7000000000 -weights model.bin
|
||||
// go run ./cmd/provenance -name "zen-7b" -version "1.0" -params 7000000000 -weights model.bin -verify candidate.bin
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
name := flag.String("name", "zen-7b", "model name")
|
||||
version := flag.String("version", "1.0", "model version")
|
||||
paramCount := flag.Int64("params", 7_000_000_000, "parameter count")
|
||||
weightsPath := flag.String("weights", "", "path to model weights file")
|
||||
verifyPath := flag.String("verify", "", "path to candidate file to verify against sealed hash")
|
||||
bits := flag.Int("bits", 8, "hash bits to seal (more = slower, stronger)")
|
||||
flag.Parse()
|
||||
|
||||
if *weightsPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -weights is required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if *bits < 1 || *bits > 256 {
|
||||
fmt.Fprintln(os.Stderr, "error: -bits must be 1..256")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Print model card.
|
||||
fmt.Println("=== Model Provenance Record ===")
|
||||
fmt.Printf(" Name: %s\n", *name)
|
||||
fmt.Printf(" Version: %s\n", *version)
|
||||
fmt.Printf(" Parameters: %d\n", *paramCount)
|
||||
|
||||
// Hash weights.
|
||||
hash, err := hashFile(*weightsPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error hashing weights: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" Weights SHA-256: %x\n", hash)
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("\nInitialising FHE...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Encrypt the weights hash.
|
||||
hashBits := bytesToBits(hash[:], *bits)
|
||||
fmt.Printf("Encrypting %d hash bits...\n", *bits)
|
||||
sealCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range hashBits {
|
||||
sealCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
fmt.Printf("Provenance seal created: %d encrypted ciphertexts\n", len(sealCts))
|
||||
|
||||
if *verifyPath == "" {
|
||||
fmt.Println("\nRun with -verify <candidate.bin> to check a model against this seal.")
|
||||
return
|
||||
}
|
||||
|
||||
// --- Verification ---
|
||||
fmt.Println("\n=== Verification ===")
|
||||
fmt.Printf("Candidate: %s\n", *verifyPath)
|
||||
|
||||
candidateHash, err := hashFile(*verifyPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error hashing candidate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Candidate SHA-256: %x\n", candidateHash)
|
||||
|
||||
candidateBits := bytesToBits(candidateHash[:], *bits)
|
||||
candidateCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range candidateBits {
|
||||
candidateCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
|
||||
// Homomorphic comparison.
|
||||
fmt.Printf("Comparing %d encrypted bit-pairs...\n", *bits)
|
||||
t0 := time.Now()
|
||||
match, err := eval.XNOR(sealCts[0], candidateCts[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for i := 1; i < *bits; i++ {
|
||||
eq, err := eval.XNOR(sealCts[i], candidateCts[i])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error at bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
match, err = eval.AND(match, eq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error combining bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
result := dec.Decrypt(match)
|
||||
fmt.Printf("\nResult: match=%v (%v)\n", result, elapsed)
|
||||
if result {
|
||||
fmt.Println("VERIFIED: candidate model matches the sealed provenance record.")
|
||||
} else {
|
||||
fmt.Println("REJECTED: candidate does NOT match the sealed provenance record.")
|
||||
}
|
||||
fmt.Println("Note: the original weights hash was never revealed.")
|
||||
}
|
||||
|
||||
func hashFile(path string) ([32]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
var d [32]byte
|
||||
copy(d[:], h.Sum(nil))
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func bytesToBits(data []byte, n int) []bool {
|
||||
bits := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
byteIdx := i / 8
|
||||
bitIdx := i % 8
|
||||
if byteIdx < len(data) {
|
||||
bits[i] = (data[byteIdx]>>bitIdx)&1 == 1
|
||||
}
|
||||
}
|
||||
return bits
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command seal demonstrates document integrity sealing with FHE.
|
||||
//
|
||||
// A document hash (SHA-256) is encrypted bit-by-bit under FHE, creating a
|
||||
// tamper-proof "seal." Verification re-hashes the file and compares
|
||||
// against the sealed hash homomorphically (XNOR + AND chain) -- the
|
||||
// original hash is never revealed during verification.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/seal -file document.txt # create seal
|
||||
// go run ./cmd/seal -file document.txt -verify # verify seal
|
||||
// go run ./cmd/seal -bits 16 # compare 16 hash bits (default 8)
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
filePath := flag.String("file", "", "path to the file to seal/verify")
|
||||
verify := flag.Bool("verify", false, "verify mode: re-hash and compare homomorphically")
|
||||
bits := flag.Int("bits", 8, "number of hash bits to seal (more bits = slower but stronger)")
|
||||
flag.Parse()
|
||||
|
||||
if *filePath == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -file is required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if *bits < 1 || *bits > 256 {
|
||||
fmt.Fprintln(os.Stderr, "error: -bits must be 1..256")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Hash the file.
|
||||
hash, err := hashFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("SHA-256: %x\n", hash)
|
||||
|
||||
// Expand hash to bit slice (LSB-first per byte).
|
||||
hashBits := bytesToBits(hash[:], *bits)
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("Initialising FHE parameters...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Encrypt the hash bits (the "seal").
|
||||
fmt.Printf("Encrypting %d hash bits...\n", *bits)
|
||||
t0 := time.Now()
|
||||
sealCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range hashBits {
|
||||
sealCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
fmt.Printf("Seal created in %v\n", time.Since(t0))
|
||||
|
||||
if !*verify {
|
||||
// In create mode we just show the seal was produced.
|
||||
fmt.Printf("Seal: %d encrypted ciphertexts (%d bits)\n", len(sealCts), *bits)
|
||||
fmt.Println("Run with -verify to check the file against this seal.")
|
||||
return
|
||||
}
|
||||
|
||||
// --- Verify mode ---
|
||||
// Re-hash (same file or a potentially tampered file) and compare.
|
||||
fmt.Println("\n--- Verification ---")
|
||||
verifyHash, err := hashFile(*filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
verifyBits := bytesToBits(verifyHash[:], *bits)
|
||||
|
||||
// Encrypt verify bits.
|
||||
verifyCts := make([]*fhe.Ciphertext, *bits)
|
||||
for i, b := range verifyBits {
|
||||
verifyCts[i] = enc.Encrypt(b)
|
||||
}
|
||||
|
||||
// Homomorphic equality: XNOR each pair, AND-reduce.
|
||||
fmt.Printf("Comparing %d encrypted bit-pairs homomorphically...\n", *bits)
|
||||
t0 = time.Now()
|
||||
|
||||
match, err := eval.XNOR(sealCts[0], verifyCts[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error at bit 0: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for i := 1; i < *bits; i++ {
|
||||
eq, err := eval.XNOR(sealCts[i], verifyCts[i])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error at bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
match, err = eval.AND(match, eq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error combining bit %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
// Decrypt the single result bit.
|
||||
result := dec.Decrypt(match)
|
||||
fmt.Printf("Verification result: %v (%v, %d gates)\n", result, elapsed, *bits*2-1)
|
||||
if result {
|
||||
fmt.Println("PASS: document matches the seal.")
|
||||
} else {
|
||||
fmt.Println("FAIL: document does NOT match the seal.")
|
||||
}
|
||||
}
|
||||
|
||||
// hashFile returns the SHA-256 digest of a file.
|
||||
func hashFile(path string) ([32]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
var digest [32]byte
|
||||
copy(digest[:], h.Sum(nil))
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
// bytesToBits extracts n bits from data (LSB-first per byte).
|
||||
func bytesToBits(data []byte, n int) []bool {
|
||||
bits := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
byteIdx := i / 8
|
||||
bitIdx := i % 8
|
||||
if byteIdx < len(data) {
|
||||
bits[i] = (data[byteIdx]>>bitIdx)&1 == 1
|
||||
}
|
||||
}
|
||||
return bits
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command stats demonstrates secure multiparty statistics using FHE.
|
||||
//
|
||||
// N parties each encrypt a private value (salary, score, etc.) as
|
||||
// individual bits. The system computes the sum homomorphically using
|
||||
// a ripple-carry adder built from XOR and AND gates. Only the aggregate
|
||||
// sum and count are decrypted -- individual values remain private.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/stats # 4 parties, random 4-bit values
|
||||
// go run ./cmd/stats -values 10,20,30,40 # explicit values (max 255)
|
||||
// go run ./cmd/stats -values 5,15,25 # 3 parties
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"math/rand/v2"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
valuesStr := flag.String("values", "", "comma-separated private values (max 255 each)")
|
||||
parties := flag.Int("parties", 4, "number of parties (used when -values not set)")
|
||||
maxVal := flag.Int("max", 15, "max random value per party (used when -values not set)")
|
||||
flag.Parse()
|
||||
|
||||
// Parse or generate values.
|
||||
var values []uint8
|
||||
if *valuesStr != "" {
|
||||
for _, s := range strings.Split(*valuesStr, ",") {
|
||||
v, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil || v < 0 || v > 255 {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid value %q (must be 0..255)\n", s)
|
||||
os.Exit(1)
|
||||
}
|
||||
values = append(values, uint8(v))
|
||||
}
|
||||
} else {
|
||||
values = make([]uint8, *parties)
|
||||
for i := range values {
|
||||
values[i] = uint8(rand.IntN(*maxVal + 1))
|
||||
}
|
||||
}
|
||||
n := len(values)
|
||||
if n < 2 {
|
||||
fmt.Fprintln(os.Stderr, "error: need at least 2 parties")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Compute expected results.
|
||||
var expectedSum int
|
||||
for _, v := range values {
|
||||
expectedSum += int(v)
|
||||
}
|
||||
expectedAvg := float64(expectedSum) / float64(n)
|
||||
|
||||
fmt.Println("=== Secure Multiparty Statistics ===")
|
||||
fmt.Printf("Parties: %d\n", n)
|
||||
for i, v := range values {
|
||||
fmt.Printf(" Party %d: [PRIVATE] %d\n", i+1, v)
|
||||
}
|
||||
fmt.Printf("Expected sum: %d, average: %.2f\n\n", expectedSum, expectedAvg)
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("Initialising FHE...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Determine accumulator width: enough bits for max possible sum.
|
||||
// max sum = n * 255, need ceil(log2(n*255+1)) bits, cap at 16.
|
||||
accBits := 8
|
||||
maxSum := n * 255
|
||||
for (1 << accBits) <= maxSum {
|
||||
accBits++
|
||||
}
|
||||
if accBits > 16 {
|
||||
accBits = 16
|
||||
}
|
||||
fmt.Printf("Accumulator: %d bits (max representable sum: %d)\n", accBits, (1<<accBits)-1)
|
||||
|
||||
// Encrypt each party's value as 8 bits.
|
||||
fmt.Println("Encrypting private values...")
|
||||
encValues := make([][8]*fhe.Ciphertext, n)
|
||||
for i, v := range values {
|
||||
encValues[i] = enc.EncryptByte(byte(v))
|
||||
}
|
||||
|
||||
// Initialize accumulator to zero.
|
||||
acc := make([]*fhe.Ciphertext, accBits)
|
||||
for i := range acc {
|
||||
acc[i] = enc.Encrypt(false)
|
||||
}
|
||||
|
||||
// Homomorphic summation: add each 8-bit value into the accumulator.
|
||||
fmt.Printf("Summing %d encrypted values...\n", n)
|
||||
t0 := time.Now()
|
||||
for p := 0; p < n; p++ {
|
||||
fmt.Printf(" Adding party %d/%d...\n", p+1, n)
|
||||
acc, err = addUint8(eval, acc, encValues[p][:])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error adding party %d: %v\n", p+1, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
// Decrypt the sum.
|
||||
var decSum uint32
|
||||
for i, ct := range acc {
|
||||
if dec.Decrypt(ct) {
|
||||
decSum |= 1 << i
|
||||
}
|
||||
}
|
||||
decAvg := float64(decSum) / float64(n)
|
||||
|
||||
fmt.Printf("\n--- Aggregated Results ---\n")
|
||||
fmt.Printf("Count: %d parties\n", n)
|
||||
fmt.Printf("Sum: %d (encrypted computation)\n", decSum)
|
||||
fmt.Printf("Average: %.2f\n", decAvg)
|
||||
fmt.Printf("Elapsed: %v\n", elapsed)
|
||||
|
||||
if int(decSum) == expectedSum {
|
||||
fmt.Println("PASS: encrypted sum matches expected value.")
|
||||
} else {
|
||||
fmt.Printf("FAIL: expected %d, got %d\n", expectedSum, decSum)
|
||||
}
|
||||
fmt.Println("\nNote: individual values were never revealed to any party.")
|
||||
}
|
||||
|
||||
// addUint8 adds an 8-bit encrypted value into a wider accumulator
|
||||
// using a ripple-carry adder (XOR for sum, AND for carry).
|
||||
func addUint8(eval *fhe.Evaluator, acc, val []*fhe.Ciphertext) ([]*fhe.Ciphertext, error) {
|
||||
result := make([]*fhe.Ciphertext, len(acc))
|
||||
var carry *fhe.Ciphertext
|
||||
// halfAdd computes sum=a^b, carry=a&b.
|
||||
halfAdd := func(a, b *fhe.Ciphertext) (*fhe.Ciphertext, *fhe.Ciphertext, error) {
|
||||
s, err := eval.XOR(a, b)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
c, err := eval.AND(a, b)
|
||||
return s, c, err
|
||||
}
|
||||
for i := 0; i < len(acc); i++ {
|
||||
var addend *fhe.Ciphertext
|
||||
if i < len(val) {
|
||||
addend = val[i]
|
||||
}
|
||||
switch {
|
||||
case addend == nil && carry == nil:
|
||||
result[i] = acc[i]
|
||||
case carry == nil:
|
||||
result[i], carry, _ = halfAdd(acc[i], addend)
|
||||
case addend == nil:
|
||||
result[i], carry, _ = halfAdd(acc[i], carry)
|
||||
default:
|
||||
// Full adder: sum = a^b^carry, newCarry = MAJ(a,b,carry).
|
||||
ab, err := eval.XOR(acc[i], addend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result[i], err = eval.XOR(ab, carry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if carry, err = eval.MAJORITY(acc[i], addend, carry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command vote demonstrates an encrypted voting system using FHE.
|
||||
//
|
||||
// Each voter encrypts a single yes/no ballot (1 bit) under FHE.
|
||||
// The system tallies votes homomorphically using a ripple-carry adder
|
||||
// built from XOR and AND gates -- individual votes are never decrypted.
|
||||
// Only the final aggregate count is decrypted.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/vote # 5 voters, random ballots
|
||||
// go run ./cmd/vote -voters 8 -yes 5 # 8 voters, 5 yes votes
|
||||
// go run ./cmd/vote -voters 4 -yes 4 # unanimous yes
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nVoters := flag.Int("voters", 5, "number of voters (max 15 for 4-bit tally)")
|
||||
nYes := flag.Int("yes", -1, "number of yes votes (-1 = random)")
|
||||
flag.Parse()
|
||||
|
||||
if *nVoters < 2 || *nVoters > 15 {
|
||||
fmt.Fprintln(os.Stderr, "error: -voters must be 2..15")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Decide votes.
|
||||
votes := make([]bool, *nVoters)
|
||||
if *nYes >= 0 {
|
||||
if *nYes > *nVoters {
|
||||
fmt.Fprintln(os.Stderr, "error: -yes cannot exceed -voters")
|
||||
os.Exit(1)
|
||||
}
|
||||
for i := 0; i < *nYes; i++ {
|
||||
votes[i] = true
|
||||
}
|
||||
} else {
|
||||
for i := range votes {
|
||||
votes[i] = rand.IntN(2) == 1
|
||||
}
|
||||
}
|
||||
|
||||
// Print plaintext votes (for demo verification).
|
||||
expectedYes := 0
|
||||
for i, v := range votes {
|
||||
label := "no"
|
||||
if v {
|
||||
label = "yes"
|
||||
expectedYes++
|
||||
}
|
||||
fmt.Printf(" Voter %d: %s\n", i+1, label)
|
||||
}
|
||||
fmt.Printf("Expected tally: %d yes / %d no\n\n", expectedYes, *nVoters-expectedYes)
|
||||
|
||||
// Setup FHE.
|
||||
fmt.Println("Initialising FHE...")
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
keygen := fhe.NewKeyGenerator(params)
|
||||
sk, _ := keygen.GenKeyPair()
|
||||
bsk := keygen.GenBootstrapKey(sk)
|
||||
|
||||
enc := fhe.NewEncryptor(params, sk)
|
||||
dec := fhe.NewDecryptor(params, sk)
|
||||
eval := fhe.NewEvaluator(params, bsk)
|
||||
|
||||
// Encrypt ballots.
|
||||
fmt.Println("Encrypting ballots...")
|
||||
ballots := make([]*fhe.Ciphertext, *nVoters)
|
||||
for i, v := range votes {
|
||||
ballots[i] = enc.Encrypt(v)
|
||||
}
|
||||
|
||||
// Tally: 4-bit ripple-carry accumulator (supports up to 15 voters).
|
||||
const tallyBits = 4
|
||||
tally := [tallyBits]*fhe.Ciphertext{
|
||||
enc.Encrypt(false),
|
||||
enc.Encrypt(false),
|
||||
enc.Encrypt(false),
|
||||
enc.Encrypt(false),
|
||||
}
|
||||
|
||||
fmt.Printf("Tallying %d ballots homomorphically (%d-bit accumulator)...\n", *nVoters, tallyBits)
|
||||
t0 := time.Now()
|
||||
|
||||
for i, ballot := range ballots {
|
||||
fmt.Printf(" Adding ballot %d/%d...\n", i+1, *nVoters)
|
||||
tally, err = addBit(eval, tally, ballot)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error adding ballot %d: %v\n", i+1, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(t0)
|
||||
|
||||
// Decrypt only the tally.
|
||||
var result uint8
|
||||
for i := 0; i < tallyBits; i++ {
|
||||
if dec.Decrypt(tally[i]) {
|
||||
result |= 1 << i
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n--- Result ---\n")
|
||||
fmt.Printf("Encrypted tally decrypted: %d yes votes\n", result)
|
||||
fmt.Printf("Total voters: %d | No votes: %d\n", *nVoters, *nVoters-int(result))
|
||||
fmt.Printf("Elapsed: %v\n", elapsed)
|
||||
|
||||
if int(result) == expectedYes {
|
||||
fmt.Println("PASS: tally matches expected count.")
|
||||
} else {
|
||||
fmt.Println("FAIL: tally mismatch!")
|
||||
}
|
||||
fmt.Println("\nNote: individual ballots were never decrypted.")
|
||||
}
|
||||
|
||||
// addBit adds a single encrypted bit to an n-bit encrypted accumulator
|
||||
// using a ripple-carry adder (XOR for sum, AND for carry).
|
||||
func addBit(eval *fhe.Evaluator, acc [4]*fhe.Ciphertext, bit *fhe.Ciphertext) ([4]*fhe.Ciphertext, error) {
|
||||
carry := bit
|
||||
var result [4]*fhe.Ciphertext
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
// sum = acc[i] XOR carry
|
||||
sum, err := eval.XOR(acc[i], carry)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("bit %d XOR: %w", i, err)
|
||||
}
|
||||
// newCarry = acc[i] AND carry
|
||||
newCarry, err := eval.AND(acc[i], carry)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("bit %d AND: %w", i, err)
|
||||
}
|
||||
result[i] = sum
|
||||
carry = newCarry
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user