Skip to content

Commit

Permalink
[NEW] Support for end to end encryption (#10094)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrinaldhar authored and sampaiodiego committed Sep 21, 2018
1 parent a4ce812 commit 808619b
Show file tree
Hide file tree
Showing 44 changed files with 1,251 additions and 5 deletions.
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ steffo:meteor-accounts-saml
todda00:friendly-slugs
yasaricli:slugify
yasinuslu:blaze-meta
rocketchat:e2e

rocketchat:blockstack
rocketchat:version-check
Expand Down
1 change: 1 addition & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"bugsnag": "^2.4.0",
"bunyan": "^1.8.12",
"busboy": "^0.2.14",
"bytebuffer": "5.0.1",
"cas": "https://github.com/kcbanner/node-cas/tarball/fcd27dad333223b3b75a048bce27973fb3ca0f62",
"chart.js": "^2.7.2",
"clipboard": "^2.0.1",
Expand Down
46 changes: 46 additions & 0 deletions packages/rocketchat-api/server/v1/e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
RocketChat.API.v1.addRoute('e2e.fetchKeychain', { authRequired: true }, {
get() {
const { uid } = this.queryParams;

let result;
Meteor.runAsUser(this.userId, () => result = Meteor.call('fetchKeychain', uid));

return RocketChat.API.v1.success(result);
},
});

RocketChat.API.v1.addRoute('e2e.fetchMyKeys', { authRequired: true }, {
get() {
let result;
Meteor.runAsUser(this.userId, () => result = Meteor.call('fetchMyKeys'));

return RocketChat.API.v1.success(result);
},
});

RocketChat.API.v1.addRoute('e2e.addKeyToChain', { authRequired: true }, {
post() {
const { RSAPubKey, RSAEPrivKey } = this.bodyParams;

Meteor.runAsUser(this.userId, () => {
RocketChat.API.v1.success(Meteor.call('addKeyToChain', {
public_key: RSAPubKey,
private_key: RSAEPrivKey,
}));
});

return RocketChat.API.v1.success();
},
});

RocketChat.API.v1.addRoute('e2e.updateGroupE2EKey', { authRequired: true }, {
post() {
const { uid, rid, key } = this.bodyParams;

Meteor.runAsUser(this.userId, () => {
RocketChat.API.v1.success(Meteor.call('updateGroupE2EKey', rid, uid, key));
});

return RocketChat.API.v1.success();
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,24 @@
{{/if}}
{{/with}}

{{#with settings.encrypted}}
{{#if canView}}
<div class="rc-user-info__row">
<div class="rc-switch rc-switch--blue">
<label class="rc-switch__label">
<span class="rc-switch__text">
{{_ label}}{{equal default value '*'}}
</span>
<input type="checkbox" class="rc-switch__input js-input-check" name="encrypted" checked="{{checked}}" disabled="{{./disabled}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
</label>
</div>
</div>
{{/if}}
{{/with}}

{{#with settings.broadcast}}
{{#if canView}}
<div class="rc-user-info__row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,25 @@ Template.channelSettingsEditing.onCreated(function() {
);
},
},
encrypted: {
type: 'boolean',
label: 'Encrypted',
isToggle: true,
processing: new ReactiveVar(false),
canView() {
return RocketChat.roomTypes.roomTypes[room.t].allowRoomSettingChange(room, RoomSettingsEnum.E2E);
},
canEdit() {
return RocketChat.authz.hasAllPermission('edit-room', room._id);
},
save(value) {
return call('saveRoomSettings', room._id, 'encrypted', value).then(() => {
toastr.success(
t('Encrypted_setting_changed_successfully')
);
});
},
},
};
Object.keys(this.settings).forEach((key) => {
const setting = this.settings[key];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const fields = ['roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionOverrideGlobal'];
const fields = ['roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionOverrideGlobal', 'encrypted'];
Meteor.methods({
saveRoomSettings(rid, settings, value) {
const userId = Meteor.userId();
Expand Down Expand Up @@ -72,6 +72,12 @@ Meteor.methods({
action: 'Change_Room_Type',
});
}
if (setting === 'encrypted' && value !== room.encrypted && (room.t !== 'd' && room.t !== 'p')) {
throw new Meteor.Error('error-action-not-allowed', 'Only groups or direct channels can enable encryption', {
method: 'saveRoomSettings',
action: 'Change_Room_Encrypted',
});
}

if (setting === 'retentionEnabled' && !RocketChat.authz.hasPermission(userId, 'edit-room-retention-policy', rid) && value !== room.retention.enabled) {
throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', {
Expand Down Expand Up @@ -184,6 +190,9 @@ Meteor.methods({
case 'retentionOverrideGlobal':
RocketChat.models.Rooms.saveRetentionOverrideGlobalById(rid, value);
break;
case 'encrypted':
RocketChat.models.Rooms.saveEncryptedById(rid, value);
break;
}
});

Expand Down
4 changes: 4 additions & 0 deletions packages/rocketchat-e2e/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["@rocket.chat/eslint-config"],
"root": true
}
117 changes: 117 additions & 0 deletions packages/rocketchat-e2e/client/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* eslint-disable new-cap, no-proto */

import ByteBuffer from 'bytebuffer';

const StaticArrayBufferProto = new ArrayBuffer().__proto__;

export function toString(thing) {
if (typeof thing === 'string') {
return thing;
}
return new ByteBuffer.wrap(thing).toString('binary');
}

export function toArrayBuffer(thing) {
if (thing === undefined) {
return undefined;
}
if (thing === Object(thing)) {
if (thing.__proto__ === StaticArrayBufferProto) {
return thing;
}
}

if (typeof thing !== 'string') {
throw new Error(`Tried to convert a non-string of type ${ typeof thing } to an array buffer`);
}
return new ByteBuffer.wrap(thing, 'binary').toArrayBuffer();
}

export function joinVectorAndEcryptedData(vector, encryptedData) {
const cipherText = new Uint8Array(encryptedData);
const output = new Uint8Array(vector.length + cipherText.length);
output.set(vector, 0);
output.set(cipherText, vector.length);
return output;
}

export function splitVectorAndEcryptedData(cipherText) {
const vector = cipherText.slice(0, 16);
const encryptedData = cipherText.slice(16);

return [vector, encryptedData];
}

export async function encryptRSA(key, data) {
return await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, data);
}

export async function encryptAES(vector, key, data) {
return await crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data);
}

export async function decryptRSA(key, data) {
return await crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data);
}

export async function decryptAES(vector, key, data) {
return await crypto.subtle.decrypt({ name: 'AES-CBC', iv: vector }, key, data);
}

export async function generateAESKey() {
return await crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']);
}

export async function generateRSAKey() {
return await crypto.subtle.generateKey({ name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: 'SHA-256' } }, true, ['encrypt', 'decrypt']);
}

export async function exportJWKKey(key) {
return await crypto.subtle.exportKey('jwk', key);
}

export async function importRSAKey(keyData, keyUsages = ['encrypt', 'decrypt']) {
return await crypto.subtle.importKey('jwk', keyData, { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: 'SHA-256' } }, true, keyUsages);
}

export async function importAESKey(keyData, keyUsages = ['encrypt', 'decrypt']) {
return await crypto.subtle.importKey('jwk', keyData, { name: 'AES-CBC' }, true, keyUsages);
}

export async function importRawKey(keyData, keyUsages = ['deriveKey']) {
return await crypto.subtle.importKey('raw', keyData, { name: 'PBKDF2' }, false, keyUsages);
}

export async function deriveKey(salt, baseKey, keyUsages = ['encrypt', 'decrypt']) {
const iterations = 1000;
const hash = 'SHA-256';

return await crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations, hash }, baseKey, { name: 'AES-CBC', length: 256 }, true, keyUsages);
}

export async function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(evt) {
resolve(evt.target.result);
};
reader.onerror = function(evt) {
reject(evt);
};
reader.readAsArrayBuffer(file);
});
}

export class Deferred {
constructor() {
const p = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});

p.resolve = this.resolve;
p.reject = this.reject;

return p;
}
}
Loading

0 comments on commit 808619b

Please sign in to comment.