encryption, code cleanup, nice types, bug fixes, and more

This commit is contained in:
Zoe
2025-09-03 17:36:21 -05:00
parent 2357e44923
commit 1b8ac362b6
12 changed files with 577 additions and 150 deletions

View File

@@ -1,13 +1,7 @@
import { get } from 'svelte/store';
import { ws } from '../stores/websocketStore';
interface WebRTCPeerCallbacks {
onConnected: () => void;
onMessage: (message: string | ArrayBuffer) => void;
onDataChannelOpen: () => void;
onNegotiationNeeded: () => void;
onError: (error: any) => void;
}
import { roomKey } from '../utils/webrtcUtil';
import { WebRTCPacketType, type KeyStore, type WebRTCPeerCallbacks } from '../types/webrtc';
export class WebRTCPeer {
private peer: RTCPeerConnection | null = null;
@@ -15,10 +9,16 @@ export class WebRTCPeer {
private isInitiator: boolean;
private roomId: string;
private callbacks: WebRTCPeerCallbacks;
private keys: KeyStore = {
localKeys: null,
peersPublicKey: null,
};
private iceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun.l.google.com:5349" },
{ urls: "stun:stun1.l.google.com:3478" },
{ urls: "stun:stun1.l.google.com:5349" },
];
constructor(roomId: string, isInitiator: boolean, callbacks: WebRTCPeerCallbacks) {
@@ -80,14 +80,90 @@ export class WebRTCPeer {
}
private setupDataChannelEvents(channel: RTCDataChannel) {
channel.onopen = () => {
channel.binaryType = "arraybuffer";
channel.onopen = async () => {
console.log('data channel open');
this.callbacks.onDataChannelOpen();
try {
if (this.isInitiator) {
await this.startKeyExchange();
}
} catch (e) {
console.error("Error starting key exchange:", e);
this.callbacks.onError(e);
}
};
channel.onmessage = (event) => {
channel.onmessage = async (event: MessageEvent<ArrayBuffer>) => {
console.log('data channel message:', event.data);
this.callbacks.onMessage(event.data);
// event is binary data, we need to parse it, convert it into a WebRTCMessage, and then decrypt it if
// necessary
let data = new Uint8Array(event.data);
const encrypted = (data[0] >> 7) & 1;
const type = data[0] & 0b01111111;
data = data.slice(1);
console.log("parsed data", data, encrypted, type);
if (type == WebRTCPacketType.KEY_EXCHANGE) {
if (this.keys.peersPublicKey) {
console.error("Key exchange already done");
return;
}
console.log("Received key exchange", data.buffer);
// let textDecoder = new TextDecoder();
// let dataString = textDecoder.decode(data.buffer);
// console.log("Received key exchange", dataString);
// let json = JSON.parse(dataString);
let unwrappingKey = get(roomKey);
if (!unwrappingKey.key) throw new Error("Room key not set");
this.keys.peersPublicKey = await window.crypto.subtle.unwrapKey(
"jwk",
data,
unwrappingKey.key,
{
name: "AES-KW",
length: 256,
},
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt"],
);
// if our keys are not generated, start the reponding side of the key exchange
if (!this.keys.localKeys) {
await this.startKeyExchange();
}
// by this point, both peers should have exchanged their keys
this.callbacks.onKeyExchangeDone();
return;
}
if (encrypted) {
data = new Uint8Array(await this.decrypt(data.buffer));
}
let message = {
type: type as WebRTCPacketType,
data: data.buffer,
};
this.callbacks.onMessage(message);
};
channel.onclose = () => {
@@ -105,8 +181,11 @@ export class WebRTCPeer {
if (!this.peer) throw new Error('Peer not initialized');
try {
const offer = await this.peer.createOffer();
await this.peer.setLocalDescription(offer);
const offer = await this.peer.createOffer()
console.log("Sending offer", offer);
await this.peer.setLocalDescription(offer)
get(ws).send(JSON.stringify({
type: 'offer',
@@ -115,10 +194,9 @@ export class WebRTCPeer {
sdp: offer,
},
}));
} catch (error) {
console.error('Error creating offer:', error);
this.callbacks.onError(error);
console.info('Error creating offer:', error);
// should trigger re-negotiation
}
}
@@ -141,6 +219,8 @@ export class WebRTCPeer {
const answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
console.log("Sending answer", answer);
get(ws).send(JSON.stringify({
type: 'answer',
data: {
@@ -166,9 +246,112 @@ export class WebRTCPeer {
}
}
public send(data: string | ArrayBuffer) {
private async generateKeyPair() {
console.log("Generating key pair");
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt"],
);
if (keyPair instanceof CryptoKey) {
throw new Error("Key pair not generated");
}
this.keys.localKeys = keyPair;
}
private async startKeyExchange() {
console.log("Starting key exchange");
await this.generateKeyPair();
if (!this.keys.localKeys) throw new Error("Key pair not generated");
let wrappingKey = get(roomKey);
if (!wrappingKey.key) throw new Error("Room key not set");
console.log("wrapping key", this.keys.localKeys.publicKey, wrappingKey.key);
const exported = await window.crypto.subtle.wrapKey(
"jwk",
this.keys.localKeys.publicKey,
wrappingKey.key,
{
name: "AES-KW",
length: 256,
},
);
console.log("wrapping key exported", exported);
const exportedKeyBuffer = exported;
console.log("exported key buffer", exportedKeyBuffer);
this.send(exportedKeyBuffer, WebRTCPacketType.KEY_EXCHANGE);
}
private async encrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
if (!this.keys.peersPublicKey) throw new Error("Peer's public key not set");
return await window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
this.keys.peersPublicKey,
data,
);
}
private async decrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
if (!this.keys.localKeys) throw new Error("Local keypair not generated");
return await window.crypto.subtle.decrypt(
{
name: "RSA-OAEP",
},
this.keys.localKeys.privateKey,
data,
);
}
public async send(data: ArrayBuffer, type: WebRTCPacketType) {
console.log("Sending message of type", type, "with data", data);
if (!this.dataChannel || this.dataChannel.readyState !== 'open') throw new Error('Data channel not initialized');
this.dataChannel.send(data);
console.log(this.keys)
let header = (type & 0x7F);
// the key exchange is done, encrypt the message
if (this.keys.peersPublicKey && type != WebRTCPacketType.KEY_EXCHANGE) {
console.log("Sending encrypted message", data);
let encryptedData = await this.encrypt(data);
console.log("Encrypted data", encryptedData);
header |= 1 << 7;
let buf = new Uint8Array(encryptedData.byteLength + 1);
buf[0] = header;
buf.subarray(1).set(new Uint8Array(encryptedData));
this.dataChannel.send(buf.buffer);
} else {
console.log("Sending unencrypted message", data);
// the key exchange is not done yet, send the message unencrypted
let buf = new Uint8Array(data.byteLength + 1);
buf[0] = header;
buf.subarray(1).set(new Uint8Array(data));
this.dataChannel.send(buf.buffer);
}
}
public close() {