import EJSON from 'ejson'; import SimpleCrypto from 'react-native-simple-crypto'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { Q } from '@nozbe/watermelondb'; import { toString, utf8ToBuffer, splitVectorData, joinVectorData, randomPassword } from './utils'; import { E2E_PUBLIC_KEY, E2E_PRIVATE_KEY, E2E_RANDOM_PASSWORD_KEY, E2E_STATUS, E2E_MESSAGE_TYPE, E2E_BANNER_TYPE } from './constants'; import RocketChat from '../rocketchat'; import { EncryptionRoom } from './index'; import UserPreferences from '../userPreferences'; import database from '../database'; import protectedFunction from '../methods/helpers/protectedFunction'; import Deferred from '../../utils/deferred'; import log from '../../utils/log'; import store from '../createStore'; class Encryption { constructor() { this.ready = false; this.privateKey = null; this.roomInstances = {}; this.readyPromise = new Deferred(); this.readyPromise .then(() => { this.ready = true; }) .catch(() => { this.ready = false; }); } // Initialize Encryption client initialize = (userId) => { this.userId = userId; this.roomInstances = {}; // Don't await these promises // so they can run parallelized this.decryptPendingSubscriptions(); this.decryptPendingMessages(); // Mark Encryption client as ready this.readyPromise.resolve(); } get establishing() { const { banner } = store.getState().encryption; // If the password was not inserted yet if (!banner || banner === E2E_BANNER_TYPE.REQUEST_PASSWORD) { // We can't decrypt/encrypt, so, reject this try return Promise.reject(); } // Wait the client ready state return this.readyPromise; } // Stop Encryption client stop = () => { this.userId = null; this.privateKey = null; this.roomInstances = {}; // Cancel ongoing encryption/decryption requests this.readyPromise.reject(); // Reset Deferred this.ready = false; this.readyPromise = new Deferred(); this.readyPromise .then(() => { this.ready = true; }) .catch(() => { this.ready = false; }); } // When a new participant join and request a new room encryption key provideRoomKeyToUser = async(keyId, rid) => { // If the client is not ready if (!this.ready) { try { // Wait for ready status await this.establishing; } catch { // If it can't be initialized (missing password) // return and don't provide a key return; } } const roomE2E = await this.getRoomInstance(rid); return roomE2E.provideKeyToUser(keyId); } // Persist keys on UserPreferences persistKeys = async(server, publicKey, privateKey) => { this.privateKey = await SimpleCrypto.RSA.importKey(EJSON.parse(privateKey)); await UserPreferences.setStringAsync(`${ server }-${ E2E_PUBLIC_KEY }`, EJSON.stringify(publicKey)); await UserPreferences.setStringAsync(`${ server }-${ E2E_PRIVATE_KEY }`, privateKey); } // Could not obtain public-private keypair from server. createKeys = async(userId, server) => { // Generate new keys const key = await SimpleCrypto.RSA.generateKeys(2048); // Cast these keys to the properly server format const publicKey = await SimpleCrypto.RSA.exportKey(key.public); const privateKey = await SimpleCrypto.RSA.exportKey(key.private); // Persist these new keys this.persistKeys(server, publicKey, EJSON.stringify(privateKey)); // Create a password to encode the private key const password = await this.createRandomPassword(server); // Encode the private key const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, userId); // Send the new keys to the server await RocketChat.e2eSetUserPublicAndPrivateKeys(EJSON.stringify(publicKey), encodedPrivateKey); // Request e2e keys of all encrypted rooms await RocketChat.e2eRequestSubscriptionKeys(); } // Encode a private key before send it to the server encodePrivateKey = async(privateKey, password, userId) => { const masterKey = await this.generateMasterKey(password, userId); const vector = await SimpleCrypto.utils.randomBytes(16); const data = await SimpleCrypto.AES.encrypt( utf8ToBuffer(privateKey), masterKey, vector ); return EJSON.stringify(new Uint8Array(joinVectorData(vector, data))); } // Decode a private key fetched from server decodePrivateKey = async(privateKey, password, userId) => { const masterKey = await this.generateMasterKey(password, userId); const [vector, cipherText] = splitVectorData(EJSON.parse(privateKey)); const privKey = await SimpleCrypto.AES.decrypt( cipherText, masterKey, vector ); return toString(privKey); } // Generate a user master key, this is based on userId and a password generateMasterKey = async(password, userId) => { const iterations = 1000; const hash = 'SHA256'; const keyLen = 32; const passwordBuffer = utf8ToBuffer(password); const saltBuffer = utf8ToBuffer(userId); const masterKey = await SimpleCrypto.PBKDF2.hash( passwordBuffer, saltBuffer, iterations, keyLen, hash ); return masterKey; } // Create a random password to local created keys createRandomPassword = async(server) => { const password = randomPassword(); await UserPreferences.setStringAsync(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`, password); return password; } changePassword = async(server, password) => { // Cast key to the format server is expecting const privateKey = await SimpleCrypto.RSA.exportKey(this.privateKey); // Encode the private key const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, this.userId); const publicKey = await UserPreferences.getStringAsync(`${ server }-${ E2E_PUBLIC_KEY }`); // Send the new keys to the server await RocketChat.e2eSetUserPublicAndPrivateKeys(EJSON.stringify(publicKey), encodedPrivateKey); } // get a encryption room instance getRoomInstance = async(rid) => { // Prevent handshake again if (this.roomInstances[rid]?.ready) { return this.roomInstances[rid]; } // If doesn't have a instance of this room if (!this.roomInstances[rid]) { this.roomInstances[rid] = new EncryptionRoom(rid, this.userId); } const roomE2E = this.roomInstances[rid]; // Start Encryption Room instance handshake await roomE2E.handshake(); return roomE2E; } // Logic to decrypt all pending messages/threads/threadMessages // after initialize the encryption client decryptPendingMessages = async(roomId) => { const db = database.active; const messagesCollection = db.collections.get('messages'); const threadsCollection = db.collections.get('threads'); const threadMessagesCollection = db.collections.get('thread_messages'); // e2e status is null or 'pending' and message type is 'e2e' const whereClause = [ Q.where('t', E2E_MESSAGE_TYPE), Q.or( Q.where('e2e', null), Q.where('e2e', E2E_STATUS.PENDING) ) ]; // decrypt messages of a room if (roomId) { whereClause.push(Q.where('rid', roomId)); } try { // Find all messages/threads/threadsMessages that have pending e2e status const messagesToDecrypt = await messagesCollection.query(...whereClause).fetch(); const threadsToDecrypt = await threadsCollection.query(...whereClause).fetch(); const threadMessagesToDecrypt = await threadMessagesCollection.query(...whereClause).fetch(); // Concat messages/threads/threadMessages let toDecrypt = [...messagesToDecrypt, ...threadsToDecrypt, ...threadMessagesToDecrypt]; toDecrypt = await Promise.all(toDecrypt.map(async(message) => { const { t, msg, tmsg } = message; const { id: rid } = message.subscription; // WM Object -> Plain Object const newMessage = await this.decryptMessage({ t, rid, msg, tmsg }); if (message._hasPendingUpdate) { console.log(message); return; } return message.prepareUpdate(protectedFunction((m) => { Object.assign(m, newMessage); })); })); await db.action(async() => { await db.batch(...toDecrypt); }); } catch (e) { log(e); } } // Logic to decrypt all pending subscriptions // after initialize the encryption client decryptPendingSubscriptions = async() => { const db = database.active; const subCollection = db.collections.get('subscriptions'); try { // Find all rooms that can have a lastMessage encrypted // If we select only encrypted rooms we can miss some room that changed their encrypted status const subsEncrypted = await subCollection.query(Q.where('e2e_key_id', Q.notEq(null))).fetch(); // We can't do this on database level since lastMessage is not a database object const subsToDecrypt = subsEncrypted.filter(sub => ( // Encrypted message sub?.lastMessage?.t === E2E_MESSAGE_TYPE // Message pending decrypt && sub?.lastMessage?.e2e === E2E_STATUS.PENDING )); await Promise.all(subsToDecrypt.map(async(sub) => { const { rid, lastMessage } = sub; const newSub = await this.decryptSubscription({ rid, lastMessage }); if (sub._hasPendingUpdate) { console.log(sub); return; } return sub.prepareUpdate(protectedFunction((m) => { Object.assign(m, newSub); })); })); await db.action(async() => { await db.batch(...subsToDecrypt); }); } catch (e) { log(e); } } // Decrypt a subscription lastMessage decryptSubscription = async(subscription) => { // If the subscription doesn't have a lastMessage just return if (!subscription?.lastMessage) { return subscription; } const { lastMessage } = subscription; const { t, e2e } = lastMessage; // If it's not a encrypted message or was decrypted before if (t !== E2E_MESSAGE_TYPE || e2e === E2E_STATUS.DONE) { return subscription; } // If the client is not ready if (!this.ready) { try { // Wait for ready status await this.establishing; } catch { // If it can't be initialized (missing password) // return the encrypted message return subscription; } } const { rid } = subscription; const db = database.active; const subCollection = db.collections.get('subscriptions'); let subRecord; try { subRecord = await subCollection.find(rid); } catch { // Do nothing } try { const batch = []; // If the subscription doesn't exists yet if (!subRecord) { // Let's create the subscription with the data received batch.push(subCollection.prepareCreate((s) => { s._raw = sanitizedRaw({ id: rid }, subCollection.schema); Object.assign(s, subscription); })); // If the subscription already exists but doesn't have the E2EKey yet } else if (!subRecord.E2EKey && subscription.E2EKey) { if (!subRecord._hasPendingUpdate) { // Let's update the subscription with the received E2EKey batch.push(subRecord.prepareUpdate((s) => { s.E2EKey = subscription.E2EKey; })); } } // If batch has some operation if (batch.length) { await db.action(async() => { await db.batch(...batch); }); } } catch { // Abort the decryption process // Return as received return subscription; } // Get a instance using the subscription const roomE2E = await this.getRoomInstance(rid); const decryptedMessage = await roomE2E.decrypt(lastMessage); return { ...subscription, lastMessage: decryptedMessage }; } // Encrypt a message encryptMessage = async(message) => { const { rid } = message; const db = database.active; const subCollection = db.collections.get('subscriptions'); try { // Find the subscription const subRecord = await subCollection.find(rid); // Subscription is not encrypted at the moment if (!subRecord.encrypted) { // Send a non encrypted message return message; } // If the client is not ready if (!this.ready) { // Wait for ready status await this.establishing; } const roomE2E = await this.getRoomInstance(rid); return roomE2E.encrypt(message); } catch { // Subscription not found // or client can't be initialized (missing password) } // Send a non encrypted message return message; } // Decrypt a message decryptMessage = async(message) => { const { t, e2e } = message; // Prevent create a new instance if this room was encrypted sometime ago if (t !== E2E_MESSAGE_TYPE || e2e === E2E_STATUS.DONE) { return message; } // If the client is not ready if (!this.ready) { try { // Wait for ready status await this.establishing; } catch { // If it can't be initialized (missing password) // return the encrypted message return message; } } const { rid } = message; const roomE2E = await this.getRoomInstance(rid); return roomE2E.decrypt(message); } // Decrypt multiple messages decryptMessages = messages => Promise.all(messages.map(m => this.decryptMessage(m))) // Decrypt multiple subscriptions decryptSubscriptions = subscriptions => Promise.all(subscriptions.map(s => this.decryptSubscription(s))) } const encryption = new Encryption(); export default encryption;