[security] Add security number to PeerUI to make verification of peer-to-peer encryption possible.
This commit is contained in:
parent
e9b23bfdb0
commit
c5d0eaa034
9 changed files with 131 additions and 23 deletions
|
@ -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.
|
||||
|
||||
### 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
|
||||
Naturally, if traffic needs to be routed through the turn server transfer speed decreases.
|
||||
|
|
|
@ -558,7 +558,7 @@ class RTCPeer extends Peer {
|
|||
|
||||
_onChannelOpened(event) {
|
||||
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;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
|
@ -568,6 +568,32 @@ class RTCPeer extends Peer {
|
|||
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) {
|
||||
if (this._busy) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -19,7 +19,7 @@ class PeersUI {
|
|||
|
||||
constructor() {
|
||||
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('peers', e => this._onPeers(e.detail));
|
||||
Events.on('set-progress', e => this._onSetProgress(e.detail));
|
||||
|
@ -63,9 +63,9 @@ class PeersUI {
|
|||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
_onPeerConnected(peerId, connectionHash) {
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId]);
|
||||
new PeerUI(this.peers[peerId], connectionHash);
|
||||
}
|
||||
|
||||
_redrawPeer(peer) {
|
||||
|
@ -235,17 +235,21 @@ class PeerUI {
|
|||
<div class="name font-subheading"></div>
|
||||
<div class="device-name 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>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
this.$el.querySelector('.name').textContent = this._displayName();
|
||||
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._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -304,7 +304,8 @@ x-peer[status] x-icon {
|
|||
}
|
||||
|
||||
.status,
|
||||
.device-name {
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@ -314,6 +315,11 @@ x-peer[status] x-icon {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.connection-hash {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
x-peer[status=transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
|
|
@ -568,7 +568,7 @@ class RTCPeer extends Peer {
|
|||
|
||||
_onChannelOpened(event) {
|
||||
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;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
|
@ -578,6 +578,32 @@ class RTCPeer extends Peer {
|
|||
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) {
|
||||
if (this._busy) {
|
||||
e.preventDefault();
|
||||
|
@ -679,11 +705,16 @@ class WSPeer extends Peer {
|
|||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
Events.fire('peer-connected', message.sender.id)
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (this._peerId) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
// Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
|
|
|
@ -19,7 +19,7 @@ class PeersUI {
|
|||
|
||||
constructor() {
|
||||
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('peers', e => this._onPeers(e.detail));
|
||||
Events.on('set-progress', e => this._onSetProgress(e.detail));
|
||||
|
@ -63,9 +63,9 @@ class PeersUI {
|
|||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
_onPeerConnected(peerId, connectionHash) {
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId]);
|
||||
new PeerUI(this.peers[peerId], connectionHash);
|
||||
}
|
||||
|
||||
_redrawPeer(peer) {
|
||||
|
@ -235,17 +235,21 @@ class PeerUI {
|
|||
<div class="name font-subheading"></div>
|
||||
<div class="device-name 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>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
this.$el.querySelector('.name').textContent = this._displayName();
|
||||
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._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
|
|
|
@ -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) {
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
|
|
|
@ -313,7 +313,8 @@ x-peer[status] x-icon {
|
|||
}
|
||||
|
||||
.status,
|
||||
.device-name {
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@ -323,6 +324,11 @@ x-peer[status] x-icon {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.connection-hash {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
x-peer[status=transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
@ -389,22 +395,17 @@ footer .logo {
|
|||
|
||||
footer .font-body2 {
|
||||
color: var(--primary-color);
|
||||
text-underline-position: under;
|
||||
margin: auto 18px;
|
||||
}
|
||||
|
||||
#on-this-network {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-color: var(--primary-color);
|
||||
text-decoration-thickness: 4px;
|
||||
border-bottom: solid 4px var(--primary-color);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#paired-devices {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-color: var(--paired-device-color);
|
||||
text-decoration-thickness: 4px;
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
|
Loading…
Reference in a new issue