allow encryption and authorization over untrusted signaling server

This commit is contained in:
Kevin Jahns 2019-12-10 00:47:21 +01:00
parent fcc52ff25c
commit b6466fb9ba
10 changed files with 3122 additions and 158 deletions

View file

@ -3,7 +3,8 @@
It propagates document updates directly to all users via WebRTC. It propagates document updates directly to all users via WebRTC.
* Fast message propagation * Fast message propagation
* No setup required, a default signalling server is available * Encryption and authorization over untrusted signaling server
* No setup required, public signaling servers are available
* Very little server load * Very little server load
* Not suited for a large amount of collaborators on a single document (each peer is connected to each other) * Not suited for a large amount of collaborators on a single document (each peer is connected to each other)
@ -23,23 +24,23 @@ import { WebrtcProvider } from '../src/y-webrtc.js'
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
// clients connected to the same room-name share document updates // clients connected to the same room-name share document updates
const provider = new WebrtcProvider('your-room-name', ydoc) const provider = new WebrtcProvider('your-room-name', ydoc, { password: 'optional-room-password' })
const yarray = ydoc.get('array', Y.Array) const yarray = ydoc.get('array', Y.Array)
``` ```
##### Signalling ##### Signaling
The peers find each other by connecting to a signalling server. This package implements a small signalling server in `./bin/server.js`. The peers find each other by connecting to a signaling server. This package implements a small signaling server in `./bin/server.js`.
```sh ```sh
# start signalling server # start signaling server
PORT=4444 node ./bin/server.js PORT=4444 node ./bin/server.js
``` ```
Peers using the same signalling server will find each other. You can specify several custom signalling servers like so: Peers using the same signaling server will find each other. You can specify several custom signaling servers like so:
```js ```js
const provider = new WebrtcProvider('your-room-name', ydoc, { signalling: ['wss://y-webrtc-ckynwnzncc.now.sh', 'ws://localhost:4444'] }) const provider = new WebrtcProvider('your-room-name', ydoc, { signaling: ['wss://y-webrtc-ckynwnzncc.now.sh', 'ws://localhost:4444'] })
``` ```
### Logging ### Logging

View file

@ -106,15 +106,13 @@ const onconnection = conn => {
}) })
break break
case 'publish': case 'publish':
if (message.topics) { if (message.topic) {
/** const receivers = topics.get(message.topic)
* @type {Set<any>} if (receivers) {
*/ receivers.forEach(receiver =>
const receivers = new Set() send(receiver, message)
message.topics.forEach(/** @param {string} topicName */ topicName => { )
(topics.get(topicName) || []).forEach(sub => receivers.add(sub)) }
})
receivers.forEach(receiver => send(receiver, message))
} }
break break
case 'ping': case 'ping':
@ -138,4 +136,4 @@ server.on('upgrade', (request, socket, head) => {
server.listen(port) server.listen(port)
console.log('Signalling server running on localhost:', port) console.log('Signaling server running on localhost:', port)

9
index.html Normal file
View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Testing y-webrtc</title>
</head>
<body>
<script type="module" src="./dist/test.js"></script>
</body>
</html>

2751
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,12 +7,14 @@
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"start": "node ./bin/server.js", "start": "node ./bin/server.js",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
"dist": "rm -rf dist && rollup -c", "dist": "rm -rf dist && rollup -c",
"watch": "rollup -wc",
"lint": "standard", "lint": "standard",
"preversion": "npm run lint && npm run dist" "preversion": "npm run lint && npm run dist"
}, },
"bin": { "bin": {
"y-webrtc-signalling": "./bin/server.js" "y-webrtc-signaling": "./bin/server.js"
}, },
"files": [ "files": [
"dist/*", "dist/*",
@ -40,9 +42,11 @@
}, },
"dependencies": { "dependencies": {
"simple-peer": "^9.6.2", "simple-peer": "^9.6.2",
"lib0": "^0.1.5" "lib0": "^0.1.6"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^5.0.0",
"live-server": "^1.2.1",
"rollup": "^1.27.8", "rollup": "^1.27.8",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
@ -50,8 +54,7 @@
"rollup-plugin-terser": "^5.1.2", "rollup-plugin-terser": "^5.1.2",
"standard": "^12.0.1", "standard": "^12.0.1",
"y-protocols": "^0.1.0", "y-protocols": "^0.1.0",
"lib0": "^0.1.5", "yjs": "^13.0.0-102"
"yjs": "13.0.0-102"
}, },
"peerDependenies": { "peerDependenies": {
"yjs": ">=13.0.0-102" "yjs": ">=13.0.0-102"

View file

@ -79,6 +79,15 @@ export default [
sourcemap: true sourcemap: true
}], }],
plugins plugins
}, {
input: './test/index.js',
output: [{
name: 'test',
file: 'dist/test.js',
format: 'iife',
sourcemap: true
}],
plugins
}, { }, {
input: './src/y-webrtc.js', input: './src/y-webrtc.js',
external: id => /^(lib0|yjs|y-protocols|simple-peer)/.test(id), external: id => /^(lib0|yjs|y-protocols|simple-peer)/.test(id),

95
src/crypto.js Normal file
View file

@ -0,0 +1,95 @@
/* eslint-env browser */
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as buffer from 'lib0/buffer.js'
import * as promise from 'lib0/promise.js'
import * as error from 'lib0/error.js'
import * as string from 'lib0/string.js'
/**
* @param {string} secret
* @param {string} roomName
* @return {PromiseLike<CryptoKey>}
*/
export const deriveKey = (secret, roomName) => {
const secretBuffer = string.encodeUtf8(secret).buffer
const salt = string.encodeUtf8(roomName).buffer
return crypto.subtle.importKey(
'raw',
secretBuffer,
'PBKDF2',
false,
['deriveKey']
).then(keyMaterial =>
crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{
name: 'AES-GCM',
length: 256
},
true,
[ 'encrypt', 'decrypt' ]
)
)
}
/**
* @param {any} data A json object to be encrypted
* @param {CryptoKey} key
* @return {PromiseLike<string>} encrypted, base64 encoded message
*/
export const encrypt = (data, key) => {
const iv = crypto.getRandomValues(new Uint8Array(12))
const dataEncoder = encoding.createEncoder()
encoding.writeAny(dataEncoder, data)
const dataBuffer = encoding.toUint8Array(dataEncoder)
return crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
dataBuffer
).then(cipher => {
const encryptedDataEncoder = encoding.createEncoder()
encoding.writeVarString(encryptedDataEncoder, 'AES-GCM')
encoding.writeVarUint8Array(encryptedDataEncoder, iv)
encoding.writeVarUint8Array(encryptedDataEncoder, new Uint8Array(cipher))
return buffer.toBase64(encoding.toUint8Array(encryptedDataEncoder))
})
}
/**
* @param {string} data
* @param {CryptoKey} key
* @return {PromiseLike<any>} decrypted object
*/
export const decrypt = (data, key) => {
if (typeof data !== 'string') {
return promise.reject()
}
const dataDecoder = decoding.createDecoder(buffer.fromBase64(data))
const algorithm = decoding.readVarString(dataDecoder)
if (algorithm !== 'AES-GCM') {
promise.reject(error.create('Unknown encryption algorithm'))
}
const iv = decoding.readVarUint8Array(dataDecoder)
const cipher = decoding.readVarUint8Array(dataDecoder)
return crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
key,
cipher
).then(decryptedValue =>
decoding.readAny(decoding.createDecoder(new Uint8Array(decryptedValue)))
)
}

View file

@ -5,13 +5,16 @@ import * as random from 'lib0/random.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable.js'
import * as logging from 'lib0/logging.js'
import * as promise from 'lib0/promise.js'
import * as Y from 'yjs' // eslint-disable-line import * as Y from 'yjs' // eslint-disable-line
import Peer from 'simple-peer/simplepeer.min.js' import Peer from 'simple-peer/simplepeer.min.js'
import * as syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync.js'
import * as awarenessProtocol from 'y-protocols/awareness.js' import * as awarenessProtocol from 'y-protocols/awareness.js'
import * as logging from 'lib0/logging.js' import * as cryptoutils from './crypto.js'
const log = logging.createModuleLogger('y-webrtc') const log = logging.createModuleLogger('y-webrtc')
@ -19,36 +22,30 @@ const messageSync = 0
const messageQueryAwareness = 3 const messageQueryAwareness = 3
const messageAwareness = 1 const messageAwareness = 1
const peerId = random.uuidv4() /**
* @type {Map<string, SignalingConn>}
*/
const signalingConns = new Map()
/** /**
* @type {Map<string, SignallingConn>} * @type {Map<string,Room>}
*/ */
const signallingConns = new Map() const rooms = new Map()
/**
* @type {Map<string, WebrtcConn>}
*/
const webrtcConns = new Map()
/** /**
* @type {Map<string,WebrtcRoom>} * @param {Room} room
*/ */
const webrtcRooms = new Map() const checkIsSynced = room => {
/**
* @param {WebrtcRoom} webrtcRoom
*/
const checkIsSynced = webrtcRoom => {
let synced = true let synced = true
webrtcRoom.peers.forEach(peer => { room.webrtcConns.forEach(peer => {
if (!peer.syncedRooms.has(webrtcRoom.name)) { if (!peer.synced) {
synced = false synced = false
} }
}) })
if ((!synced && webrtcRoom.synced) || (synced && !webrtcRoom.synced)) { if ((!synced && room.synced) || (synced && !room.synced)) {
webrtcRoom.synced = synced room.synced = synced
webrtcRoom.provider.emit('synced', [{ synced }]) room.provider.emit('synced', [{ synced }])
log('synced ', logging.BOLD, webrtcRoom.name, logging.UNBOLD, ' with all peers') log('synced ', logging.BOLD, room.name, logging.UNBOLD, ' with all peers')
} }
} }
@ -61,23 +58,21 @@ const readPeerMessage = (peerConn, buf) => {
const decoder = decoding.createDecoder(buf) const decoder = decoding.createDecoder(buf)
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
const messageType = decoding.readVarUint(decoder) const messageType = decoding.readVarUint(decoder)
const roomName = decoding.readVarString(decoder) const room = peerConn.room
const webrtcRoom = webrtcRooms.get(roomName) if (room === undefined) {
if (webrtcRoom === undefined) {
return null return null
} }
const provider = webrtcRoom.provider const provider = room.provider
const doc = webrtcRoom.doc const doc = room.doc
let sendReply = false let sendReply = false
switch (messageType) { switch (messageType) {
case messageSync: case messageSync:
encoding.writeVarUint(encoder, messageSync) encoding.writeVarUint(encoder, messageSync)
encoding.writeVarString(encoder, roomName) const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, room.provider)
const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, webrtcRoom.provider) if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !room.synced) {
if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !webrtcRoom.synced) { peerConn.synced = true
peerConn.syncedRooms.add(roomName) log('synced ', logging.BOLD, room.name, logging.UNBOLD, ' with ', logging.BOLD, peerConn.remotePeerId)
log('synced ', logging.BOLD, roomName, logging.UNBOLD, ' with ', logging.BOLD, peerConn.remotePeerId) checkIsSynced(room)
checkIsSynced(webrtcRoom)
} }
if (syncMessageType === syncProtocol.messageYjsSyncStep1) { if (syncMessageType === syncProtocol.messageYjsSyncStep1) {
sendReply = true sendReply = true
@ -85,7 +80,6 @@ const readPeerMessage = (peerConn, buf) => {
break break
case messageQueryAwareness: case messageQueryAwareness:
encoding.writeVarUint(encoder, messageAwareness) encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarString(encoder, roomName)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))) encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())))
sendReply = true sendReply = true
break break
@ -107,78 +101,70 @@ const readPeerMessage = (peerConn, buf) => {
* @param {WebrtcConn} webrtcConn * @param {WebrtcConn} webrtcConn
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
*/ */
const send = (webrtcConn, encoder) => { const sendWebrtcConn = (webrtcConn, encoder) => {
if (webrtcConn.connected) {
webrtcConn.peer.send(encoding.toUint8Array(encoder)) webrtcConn.peer.send(encoding.toUint8Array(encoder))
}
} }
/** /**
* @param {WebrtcRoom} webrtcRoom * @param {Room} room
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
*/ */
const broadcast = (webrtcRoom, encoder) => { const broadcastWebrtcConn = (room, encoder) => {
const m = encoding.toUint8Array(encoder) const m = encoding.toUint8Array(encoder)
webrtcRoom.peers.forEach(peer => peer.peer.send(m)) room.webrtcConns.forEach(conn => {
if (conn.connected) {
conn.peer.send(m)
}
})
} }
export class WebrtcConn { export class WebrtcConn {
/** /**
* @param {SignallingConn} signalingConn * @param {SignalingConn} signalingConn
* @param {boolean} initiator * @param {boolean} initiator
* @param {string} remotePeerId * @param {string} remotePeerId
* @param {Array<string>} announcedTopics * @param {Room} room
*/ */
constructor (signalingConn, initiator, remotePeerId, announcedTopics) { constructor (signalingConn, initiator, remotePeerId, room) {
log('establishing connection to ', logging.BOLD, remotePeerId) log('establishing connection to ', logging.BOLD, remotePeerId)
this.room = room
this.remotePeerId = remotePeerId this.remotePeerId = remotePeerId
this.closed = false this.closed = false
this.connected = false this.connected = false
/** this.synced = false
* @type {Set<string>}
*/
this.syncedRooms = new Set()
/** /**
* @type {any} * @type {any}
*/ */
this.peer = new Peer({ initiator }) this.peer = new Peer({ initiator })
this.peer.on('signal', data => { this.peer.on('signal', signal => {
signalingConn.send({ type: 'publish', topics: announcedTopics, to: remotePeerId, from: peerId, messageType: 'signal', data }) publishSignalingMessage(signalingConn, room, { to: remotePeerId, from: room.peerId, type: 'signal', signal })
}) })
this.peer.on('connect', () => { this.peer.on('connect', () => {
log('connected to ', logging.BOLD, remotePeerId) log('connected to ', logging.BOLD, remotePeerId)
this.connected = true this.connected = true
announcedTopics.forEach(roomName => {
const room = webrtcRooms.get(roomName)
if (room) {
// add peer to room
room.peers.add(this)
// send sync step 1 // send sync step 1
const provider = room.provider const provider = room.provider
const doc = provider.doc const doc = provider.doc
const awareness = provider.awareness const awareness = provider.awareness
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync) encoding.writeVarUint(encoder, messageSync)
encoding.writeVarString(encoder, room.name)
syncProtocol.writeSyncStep1(encoder, doc) syncProtocol.writeSyncStep1(encoder, doc)
send(this, encoder) sendWebrtcConn(this, encoder)
const awarenessStates = awareness.getStates() const awarenessStates = awareness.getStates()
if (awarenessStates.size > 0) { if (awarenessStates.size > 0) {
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness) encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarString(encoder, room.name)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys()))) encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys())))
send(this, encoder) sendWebrtcConn(this, encoder)
} }
}
})
}) })
this.peer.on('close', () => { this.peer.on('close', () => {
this.connected = false this.connected = false
this.closed = true this.closed = true
webrtcConns.delete(this.remotePeerId) room.webrtcConns.delete(this.remotePeerId)
webrtcRooms.forEach(room => {
room.peers.delete(this)
checkIsSynced(room) checkIsSynced(room)
})
this.peer.destroy() this.peer.destroy()
log('closed connection to ', logging.BOLD, remotePeerId) log('closed connection to ', logging.BOLD, remotePeerId)
}) })
@ -190,71 +176,128 @@ export class WebrtcConn {
this.peer.on('data', data => { this.peer.on('data', data => {
const answer = readPeerMessage(this, data) const answer = readPeerMessage(this, data)
if (answer !== null) { if (answer !== null) {
send(this, answer) sendWebrtcConn(this, answer)
} }
}) })
} }
} }
export class WebrtcRoom { export class Room {
/** /**
* @param {Y.Doc} doc * @param {Y.Doc} doc
* @param {WebrtcProvider} provider * @param {WebrtcProvider} provider
* @param {string} name * @param {string} name
* @param {CryptoKey|null} key
*/ */
constructor (doc, provider, name) { constructor (doc, provider, name, key) {
/** /**
* @type {Set<WebrtcConn>} * Do not assume that peerId is unique. This is only meant for sending signaling messages.
*
* @type {string}
*/ */
this.peers = new Set() this.peerId = random.uuidv4()
this.doc = doc this.doc = doc
this.provider = provider this.provider = provider
this.synced = false this.synced = false
this.name = name this.name = name
this.key = key
/**
* @type {Map<string, WebrtcConn>}
*/
this.webrtcConns = new Map()
} }
} }
export class SignallingConn extends ws.WebsocketClient { /**
* @param {Y.Doc} doc
* @param {WebrtcProvider} provider
* @param {string} name
* @param {CryptoKey|null} key
* @return {Room}
*/
const openRoom = (doc, provider, name, key) => {
// there must only be one room
if (rooms.has(name)) {
throw error.create('A Yjs Doc connected to that room already exists!')
}
const room = new Room(doc, provider, name, key)
rooms.set(name, /** @type {Room} */ (room))
// signal through all available signaling connections
signalingConns.forEach(conn => {
// only subcribe if connection is established, otherwise the conn automatically subscribes to all rooms
if (conn.connected) {
conn.send({ type: 'subscribe', topics: [name] })
publishSignalingMessage(conn, room, { type: 'announce', from: room.peerId })
}
})
return room
}
/**
* @param {SignalingConn} conn
* @param {Room} room
* @param {any} data
*/
const publishSignalingMessage = (conn, room, data) => {
if (room.key) {
cryptoutils.encrypt(data, room.key).then(data => {
conn.send({ type: 'publish', topic: room.name, data })
})
} else {
conn.send({ type: 'publish', topic: room.name, data })
}
}
export class SignalingConn extends ws.WebsocketClient {
constructor (url) { constructor (url) {
super(url) super(url)
/** /**
* @type {Set<WebrtcProvider>} * @type {Set<WebrtcProvider>}
*/ */
this.providers = new Set() this.providers = new Set()
this.afterOpen.push(() => ({ type: 'subscribe', topics: Array.from(webrtcRooms.keys()) })) this.on('connect', () => {
this.afterOpen.push(() => ({ type: 'publish', messageType: 'announce', topics: Array.from(webrtcRooms.keys()), from: peerId })) const topics = Array.from(rooms.keys())
this.send({ type: 'subscribe', topics })
rooms.forEach(room =>
publishSignalingMessage(this, room, { type: 'announce', from: room.peerId })
)
})
this.on('message', m => { this.on('message', m => {
if (m.from === peerId || (m.to !== undefined && m.to !== peerId)) {
return
}
switch (m.type) { switch (m.type) {
case 'publish': { case 'publish': {
switch (m.messageType) { const roomName = m.topic
const room = rooms.get(roomName)
if (room == null || typeof roomName !== 'string') {
return
}
const execMessage = data => {
const webrtcConns = room.webrtcConns
const peerId = room.peerId
if (data == null || data.from === peerId || (data.to !== undefined && data.to !== peerId)) {
return
}
switch (data.type) {
case 'announce': case 'announce':
map.setIfUndefined(webrtcConns, m.from, () => new WebrtcConn(this, true, m.from, m.topics)) map.setIfUndefined(webrtcConns, data.from, () => new WebrtcConn(this, true, data.from, room))
break break
case 'signal': case 'signal':
if (m.to === peerId) { if (data.to === peerId) {
map.setIfUndefined(webrtcConns, m.from, () => new WebrtcConn(this, false, m.from, m.topics)).peer.signal(m.data) map.setIfUndefined(webrtcConns, data.from, () => new WebrtcConn(this, false, data.from, room)).peer.signal(data.signal)
} }
break break
} }
} }
if (room.key) {
cryptoutils.decrypt(m.data, room.key).then(execMessage)
} else {
execMessage(m.data)
}
}
} }
}) })
this.on('connect', () => log(`connected (${url})`)) this.on('connect', () => log(`connected (${url})`))
this.on('disconnect', () => log(`disconnect (${url})`)) this.on('disconnect', () => log(`disconnect (${url})`))
} }
/**
* @param {Array<string>} rooms
*/
subscribe (rooms) {
// only subcribe if connection is established, otherwise the conn automatically subscribes to all webrtcRooms
if (this.connected) {
this.send({ type: 'subscribe', topics: rooms })
this.send({ type: 'publish', messageType: 'announce', topics: Array.from(webrtcRooms.keys()), from: peerId })
}
}
} }
/** /**
@ -262,27 +305,33 @@ export class SignallingConn extends ws.WebsocketClient {
*/ */
export class WebrtcProvider extends Observable { export class WebrtcProvider extends Observable {
/** /**
* @param {string} room * @param {string} roomName
* @param {Y.Doc} doc * @param {Y.Doc} doc
* @param {Object} [opts] * @param {Object} [opts]
* @param {Array<string>} [opts.signalling] * @param {Array<string>} [opts.signaling]
* @param {string?} [opts.password]
*/ */
constructor (room, doc, { signalling = ['wss://signalling.yjs.dev', 'wss://y-webrtc-hrxsloqrim.now.sh', 'wss://y-webrtc-signalling-eu.herokuapp.com', 'wss://y-webrtc-signalling-us.herokuapp.com'] } = {}) { constructor (roomName, doc, { signaling = ['wss://signaling.yjs.dev', 'wss://y-webrtc-uchplqjsol.now.sh', 'wss://y-webrtc-signaling-eu.herokuapp.com', 'wss://y-webrtc-signaling-us.herokuapp.com'], password = null } = {}) {
super() super()
this.room = room this.roomName = roomName
this.doc = doc this.doc = doc
this.signallingConns = [] this.signalingConns = []
signalling.forEach(url => { /**
const signallingConn = map.setIfUndefined(signallingConns, url, () => new SignallingConn(url)) * @type {PromiseLike<CryptoKey | null>}
this.signallingConns.push(signallingConn) */
signallingConn.providers.add(this) this.key = password ? cryptoutils.deriveKey(password, roomName) : /** @type {PromiseLike<null>} */ (promise.resolve(null))
signallingConn.subscribe([this.room]) signaling.forEach(url => {
const signalingConn = map.setIfUndefined(signalingConns, url, () => new SignalingConn(url))
this.signalingConns.push(signalingConn)
signalingConn.providers.add(this)
})
/**
* @type {Room|null}
*/
let room = null
this.key.then(key => {
room = openRoom(doc, this, roomName, key)
}) })
if (webrtcRooms.has(room)) {
throw error.create('A Yjs Doc connected to that room already exists!')
}
const webrtcRoom = new WebrtcRoom(doc, this, room)
webrtcRooms.set(room, webrtcRoom)
/** /**
* @type {awarenessProtocol.Awareness} * @type {awarenessProtocol.Awareness}
*/ */
@ -294,12 +343,11 @@ export class WebrtcProvider extends Observable {
* @param {any} origin * @param {any} origin
*/ */
this._docUpdateHandler = (update, origin) => { this._docUpdateHandler = (update, origin) => {
if (origin !== this || origin === null) { if (room !== null && (origin !== this || origin === null)) {
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync) encoding.writeVarUint(encoder, messageSync)
encoding.writeVarString(encoder, room)
syncProtocol.writeUpdate(encoder, update) syncProtocol.writeUpdate(encoder, update)
broadcast(webrtcRoom, encoder) broadcastWebrtcConn(room, encoder)
} }
} }
/** /**
@ -309,12 +357,13 @@ export class WebrtcProvider extends Observable {
* @param {any} origin * @param {any} origin
*/ */
this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => { this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
if (room !== null) {
const changedClients = added.concat(updated).concat(removed) const changedClients = added.concat(updated).concat(removed)
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness) encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarString(encoder, room)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)) encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
broadcast(webrtcRoom, encoder) broadcastWebrtcConn(room, encoder)
}
} }
this.doc.on('update', this._docUpdateHandler) this.doc.on('update', this._docUpdateHandler)
this.awareness.on('change', this._awarenessUpdateHandler) this.awareness.on('change', this._awarenessUpdateHandler)
@ -323,16 +372,20 @@ export class WebrtcProvider extends Observable {
}) })
} }
destroy () { destroy () {
this.signallingConns.forEach(conn => { super.destroy()
this.signalingConns.forEach(conn => {
conn.providers.delete(this) conn.providers.delete(this)
if (conn.providers.size === 0) { if (conn.providers.size === 0) {
conn.destroy() conn.destroy()
signallingConns.delete(this.room) signalingConns.delete(this.roomName)
} else { } else {
conn.send({ type: 'unsubscribe', topics: [this.room] }) conn.send({ type: 'unsubscribe', topics: [this.roomName] })
} }
}) })
webrtcRooms.delete(this.room) // need to wait for key before deleting room
this.key.then(() => {
rooms.delete(this.roomName)
})
this.doc.off('update', this._docUpdateHandler) this.doc.off('update', this._docUpdateHandler)
this.awareness.off('change', this._awarenessUpdateHandler) this.awareness.off('change', this._awarenessUpdateHandler)
super.destroy() super.destroy()

33
test/crypto.test.js Normal file
View file

@ -0,0 +1,33 @@
import * as cryptutils from '../src/crypto.js'
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
/**
* @param {t.TestCase} tc
*/
export const testReapeatEncryption = async tc => {
const secret = prng.word(tc.prng)
const roomName = prng.word(tc.prng)
const data = {
content: 'just a test',
number: 4
}
/**
* @type {any}
*/
let encrypted, decrypted, key
await t.measureTime('Key generation', async () => {
key = await cryptutils.deriveKey(secret, roomName)
})
await t.measureTime('Encryption', async () => {
encrypted = await cryptutils.encrypt(data, key)
})
t.info(`stringified len: ${JSON.stringify(data).length}b`)
t.info(`Encrypted len: ${encrypted.length}b`)
await t.measureTime('Decryption', async () => {
decrypted = await cryptutils.decrypt(encrypted, key)
})
t.compare(data, decrypted)
}

18
test/index.js Normal file
View file

@ -0,0 +1,18 @@
import * as crypto from './crypto.test.js'
import { runTests } from 'lib0/testing.js'
import { isBrowser, isNode } from 'lib0/environment.js'
import * as log from 'lib0/logging.js'
if (isBrowser) {
log.createVConsole(document.body)
}
runTests({
crypto
}).then(success => {
/* istanbul ignore next */
if (isNode) {
process.exit(success ? 0 : 1)
}
})