Simple math using WebAssembly and Homomorphic Encryption

2022-01-14

About a year ago i was reading up on the new hotness that is homomorphic encryption and tried to understand what it is and how it works. I guess i know the former just barely but the latter simply is way, way beyond my skills. If thats’ what your’e looking to find here, please see the articles cited below (esp A Homomorphic Encryption Illustrated Primer).

Anyway, in an effort to learn it, i decided to writeup a simple “ride sharing” app that i read about in a paper in both Microsoft SEAL and go’s Lttigo libraries. It worked pretty nicely if for nothing else but a ‘helloworld’ (TBH, i can’t cite a real word, practical use of it other than a helloworld..but thats just probably my ignorance)

Today, wanted to learn a bit about browser webassembly. I had done some prior work in Envoy proxy-wasm and thought…why cant use a browser to do homomorphic encryption?

The general idea is that your browser would create or store the homomorphic keys but delegate to a remote server to do the ‘heavy lifting’ operations for FHE like add, multiply, divide, etc. Basically, your browser is just encrypting/decrypting data but a remote server is doing the actual work without even knowing anything about the data….yeah, the last bit is just like magic to me…

So the original idea i had about a fun demo would’ve been to create a simple US Department of Taxation “1040EZ” form…the app i had in mind i’d fill in many of the values for my taxes, the wasm would encrypt them…then transmit each cell sequentially to a server …the server would do the rest and send me back how much i’d invariably owe the man.

long story short, i got lazy,real lazy….i changed the app to be just simple adding machine…a Soroban that just adds…

(if nothing else, look at the nice picture, thats taken from the excellent article cited)

images/thumbnail.png


You can find the source here A simple soroban using WebAssembly and Homomorphic Encryption


References

FHE

WASM


Architecture

So in this tutorial, we will create a wasm binary in go that itself loads up the lattigo library for FHE operations. We will interact with the browsers’ javascript obects and also use fetch to send the encrypted data to a remote server that will just add a+b…but under encryption without even knowing what a and b are…

images/wasm_fhe.png

Setup

To use this demo, we will need go 1.17…really, thats it.

First compile the wasm binary

# copy your own `wasm_exec.js` if you want...or just use the one in this repo
# cp $GOROOT/misc/wasm/wasm_exec.js html/wasm_exec.js

# compile
GOOS=js GOARCH=wasm go build -o  server/static/wasm/main.wasm  main.go

(why not tinygo? it didn’t compile lattigo (something about a gob…))

Run Server

Run the backend server:

cd server
go run server.go

do Math

Open up a browser and goto

http://localhost:8080/static/

On startup, a brand new public and private FHE keys are created but persist for the duration of this instance of wasm.

Change the value of a or b in the input boxes.

What you’ll see a callback from javascript to wasm to encrypt the fields. The hash of the encrypted values are shown in the browser (i woud’ve shown the full polynomial representation of the encrypted values but its….long (and i’m lazy)).

once the values are filled in, wasm will call a function in javascript to fetch() an external URL (wasm can’t make http calls on its own from a browser…it needs to ask javascript).

The fetch call contacts the server and provides the encrypted values of a and b.

The server simply adds the number and responds back..again, to emphasize, without even knowing what those values are…it addd them nonetheless…

The browser will callback wasm, provide the encrypted value and wasm will then use its secret FHE key to decrypt the data and render the sum…


thats about it…someday i’ll begin to learn the math..


you can find the full source in the github repo above. The copy here is for inline reference.

  • index.html
<!DOCTYPE html>

<html>

<head>
	<meta charset="utf-8" />
	<title>sals' simple soroban</title>
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<link rel="stylesheet" href="stylesheets/main.css">
	<script src="js/wasm_exec.js" defer></script>
	<script src="js/wasm.js" defer></script>
</head>

<body>
	<h3>soroban that just adds using WebAssembly and Homomorphic Encryption</h3>

	<div class="ex1">

		<div class="myDiv">
			<h3>Key parameters</h3>
			<p id="parameters"></p>
		</div>
		<p>Add two numbers, using WebAssembly:</p>
		a: <input type="number" id="a" onkeypress="return event.charCode >= 48" min="1"  value="0"/>  + 
		b: <input type="number" id="b" onkeypress="return event.charCode >= 48" min="1"  value="0"/> =
		 <input type="number"  id="result" readonly />

		<br/>		
		<h3>hash(encrypt(a))</h3>
		<div class="myDiv">
			<p id="ea"></p>
		</div>
		<br/>
		<h3>hash(encrypt(b))</h3>		
		<div class="myDiv">
			<p id="eb"></p>
		</div>
		<br/>	

	</div>
</body>

</html>
  • wasm.js
'use strict';

const WASM_URL = 'wasm/main.wasm';

var wasm;

function init() {
  const go = new Go();
  if ('instantiateStreaming' in WebAssembly) {
    WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
      wasm = obj.instance;
      go.run(wasm);
    })
  } else {
    fetch(WASM_URL).then(resp =>
      resp.arrayBuffer()
    ).then(bytes =>
      WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
        wasm = obj.instance;
        go.run(wasm);
      })
    )
  }
}

const ADD_URL = '/add';
function add(a,b) {
    const data = {
        "a": a,
        "b": b
    }
    fetch(ADD_URL,
        {
            method: 'POST',
            body: JSON.stringify(data),
            headers: {
                'Content-Type': 'application/json'
            },            
        }
    ).then(response => response.json())
    .then(respData => {        
         const r = decrypt(respData.result);
         document.getElementById("result").value = r;
        }
    );
  
}


init();
  • main.go
package main

import (
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"strconv"
	"syscall/js"

	"github.com/ldsec/lattigo/v2/bfv"
	"github.com/ldsec/lattigo/v2/rlwe"
)

var (
	a, b        int
	r           string
	params      bfv.Parameters
	encoder     bfv.Encoder
	encryptorPk bfv.Encryptor
	decryptorSk bfv.Decryptor
	evaluator   bfv.Evaluator
)

func main() {
	fmt.Println("============================================")
	fmt.Println("init keys")
	fmt.Println("============================================")
	fmt.Println()

	// BFV parameters (128 bit security) with plaintext modulus 65929217
	paramDef := bfv.PN13QP218
	paramDef.T = 0x3ee0001
	var err error
	params, err = bfv.NewParametersFromLiteral(paramDef)
	if err != nil {
		fmt.Printf("%v", err)
		return
	}

	encoder = bfv.NewEncoder(params)
	kgen := bfv.NewKeyGenerator(params)

	sk, pk := kgen.GenKeyPair()

	encryptorPk = bfv.NewEncryptor(params, pk)
	decryptorSk = bfv.NewDecryptor(params, sk)

	evaluator = bfv.NewEvaluator(params, rlwe.EvaluationKey{})

	keyParameters := fmt.Sprintf("Parameters : N=%d, T=%d, Q = %d bits, sigma = %f \n",
		1<<params.LogN(), params.T(), params.LogQP(), params.Sigma())
	fmt.Println(keyParameters)
	js.Global().Get("document").Call("getElementById", "parameters").Set("innerHTML", keyParameters)
	fmt.Println()

	document := js.Global().Get("document")
	document.Call("getElementById", "a").Set("oninput", updateEncrypter(&a))
	document.Call("getElementById", "b").Set("oninput", updateEncrypter(&b))
	js.Global().Set("decrypt", js.FuncOf(Decrypt))

	<-make(chan bool)
}

//export updater
func updateEncrypter(n *int) js.Func {
	fmt.Printf("callback for operands %d\n", *n)
	return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		*n, _ = strconv.Atoi(this.Get("value").String())
		Encrypt()
		return nil
	})
}

//export result
func Decrypt(this js.Value, p []js.Value) interface{} {
	fmt.Println("result callback")

	aPlusBciphertextEncoded := p[0].String()

	aPlusBciphertextEncodedBytes, err := base64.StdEncoding.DecodeString(aPlusBciphertextEncoded)
	if err != nil {
		fmt.Printf("Error converting aPlusBciphertextEncoded from binary %v", err)
		return nil
	}

	aPlusBciphertext := bfv.NewCiphertext(params, 1)
	err = aPlusBciphertext.UnmarshalBinary(aPlusBciphertextEncodedBytes)
	if err != nil {
		fmt.Printf("Error unmarshalling aPlusBciphertextEncodedBytes from binary %v", err)
		return nil
	}

	result := encoder.DecodeUintNew(decryptorSk.DecryptNew(aPlusBciphertext))
	var computedDist uint64

	for i := 0; i < len(result); i++ {
		computedDist = computedDist + result[i]
	}

	fmt.Printf("Decrypted: %d\n", computedDist)

	return js.ValueOf(computedDist)
}

func Encrypt() {

	fmt.Println("Adding numbers")

	plaintextA := bfv.NewPlaintext(params)

	r1 := make([]uint64, 1<<params.LogN())
	r1[0] = uint64(a)
	encoder.EncodeUint(r1, plaintextA)

	plaintextB := bfv.NewPlaintext(params)
	r2 := make([]uint64, 1<<params.LogN())
	r2[1] = uint64(b)
	encoder.EncodeUint(r2, plaintextB)

	ciphertextA := encryptorPk.EncryptNew(plaintextA)
	ciphertextB := encryptorPk.EncryptNew(plaintextB)

	eaBytes, err := ciphertextA.MarshalBinary()
	if err != nil {
		fmt.Printf("Error converting ea to binary %v", err)
		return
	}

	ebBytes, err := ciphertextB.MarshalBinary()
	if err != nil {
		fmt.Printf("Error converting eb to binary %v", err)
		return
	}
	eaEncoded := base64.StdEncoding.EncodeToString(eaBytes)
	ebEncoded := base64.StdEncoding.EncodeToString(ebBytes)

	eah := sha256.Sum256(eaBytes)
	ebh := sha256.Sum256(ebBytes)

	js.Global().Get("document").Call("getElementById", "ea").Set("innerHTML", fmt.Sprintf("%s", base64.StdEncoding.EncodeToString((eah[:]))))
	js.Global().Get("document").Call("getElementById", "eb").Set("innerHTML", fmt.Sprintf("%s", base64.StdEncoding.EncodeToString((ebh[:]))))
	js.Global().Call("add", eaEncoded, ebEncoded)
}
  • server.go
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/bitly/go-simplejson"
	"github.com/gorilla/mux"
	"github.com/ldsec/lattigo/v2/bfv"
	"github.com/ldsec/lattigo/v2/rlwe"
)

type Operands struct {
	A string
	B string
}

var (
	params    bfv.Parameters
	evaluator bfv.Evaluator
)

func gethandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "ok")
}

func add(w http.ResponseWriter, r *http.Request) {
	fmt.Println("add() called")
	var p Operands
	err := json.NewDecoder(r.Body).Decode(&p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	//fmt.Printf("Operands: %+v\n", p)

	eaBytes, err := base64.StdEncoding.DecodeString(p.A)
	if err != nil {
		fmt.Printf("Error decoding ea to binary %v", err)
		return
	}
	ciphertextA := bfv.NewCiphertext(params, 1)
	err = ciphertextA.UnmarshalBinary(eaBytes)
	if err != nil {
		fmt.Printf("Error converting ea to binary %v", err)
		return
	}

	ebBytes, err := base64.StdEncoding.DecodeString(p.B)
	if err != nil {
		fmt.Printf("Error decoding eb to binary %v", err)
		return
	}
	ciphertextB := bfv.NewCiphertext(params, 1)
	err = ciphertextB.UnmarshalBinary(ebBytes)
	if err != nil {
		fmt.Printf("Error converting eb to binary %v", err)
		return
	}

	aPlusBciphertext := bfv.NewCiphertext(params, 1)

	evaluator.Add(ciphertextA, ciphertextB, aPlusBciphertext)

	resultBytes, err := aPlusBciphertext.MarshalBinary()
	if err != nil {
		fmt.Printf("Error converting aPlusBciphertext to binary %v", err)
		return
	}

	//result := encoder.DecodeUintNew(decryptorSk.DecryptNew(aPlusBciphertext))

	resp := simplejson.New()

	resp.Set("result", base64.StdEncoding.EncodeToString(resultBytes))

	payload, err := resp.MarshalJSON()
	if err != nil {
		log.Println(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(payload)
}

func main() {
	// BFV parameters (128 bit security) with plaintext modulus 65929217
	paramDef := bfv.PN13QP218
	paramDef.T = 0x3ee0001
	var err error
	params, err = bfv.NewParametersFromLiteral(paramDef)
	if err != nil {
		fmt.Printf("%v", err)
		return
	}
	evaluator = bfv.NewEvaluator(params, rlwe.EvaluationKey{})

	router := mux.NewRouter()
	router.Methods(http.MethodGet).Path("/get").HandlerFunc(gethandler)
	router.Methods(http.MethodPost).Path("/add").HandlerFunc(add)
	router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
	http.Handle("/", router)
	fmt.Println("Starting Server")
	err = http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}
}

This site supports webmentions. Send me a mention via this form.