text: 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.get('messages');
  		const threadsCollection = db.get('threads');
  		const threadMessagesCollection = db.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.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.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.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;
  