#![cfg(target_arch = "wasm32")]
use gap::{GapAccept, GapClient as RustGapClient};
use gbp_core::MemberId;
use gbp_node::{Event, GroupNode as RustGroupNode};
use gbp_sframe::{
CipherSuite as RustCipherSuite, SFrameDecryptor as RustSFrameDecryptor,
SFrameEncryptor as RustSFrameEncryptor, SFrameSession as RustSFrameSession,
};
use gsp::{GspAccept, GspClient as RustGspClient, GspError};
use gtp::{GtpAccept, GtpClient as RustGtpClient};
use js_sys::{Array, Object, Reflect, Uint8Array};
use openmls::prelude::tls_codec::Serialize as TlsSerialize;
use openmls::prelude::{KeyPackageIn, OpenMlsProvider, ProtocolVersion};
use tls_codec::Deserialize as TlsDeserialize;
use std::cell::RefCell;
use wasm_bindgen::prelude::*;
fn set(obj: &Object, key: &str, val: &JsValue) {
Reflect::set(obj, &JsValue::from_str(key), val).unwrap_throw();
}
fn u8s(bytes: &[u8]) -> JsValue {
Uint8Array::from(bytes).into()
}
fn js_err(msg: impl std::fmt::Display) -> JsValue {
JsValue::from_str(&msg.to_string())
}
fn event_to_js(ev: Event) -> JsValue {
let obj = Object::new();
match ev {
Event::PayloadReceived(p) => {
set(&obj, "kind", &"payload_received".into());
set(&obj, "streamType", &JsValue::from_f64(p.stream_type.as_u8() as f64));
set(&obj, "plaintext", &u8s(&p.plaintext));
set(&obj, "sequenceNo", &JsValue::from_f64(p.sequence_no as f64));
set(&obj, "codec", &JsValue::from_f64(p.codec as u8 as f64));
}
Event::StateChanged { from, to } => {
set(&obj, "kind", &"state_changed".into());
set(&obj, "from", &JsValue::from_str(&from.to_string()));
set(&obj, "to", &JsValue::from_str(&to.to_string()));
}
Event::EpochAdvanced { epoch, transition_id } => {
set(&obj, "kind", &"epoch_advanced".into());
set(&obj, "epoch", &js_sys::BigInt::from(epoch).into());
set(&obj, "transitionId", &JsValue::from_f64(transition_id as f64));
}
Event::Error { code, reason, fatal, retryable, .. } => {
set(&obj, "kind", &"error".into());
set(&obj, "code", &JsValue::from_f64(code as f64));
set(&obj, "reason", &JsValue::from_str(&reason));
set(&obj, "fatal", &JsValue::from_bool(fatal));
set(&obj, "retryable", &JsValue::from_bool(retryable));
}
Event::Control { from, opcode, transition_id, .. } => {
set(&obj, "kind", &"control".into());
set(&obj, "from", &JsValue::from_f64(from as f64));
set(&obj, "opcode", &JsValue::from_f64(opcode as u8 as f64));
set(&obj, "transitionId", &JsValue::from_f64(transition_id as f64));
}
_ => {
set(&obj, "kind", &"other".into());
}
}
obj.into()
}
fn codec_from(c: Option<u8>) -> gbp_core::PayloadCodec {
c.and_then(gbp_core::PayloadCodec::from_u8)
.unwrap_or(gbp_core::PayloadCodec::Cbor)
}
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PayloadCodec {
Cbor = 0,
Protobuf = 1,
FlatBuffers = 2,
}
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SignalType {
Join = 100,
Leave = 101,
RoleChange = 102,
Mute = 200,
Unmute = 201,
StreamStart = 300,
StreamStop = 301,
CodecUpdate = 400,
}
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ControlOpcode {
PrepareTransition = 0x0001,
ReadyForTransition = 0x0002,
ExecuteTransition = 0x0003,
AbortTransition = 0x0004,
GroupStateDigestRequest = 0x0005,
GroupStateDigestResponse = 0x0006,
ReportInvalidCommit = 0x0007,
CapabilitiesAdvertise = 0x0008,
Ack = 0x0009,
Nack = 0x000A,
}
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CipherSuite {
Aes128Gcm = 0,
Aes256Gcm = 1,
}
#[wasm_bindgen]
pub struct MlsContext {
inner: RefCell<gbp_mls::MlsContext>,
kp_bytes: Vec<u8>,
}
#[wasm_bindgen]
impl MlsContext {
#[wasm_bindgen(js_name = "create")]
pub fn create(user_id: &str) -> Result<MlsContext, JsValue> {
let (ctx, kpb) = gbp_mls::MlsContext::new_member(user_id.as_bytes())
.map_err(|e| js_err(e))?;
let kp_bytes = kpb.key_package()
.tls_serialize_detached()
.map_err(|e| js_err(format!("kp serialize: {e:?}")))?;
Ok(MlsContext { inner: RefCell::new(ctx), kp_bytes })
}
#[wasm_bindgen(getter, js_name = "keyPackage")]
pub fn key_package(&self) -> Uint8Array {
Uint8Array::from(self.kp_bytes.as_slice())
}
#[wasm_bindgen(getter)]
pub fn epoch(&self) -> u64 {
self.inner.borrow().epoch()
}
#[wasm_bindgen(getter, js_name = "groupId")]
pub fn group_id(&self) -> Uint8Array {
Uint8Array::from(self.inner.borrow().group_id_16().as_slice())
}
#[wasm_bindgen(js_name = "invite")]
pub fn invite(&self, key_package_bytes: &[u8]) -> Result<Uint8Array, JsValue> {
let mut ctx = self.inner.borrow_mut();
let kp_in = KeyPackageIn::tls_deserialize(&mut key_package_bytes.as_ref())
.map_err(|e| js_err(format!("kp parse: {e:?}")))?;
let kp = kp_in
.validate(ctx.provider.crypto(), ProtocolVersion::Mls10)
.map_err(|e| js_err(format!("kp validate: {e:?}")))?;
let welcome = ctx.invite(&[kp]).map_err(|e| js_err(e))?;
Ok(Uint8Array::from(welcome.as_slice()))
}
#[wasm_bindgen(js_name = "inviteMany")]
pub fn invite_many(&self, key_packages: Array) -> Result<Uint8Array, JsValue> {
let mut ctx = self.inner.borrow_mut();
let mut kps = Vec::with_capacity(key_packages.length() as usize);
for v in key_packages.iter() {
let bytes = Uint8Array::new(&v).to_vec();
let kp_in = KeyPackageIn::tls_deserialize(&mut bytes.as_slice())
.map_err(|e| js_err(format!("kp parse: {e:?}")))?;
let kp = kp_in
.validate(ctx.provider.crypto(), ProtocolVersion::Mls10)
.map_err(|e| js_err(format!("kp validate: {e:?}")))?;
kps.push(kp);
}
if kps.is_empty() {
return Err(js_err("inviteMany: no key packages".to_string()));
}
let welcome = ctx.invite(&kps).map_err(|e| js_err(e))?;
Ok(Uint8Array::from(welcome.as_slice()))
}
#[wasm_bindgen(js_name = "acceptWelcome")]
pub fn accept_welcome(&self, welcome_bytes: &[u8]) -> Result<(), JsValue> {
self.inner.borrow_mut()
.accept_welcome(welcome_bytes)
.map_err(|e| js_err(e))
}
#[wasm_bindgen(js_name = "inviteFull")]
pub fn invite_full(&self, key_package_bytes: &[u8]) -> Result<JsValue, JsValue> {
let mut ctx = self.inner.borrow_mut();
let kp_in = KeyPackageIn::tls_deserialize(&mut key_package_bytes.as_ref())
.map_err(|e| js_err(format!("kp parse: {e:?}")))?;
let kp = kp_in
.validate(ctx.provider.crypto(), ProtocolVersion::Mls10)
.map_err(|e| js_err(format!("kp validate: {e:?}")))?;
let (commit, welcome) = ctx.invite_full(&[kp]).map_err(|e| js_err(e))?;
let obj = Object::new();
set(&obj, "commit", &u8s(&commit));
set(&obj, "welcome", &u8s(&welcome));
Ok(obj.into())
}
#[wasm_bindgen(js_name = "removeMember")]
pub fn remove_member(&self, leaf_index: u32) -> Result<Uint8Array, JsValue> {
let mut ctx = self.inner.borrow_mut();
let commit = ctx.remove_members(&[leaf_index]).map_err(|e| js_err(e))?;
Ok(Uint8Array::from(commit.as_slice()))
}
#[wasm_bindgen(js_name = "processMessage")]
pub fn process_message(&self, msg_bytes: &[u8]) -> Result<String, JsValue> {
let mut ctx = self.inner.borrow_mut();
let kind = ctx.process_message(msg_bytes).map_err(|e| js_err(e))?;
Ok(match kind {
gbp_mls::ProcessedKind::Commit => "commit",
gbp_mls::ProcessedKind::Application => "application",
gbp_mls::ProcessedKind::Proposal => "proposal",
gbp_mls::ProcessedKind::External => "external",
}
.to_string())
}
#[wasm_bindgen(js_name = "finalizeCommit")]
pub fn finalize_commit(&self) -> Result<(), JsValue> {
self.inner.borrow_mut()
.finalize_pending_commit()
.map_err(|e| js_err(e))
}
#[wasm_bindgen(js_name = "clearPendingCommit")]
pub fn clear_pending_commit(&self) -> Result<(), JsValue> {
self.inner.borrow_mut()
.clear_pending_commit()
.map_err(|e| js_err(e))
}
#[wasm_bindgen(js_name = "exportState")]
pub fn export_state(&self) -> Result<Uint8Array, JsValue> {
let bytes = self.inner.borrow().export_state().map_err(|e| js_err(e))?;
Ok(Uint8Array::from(bytes.as_slice()))
}
#[wasm_bindgen(js_name = "restoreState")]
pub fn restore_state(blob: &[u8]) -> Result<MlsContext, JsValue> {
let ctx = gbp_mls::MlsContext::restore_state(blob).map_err(|e| js_err(e))?;
Ok(MlsContext { inner: RefCell::new(ctx), kp_bytes: Vec::new() })
}
}
#[wasm_bindgen]
pub struct GroupNode {
inner: RefCell<RustGroupNode>,
}
#[wasm_bindgen]
impl GroupNode {
#[wasm_bindgen(js_name = "create")]
pub fn create(leaf_index: u32, group_id_bytes: &[u8]) -> GroupNode {
let gid: [u8; 16] = group_id_bytes.try_into().unwrap_or([0u8; 16]);
GroupNode { inner: RefCell::new(RustGroupNode::new(leaf_index as MemberId, gid)) }
}
#[wasm_bindgen(js_name = "bootstrapAsCreator")]
pub fn bootstrap_as_creator(&self, epoch: u64) {
self.inner.borrow_mut().bootstrap_as_creator(epoch);
}
#[wasm_bindgen(js_name = "bootstrapAsJoiner")]
pub fn bootstrap_as_joiner(&self, epoch: u64, expected_first_tid: u32) {
self.inner.borrow_mut().bootstrap_as_joiner(epoch, expected_first_tid);
}
#[wasm_bindgen(js_name = "exportOutSeq")]
pub fn export_out_seq(&self) -> Uint8Array {
Uint8Array::from(self.inner.borrow().export_out_seq().as_slice())
}
#[wasm_bindgen(js_name = "restoreOutSeq")]
pub fn restore_out_seq(&self, bytes: &[u8]) {
self.inner.borrow_mut().restore_out_seq(bytes);
}
#[wasm_bindgen(js_name = "onWire")]
pub fn on_wire(&self, mls: &MlsContext, wire_bytes: &[u8]) -> Array {
let mut node = self.inner.borrow_mut();
let mut mls_inner = mls.inner.borrow_mut();
let events = node.on_wire(&mut *mls_inner, wire_bytes).unwrap_or_default();
let arr = Array::new();
for ev in events {
arr.push(&event_to_js(ev));
}
arr
}
#[wasm_bindgen(js_name = "checkTimeouts")]
pub fn check_timeouts(&self) -> Array {
let arr = Array::new();
for ev in self.inner.borrow_mut().check_timeouts() {
arr.push(&event_to_js(ev));
}
arr
}
#[wasm_bindgen(getter, js_name = "lastTransitionId")]
pub fn last_transition_id(&self) -> u32 {
self.inner.borrow().last_transition_id
}
#[wasm_bindgen(getter, js_name = "currentEpoch")]
pub fn current_epoch(&self) -> u64 {
self.inner.borrow().current_epoch
}
#[wasm_bindgen(getter, js_name = "memberId")]
pub fn member_id(&self) -> u32 {
self.inner.borrow().member_id
}
#[wasm_bindgen(js_name = "sendControl")]
pub fn send_control(
&self,
mls: &MlsContext,
target: u32,
opcode: u16,
transition_id: u32,
request_id: u32,
args: &[u8],
) -> Result<JsValue, JsValue> {
let op = gbp_core::ControlOpcode::try_from(opcode)
.map_err(|_| js_err(format!("bad opcode 0x{opcode:04X}")))?;
let mut node = self.inner.borrow_mut();
let mut m = mls.inner.borrow_mut();
let of = node
.send_control(&mut *m, target as MemberId, op, transition_id, request_id, args.to_vec())
.map_err(|e| js_err(e))?;
let obj = Object::new();
set(&obj, "wire", &u8s(&of.wire));
set(&obj, "to", &JsValue::from_f64(of.to as f64));
Ok(obj.into())
}
#[wasm_bindgen(js_name = "applyTransition")]
pub fn apply_transition(&self, tid: u32) {
self.inner.borrow_mut().apply_transition(tid);
}
#[wasm_bindgen(js_name = "drainEvents")]
pub fn drain_events(&self) -> Array {
let arr = Array::new();
for ev in self.inner.borrow_mut().drain_events() {
arr.push(&event_to_js(ev));
}
arr
}
}
#[wasm_bindgen]
pub struct GtpClient {
inner: RefCell<RustGtpClient>,
}
#[wasm_bindgen]
impl GtpClient {
#[wasm_bindgen(js_name = "create")]
pub fn create() -> GtpClient {
GtpClient { inner: RefCell::new(RustGtpClient::new()) }
}
#[wasm_bindgen(js_name = "send")]
pub fn send(
&self,
node: &GroupNode,
mls: &MlsContext,
target: u32,
message_id: u64,
text: &str,
codec: Option<u8>,
) -> JsValue {
let mut gtp = self.inner.borrow_mut();
let mut n = node.inner.borrow_mut();
let mut m = mls.inner.borrow_mut();
match gtp.send(&mut *n, &mut *m, target as MemberId, message_id, text, codec_from(codec)) {
Ok(frame) => {
let obj = Object::new();
set(&obj, "wire", &u8s(&frame.wire));
set(&obj, "to", &JsValue::from_f64(frame.to as f64));
obj.into()
}
Err(_) => JsValue::NULL,
}
}
#[wasm_bindgen(js_name = "accept")]
pub fn accept(&self, plaintext: &[u8], epoch: u64, codec: Option<u8>) -> JsValue {
let mut gtp = self.inner.borrow_mut();
match gtp.accept(plaintext, epoch, codec_from(codec)) {
Ok(result) => {
let (msg, status) = match result {
GtpAccept::New(m) => (m, "new"),
GtpAccept::Duplicate(m) => (m, "duplicate"),
};
let text = String::from_utf8_lossy(&msg.content).into_owned();
let obj = Object::new();
set(&obj, "text", &JsValue::from_str(&text));
set(&obj, "messageId", &js_sys::BigInt::from(msg.message_id).into());
set(&obj, "senderId", &JsValue::from_f64(msg.sender_id as f64));
set(&obj, "status", &JsValue::from_str(status));
obj.into()
}
Err(_) => JsValue::NULL,
}
}
#[wasm_bindgen(js_name = "reset")]
pub fn reset(&self) {
self.inner.borrow_mut().reset();
}
}
fn outbound_to_js(of: gbp_node::OutboundFrame) -> JsValue {
let obj = Object::new();
set(&obj, "wire", &u8s(&of.wire));
set(&obj, "to", &JsValue::from_f64(of.to as f64));
obj.into()
}
fn gap_payload_to_js(status: &str, p: gap::GapPayload) -> JsValue {
let obj = Object::new();
set(&obj, "status", &JsValue::from_str(status));
set(&obj, "source", &JsValue::from_f64(p.media_source_id as f64));
set(&obj, "seq", &JsValue::from_f64(p.rtp_sequence as f64));
set(&obj, "rtpTimestamp", &js_sys::BigInt::from(p.rtp_timestamp).into());
set(&obj, "opus", &u8s(&p.opus_frame.into_vec()));
obj.into()
}
fn cipher_suite_from(v: u8) -> Result<RustCipherSuite, JsValue> {
RustCipherSuite::from_u8(v).ok_or_else(|| js_err(format!("unknown ciphersuite {v}")))
}
#[wasm_bindgen]
pub struct GapClient {
inner: RefCell<RustGapClient>,
}
#[wasm_bindgen]
impl GapClient {
#[wasm_bindgen(js_name = "create")]
pub fn create() -> GapClient {
GapClient { inner: RefCell::new(RustGapClient::new()) }
}
#[wasm_bindgen(js_name = "send")]
pub fn send(
&self,
node: &GroupNode,
mls: &MlsContext,
target: u32,
media_source_id: u32,
rtp_timestamp: u64,
opus: &[u8],
codec: Option<u8>,
) -> JsValue {
let mut gap = self.inner.borrow_mut();
let mut n = node.inner.borrow_mut();
let mut m = mls.inner.borrow_mut();
match gap.send(
&mut *n,
&mut *m,
target as MemberId,
media_source_id,
rtp_timestamp,
opus.to_vec(),
codec_from(codec),
) {
Ok(of) => outbound_to_js(of),
Err(_) => JsValue::NULL,
}
}
#[wasm_bindgen(js_name = "accept")]
pub fn accept(&self, plaintext: &[u8], epoch: u64, codec: Option<u8>) -> JsValue {
let mut gap = self.inner.borrow_mut();
match gap.accept(plaintext, epoch, codec_from(codec)) {
Ok(GapAccept::New(p)) => gap_payload_to_js("new", p),
Ok(GapAccept::Late(p)) => gap_payload_to_js("late", p),
Err(_) => JsValue::NULL,
}
}
#[wasm_bindgen(js_name = "reset")]
pub fn reset(&self) {
self.inner.borrow_mut().reset();
}
}
#[wasm_bindgen]
pub struct GspClient {
inner: RefCell<RustGspClient>,
}
#[wasm_bindgen]
impl GspClient {
#[wasm_bindgen(js_name = "create")]
pub fn create() -> GspClient {
GspClient { inner: RefCell::new(RustGspClient::new()) }
}
#[wasm_bindgen(js_name = "send")]
pub fn send(
&self,
node: &GroupNode,
mls: &MlsContext,
target: u32,
signal_type: u32,
role_claim: u32,
request_id: u32,
codec: Option<u8>,
) -> Result<JsValue, JsValue> {
let sig = gbp_core::SignalType::try_from(signal_type)
.map_err(|_| js_err(format!("bad signal {signal_type}")))?;
let mut gsp = self.inner.borrow_mut();
let mut n = node.inner.borrow_mut();
let mut m = mls.inner.borrow_mut();
gsp.send(&mut *n, &mut *m, target as MemberId, sig, role_claim, request_id, codec_from(codec))
.map(outbound_to_js)
.map_err(|e| js_err(e))
}
#[wasm_bindgen(js_name = "sendWithArgs")]
pub fn send_with_args(
&self,
node: &GroupNode,
mls: &MlsContext,
target: u32,
signal_type: u32,
role_claim: u32,
request_id: u32,
args: &[u8],
codec: Option<u8>,
) -> Result<JsValue, JsValue> {
let sig = gbp_core::SignalType::try_from(signal_type)
.map_err(|_| js_err(format!("bad signal {signal_type}")))?;
let mut gsp = self.inner.borrow_mut();
let mut n = node.inner.borrow_mut();
let mut m = mls.inner.borrow_mut();
gsp.send_with_args(&mut *n, &mut *m, target as MemberId, sig, role_claim, request_id, args, codec_from(codec))
.map(outbound_to_js)
.map_err(|e| js_err(e))
}
#[wasm_bindgen(js_name = "accept")]
pub fn accept(&self, plaintext: &[u8], epoch: u64, codec: Option<u8>) -> Result<JsValue, JsValue> {
let mut gsp = self.inner.borrow_mut();
match gsp.accept(plaintext, epoch, codec_from(codec)) {
Ok(GspAccept { signal, sender_id, role_claim, request_id }) => {
let obj = Object::new();
set(&obj, "status", &JsValue::from_str("new"));
set(&obj, "signal", &JsValue::from_str(signal.name()));
set(&obj, "signalCode", &JsValue::from_f64(signal as u32 as f64));
set(&obj, "sender", &JsValue::from_f64(sender_id as f64));
set(&obj, "roleClaim", &JsValue::from_f64(role_claim as f64));
set(&obj, "requestId", &JsValue::from_f64(request_id as f64));
Ok(obj.into())
}
Err(GspError::DuplicateRequest(rid)) => {
let obj = Object::new();
set(&obj, "status", &JsValue::from_str("duplicate"));
set(&obj, "requestId", &JsValue::from_f64(rid as f64));
Ok(obj.into())
}
Err(e) => Err(js_err(e)),
}
}
#[wasm_bindgen(js_name = "reset")]
pub fn reset(&self) {
self.inner.borrow_mut().reset();
}
}
#[wasm_bindgen]
pub struct SFrameSession {
inner: RefCell<RustSFrameDecryptor>,
}
#[wasm_bindgen]
impl SFrameSession {
#[wasm_bindgen(js_name = "create")]
pub fn create(mls: &MlsContext, label: &str, suite: u8) -> Result<SFrameSession, JsValue> {
let suite = cipher_suite_from(suite)?;
let m = mls.inner.borrow();
let session = RustSFrameSession::from_mls(&m, label, suite).map_err(|e| js_err(e))?;
Ok(SFrameSession { inner: RefCell::new(session.decryptor()) })
}
#[wasm_bindgen(js_name = "createEncryptor")]
pub fn create_encryptor(
&self,
mls: &MlsContext,
leaf_index: u32,
label: &str,
suite: u8,
) -> Result<SFrameEncryptor, JsValue> {
let suite = cipher_suite_from(suite)?;
let m = mls.inner.borrow();
let session = RustSFrameSession::from_mls(&m, label, suite).map_err(|e| js_err(e))?;
Ok(SFrameEncryptor { inner: RefCell::new(session.encryptor(leaf_index)) })
}
#[wasm_bindgen(js_name = "decrypt")]
pub fn decrypt(&self, payload: &[u8], aad: &[u8]) -> Result<JsValue, JsValue> {
let mut dec = self.inner.borrow_mut();
match dec.decrypt(payload, aad) {
Ok((plaintext, leaf)) => {
let obj = Object::new();
set(&obj, "plaintext", &u8s(&plaintext));
set(&obj, "senderLeaf", &JsValue::from_f64(leaf as f64));
Ok(obj.into())
}
Err(e) => Err(js_err(e)),
}
}
}
#[wasm_bindgen]
pub struct SFrameEncryptor {
inner: RefCell<RustSFrameEncryptor>,
}
#[wasm_bindgen]
impl SFrameEncryptor {
#[wasm_bindgen(js_name = "encrypt")]
pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Uint8Array, JsValue> {
let mut enc = self.inner.borrow_mut();
let ct = enc.encrypt(plaintext, aad).map_err(|e| js_err(e))?;
Ok(Uint8Array::from(ct.as_slice()))
}
}
#[cfg(test)]
mod tests;