verdnatura-chat/app/lib/encryption/room.js

256 lines
6.4 KiB
JavaScript

import EJSON from 'ejson';
import { Base64 } from 'js-base64';
import SimpleCrypto from 'react-native-simple-crypto';
import {
toString,
b64ToBuffer,
bufferToUtf8,
bufferToB64,
bufferToB64URI,
utf8ToBuffer,
splitVectorData,
joinVectorData
} from './utils';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from './constants';
import RocketChat from '../rocketchat';
import Deferred from '../../utils/deferred';
import debounce from '../../utils/debounce';
import { Encryption } from './index';
import database from '../database';
import log from '../../utils/log';
export default class EncryptionRoom {
constructor(roomId) {
this.ready = false;
this.roomId = roomId;
this.establishing = false;
this.readyPromise = new Deferred();
this.readyPromise.then(() => {
// Mark as ready
this.ready = true;
// Mark as established
this.establishing = false;
});
}
// Initialize the E2E room
handshake = async() => {
// If it's already ready we don't need to handshake again
if (this.ready) {
return;
}
// If it's already establishing
if (this.establishing) {
// Return the ready promise to wait this client ready
return this.readyPromise;
}
const db = database.active;
const subCollection = db.collections.get('subscriptions');
try {
// Find the subscription
const subscription = await subCollection.find(this.roomId);
const { E2EKey, e2eKeyId } = subscription;
// If this room has a E2EKey, we import it
if (E2EKey) {
// We're establishing a new room encryption client
this.establishing = true;
await this.importRoomKey(E2EKey, Encryption.privateKey);
this.readyPromise.resolve();
return;
}
// If it doesn't have a e2eKeyId, we need to create keys to the room
if (!e2eKeyId) {
// We're establishing a new room encryption client
this.establishing = true;
await this.createRoomKey();
this.readyPromise.resolve();
return;
}
// Request a E2EKey for this room to other users
await this.requestRoomKey(e2eKeyId);
} catch (e) {
log(e);
}
}
// Import roomKey as an AES Decrypt key
importRoomKey = async(E2EKey, privateKey) => {
const roomE2EKey = E2EKey.slice(12);
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
this.sessionKeyExportedString = toString(decryptedKey);
this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12);
// Extract K from Web Crypto Secret Key
// K is a base64URL encoded array of bytes
// Web Crypto API uses this as a private key to decrypt/encrypt things
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
const { k } = EJSON.parse(this.sessionKeyExportedString);
this.roomKey = b64ToBuffer(k);
}
// Create a key to a room
createRoomKey = async() => {
const key = await SimpleCrypto.utils.randomBytes(16);
this.roomKey = key;
// Web Crypto format of a Secret Key
const sessionKeyExported = {
// Type of Secret Key
kty: 'oct',
// Algorithm
alg: 'A128CBC',
// Base64URI encoded array of bytes
k: bufferToB64URI(this.roomKey),
// Specific Web Crypto properties
ext: true,
key_ops: ['encrypt', 'decrypt']
};
this.sessionKeyExportedString = EJSON.stringify(sessionKeyExported);
this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12);
await RocketChat.e2eSetRoomKeyID(this.roomId, this.keyID);
await this.encryptRoomKey();
}
// Request a key to this room
// We're debouncing this function to avoid multiple calls
// when you join a room with a lot of messages and nobody
// can send the encryption key at the moment.
// Each time you see a encrypted message of a room that you don't have a key
// this will be called again and run once in 5 seconds
requestRoomKey = debounce(async(e2eKeyId) => {
await RocketChat.e2eRequestRoomKey(this.roomId, e2eKeyId);
}, 5000, true)
// Create an encrypted key for this room based on users
encryptRoomKey = async() => {
const result = await RocketChat.e2eGetUsersOfRoomWithoutKey(this.roomId);
if (result.success) {
const { users } = result;
await Promise.all(users.map(user => this.encryptRoomKeyForUser(user)));
}
}
// Encrypt the room key to each user in
encryptRoomKeyForUser = async(user) => {
if (user?.e2e?.public_key) {
const { public_key: publicKey } = user.e2e;
const userKey = await SimpleCrypto.RSA.importKey(EJSON.parse(publicKey));
const encryptedUserKey = await SimpleCrypto.RSA.encrypt(this.sessionKeyExportedString, userKey);
await RocketChat.e2eUpdateGroupKey(user?._id, this.roomId, this.keyID + encryptedUserKey);
}
}
// Provide this room key to a user
provideKeyToUser = async(keyId) => {
// Don't provide a key if the keyId received
// is different than the current one
if (this.keyID !== keyId) {
return;
}
await this.encryptRoomKey();
}
// Encrypt text
encryptText = async(text) => {
text = utf8ToBuffer(text);
const vector = await SimpleCrypto.utils.randomBytes(16);
const data = await SimpleCrypto.AES.encrypt(
text,
this.roomKey,
vector
);
return this.keyID + bufferToB64(joinVectorData(vector, data));
}
// Encrypt messages
encrypt = async(message) => {
if (!this.ready) {
return message;
}
try {
const msg = await this.encryptText(EJSON.stringify({
_id: message._id,
text: message.msg,
userId: this.userId,
ts: new Date()
}));
return {
...message,
t: E2E_MESSAGE_TYPE,
e2e: E2E_STATUS.PENDING,
msg
};
} catch {
// Do nothing
}
return message;
}
// Decrypt text
decryptText = async(msg) => {
msg = b64ToBuffer(msg.slice(12));
const [vector, cipherText] = splitVectorData(msg);
const decrypted = await SimpleCrypto.AES.decrypt(
cipherText,
this.roomKey,
vector
);
const m = EJSON.parse(bufferToUtf8(decrypted));
return m.text;
}
// Decrypt messages
decrypt = async(message) => {
if (!this.ready) {
return message;
}
try {
const { t, e2e } = message;
// If message type is e2e and it's encrypted still
if (t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE) {
let { msg, tmsg } = message;
// Decrypt msg
msg = await this.decryptText(msg);
// Decrypt tmsg
if (tmsg) {
tmsg = await this.decryptText(tmsg);
}
return {
...message,
tmsg,
msg,
e2e: E2E_STATUS.DONE
};
}
} catch {
// Do nothing
}
return message;
}
}