/* * encryption.js * Copyright (c) 2019, Texx * License: MIT * See https://github.com/texxme/Texx/blob/master/LICENSE */ const Dexie = require('dexie'); const moment = require('moment'); const crypto = require('crypto'); const JsSHA = require('jssha'); const fingerprintJs = require('fingerprintjs2'); const openpgp = require('openpgp'); const swal = require('sweetalert'); let db; // compress encryption data openpgp.config.compression = openpgp.enums.compression.zlib; const self = module.exports = { fingerprint: '', /** * Generates database and tables * @returns Boolean */ setupDatabase: () => { db = new Dexie('texx'); db.version(2) .stores({ own_keys: '&key_type, key_data', peer_keys: 'peer_id, key_data', messages: '++id, peer_id, message, time, self', contacts: 'peer_id, fingerprint', }); localStorage.setItem('database', 'success'); db.open() .catch((err) => { localStorage.setItem('database', 'failed'); console.error(`Database failed: ${err.stack}`); swal('Could not create the local database!', 'Please try loading this site from a different browser', 'error'); }); return true; }, /** * Generates and stores encrypted private key, public key and a revocation certificate * @param peerId * @returns {Promise} */ generateKeys: async (peerId) => { await self.generatePublicFingerprint(); const options = { userIds: [{ name: peerId, comment: await self.getPublicFingerprint(), }], curve: 'ed25519', passphrase: self.fingerprint, }; openpgp.generateKey(options) .then(async (key) => { await db.own_keys.put({ key_type: 'private_key', key_data: key.privateKeyArmored, }); db.own_keys.put({ key_type: 'public_key', key_data: key.publicKeyArmored, }) .then(() => console.log('[LOG] Successfully generated and stored keys!')); }); }, /** * Gets the peers private key * @returns {Dexie.Promise>} */ getPrivateKey: async () => db.own_keys.where('key_type') .equals('private_key') .limit(1) .toArray() .then(res => (res.length > 0 ? res[0].key_data : '')), /** * Gets the peers public key * @returns {Dexie.Promise>} */ getPublicKey: async () => db.own_keys.where('key_type') .equals('public_key') .limit(1) .toArray() .then(res => (res.length > 0 ? res[0].key_data : '')), /** * Encrypts the data with a public key (e.g the one of the peer with which you're chatting) * @param data * @param publicKey * @returns {Promise} */ encrypt: async (data, publicKey) => { const privateKeyObj = await self.decryptPrivateKey(); const options = { message: openpgp.message.fromText(data), publicKeys: (await openpgp.key.readArmored(publicKey)).keys, privateKeys: [privateKeyObj], // for signing }; return openpgp.encrypt(options) .then(ciphertext => ciphertext.data); }, /** * Decrypts encrypted data with own encrypted private key and * verifies the data with the public key * @param data * @param publicKey * @returns {Promise} */ decrypt: async (data, publicKey) => { const privateKeyObj = await self.decryptPrivateKey(); const options = { message: await openpgp.message.readArmored(data), publicKeys: (await openpgp.key.readArmored(publicKey)).keys, // for verification privateKeys: [privateKeyObj], }; return openpgp.decrypt(options) .then(plaintext => plaintext.data); }, /** * Decrypts the private key * @returns {Promise} */ decryptPrivateKey: async () => { const privateKeyObj = (await openpgp.key.readArmored(await self.getPrivateKey())).keys[0]; await privateKeyObj.decrypt(self.fingerprint); return privateKeyObj; }, /** * Checks whether the peer has keys * @returns {boolean} */ isEncrypted: async () => Dexie.exists('texx') .then(async (exists) => { if (exists) { const hasPrivateKey = self.getPrivateKey() .then(res => res !== ''); const hasPublicKey = self.getPublicKey() .then(res => res !== ''); return (hasPrivateKey && hasPublicKey); } return false; }), /** * Encrypts a message * @param message * @returns {string} */ encryptMessage: (message) => { const cipher = crypto.createCipher('aes-256-ctr', self.fingerprint); const encrypted = cipher.update(message, 'utf8', 'hex'); console.log('[LOG] Encrypted message successfully!'); return encrypted; }, /** * Decrypts a message * @param message * @returns {string} */ decryptMessage: (message) => { const cipher = crypto.createCipher('aes-256-ctr', self.fingerprint); const plaintext = cipher.update(message, 'hex', 'utf8'); console.log('[LOG] Decrypted message successfully!'); return plaintext; }, /** * Stores a message * @param peerId * @param message * @param isSelf */ storeMessage: async (peerId, message, isSelf = false) => { db.messages.put({ peer_id: peerId, message: self.encryptMessage(message), time: new Date(), self: isSelf, }) .then(() => console.log(`[LOG] Stored message of ${peerId}`)); }, /** * Gets the messages with a peer * @param peerId * @param publicKey * @returns {Promise} */ getMessages: async (peerId, publicKey) => { console.log('[LOG] Getting messages...'); try { const messages = await db.messages.where('peer_id') .equals(peerId) .reverse() .sortBy('id'); const messageArray = []; for (let i = messages.length; i--;) { let plainTextMessage; if (messages[i].self) { plainTextMessage = self.decryptMessage(messages[i].message); } else { plainTextMessage = await self.decrypt( self.decryptMessage(messages[i].message), publicKey, ); } messageArray.push({ type: 'decrypted', self: messages[i].self, message: plainTextMessage, time: moment(messages[i].time) .fromNow(), }); } return messageArray; } catch (err) { console.error(err); console.log('[LOG] No messages found!'); return []; } }, /** * Stores a peer to the contacts * @param peerId * @returns {Promise} */ storePeer: async (peerId) => { await db.contacts.put({ peer_id: peerId, fingerprint: await self.getPublicKeyFingerprint(await self.getPeerPublicKey(peerId)), }) .then(() => console.log(`[LOG] Stored fingerprint of ${peerId}`)) .catch(err => console.error(err)); }, /** * Gets every stored peer * @returns {Promise} */ getStoredPeers: async () => db.contacts.toArray(), /** * Gets the public fingerprint of a peer * @param peerId * @returns {Dexie.Promise>>} */ getPeerFingerprint: async peerId => db.contacts.where('peer_id') .equals(peerId) .limit(1) .toArray() .then(res => (res.length > 0 ? res[0].key_data : '')), /** * Stores the public key of a peer * @param peerId * @param key */ storePeerPublicKey: async (peerId, key) => { await db.peer_keys.put({ peer_id: peerId, key_data: key, }) .then(async () => { await self.storePeer(peerId); console.log(`[LOG] Stored public key of ${peerId}`); }) .catch(err => console.error(err)); }, /** * Gets and verifies the public key of a peer * @param peerId * @returns {Dexie.Promise>} */ getPeerPublicKey: async peerId => db.peer_keys.where('peer_id') .equals(peerId) .limit(1) .toArray() .then(async (res) => { let publicKey; if (res.length > 0) { publicKey = res[0].key_data; const publicKeyPeerId = await self.getPublicKeyPeerId(publicKey); if (publicKeyPeerId !== peerId && await self.getPeerFingerprint(peerId) === await self.getPublicKeyFingerprint(await self.getPeerPublicKey(peerId))) { publicKey = ''; console.error(`[LOG] Public key verification failed! The peers real identity is ${publicKeyPeerId}`); swal('There\'s something strange going on here!', `The peers ID could not be verified! His real ID is ${publicKeyPeerId}`, 'error'); } else { console.log('[LOG] Public key verification succeeded!'); } } else { publicKey = ''; } return publicKey; }), /** * Gets the peer id of a public key * @param publicKey * @returns {Promise} */ getPublicKeyPeerId: async publicKey => (await openpgp.key.readArmored(publicKey)).keys[0] .getPrimaryUser() .then(obj => obj.user.userId.userid.replace(/ \((.+?)\)/g, '')) || '', /** * Generates the unique fingerprint of the peer using every data javascript can get * from the browser and the hashed passphrase of the peer * @param passphrase * @returns {Promise} */ generatePrivateFingerprint: passphrase => fingerprintJs.getPromise({ excludes: { // TODO: Use more reliable fingerprinting method enumerateDevices: true, screenResolution: true, availableScreenResolution: true, webglVendorAndRenderer: true, userAgent: true, webgl: true, pixelRatio: true, }, }) .then(async (components) => { localStorage.setItem(Date.now() .toString(), JSON.stringify(components)); const fingerprintHash = fingerprintJs.x64hash128(components.map(pair => pair.value) .join(), 31); let shaObj = new JsSHA('SHA3-512', 'TEXT'); shaObj.update(passphrase); const passphraseHash = shaObj.getHash('HEX'); shaObj = new JsSHA('SHA3-512', 'TEXT'); shaObj.update(passphraseHash); shaObj.update(fingerprintHash); self.fingerprint = shaObj.getHash('HEX'); }), /** * Generates the unique fingerprint of the peer using every data javascript can get from the * browser and a randomly generated string * @returns {Promise} */ generatePublicFingerprint: () => fingerprintJs.getPromise({ excludes: { enumerateDevices: true, screenResolution: true, availableScreenResolution: true, webglVendorAndRenderer: true, userAgent: true, webgl: true, pixelRatio: true, }, }) .then(async (components) => { const fingerprintHash = fingerprintJs.x64hash128(components.map(pair => pair.value) .join(), 31); console.log(`[LOG] Your fingerprint is: ${fingerprintHash}`); const shaObj = new JsSHA('SHA3-512', 'TEXT'); shaObj.update(fingerprintHash); shaObj.update(Math.random() .toString(10)); await db.own_keys.put({ key_type: 'public_fingerprint', key_data: shaObj.getHash('HEX'), }); }), /** * Gets the public fingerprint of the peer * @returns {Dexie.Promise>} */ getPublicFingerprint: async () => db.own_keys.where('key_type') .equals('public_fingerprint') .limit(1) .toArray() .then(res => (res.length > 0 ? res[0].key_data : '')), /** * Gets the fingerprint of a public key * @param publicKey * @returns {Promise} */ getPublicKeyFingerprint: async publicKey => (await openpgp.key.readArmored(publicKey)).keys[0] .getPrimaryUser() .then(obj => obj.user.userId.userid.match(/\((.*)\)/)[1]) || '', /** * Resets the database/encryption */ reset: () => { db.delete(); localStorage.removeItem('database'); localStorage.removeItem('peer_id'); console.log('[LOG] Database has been deleted!'); }, };