Skip to content

Commit

Permalink
Encrypt and Decrypt support (#39)
Browse files Browse the repository at this point in the history
* Initial commit

* Fix failing test

* Switch to a RefCell

* Use regular borrow when possible
  • Loading branch information
neekolas authored Apr 3, 2023
1 parent 61f0bf4 commit 7701394
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 44 deletions.
82 changes: 59 additions & 23 deletions bindings/wasm/crate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Mutex;

Expand All @@ -11,7 +11,7 @@ extern crate lazy_static;

// This whole strategy is to keep a singleton in WASM memory world
lazy_static! {
static ref INSTANCE_MAP: Mutex<HashMap<String, Cell<VoodooInstance>>> =
static ref INSTANCE_MAP: Mutex<HashMap<String, RefCell<VoodooInstance>>> =
Mutex::new(HashMap::new());
}

Expand All @@ -20,7 +20,7 @@ lazy_static! {
pub fn new_voodoo_instance() -> String {
let mut instances = INSTANCE_MAP.lock().unwrap();
let handle = (instances.len() as u64).to_string();
instances.insert(handle.clone(), Cell::new(VoodooInstance::new()));
instances.insert(handle.clone(), RefCell::new(VoodooInstance::new()));
handle
}

Expand All @@ -29,32 +29,31 @@ pub fn create_outbound_session(
sending_handle: &str,
receiving_handle: &str,
message: &str,
) -> Result<String, JsValue> {
) -> Result<Box<[JsValue]>, JsValue> {
// Look up both handles in INSTANCE_MAP
let instances = INSTANCE_MAP.lock().unwrap();
let mut sending_instance = instances
.get(sending_handle)
.ok_or("sending_handle not found")?
.take();
.borrow_mut();

let receiving_instance = instances
.get(receiving_handle)
.ok_or("receiving_handle not found")?
.take();
.borrow();

// Get other party's public situation
let receiving_public = receiving_instance.public_account();

// Create the session
let result = sending_instance.create_outbound_session_serialized(&receiving_public, message);

// Put the sending_instance and receiving_instance back, we know the cells exist
instances.get(sending_handle).unwrap().set(sending_instance);
instances
.get(receiving_handle)
.unwrap()
.set(receiving_instance);
match result {
Ok((_, ciphertext_json)) => Ok(ciphertext_json),
Ok((session_id, ciphertext_json)) => Ok(vec![
JsValue::from_str(&session_id),
JsValue::from_str(&ciphertext_json),
]
.into_boxed_slice()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
Expand All @@ -64,33 +63,70 @@ pub fn create_inbound_session(
sending_handle: &str,
receiving_handle: &str,
message: &str,
) -> Result<String, JsValue> {
) -> Result<Box<[JsValue]>, JsValue> {
// Look up both handles in INSTANCE_MAP
let instances = INSTANCE_MAP.lock().unwrap();
let sending_instance = instances
.get(sending_handle)
.ok_or("sending_handle not found")?
.take();
.borrow();
let mut receiving_instance = instances
.get(receiving_handle)
.ok_or("receiving_handle not found")?
.take();
.borrow_mut();

// Get sender party public
let sending_public = sending_instance.public_account();

// Create the session
let result = receiving_instance.create_inbound_session_serialized(&sending_public, message);

// Put the instances back in their cells
instances.get(sending_handle).unwrap().set(sending_instance);
instances
.get(receiving_handle)
.unwrap()
.set(receiving_instance);
match result {
Ok((session_id, ciphertext_json)) => Ok(vec![
JsValue::from_str(&session_id),
JsValue::from_str(&ciphertext_json),
]
.into_boxed_slice()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}

#[wasm_bindgen]
pub fn encrypt_message(
sending_handle: &str,
session_id: &str,
message: &str,
) -> Result<String, JsValue> {
let instances = INSTANCE_MAP.lock().unwrap();
let mut instance = instances
.get(sending_handle)
.ok_or("sending_handle not found")?
.borrow_mut();

let result = instance.encrypt_message_serialized(session_id, message);

match result {
Ok(ciphertext_json) => Ok(ciphertext_json),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}

#[wasm_bindgen]
pub fn decrypt_message(
handle: &str,
session_id: &str,
ciphertext: &str,
) -> Result<String, JsValue> {
let instances = INSTANCE_MAP.lock().unwrap();
let mut instance = instances
.get(handle)
.ok_or("handle not found")?
.borrow_mut();

let result = instance.decrypt_message_serialized(session_id, ciphertext);

match result {
Ok((_, plaintext)) => Ok(plaintext),
Ok(plaintext) => Ok(plaintext),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
Expand Down
83 changes: 70 additions & 13 deletions bindings/wasm/src/xmtpv3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import init, {
new_voodoo_instance,
create_outbound_session,
create_inbound_session,
decrypt_message,
encrypt_message,
e2e_selftest,
} from "./pkg/libxmtp.js";

Expand All @@ -20,6 +22,11 @@ export const setWasmInit = (arg: () => InitInput) => {

let initialized: Promise<void> | undefined = undefined;

type SessionResult = {
sessionId: string;
payload: string;
};

export class VoodooInstance {
// Handle to the voodooinstance object in the Wasm module
public handle: string = "";
Expand All @@ -31,16 +38,37 @@ export class VoodooInstance {
this.handle = handle;
}

createOutboundSession(other: VoodooInstance, msg: string): Promise<string> {
return new Promise((resolve, reject) => {
resolve(this.wasmModule.createOutboundSession(this.handle, other.handle, msg));
});
async createOutboundSession(
other: VoodooInstance,
msg: string
): Promise<SessionResult> {
const [sessionId, payload] = this.wasmModule.createOutboundSession(
this.handle,
other.handle,
msg
);

return { sessionId, payload };
}

async createInboundSession(
other: VoodooInstance,
msg: string
): Promise<SessionResult> {
const [sessionId, payload] = this.wasmModule.createInboundSession(
other.handle,
this.handle,
msg
);
return { sessionId, payload };
}

async encryptMessage(sessionId: string, msg: string): Promise<string> {
return this.wasmModule.encryptMessage(this.handle, sessionId, msg);
}

createInboundSession(other: VoodooInstance, msg: string): Promise<string> {
return new Promise((resolve, reject) => {
resolve(this.wasmModule.createInboundSession(other.handle, this.handle, msg));
});
async decryptMessage(sessionId: string, ciphertext: string): Promise<string> {
return this.wasmModule.decryptMessage(this.handle, sessionId, ciphertext);
}
}

Expand All @@ -54,7 +82,6 @@ export class XMTPv3 {
}
}


// Manages the Wasm module, which loads a singleton version of our Rust code
export class XMTPWasm {
private constructor() {}
Expand All @@ -69,12 +96,42 @@ export class XMTPWasm {
return new VoodooInstance(this, handle);
}

createOutboundSession(sendHandle: string, receiveHandle: string, msg: string): string {
return create_outbound_session(sendHandle, receiveHandle, msg);
createOutboundSession(
sendHandle: string,
receiveHandle: string,
msg: string
): [string, string] {
return create_outbound_session(sendHandle, receiveHandle, msg) as [
string,
string
];
}

createInboundSession(
sendHandle: string,
receiveHandle: string,
msg: string
): [string, string] {
return create_inbound_session(sendHandle, receiveHandle, msg) as [
string,
string
];
}

encryptMessage(
sendHandle: string,
sessionId: string,
message: string
): string {
return encrypt_message(sendHandle, sessionId, message);
}

createInboundSession(sendHandle: string, receiveHandle: string, msg: string): string {
return create_inbound_session(sendHandle, receiveHandle, msg);
decryptMessage(
handle: string,
sessionId: string,
ciphertext: string
): string {
return decrypt_message(handle, sessionId, ciphertext);
}

/**
Expand Down
45 changes: 37 additions & 8 deletions bindings/wasm/tests/wasmpkg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,48 @@ it("can instantiate", async () => {

it("can run self test", async () => {
const xmtp = await XMTPWasm.initialize();
const xmtpv3 = await xmtp.getXMTPv3();
const res = await xmtpv3.selfTest();
const xmtpv3 = xmtp.getXMTPv3();
const res = xmtpv3.selfTest();
expect(res).toBe(true);
});

it("can do a simple conversation", async () => {
const wasm = await XMTPWasm.initialize();
const alice = await wasm.newVoodooInstance();
const bob = await wasm.newVoodooInstance();
const alice = wasm.newVoodooInstance();
const bob = wasm.newVoodooInstance();

const outboundJson = await alice.createOutboundSession(bob, "hello there");
const { sessionId, payload } = await alice.createOutboundSession(
bob,
"hello there"
);
expect(typeof sessionId).toBe("string");
// Unused, but test JSON parseable
const _ = JSON.parse(outboundJson);
const inboundPlaintext = await bob.createInboundSession(alice, outboundJson);
expect(inboundPlaintext).toBe("hello there");
const _ = JSON.parse(payload);
const { payload: inboundPayload } = await bob.createInboundSession(
alice,
payload
);
expect(inboundPayload).toBe("hello there");
});

it("can send a message", async () => {
const wasm = await XMTPWasm.initialize();
const alice = wasm.newVoodooInstance();
const bob = wasm.newVoodooInstance();

const { sessionId, payload } = await alice.createOutboundSession(
bob,
"hello there"
);
await bob.createInboundSession(alice, payload);
expect(typeof sessionId).toBe("string");

const msg = "hello there";
const encrypted = await alice.encryptMessage(sessionId, msg);
expect(typeof encrypted).toBe("string");

// Alice can't decrypt her own message. Does work for Bob though
// const decrypted = await alice.decryptMessage(sessionId, encrypted);
const decrypted = await bob.decryptMessage(sessionId, encrypted);
expect(decrypted).toBe(msg);
});

0 comments on commit 7701394

Please sign in to comment.