[security] Add security number to PeerUI to make verification of peer-to-peer encryption possible.

This commit is contained in:
schlagmichdoch 2023-02-16 02:19:14 +01:00
parent e9b23bfdb0
commit c5d0eaa034
9 changed files with 131 additions and 23 deletions

View file

@ -51,7 +51,7 @@ WebRTC encrypts the files on transit.
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
### What about security? Are my files encrypted while being sent between the computers? ### What about security? Are my files encrypted while being sent between the computers?
Yes. Your files are sent using WebRTC, which encrypts them on transit. Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
### Transferring many files with paired devices takes too long ### Transferring many files with paired devices takes too long
Naturally, if traffic needs to be routed through the turn server transfer speed decreases. Naturally, if traffic needs to be routed through the turn server transfer speed decreases.

View file

@ -558,7 +558,7 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', this._peerId); Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
@ -568,6 +568,32 @@ class RTCPeer extends Peer {
this._channel = channel; this._channel = channel;
} }
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
let localConnectionFingerprint, remoteConnectionFingerprint;
for (let i=0; i<localDescriptionLines.length; i++) {
if (localDescriptionLines[i].startsWith("a=fingerprint:")) {
localConnectionFingerprint = localDescriptionLines[i].substring(14);
break;
}
}
for (let i=0; i<remoteDescriptionLines.length; i++) {
if (remoteDescriptionLines[i].startsWith("a=fingerprint:")) {
remoteConnectionFingerprint = remoteDescriptionLines[i].substring(14);
break;
}
}
const combinedFingerprints = this._isCaller
? localConnectionFingerprint + remoteConnectionFingerprint
: remoteConnectionFingerprint + localConnectionFingerprint;
let hash = cyrb53(combinedFingerprints).toString();
while (hash.length < 16) {
hash = "0" + hash;
}
return hash;
}
_onBeforeUnload(e) { _onBeforeUnload(e) {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();

View file

@ -19,7 +19,7 @@ class PeersUI {
constructor() { constructor() {
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail)); Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('peers', e => this._onPeers(e.detail)); Events.on('peers', e => this._onPeers(e.detail));
Events.on('set-progress', e => this._onSetProgress(e.detail)); Events.on('set-progress', e => this._onSetProgress(e.detail));
@ -63,9 +63,9 @@ class PeersUI {
this.peers[peer.id] = peer; this.peers[peer.id] = peer;
} }
_onPeerConnected(peerId) { _onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId)) if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId]); new PeerUI(this.peers[peerId], connectionHash);
} }
_redrawPeer(peer) { _redrawPeer(peer) {
@ -235,17 +235,21 @@ class PeerUI {
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status font-body2"></div> <div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
</label>`; </label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
constructor(peer) { constructor(peer, connectionHash) {
this._peer = peer; this._peer = peer;
this._roomType = peer.roomType; this._roomType = peer.roomType;
this._roomSecret = peer.roomSecret; this._roomSecret = peer.roomSecret;
this._connectionHash = connectionHash;
this._initDom(); this._initDom();
this._bindListeners(); this._bindListeners();
$$('x-peers').appendChild(this.$el); $$('x-peers').appendChild(this.$el);

View file

@ -380,3 +380,21 @@ const mime = (() => {
}; };
})(); })();
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};

View file

@ -304,7 +304,8 @@ x-peer[status] x-icon {
} }
.status, .status,
.device-name { .device-name,
.connection-hash {
height: 18px; height: 18px;
opacity: 0.7; opacity: 0.7;
} }
@ -314,6 +315,11 @@ x-peer[status] x-icon {
white-space: nowrap; white-space: nowrap;
} }
.connection-hash {
font-size: 12px;
white-space: nowrap;
}
x-peer[status=transfer] .status:before { x-peer[status=transfer] .status:before {
content: 'Transferring...'; content: 'Transferring...';
} }

View file

@ -568,7 +568,7 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', this._peerId); Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
@ -578,6 +578,32 @@ class RTCPeer extends Peer {
this._channel = channel; this._channel = channel;
} }
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
let localConnectionFingerprint, remoteConnectionFingerprint;
for (let i=0; i<localDescriptionLines.length; i++) {
if (localDescriptionLines[i].startsWith("a=fingerprint:")) {
localConnectionFingerprint = localDescriptionLines[i].substring(14);
break;
}
}
for (let i=0; i<remoteDescriptionLines.length; i++) {
if (remoteDescriptionLines[i].startsWith("a=fingerprint:")) {
remoteConnectionFingerprint = remoteDescriptionLines[i].substring(14);
break;
}
}
const combinedFingerprints = this._isCaller
? localConnectionFingerprint + remoteConnectionFingerprint
: remoteConnectionFingerprint + localConnectionFingerprint;
let hash = cyrb53(combinedFingerprints).toString();
while (hash.length < 16) {
hash = "0" + hash;
}
return hash;
}
_onBeforeUnload(e) { _onBeforeUnload(e) {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();
@ -679,11 +705,16 @@ class WSPeer extends Peer {
} }
onServerMessage(message) { onServerMessage(message) {
Events.fire('peer-connected', message.sender.id) Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return; if (this._peerId) return;
this._peerId = message.sender.id; this._peerId = message.sender.id;
this._sendSignal(); this._sendSignal();
} }
getConnectionHash() {
// Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys
return "";
}
} }
class PeersManager { class PeersManager {

View file

@ -19,7 +19,7 @@ class PeersUI {
constructor() { constructor() {
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail)); Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('peers', e => this._onPeers(e.detail)); Events.on('peers', e => this._onPeers(e.detail));
Events.on('set-progress', e => this._onSetProgress(e.detail)); Events.on('set-progress', e => this._onSetProgress(e.detail));
@ -63,9 +63,9 @@ class PeersUI {
this.peers[peer.id] = peer; this.peers[peer.id] = peer;
} }
_onPeerConnected(peerId) { _onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId)) if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId]); new PeerUI(this.peers[peerId], connectionHash);
} }
_redrawPeer(peer) { _redrawPeer(peer) {
@ -235,17 +235,21 @@ class PeerUI {
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status font-body2"></div> <div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
</label>`; </label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
constructor(peer) { constructor(peer, connectionHash) {
this._peer = peer; this._peer = peer;
this._roomType = peer.roomType; this._roomType = peer.roomType;
this._roomSecret = peer.roomSecret; this._roomSecret = peer.roomSecret;
this._connectionHash = connectionHash;
this._initDom(); this._initDom();
this._bindListeners(); this._bindListeners();
$$('x-peers').appendChild(this.$el); $$('x-peers').appendChild(this.$el);

View file

@ -381,6 +381,24 @@ const mime = (() => {
})(); })();
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function arrayBufferToBase64(buffer) { function arrayBufferToBase64(buffer) {
var binary = ''; var binary = '';
var bytes = new Uint8Array(buffer); var bytes = new Uint8Array(buffer);

View file

@ -313,7 +313,8 @@ x-peer[status] x-icon {
} }
.status, .status,
.device-name { .device-name,
.connection-hash {
height: 18px; height: 18px;
opacity: 0.7; opacity: 0.7;
} }
@ -323,6 +324,11 @@ x-peer[status] x-icon {
white-space: nowrap; white-space: nowrap;
} }
.connection-hash {
font-size: 12px;
white-space: nowrap;
}
x-peer[status=transfer] .status:before { x-peer[status=transfer] .status:before {
content: 'Transferring...'; content: 'Transferring...';
} }
@ -389,22 +395,17 @@ footer .logo {
footer .font-body2 { footer .font-body2 {
color: var(--primary-color); color: var(--primary-color);
text-underline-position: under;
margin: auto 18px; margin: auto 18px;
} }
#on-this-network { #on-this-network {
text-decoration-line: underline; border-bottom: solid 4px var(--primary-color);
text-decoration-style: solid; padding-bottom: 1px;
text-decoration-color: var(--primary-color);
text-decoration-thickness: 4px;
} }
#paired-devices { #paired-devices {
text-decoration-line: underline; border-bottom: solid 4px var(--paired-device-color);
text-decoration-style: solid; padding-bottom: 1px;
text-decoration-color: var(--paired-device-color);
text-decoration-thickness: 4px;
} }
/* Dialog */ /* Dialog */