init npm module, add types

This commit is contained in:
Andrew Morris
2024-10-14 10:29:04 +11:00
parent 34ada82139
commit a29a97b2a1
5 changed files with 179 additions and 104 deletions

4
.gitignore vendored
View File

@@ -1 +1,3 @@
/2pc
/2pc
/node_modules
/dist

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.rulers": [80]
}

29
package-lock.json generated Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "emp-wasm",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "emp-wasm",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"typescript": "^5.6.3"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "emp-wasm",
"version": "0.1.0",
"description": "Wasm build of authenticated garbling from [emp-toolkit/emp-ag2pc](https://github.com/emp-toolkit/emp-ag2pc).",
"main": "dist/src/ts/index.js",
"scripts": {
"build": "rm -rf dist build && tsx scripts/build.ts && tsc",
"test": "mocha --import=tsx tests/**/*.test.ts"
},
"keywords": [],
"author": "Andrew Morris <voltrevo@gmail.com>",
"license": "MIT",
"devDependencies": {
"typescript": "^5.6.3"
}
}

View File

@@ -1,4 +1,4 @@
async function simpleDemo(party, input) {
async function simpleDemo(party: 'alice' | 'bob', input: number): Promise<void> {
const bits = await secure2PC(
party,
add32BitCircuit,
@@ -9,24 +9,49 @@ async function simpleDemo(party, input) {
alert(numberFrom32Bits(bits));
}
declare const Module: {
emp?: {
circuit?: string;
input?: Uint8Array;
io?: { send: (data: Uint8Array) => void; recv: (len: number) => Promise<Uint8Array> };
handleOutput?: (value: Uint8Array) => void;
};
_run(party: number): void;
};
/**
* Runs a secure two-party computation (2PC) using a specified circuit.
*
* @param party - The party initiating the computation ('alice' or 'bob').
* @param circuit - The circuit to run (in this case, a 32-bit addition circuit).
* @param input - The input to the circuit, represented as a 32-bit binary array.
* @param io - Input/output channels for communication between the two parties.
* @returns A promise resolving with the output of the circuit (a 32-bit binary array).
*/
async function secure2PC(
party,
circuit,
input,
io,
) {
party: 'alice' | 'bob',
circuit: string,
input: Uint8Array,
io: { send: (data: Uint8Array) => void; recv: (len: number) => Promise<Uint8Array> }
): Promise<Uint8Array> {
if (Module.emp) {
throw new Error('Can only run one secure2PC at a time');
}
const emp = {};
const emp: {
circuit?: string;
input?: Uint8Array;
io?: { send: (data: Uint8Array) => void; recv: (len: number) => Promise<Uint8Array> };
handleOutput?: (value: Uint8Array) => void
} = {};
Module.emp = emp;
emp.circuit = circuit;
emp.input = input;
emp.io = io;
const result = new Promise((resolve, reject) => {
const result = new Promise<Uint8Array>((resolve, reject) => {
try {
emp.handleOutput = resolve;
// TODO: emp.handleError
@@ -44,7 +69,13 @@ async function secure2PC(
}
}
function numberTo32Bits(x) {
/**
* Converts a number into its 32-bit binary representation.
*
* @param x - The number to convert.
* @returns A 32-bit binary representation of the number in the form of a Uint8Array.
*/
function numberTo32Bits(x: number): Uint8Array {
const result = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
@@ -54,7 +85,13 @@ function numberTo32Bits(x) {
return result;
}
function numberFrom32Bits(arr) {
/**
* Converts a 32-bit binary representation back into a number.
*
* @param arr - A 32-bit binary array.
* @returns The number represented by the 32-bit array.
*/
function numberFrom32Bits(arr: Uint8Array): number {
let result = 0;
for (let i = 0; i < 32; i++) {
@@ -64,7 +101,14 @@ function numberFrom32Bits(arr) {
return result;
}
function partyToIndex(party) {
/**
* Maps a party ('alice' or 'bob') to an index number.
*
* @param party - The party ('alice' or 'bob').
* @returns 1 for 'alice', 2 for 'bob'.
* @throws Will throw an error if the party is invalid.
*/
function partyToIndex(party: 'alice' | 'bob'): number {
if (party === 'alice') {
return 1;
}
@@ -76,22 +120,30 @@ function partyToIndex(party) {
throw new Error(`Invalid party ${party} (must be 'alice' or 'bob')`);
}
/**
* A queue for managing buffered data that allows pushing and popping of data chunks.
*/
class BufferQueue {
constructor(initialCapacity = 1024) {
// Initialize the buffer with a given capacity
private buffer: Uint8Array;
private bufferStart: number;
private bufferEnd: number;
private pendingPops: number[];
private pendingPopsResolvers: ((value: Uint8Array) => void)[];
constructor(initialCapacity: number = 1024) {
this.buffer = new Uint8Array(initialCapacity);
this.bufferStart = 0; // Start pointer
this.bufferEnd = 0; // End pointer
this.pendingPops = []; // Queue of pending pop requests
this.pendingPopsResolvers = []; // Resolvers for pending pops
this.bufferStart = 0;
this.bufferEnd = 0;
this.pendingPops = [];
this.pendingPopsResolvers = [];
}
/**
* Ensures that the buffer has enough capacity to accommodate additional bytes.
* If not, it resizes the buffer by doubling its current size.
* @param {number} additionalLength
* @param additionalLength - The additional length of data to be accommodated.
*/
_ensureCapacity(additionalLength) {
private _ensureCapacity(additionalLength: number): void {
const required = this.bufferEnd + additionalLength;
if (required > this.buffer.length) {
let newLength = this.buffer.length * 2;
@@ -108,44 +160,37 @@ class BufferQueue {
/**
* Pushes new data into the buffer and resolves any pending pop requests if possible.
* @param {Uint8Array} data
* @param data - The data to push into the buffer (must be a Uint8Array).
*/
push(data) {
push(data: Uint8Array): void {
if (!(data instanceof Uint8Array)) {
throw new TypeError('Data must be a Uint8Array');
}
// Ensure buffer has enough capacity
this._ensureCapacity(data.length);
// Append data to the buffer
this.buffer.set(data, this.bufferEnd);
this.bufferEnd += data.length;
// Try to resolve pending pops
this._resolvePendingPops();
}
/**
* Pops a specified number of bytes from the buffer.
* Returns a Promise that resolves with a Uint8Array of the requested length.
* @param {number} len
* @returns {Promise<Uint8Array>}
* @param len - The number of bytes to pop from the buffer.
* @returns A promise resolving with the popped data as a Uint8Array.
*/
pop(len) {
pop(len: number): Promise<Uint8Array> {
if (typeof len !== 'number' || len < 0) {
return Promise.reject(new Error('Length must be non-negative integer'));
}
// Check if enough data is available
if (this.bufferEnd - this.bufferStart >= len) {
const result = this.buffer.slice(this.bufferStart, this.bufferStart + len);
this.bufferStart += len;
this._compactBuffer();
return Promise.resolve(result);
} else {
// Not enough data, return a promise and enqueue the request
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
this.pendingPops.push(len);
this.pendingPopsResolvers.push(resolve);
});
@@ -153,20 +198,18 @@ class BufferQueue {
}
/**
* Resolves pending pop requests if enough data is available.
* Resolves pending pop requests if enough data is available in the buffer.
*/
_resolvePendingPops() {
private _resolvePendingPops(): void {
while (this.pendingPops.length > 0) {
const len = this.pendingPops[0];
if (this.bufferEnd - this.bufferStart >= len) {
// Enough data to fulfill this pop
const data = this.buffer.slice(this.bufferStart, this.bufferStart + len);
this.bufferStart += len;
this.pendingPops.shift();
const resolve = this.pendingPopsResolvers.shift();
const resolve = this.pendingPopsResolvers.shift()!;
resolve(data);
} else {
// Not enough data for the next pending pop
break;
}
}
@@ -174,15 +217,13 @@ class BufferQueue {
}
/**
* Compacts the buffer by resetting pointers if all data has been consumed.
* Compacts the buffer by resetting the start and end pointers if all data has been consumed.
*/
_compactBuffer() {
// If all data has been consumed, reset pointers to avoid buffer growing indefinitely
private _compactBuffer(): void {
if (this.bufferStart === this.bufferEnd) {
this.bufferStart = 0;
this.bufferEnd = 0;
} else if (this.bufferStart > 0) {
// Shift data to the beginning to free up space
this.buffer.set(this.buffer.subarray(this.bufferStart, this.bufferEnd));
this.bufferEnd -= this.bufferStart;
this.bufferStart = 0;
@@ -190,67 +231,59 @@ class BufferQueue {
}
}
function makeCopyPasteIO() {
/**
* Creates an I/O interface for secure communication using a BufferQueue.
* @returns An object with `send` and `recv` methods for communication.
*/
function makeCopyPasteIO(): { send: (data: Uint8Array) => void; recv: (len: number) => Promise<Uint8Array> } {
const bq = new BufferQueue();
window.write = function (base64) {
(window as any).write = function (base64: string): void {
const data = decodeBase64(base64);
bq.push(data);
};
return {
send: makeConsoleSend(),
recv: len => bq.pop(len),
recv: (len: number) => bq.pop(len),
};
}
function makeConsoleSend() {
// Buffer to accumulate Uint8Array inputs
let buffer = [];
// Timer identifier
let timer = null;
// Time window in milliseconds
/**
* Creates a function for sending data via console output using Base64 encoding.
* The data is sent in batches to optimize performance.
* @returns A function that takes Uint8Array data and logs it to the console.
*/
function makeConsoleSend(): (data: Uint8Array) => void {
let buffer: Uint8Array[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
const TIME_WINDOW = 100;
// The actual send function that will be returned
return function (data) {
// Validate that the input is a Uint8Array
return function (data: Uint8Array): void {
if (!(data instanceof Uint8Array)) {
throw new TypeError('Input must be a Uint8Array');
}
// Add the incoming data to the buffer
buffer.push(data);
// If no timer is set, start one
if (!timer) {
timer = setTimeout(() => {
// Calculate the total length of all Uint8Arrays in the buffer
const totalLength = buffer.reduce((acc, arr) => acc + arr.length, 0);
// Create a new Uint8Array with the total length
const concatenated = new Uint8Array(totalLength);
// Offset to keep track of the current position in the concatenated array
let offset = 0;
// Iterate over each Uint8Array in the buffer and set its values in the concatenated array
buffer.forEach(arr => {
concatenated.set(arr, offset);
offset += arr.length;
});
// Convert the concatenated Uint8Array to a binary string
// This is necessary because btoa works with binary strings
let binary = '';
for (let byte of concatenated) {
binary += String.fromCharCode(byte);
}
// Convert the binary string to a Base64 string
const base64 = btoa(binary);
// Print the Base64 string to the console
console.log(`write('${base64}')`);
// Reset the buffer and timer for the next batch of inputs
buffer = [];
timer = null;
}, TIME_WINDOW);
@@ -258,6 +291,36 @@ function makeConsoleSend() {
};
}
/**
* Decodes a Base64-encoded string into a Uint8Array.
*
* @param base64Str - The Base64-encoded string to decode and process.
* @throws {TypeError} If the input is not a string.
* @throws {Error} If Base64 decoding fails.
* @returns A Uint8Array representing the decoded binary data.
*/
function decodeBase64(base64Str: string): Uint8Array {
if (typeof base64Str !== 'string') {
throw new TypeError('Input must be a Base64-encoded string');
}
let decodedArray: Uint8Array;
try {
const binaryStr = atob(base64Str);
const len = binaryStr.length;
decodedArray = new Uint8Array(len);
for (let i = 0; i < len; i++) {
decodedArray[i] = binaryStr.charCodeAt(i);
}
} catch (error) {
throw new Error('Failed to decode Base64 string: ' + error.message);
}
return decodedArray;
}
const add32BitCircuit = `375 439
32 32 33
@@ -637,41 +700,3 @@ const add32BitCircuit = `375 439
2 1 65 66 64 AND
1 1 64 438 INV
`;
/**
* Function: decodeBase64
* ----------------------
* Decodes a Base64-encoded string into a Uint8Array
*
* @param {string} base64Str - The Base64-encoded string to decode and process.
*
* @throws {TypeError} Throws an error if the input is not a string.
* @throws {Error} Throws an error if Base64 decoding fails or if push is not defined.
*/
function decodeBase64(base64Str) {
// Validate that the input is a string
if (typeof base64Str !== 'string') {
throw new TypeError('Input must be a Base64-encoded string');
}
let decodedArray;
try {
// Decode the Base64 string to a binary string using atob
const binaryStr = atob(base64Str);
// Create a Uint8Array with the same length as the binary string
const len = binaryStr.length;
decodedArray = new Uint8Array(len);
// Populate the Uint8Array with the character codes from the binary string
for (let i = 0; i < len; i++) {
decodedArray[i] = binaryStr.charCodeAt(i);
}
} catch (error) {
// Handle errors that may occur during decoding
throw new Error('Failed to decode Base64 string: ' + error.message);
}
return decodedArray;
}