Skip to content
Snippets Groups Projects
Commit 393f3d15 authored by Marcelo Schmidt's avatar Marcelo Schmidt
Browse files

New implementation of OTR

parent a145bfa1
No related branches found
No related tags found
No related merge requests found
......@@ -42,12 +42,10 @@ Meteor.startup(function() {
});
RocketChat.promises.add('onClientBeforeSendMessage', function(message) {
if (message.rid && RocketChat.OTR.instancesByRoomId && RocketChat.OTR.instancesByRoomId[message.rid] && RocketChat.OTR.instancesByRoomId[message.rid].aesReady.get()) {
return RocketChat.OTR.instancesByRoomId[message.rid].encryptAES(message.msg)
.then((params) => {
[msg, iv] = params;
if (message.rid && RocketChat.OTR.instancesByRoomId && RocketChat.OTR.instancesByRoomId[message.rid] && RocketChat.OTR.instancesByRoomId[message.rid].established.get()) {
return RocketChat.OTR.instancesByRoomId[message.rid].encrypt(message.msg)
.then((msg) => {
message.msg = msg;
message.iv = iv;
return message;
});
} else {
......@@ -56,8 +54,8 @@ Meteor.startup(function() {
}, RocketChat.promises.priority.HIGH);
RocketChat.promises.add('onClientMessageReceived', function(message) {
if (message.rid && message.iv && RocketChat.OTR.instancesByRoomId && RocketChat.OTR.instancesByRoomId[message.rid] && RocketChat.OTR.instancesByRoomId[message.rid].aesReady.get()) {
return RocketChat.OTR.instancesByRoomId[message.rid].decryptAES(message.msg, message.iv)
if (message.rid && RocketChat.OTR.instancesByRoomId && RocketChat.OTR.instancesByRoomId[message.rid] && RocketChat.OTR.instancesByRoomId[message.rid].established.get()) {
return RocketChat.OTR.instancesByRoomId[message.rid].decrypt(message.msg, message.iv)
.then((msg) => {
message.msg = msg;
return message;
......
......@@ -3,66 +3,39 @@ RocketChat.OTR.Room = class {
this.userId = userId;
this.roomId = roomId;
this.peerId = roomId.replace(userId, '');
this.established = new ReactiveVar(false);
this.establishing = new ReactiveVar(false);
this.rsaReady = new ReactiveVar(false);
this.aesReady = new ReactiveVar(false);
this.publicKeyJWK = null;
this.publicKey = null;
this.privateKey = null;
this.peerPublicKey = null;
this.sharedSecret = null;
this.sharedSecretJWK = null;
this.keyPair = null;
this.exportedPublicKey = null;
this.sessionKey = null;
this.serial = "0";
this.firstPeer = null;
}
handshake() {
this.establishing.set(true);
this.rsaReady.set(false);
this.aesReady.set(false);
this.getPublicAndPrivateKeys(false).then(() => {
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'handshake', { roomId: this.roomId, userId: this.userId, publicKey: this.publicKeyJWK });
this.firstPeer = true;
this.generateKeyPair(false).then(() => {
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'handshake', { roomId: this.roomId, userId: this.userId, publicKey: this.bytesToHexString(this.exportedPublicKey) });
});
}
acknowledge() {
this.rsaReady.set(true);
this.getPublicAndPrivateKeys(false).then(() => {
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'acknowledge', { roomId: this.roomId, userId: this.userId, publicKey: this.publicKeyJWK });
});
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'acknowledge', { roomId: this.roomId, userId: this.userId, publicKey: this.bytesToHexString(this.exportedPublicKey) });
}
deny() {
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'deny', { roomId: this.roomId, userId: this.userId });
}
end() {
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'end', { roomId: this.roomId, userId: this.userId });
}
sharedSecretHandshake() {
this.establishing.set(false);
this.aesReady.set(false);
this.getSharedSecret().then(() => {
this.encryptRSA(JSON.stringify(this.sharedSecretJWK), this.peerPublicKey).then((sharedSecret) => {
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'sharedSecret-handshake', { roomId: this.roomId, userId: this.userId, sharedSecret: sharedSecret });
})
});
}
sharedSecretAcknowledge() {
this.establishing.set(false);
this.aesReady.set(true);
this.encryptAES(localStorage.getItem('sharedSecret')).then((args) => {
[sharedSecret, iv] = args;
RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'sharedSecret-acknowledge', { roomId: this.roomId, userId: this.userId, sharedSecret: sharedSecret, iv: iv });
});
}
bytesToHexString(bytes) {
if (!bytes)
return null;
......@@ -90,156 +63,112 @@ RocketChat.OTR.Room = class {
return arrayBuffer;
}
getPublicAndPrivateKeys(refreshKeys) {
if (!this.privateKey || !this.publicKey || refreshKeys) {
// Generate private and public keys
return window.crypto.subtle.generateKey({
name: "RSA-OAEP",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
)
.then((key) => {
// export private key
this.privateKey = key.privateKey;
this.publicKey = key.publicKey;
// export public key
return window.crypto.subtle.exportKey(
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
key.publicKey //can be a publicKey or privateKey, as long as extractable was true
)
.then((publicKey) => {
this.publicKeyJWK = publicKey;
})
})
.catch((err) => {
console.error(err);
});
} else {
return Promise.resolve();
}
}
importPublicKey(publicKey) {
return window.crypto.subtle.importKey(
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
publicKey,
{ //these are the algorithm options
name: "RSA-OAEP",
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt"] //"encrypt" or "wrapKey" for public key import or
)
}
importSharedSecret(sharedSecret) {
return window.crypto.subtle.importKey(
"jwk", //can be "jwk" or "raw"
sharedSecret,
{ //this is the algorithm options
name: "AES-CBC",
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
}
encryptRSA(message, publicKey) {
// Encrypt with peer's public key
return window.crypto.subtle.encrypt({
name: "RSA-OAEP",
},
publicKey,
new TextEncoder("UTF-8").encode(message) //ArrayBuffer of data you want to encrypt
)
.then((encrypted) => {
return this.bytesToHexString(encrypted);
generateKeyPair(refreshKeys) {
// Generate an ephemeral key pair.
return window.crypto.subtle.generateKey({
name: 'ECDH',
namedCurve: 'P-256'
}, false, ['deriveKey', 'deriveBits']).then((keyPair) => {
this.keyPair = keyPair;
return crypto.subtle.exportKey('spki', keyPair.publicKey);
})
.catch((err) => {
console.log(err);
return message;
});
}
decryptRSA(message) {
return window.crypto.subtle.decrypt({
name: "RSA-OAEP",
},
this.privateKey,
new this.hexStringToUint8Array(message) //ArrayBuffer of the data
)
.then((decrypted) => {
//returns an ArrayBuffer containing the decrypted data
return new TextDecoder("UTF-8").decode(new Uint8Array(decrypted));
.then((exportedPublicKey) => {
this.exportedPublicKey = exportedPublicKey;
})
.catch((err) => {
console.log(err);
return message;
console.error(err);
});
}
getSharedSecret() {
return window.crypto.subtle.generateKey({
name: "AES-CBC",
length: 256, //can be 128, 192, or 256
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
.then((sharedSecret) => {
this.sharedSecret = sharedSecret;
// export public key
return window.crypto.subtle.exportKey(
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
sharedSecret //can be a publicKey or privateKey, as long as extractable was true
)
.then((sharedSecretJWK) => {
this.sharedSecretJWK = sharedSecretJWK;
})
})
importPublicKey(publicKey) {
return window.crypto.subtle.importKey('spki', this.hexStringToUint8Array(publicKey), {
name: 'ECDH',
namedCurve: 'P-256'
}, false, []).then((peerPublicKey) => {
return crypto.subtle.deriveBits({
name: 'ECDH',
namedCurve: 'P-256',
public: peerPublicKey
}, this.keyPair.privateKey, 256);
}).then((bits) => {
return crypto.subtle.digest({
name: 'SHA-256'
}, bits);
}).then((hashedBits) => {
// We truncate the hash to 128 bits.
var sessionKeyData = new Uint8Array(hashedBits).slice(0, 16);
return crypto.subtle.importKey('raw', sessionKeyData, {
name: 'AES-GCM'
}, false, ['encrypt', 'decrypt']);
}).then((sessionKey) => {
// Session key available.
this.sessionKey = sessionKey;
});
}
encryptAES(message) {
let iv = window.crypto.getRandomValues(new Uint8Array(16));
return window.crypto.subtle.encrypt({
name: "AES-CBC",
//Don't re-use initialization vectors!
//Always generate a new iv every time your encrypt!
iv: iv,
},
this.sharedSecret, //from generateKey or importKey above
new TextEncoder("UTF-8").encode(message) //ArrayBuffer of data you want to encrypt
)
.then((encrypted) => {
return [this.bytesToHexString(encrypted), this.bytesToHexString(iv)];
})
.catch((err) => {
console.log(err);
return message;
encrypt(message) {
var clearText = new Uint8Array(message);
this.serial = (parseInt(this.serial) + 1).toString()
var data = new Uint8Array(1 + 1 + this.serial.length + clearText.length);
if (this.firstPeer) {
data[0] = 1;
}
data[1] = this.serial.length;
data.set(this.serial, 2);
data.set(clearText, 2 + this.serial.length);
var iv = crypto.getRandomValues(new Uint8Array(12));
return crypto.subtle.encrypt({
name: 'AES-GCM',
iv: iv
}, this.sessionKey, data).then((cipherText) => {
cipherText = new Uint8Array(cipherText);
var output = new Uint8Array(iv.length + cipherText.length);
output.set(iv, 0);
output.set(cipherText, iv.length);
return this.bytesToHexString(output);
});
}
decryptAES(message, iv) {
return window.crypto.subtle.decrypt(
{
name: "AES-CBC",
iv: new this.hexStringToUint8Array(iv), //The initialization vector you used to encrypt
},
this.sharedSecret, //from generateKey or importKey above
new this.hexStringToUint8Array(message) //ArrayBuffer of the data
)
.then((decrypted) => {
//returns an ArrayBuffer containing the decrypted data
return new TextDecoder("UTF-8").decode(new Uint8Array(decrypted));
})
.catch((err) => {
console.log(err);
return message;
decrypt(message) {
var cipherText = new Uint8Array(message);
var iv = cipherText.slice(0, 12);
cipherText = cipherText.slice(12);
return crypto.subtle.decrypt({
name: 'AES-GCM',
iv: iv
}, this.sessionKey, cipherText).then((data) => {
data = new Uint8Array(data);
// if (this.firstPeer) {
// if (data[0] !== 0) {
// throw new Error("Can decrypt only encrypted data from the second peer.");
// }
// }
// else {
// if (data[0] !== 1) {
// throw new Error("Can decrypt only encrypted data from the first peer.");
// }
// }
var serial = data.slice(2, 2 + data[1]);
var clearText = data.slice(2 + data[1]);
// To copy over and make sure we do not have a shallow slice with simply non-zero byteOffset.
serial = new Uint8Array(serial);
clearText = new Uint8Array(clearText);
console.log(clearText);
// This prevents any replay attacks. Or attacks where messages are changed in order.
if (parseInt(serial) - parseInt(this.serial) !== 1) {
throw new Error("Invalid serial.");
}
this.serial = serial;
return clearText;
});
}
......@@ -249,8 +178,6 @@ RocketChat.OTR.Room = class {
case 'handshake':
let timeout = null;
this.establishing.set(true);
this.rsaReady.set(false);
this.aesReady.set(false);
swal({
title: "<i class='icon-key alert-icon'></i>" + TAPi18n.__("OTR"),
text: TAPi18n.__("Username_wants_to_start_otr_Do_you_want_to_accept", { username: user.username }),
......@@ -261,11 +188,14 @@ RocketChat.OTR.Room = class {
}, (isConfirm) => {
if (isConfirm) {
Meteor.clearTimeout(timeout);
this.importPublicKey(data.publicKey).then((publicKey) => {
this.peerPublicKey = publicKey;
FlowRouter.goToRoomById(data.roomId);
Meteor.defer(() => {
this.acknowledge();
this.generateKeyPair(false).then(() => {
this.importPublicKey(data.publicKey).then(() => {
this.firstPeer = false;
FlowRouter.goToRoomById(data.roomId);
Meteor.defer(() => {
this.established.set(true);
this.acknowledge();
});
});
});
} else {
......@@ -275,60 +205,23 @@ RocketChat.OTR.Room = class {
});
timeout = Meteor.setTimeout(() => {
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
swal.close();
}, 10000);
break;
case 'acknowledge':
this.importPublicKey(data.publicKey).then((publicKey) => {
this.peerPublicKey = publicKey;
this.rsaReady.set(true);
this.sharedSecretHandshake();
});
break;
case 'sharedSecret-handshake':
this.decryptRSA(data.sharedSecret)
.then((sharedSecret) => {
this.sharedSecretJWK = JSON.parse(sharedSecret);
localStorage.setItem('sharedSecret', sharedSecret);
this.importSharedSecret(this.sharedSecretJWK)
.then((sharedSecret) => {
this.sharedSecret = sharedSecret;
this.sharedSecretAcknowledge();
})
})
.catch((err) => {
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
swal(TAPi18n.__("Error establishing encrypted connection"), null, "error");
this.importPublicKey(data.publicKey).then(() => {
this.established.set(true);
});
break;
case 'sharedSecret-acknowledge':
this.decryptAES(data.sharedSecret, data.iv)
.then((sharedSecret) => {
if (sharedSecret === JSON.stringify(this.sharedSecretJWK)) {
this.establishing.set(false);
this.aesReady.set(true);
} else {
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
swal(TAPi18n.__("Error establishing encrypted connection"), null, "error");
}
})
break;
case 'deny':
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
swal(TAPi18n.__("Denied"), null, "error");
break;
case 'end':
this.establishing.set(false);
this.rsaReady.set(false);
this.aesReady.set(false);
swal(TAPi18n.__("Ended"), null, "error");
break;
}
......
......@@ -8,7 +8,7 @@
<form>
<ul class="list clearfix">
{{#if userIsOnline}}
{{#if rsaReady}}
{{#if established}}
<button class="button refresh"><span>{{_ "Refresh_keys"}}</span></button>
<button class="button red end"><span>{{_ "End_OTR"}}</span></button>
{{else}} {{#if establishing}}
......
......@@ -16,9 +16,9 @@ Template.otrFlexTab.helpers({
}
}
},
rsaReady() {
established() {
const otr = RocketChat.OTR.getInstanceByRoomId(this.rid);
return otr && otr.rsaReady.get();
return otr && otr.established.get();
},
establishing() {
const otr = RocketChat.OTR.getInstanceByRoomId(this.rid);
......@@ -35,8 +35,6 @@ Template.otrFlexTab.events({
t.timeout = Meteor.setTimeout(() => {
swal("Timeout", "", "error");
otr.establishing.set(false);
otr.rsaReady.set(false);
otr.aesReady.set(false);
}, 10000);
}
},
......@@ -45,8 +43,6 @@ Template.otrFlexTab.events({
const otr = RocketChat.OTR.getInstanceByRoomId(this.rid);
if (otr) {
otr.end();
otr.rsaReady.set(false);
otr.aesReady.set(false);
}
}
});
......@@ -55,7 +51,7 @@ Template.otrFlexTab.onCreated(function() {
this.timeout = null;
this.autorun(() => {
const otr = RocketChat.OTR.getInstanceByRoomId(this.data.rid);
if (otr && otr.aesReady.get()) {
if (otr && otr.established.get()) {
Meteor.clearTimeout(this.timeout);
}
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment