@ -1,13 +1,13 @@
import _ from 'underscore' ;
import { Base64 } from 'meteor/base64' ;
import { ReactiveVar } from 'meteor/reactive-var' ;
import { EJSON } from 'meteor/ejson' ;
import { Random } from 'meteor/random' ;
import { Session } from 'meteor/session' ;
import { TimeSync } from 'meteor/mizzao:timesync' ;
import { Emitter } from '@rocket.chat/emitter' ;
import { e2e } from './rocketchat.e2e' ;
import {
Deferred ,
toString ,
toArrayBuffer ,
joinVectorAndEcryptedData ,
@ -22,79 +22,230 @@ import {
importRSAKey ,
readFileAsArrayBuffer ,
} from './helper' ;
import { Notifications } from '../../notifications' ;
import { Rooms , Subscriptions } from '../../models' ;
import { Notifications } from '../../notifications/client ' ;
import { Rooms , Subscriptions , Messages } from '../../models' ;
import { call } from '../../ui-utils' ;
import { roomTypes , RoomSettingsEnum } from '../../utils' ;
export class E2ERoom {
export const E2E _ROOM _STATES = {
NO _PASSWORD _SET : 'NO_PASSWORD_SET' ,
NOT _STARTED : 'NOT_STARTED' ,
DISABLED : 'DISABLED' ,
PAUSED : 'PAUSED' ,
HANDSHAKE : 'HANDSHAKE' ,
ESTABLISHING : 'ESTABLISHING' ,
CREATING _KEYS : 'CREATING_KEYS' ,
WAITING _KEYS : 'WAITING_KEYS' ,
KEYS _RECEIVED : 'KEYS_RECEIVED' ,
READY : 'READY' ,
ERROR : 'ERROR' ,
} ;
const KEY _ID = Symbol ( 'keyID' ) ;
const reduce = ( prev , next ) => {
if ( prev === next ) {
return next === E2E _ROOM _STATES . ERROR ;
}
switch ( next ) {
case E2E _ROOM _STATES . READY :
if ( prev === E2E _ROOM _STATES . PAUSED ) {
return E2E _ROOM _STATES . READY ;
}
return E2E _ROOM _STATES . DISABLED ;
case E2E _ROOM _STATES . PAUSED :
if ( prev === E2E _ROOM _STATES . READY ) {
return E2E _ROOM _STATES . PAUSED ;
}
return E2E _ROOM _STATES . DISABLED ;
}
switch ( prev ) {
case E2E _ROOM _STATES . PAUSED :
if ( next === E2E _ROOM _STATES . READY ) {
return E2E _ROOM _STATES . READY ;
}
return false ;
case E2E _ROOM _STATES . NOT _STARTED :
return [ E2E _ROOM _STATES . ESTABLISHING , E2E _ROOM _STATES . PAUSED , E2E _ROOM _STATES . DISABLED , E2E _ROOM _STATES . KEYS _RECEIVED ] . includes ( next ) && next ;
case E2E _ROOM _STATES . READY :
return [ E2E _ROOM _STATES . PAUSED , E2E _ROOM _STATES . DISABLED ] . includes ( next ) && next ;
case E2E _ROOM _STATES . ERROR :
return [ E2E _ROOM _STATES . KEYS _RECEIVED , E2E _ROOM _STATES . NOT _STARTED ] . includes ( next ) && next ;
case E2E _ROOM _STATES . WAITING _KEYS :
return [ E2E _ROOM _STATES . KEYS _RECEIVED , E2E _ROOM _STATES . ERROR , E2E _ROOM _STATES . PAUSED , E2E _ROOM _STATES . DISABLED ] . includes ( next ) && next ;
case E2E _ROOM _STATES . ESTABLISHING :
return [ E2E _ROOM _STATES . READY , E2E _ROOM _STATES . KEYS _RECEIVED , E2E _ROOM _STATES . ERROR , E2E _ROOM _STATES . PAUSED , E2E _ROOM _STATES . DISABLED , E2E _ROOM _STATES . WAITING _KEYS ] . includes ( next ) && next ;
default :
return next ;
}
} ;
export class E2ERoom extends Emitter {
log ( ... msg ) {
if ( this . roomId === Session . get ( 'openedRoom' ) ) {
console . log ( '[E2E ROOM]' , ` [STATE: ${ this . state } ] ` , ` [RID: ${ this . roomId } ] ` , ... msg ) ;
}
}
error ( ... msg ) {
if ( this . roomId === Session . get ( 'openedRoom' ) ) {
console . error ( '[E2E ROOM]' , ` [STATE: ${ this . state } ] ` , ` [RID: ${ this . roomId } ] ` , ... msg ) ;
}
}
setState ( state ) {
const prev = this . state ;
const next = reduce ( prev , state ) ;
if ( ! next ) {
this . error ( ` invalid state ${ prev } -> ${ state } ` ) ;
return ;
}
this . state = state ;
this . emit ( 'STATE_CHANGED' , prev , next , this ) ;
this . emit ( state , this ) ;
}
constructor ( userId , roomId , t ) {
super ( ) ;
this . state = undefined ;
// this.error = undefined;
this . userId = userId ;
this . roomId = roomId ;
this . typeOfRoom = t ;
this . establishing = new ReactiveVar ( false ) ;
this . _ready = new ReactiveVar ( false ) ;
this . readyPromise = new Deferred ( ) ;
this . readyPromise . then ( ( ) => {
this . _ready . set ( true ) ;
this . establishing . set ( false ) ;
this . typeOfRoom = t ;
Notifications . onRoom ( this . roomId , 'e2ekeyRequest' , async ( keyId ) => {
this . provideKeyToUser ( keyId ) ;
} ) ;
this . once ( E2E _ROOM _STATES . READY , ( ) => this . decryptPendingMessages ( ) ) ;
this . once ( E2E _ROOM _STATES . READY , ( ) => this . decryptPendingSubscription ( ) ) ;
this . on ( 'STATE_CHANGED' , ( prev ) => {
if ( this . roomId === Session . get ( 'openedRoom' ) ) {
this . log ( ` [PREV: ${ prev } ] ` , 'State CHANGED' ) ;
}
} ) ;
this . on ( 'STATE_CHANGED' , ( ) => this . handshake ( ) ) ;
this . setState ( E2E _ROOM _STATES . NOT _STARTED ) ;
}
// Initiates E2E Encryption
async handshake ( ) {
if ( ! e2e . isReady ( ) ) {
return ;
}
if ( this . _ready . get ( ) ) {
return ;
}
disable ( ) {
this . setState ( E2E _ROOM _STATES . DISABLED ) ;
}
if ( this . establishing . get ( ) ) {
return this . readyPromise ;
}
keyReceived ( ) {
this . setState ( E2E _ROOM _STATES . KEYS _RECEIVED ) ;
}
console . log ( 'E2E -> Initiating handshake' ) ;
pause ( ) {
this . setState ( this . state === E2E _ROOM _STATES . READY ? E2E _ROOM _STATES . PAUSED : E2E _ROOM _STATES . DISABLED ) ;
}
this . establishing . set ( true ) ;
enable ( ) {
this . setState ( E2E _ROOM _STATES . READY ) ;
}
// Cover private groups and direct messages
if ( ! this . isSupportedRoomType ( this . typeOfRoom ) ) {
return ;
}
shouldConvertSentMessages ( ) {
return this . isReady ( ) && ! this . isPaused ( ) ;
}
// Fetch encrypted session key from subscription model
let groupKey ;
try {
groupKey = Subscriptions . findOne ( { rid : this . roomId } ) . E2EKey ;
} catch ( error ) {
return console . error ( 'E2E -> Error fetching group key: ' , error ) ;
}
shouldConvertReceivedMessages ( ) {
return this . isReady ( ) ;
}
if ( groupKey ) {
await this . importGroupKey ( groupKey ) ;
this . readyPromise . resolve ( ) ;
return true ;
}
isDisabled ( ) {
return [ E2E _ROOM _STATES . DISABLED ] . includes ( this . state ) ;
}
isPaused ( ) {
return [ E2E _ROOM _STATES . PAUSED ] . includes ( this . state ) ;
}
wait ( state ) {
return new Promise ( ( resolve ) => ( state === this . state ? resolve ( this ) : this . once ( state , ( ) => resolve ( this ) ) ) ) . then ( ( el ) => {
this . log ( this . state , el ) ;
return el ;
} ) ;
}
isReady ( ) {
return [ E2E _ROOM _STATES . PAUSED , E2E _ROOM _STATES . READY ] . includes ( this . state ) ;
}
isWaitingKeys ( ) {
return this . state === E2E _ROOM _STATES . WAITING _KEYS ;
}
get keyID ( ) {
return this [ KEY _ID ] ;
}
set keyID ( keyID ) {
this [ KEY _ID ] = keyID ;
}
const room = Rooms . findOne ( { _id : this . roomId } ) ;
async decryptPendingSubscription ( ) {
const subscription = Subscriptions . findOne ( {
rid : this . roomId ,
} ) ;
if ( ! room . e2eKeyId ) {
await this . createGroupKey ( ) ;
this . readyPromise . resolve ( ) ;
return true ;
const data = await ( subscription . lastMessage ? . msg && this . decrypt ( subscription . lastMessage . msg ) ) ;
if ( ! data ? . text ) {
this . log ( 'decryptPendingSubscriptions nothing to do' ) ;
return ;
}
console . log ( 'E2E -> Requesting room key' ) ;
// TODO: request group key
Subscriptions . direct . update ( {
_id : subscription . _id ,
} , {
$set : {
'lastMessage.msg' : data . text ,
'lastMessage.e2e' : 'done' ,
} ,
} ) ;
this . log ( 'decryptPendingSubscriptions Done' ) ;
}
async decryptPendingMessages ( ) {
return Messages . find ( { rid : this . roomId , t : 'e2e' , e2e : 'pending' } ) . forEach ( async ( { _id , ... msg } ) => {
Messages . direct . update ( { _id } , await this . decryptMessage ( msg ) ) ;
} ) ;
}
Notifications . notifyUsersOfRoom ( this . roomId , 'e2ekeyRequest' , this . roomId , room . e2eKeyId ) ;
// Initiates E2E Encryption
async handshake ( ) {
switch ( this . state ) {
case E2E _ROOM _STATES . KEYS _RECEIVED :
case E2E _ROOM _STATES . NOT _STARTED :
this . setState ( E2E _ROOM _STATES . ESTABLISHING ) ;
try {
const groupKey = Subscriptions . findOne ( { rid : this . roomId } ) . E2EKey ;
if ( groupKey ) {
await this . importGroupKey ( groupKey ) ;
return this . setState ( E2E _ROOM _STATES . READY ) ;
}
} catch ( error ) {
this . setState ( E2E _ROOM _STATES . ERROR ) ;
// this.error = error;
return this . error ( 'Error fetching group key: ' , error ) ;
}
try {
const room = Rooms . findOne ( { _id : this . roomId } ) ;
if ( ! room . e2eKeyId ) { // TODO CHECK_PERMISSION
this . setState ( E2E _ROOM _STATES . CREATING _KEYS ) ;
await this . createGroupKey ( ) ;
return this . setState ( E2E _ROOM _STATES . READY ) ;
}
this . setState ( E2E _ROOM _STATES . WAITING _KEYS ) ;
this . log ( 'Requesting room key' ) ;
Notifications . notifyUsersOfRoom ( this . roomId , 'e2ekeyRequest' , this . roomId , room . e2eKeyId ) ;
} catch ( error ) {
// this.error = error;
this . setState ( E2E _ROOM _STATES . ERROR ) ;
}
}
}
isSupportedRoomType ( type ) {
@ -102,7 +253,7 @@ export class E2ERoom {
}
async importGroupKey ( groupKey ) {
console . log ( 'E2E -> Importing room key' ) ;
this . log ( 'Importing room key -> ' , this . roomId ) ;
// Get existing group key
// const keyID = groupKey.slice(0, 12);
groupKey = groupKey . slice ( 12 ) ;
@ -113,7 +264,7 @@ export class E2ERoom {
const decryptedKey = await decryptRSA ( e2e . privateKey , groupKey ) ;
this . sessionKeyExportedString = toString ( decryptedKey ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error decrypting group key: ' , error ) ;
return this . error ( 'Error decrypting group key: ' , error ) ;
}
this . keyID = Base64 . encode ( this . sessionKeyExportedString ) . slice ( 0 , 12 ) ;
@ -124,68 +275,59 @@ export class E2ERoom {
// Key has been obtained. E2E is now in session.
this . groupSessionKey = key ;
} catch ( error ) {
return console . error ( 'E2E -> Error importing group key: ' , error ) ;
return this . error ( 'Error importing group key: ' , error ) ;
}
}
async createGroupKey ( ) {
console . log ( 'E2E -> Creating room key' ) ;
this . log ( 'Creating room key' ) ;
// Create group key
let key ;
try {
key = await generateAESKey ( ) ;
this . groupSessionKey = key ;
this . groupSessionKey = await generateAESKey ( ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error generating group key: ' , error ) ;
console . error ( 'Error generating group key: ' , error ) ;
throw error ;
}
let sessionKeyExported ;
try {
sessionKeyExported = await exportJWKKey ( this . groupSessionKey ) ;
const sessionKeyExported = await exportJWKKey ( this . groupSessionKey ) ;
this . sessionKeyExportedString = JSON . stringify ( sessionKeyExported ) ;
this . keyID = Base64 . encode ( this . sessionKeyExportedString ) . slice ( 0 , 12 ) ;
await call ( 'e2e.setRoomKeyID' , this . roomId , this . keyID ) ;
await this . encryptKeyForOtherParticipants ( ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error exporting group key: ' , error ) ;
this . error ( 'Error exporting group key: ' , error ) ;
throw error ;
}
this . sessionKeyExportedString = JSON . stringify ( sessionKeyExported ) ;
this . keyID = Base64 . encode ( this . sessionKeyExportedString ) . slice ( 0 , 12 ) ;
await call ( 'e2e.setRoomKeyID' , this . roomId , this . keyID ) ;
await this . encryptKeyForOtherParticipants ( ) ;
}
async encryptKeyForOtherParticipants ( ) {
// Encrypt generated session key for every user in room and publish to subscription model.
let users ;
try {
users = await call ( 'e2e.getUsersOfRoomWithoutKey' , this . roomId ) ;
const { users } = await call ( 'e2e.getUsersOfRoomWithoutKey' , this . roomId ) ;
users . forEach ( ( user ) => this . encryptForParticipant ( user ) ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error getting room users: ' , error ) ;
return this . error ( 'Error getting room users: ' , error ) ;
}
users . users . forEach ( ( user ) => this . encryptForParticipand ( user ) ) ;
}
async encryptForParticipand ( user ) {
if ( user . e2e . public _key ) {
let userKey ;
try {
userKey = await importRSAKey ( JSON . parse ( user . e2e . public _key ) , [ 'encrypt' ] ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error importing user key: ' , error ) ;
}
// const vector = crypto.getRandomValues(new Uint8Array(16));
// Encrypt session key for this user with his/her public key
let encryptedUserKey ;
try {
encryptedUserKey = await encryptRSA ( userKey , toArrayBuffer ( this . sessionKeyExportedString ) ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error encrypting user key: ' , error ) ;
}
async encryptForParticipant ( user ) {
let userKey ;
try {
userKey = await importRSAKey ( JSON . parse ( user . e2e . public _key ) , [ 'encrypt' ] ) ;
} catch ( error ) {
return this . error ( 'Error importing user key: ' , error ) ;
}
// const vector = crypto.getRandomValues(new Uint8Array(16));
// Encrypt session key for this user with his/her public key
try {
const encryptedUserKey = await encryptRSA ( userKey , toArrayBuffer ( this . sessionKeyExportedString ) ) ;
// Key has been encrypted. Publish to that user's subscription model for this room.
await call ( 'e2e.updateGroupKey' , this . roomId , user . _id , this . keyID + Base64 . encode ( new Uint8Array ( encryptedUserKey ) ) ) ;
} catch ( error ) {
return this . error ( 'Error encrypting user key: ' , error ) ;
}
}
@ -202,7 +344,7 @@ export class E2ERoom {
try {
result = await encryptAES ( vector , this . groupSessionKey , fileArrayBuffer ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error encrypting group key: ' , error ) ;
return this . error ( 'Error encrypting group key: ' , error ) ;
}
const output = joinVectorAndEcryptedData ( vector , result ) ;
@ -223,7 +365,7 @@ export class E2ERoom {
try {
return await decryptAES ( vector , this . groupSessionKey , cipherText ) ;
} catch ( error ) {
console . error ( 'E2E -> Error decrypting file: ' , error ) ;
this . error ( 'Error decrypting file: ' , error ) ;
return false ;
}
@ -244,7 +386,7 @@ export class E2ERoom {
try {
result = await encryptAES ( vector , this . groupSessionKey , data ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error encrypting message: ' , error ) ;
return this . error ( 'Error encrypting message: ' , error ) ;
}
return this . keyID + Base64 . encode ( joinVectorAndEcryptedData ( vector , result ) ) ;
@ -265,11 +407,30 @@ export class E2ERoom {
userId : this . userId ,
ts ,
} ) ) ;
const enc = this . encryptText ( data ) ;
return enc ;
return this . encryptText ( data ) ;
}
// Decrypt messages
async decryptMessage ( message ) {
if ( message . t !== 'e2e' || message . e2e === 'done' ) {
return message ;
}
const data = await this . decrypt ( message . msg ) ;
if ( ! data ? . text ) {
return message ;
}
return {
... message ,
msg : data . text ,
e2e : 'done' ,
} ;
}
async decrypt ( message ) {
if ( ! this . isSupportedRoomType ( this . typeOfRoom ) ) {
return message ;
@ -289,7 +450,7 @@ export class E2ERoom {
const result = await decryptAES ( vector , this . groupSessionKey , cipherText ) ;
return EJSON . parse ( new TextDecoder ( 'UTF-8' ) . decode ( new Uint8Array ( result ) ) ) ;
} catch ( error ) {
return console . error ( 'E2E -> Error decrypting message: ' , error , message ) ;
return this . error ( 'Error decrypting message: ' , error , message ) ;
}
}