Skip to content
Snippets Groups Projects
Commit 26ea3ffe authored by Gabriel Engel's avatar Gabriel Engel
Browse files

Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into develop

parents d1ed8b8d ff719e41
No related merge requests found
......@@ -186,6 +186,7 @@
"Email_verified" : "Email verified",
"Emoji" : "Emoji",
"Enable_Desktop_Notifications" : "Enable Desktop Notifications",
"Encrypted_message" : "Encrypted message",
"Enter_info" : "Enter your information",
"Enter_to" : "Enter to",
"Error" : "Error",
......
......@@ -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,12 +54,17 @@ 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)
.then((msg) => {
message.msg = msg;
return message;
})
if (message.rid && RocketChat.OTR.instancesByRoomId && RocketChat.OTR.instancesByRoomId[message.rid] && RocketChat.OTR.instancesByRoomId[message.rid].established.get()) {
if (message.notification) {
message.msg = t("Encrypted_message");
return Promise.resolve(message);
} else {
return RocketChat.OTR.instancesByRoomId[message.rid].decrypt(message.msg)
.then((msg) => {
message.msg = msg;
return message;
})
}
} else {
return Promise.resolve(message);
}
......
......@@ -3,64 +3,47 @@ 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.peerSerial = 0;
}
handshake() {
handshake(refresh) {
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), refresh: refresh });
});
}
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);
this.reset();
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);
this.reset();
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() {
reset() {
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 });
});
this.established.set(false);
this.keyPair = null;
this.exportedPublicKey = null;
this.sessionKey = null;
this.serial = null;
this.peerSerial = null;
}
bytesToHexString(bytes) {
......@@ -90,155 +73,115 @@ 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);
})
.then((exportedPublicKey) => {
this.exportedPublicKey = exportedPublicKey;
})
.catch((err) => {
console.log(err);
return message;
console.error(err);
});
}
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));
})
.catch((err) => {
console.log(err);
return message;
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;
});
}
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;
})
})
}
encrypt(message) {
// var clearText = new Uint8Array(message);
var clearText = new TextEncoder("UTF-8").encode(message);
var userId = new TextEncoder("UTF-8").encode(this.userId);
this.serial++;
var data = new Uint8Array(1 + 1 + userId.length + clearText.length);
data[0] = this.serial;
data[1] = userId.length;
data.set(userId, 2);
data.set(clearText, 2 + userId.length);
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;
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);
}).catch((e) => {
console.log(e);
return "";
});
}
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));
decrypt(message) {
var cipherText = new this.hexStringToUint8Array(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);
var serial = data[0];
var userId = 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.
userId = new TextDecoder("UTF-8").decode(new Uint8Array(userId));
clearText = new TextDecoder("UTF-8").decode(new Uint8Array(clearText));
// This prevents any replay attacks. Or attacks where messages are changed in order.
// If message is from the same userId as me, serials must be equal
if (userId === this.userId && serial !== this.serial) {
throw new Error("Invalid serial.");
} else if (userId !== this.userId) {
// If serial difference is larger than one, message is out of order
var checkSerial = serial - this.peerSerial;
if (checkSerial !== 1 && checkSerial !== -255) {
throw new Error("Invalid serial.");
}
// update serial number to the last received serial
this.peerSerial = serial;
}
return clearText;
})
.catch((err) => {
console.log(err);
.catch((e) => {
console.log(e);
return message;
});
}
......@@ -248,88 +191,72 @@ RocketChat.OTR.Room = class {
switch(type) {
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 }),
html: true,
showCancelButton: true,
confirmButtonText: TAPi18n.__("Yes"),
cancelButtonText: TAPi18n.__("No")
}, (isConfirm) => {
if (isConfirm) {
Meteor.clearTimeout(timeout);
this.importPublicKey(data.publicKey).then((publicKey) => {
this.peerPublicKey = publicKey;
establishConnection = () => {
this.establishing.set(true);
Meteor.clearTimeout(timeout);
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 {
Meteor.clearTimeout(timeout);
this.deny();
});
}
if (data.refresh && this.established.get()) {
this.reset();
establishConnection();
} else {
if (this.established.get()) {
this.reset();
}
});
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 }),
html: true,
showCancelButton: true,
confirmButtonText: TAPi18n.__("Yes"),
cancelButtonText: TAPi18n.__("No")
}, (isConfirm) => {
if (isConfirm) {
establishConnection();
} else {
Meteor.clearTimeout(timeout);
this.deny();
}
});
}
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();
this.importPublicKey(data.publicKey).then(() => {
this.established.set(true);
});
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");
});
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");
if (this.establishing.get()) {
this.reset();
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");
if (this.established.get()) {
this.reset();
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,18 @@ Template.otrFlexTab.events({
t.timeout = Meteor.setTimeout(() => {
swal("Timeout", "", "error");
otr.establishing.set(false);
otr.rsaReady.set(false);
otr.aesReady.set(false);
}, 10000);
}
},
'click button.refresh': function(e, t) {
e.preventDefault();
const otr = RocketChat.OTR.getInstanceByRoomId(this.rid);
if (otr) {
otr.reset();
otr.handshake(true);
t.timeout = Meteor.setTimeout(() => {
swal("Timeout", "", "error");
otr.establishing.set(false);
}, 10000);
}
},
......@@ -45,8 +55,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 +63,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);
}
})
......
......@@ -88,30 +88,34 @@ class @ChatMessages
readMessage.readNow()
$('.message.first-unread').removeClass('first-unread')
if this.editing.id
this.update(this.editing.id, rid, input)
return
if this.isMessageTooLong(input)
return toastr.error t('Message_too_long')
KonchatNotification.removeRoomNotification(rid)
msg = input.value
input.value = ''
this.hasValue.set false
msgObject = { _id: Random.id(), rid: rid, msg: msg}
this.stopTyping(rid)
#Check if message starts with /command
if msg[0] is '/'
match = msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m)
if match? and RocketChat.slashCommands.commands[match[1]]
command = match[1]
param = match[2]
Meteor.call 'slashCommand', {cmd: command, params: param, msg: msgObject }
# Run to allow local encryption, and maybe other client specific actions to be run before send
RocketChat.promises.run('onClientBeforeSendMessage', msgObject).then (msgObject) =>
# checks for the final msgObject.msg size before actually sending the message
if this.isMessageTooLong(msgObject.msg)
return toastr.error t('Message_too_long')
if this.editing.id
this.update(this.editing.id, rid, msgObject.msg)
return
#Run to allow local encryption
# Meteor.call 'onClientBeforeSendMessage', {}
RocketChat.promises.run('onClientBeforeSendMessage', msgObject).then (msgObject) ->
KonchatNotification.removeRoomNotification(rid)
input.value = ''
this.hasValue.set false
this.stopTyping(rid)
#Check if message starts with /command
if msg[0] is '/'
match = msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m)
if match? and RocketChat.slashCommands.commands[match[1]]
command = match[1]
param = match[2]
Meteor.call 'slashCommand', {cmd: command, params: param, msg: msgObject }
return
Meteor.call 'sendMessage', msgObject
deleteMsg: (message) ->
......@@ -131,9 +135,8 @@ class @ChatMessages
if error
return toastr.error error.reason
update: (id, rid, input) ->
if _.trim(input.value) isnt ''
msg = input.value
update: (id, rid, msg) ->
if _.trim(msg) isnt ''
Meteor.call 'updateMessage', { _id: id, msg: msg, rid: rid }
this.clearEditing()
this.stopTyping(rid)
......@@ -232,8 +235,8 @@ class @ChatMessages
else if k is 75 and ((navigator?.platform?.indexOf('Mac') isnt -1 and event.metaKey and event.shiftKey) or (navigator?.platform?.indexOf('Mac') is -1 and event.ctrlKey and event.shiftKey))
RoomHistoryManager.clear rid
isMessageTooLong: (input) ->
input?.value.length > this.messageMaxSize
isMessageTooLong: (message) ->
message?.length > this.messageMaxSize
isEmpty: ->
return !this.hasValue.get()
......@@ -12,21 +12,23 @@
notify: (notification) ->
if window.Notification && Notification.permission == "granted"
n = new Notification notification.title,
icon: notification.icon or getAvatarUrlFromUsername notification.payload.sender.username
body: _.stripTags(notification.text)
silent: true
message = { rid: notification.payload?.rid, msg: notification.text, notification: true }
RocketChat.promises.run('onClientMessageReceived', message).then (message) ->
n = new Notification notification.title,
icon: notification.icon or getAvatarUrlFromUsername notification.payload.sender.username
body: _.stripTags(message.msg)
silent: true
if notification.payload?.rid?
n.onclick = ->
window.focus()
switch notification.payload.type
when 'd'
FlowRouter.go 'direct', {username: notification.payload.sender.username}
when 'c'
FlowRouter.go 'channel', {name: notification.payload.name}
when 'p'
FlowRouter.go 'group', {name: notification.payload.name}
if notification.payload?.rid?
n.onclick = ->
window.focus()
switch notification.payload.type
when 'd'
FlowRouter.go 'direct', {username: notification.payload.sender.username}
when 'c'
FlowRouter.go 'channel', {name: notification.payload.name}
when 'p'
FlowRouter.go 'group', {name: notification.payload.name}
showDesktop: (notification) ->
if not window.document.hasFocus?() and Meteor.user().status isnt 'busy'
......
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