Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- const firebase = require('firebase-admin');
- const _ = require('lodash');
- const keccak256 = require('js-sha3').keccak256;
- const EC = require('elliptic').ec;
- const { promisify } = require('util');
- const { encryption, keystore, upgrade } = require('eth-lightwallet');
- const ChatKey = require('../../../model/ChatKey');
- const Storage = require('../../../lib/storage');
- const stripe = require('../../../api/stripe');
- const { sendDeeplinkAsSMS } = require('../../../lib/sms');
- const { BadRequest, AccessDenied, InternalError } = require('../../../lib/error');
- keystore.pCreateVault = promisify(keystore.createVault);
- // Convert Solidity bytes16 type to string
- const bytes16ToString = function (hex) {
- return Buffer.from(hex.replace(/^0x/, ''), 'hex').toString().replace(/\0+/, '')
- };
- class Helper {
- static makeAddressWith0x(walletAddress) {
- return /^0x/.test(walletAddress) ? walletAddress : '0x' + walletAddress;
- }
- static validateArguments(argumentsStr, req) {
- const args = argumentsStr.split(' ');
- args.forEach(field => req.checkBody(field, 'Missing ' + field).notEmpty());
- if (args.includes('userType')) {
- req.checkBody('userType', 'Invalid userType').isInt();
- }
- let errors = req.validationErrors();
- if (errors)
- throw new BadRequest(errors.map(e => e.msg).join(', '));
- }
- static async checkUserAccess(chatId, firebaseTokenID) {
- // Obtain user id from Firebase token
- let userId = (await firebase.auth().verifyIdToken(firebaseTokenID)).uid;
- // Check user id vs chat users
- let chatUserIdList = _.keys((await fb.ref(`chats/${chatId}/users`).once('value')).val());
- if (!chatUserIdList.includes(userId))
- throw new AccessDenied('Access denied: chat');
- }
- static async createKeystoreObj(settings) {
- // generate a new BIP32 12-word seed
- const seedPhrase = keystore.generateRandomSeed();
- // Create new keystore
- const options = {
- password: settings('ethereum.lightwallet.password'),
- seedPhrase,
- hdPathString: settings('ethereum.lightwallet.hdPathString')
- };
- let keystoreObj = await keystore.pCreateVault(options);
- return keystoreObj;
- }
- static async getNewChatKeyAndServerPublicKey(keystoreObj, settings) {
- // New ChatKey database entry
- let chatKey = new ChatKey();
- keystoreObj.pKeyFromPassword = promisify(keystoreObj.keyFromPassword);
- const pwDerivedKey = await keystoreObj.pKeyFromPassword(settings('ethereum.lightwallet.password'));
- // generate new address/private key pairs
- // the corresponding private keys are also encrypted
- keystoreObj.generateNewAddress(pwDerivedKey, 1);
- // Create new public key for the server
- const address = keystoreObj.getAddresses()[0];
- const serverPublicKey = encryption.addressToPublicEncKey(keystoreObj, pwDerivedKey, address);
- // Save the keystore after the keys were generated
- chatKey.keystore = keystoreObj.serialize();
- if (!chatKey.keystore)
- throw new InternalError('Keystore not found for chat ' + chatId);
- return { chatKey, serverPublicKey };
- }
- static createChatInDB({ userId, propertyId, sourceId, serverPublicKey, deeplinkId, backlink, userType, publicKey }) {
- const created = firebase.database.ServerValue.TIMESTAMP;
- return fb.ref('chats').push({
- info: {
- created,
- propertyId,
- sourceId,
- serverPublicKey,
- type: 'property',
- creator: userId,
- deeplinkId: deeplinkId || null,
- backlink: backlink || null
- },
- users: {
- [userId]: {
- publicKey,
- type: userType
- }
- }
- });
- }
- static getAdressFromPublicKey(pubKey){
- const ec = new EC('secp256k1');
- // Decode public key
- const key = ec.keyFromPublic(pubKey, 'hex');
- console.log("key", key);
- // Convert to uncompressed format
- const publicKey = key.getPublic().encode('hex').slice(2);
- return keccak256(Buffer.from(publicKey, 'hex')).slice(64 - 40);
- }
- static async updateChatProperty({ chatId, userId, propertyId }) {
- const updated = (await fb.ref(`chats/${chatId}/info/created`).once('value')).val();
- // Add new chat to the user
- return fb.ref(`users/${userId}/chats/${propertyId}/${chatId}`).set({
- type: 'property',
- updated: -updated
- });
- }
- static async createStripeCharge({ serviceTariff, sourceId, stripeToken }) {
- // Get charge amount for Stripe, USD
- let stripeChargeAmt = (await fb.ref('serviceTariffs/' + serviceTariff).once('value')).val();
- if (!stripeChargeAmt)
- throw new BadRequest('Unknown serviceTariff value');
- // Get CC charge settings
- let isCardChargeRequired = (await fb.ref(`sourceSystemIDs/${sourceId}/settings/creditCardCharging/${serviceTariff}`).once('value')).val();
- /*
- TODO когда появятся тарифы(или с ними станет все понятно) вернуть проверку
- if (isCardChargeRequired === null)
- throw new BadRequest(`No settings for ${sourceId}: tariff ${serviceTariff}`);*/
- if (isCardChargeRequired && !stripeToken)
- throw new BadRequest('Missing stripeToken');
- if (isCardChargeRequired) {
- // Charge user's CC for the amount, using Stripe
- let stripeTrx = await stripe.charges.create({
- amount: stripeChargeAmt * 100,
- currency: 'usd',
- source: stripeToken,
- });
- return stripeTrx.id;
- }
- return null;
- }
- /**
- * Функция следит за созданим документа в блокчене.
- * После создания документа записывает данные в базу и рассылает смс приграшенным юзерам.
- * @param web3
- * @param contract
- * @param ShelterZoomContractAddress
- * @param walletAddress
- * @param chatId
- * @param invitations
- * @returns {Promise.<void>}
- */
- static async watchingForDocumentCreated({ web3, contract, ShelterZoomContractAddress, walletAddress, chatId, invitations }) {
- let shelterZoomObj = web3.eth.contract(contract).at(ShelterZoomContractAddress);
- let eventDocumentCreated = shelterZoomObj.documentCreated({ _from: walletAddress });
- eventDocumentCreated.watch(async function (err, result) {
- if (err)
- return console.error(err);
- try {
- if (_.get(result, 'args._binder')) {
- // Unbind the watcher
- eventDocumentCreated.stopWatching();
- // Locate first document
- let documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
- let docId = _.get(_.keys(documents), '0');
- if (!docId) {
- console.error(`Document for chat ${chatId} not found`);
- return;
- }
- // Update document address at Firebase
- await fb.ref(`chats/${chatId}/documents/${docId}/address`).set(result.args._binder);
- if (invitations && invitations.length) {
- // Send deeplinks to requested users
- await Promise.all(invitations.map(user => sendDeeplinkAsSMS(chatId, user)));
- }
- }
- else {
- console.error('Event result.args._binder is empty');
- }
- }
- catch (ex) {
- console.error(ex);
- }
- });
- }
- /**
- * Obtain smart contract settings
- * @param web3
- * @param contract
- * @param settings
- * @returns {{ShelterZoomContractAddress: *, createBinderCharge, gasPrice: (number|*)}}
- */
- static getSmartContractSettings({ web3, contract, settings }) {
- let houseKeeperObj = web3.eth.contract(contract).at(settings('ethereum.HouseKeeper.v5.address'));
- let scSettings = houseKeeperObj.getSettings.call();
- let ShelterZoomContractAddress = scSettings[0];
- let createBinderCharge = scSettings[1].toString();
- let gasPrice = scSettings[2].toNumber();
- return { ShelterZoomContractAddress, createBinderCharge, gasPrice };
- }
- static makeEthTransaction({ web3, settings, walletAddress, value, gasPrice }) {
- // Recharge client's Ethereum wallet
- return web3.eth.sendTransaction({
- from: settings('ethereum.shelterZoomWallet.address'),
- to: walletAddress,
- value,
- gasPrice
- });
- }
- static getUserPhone(firebase) {
- let userPhone = _.get(firebase, 'identities.phone.0'); // +79967888864
- if (!userPhone)
- throw new BadRequest('Cannot obtain user phone from firebaseTokenID');
- return userPhone.replace(/^\+/, '');
- }
- static async getDeepLinkInfo(deeplinkTokenID, userPhone) {
- // Fetch deeplink info
- let deeplinkInfo = (await fb.ref('invitationTokens/' + deeplinkTokenID).once('value')).val();
- if (!deeplinkInfo)
- throw new BadRequest('Given deeplinkTokenID not found');
- // Check phone numbers
- if (deeplinkInfo.phone != userPhone)
- throw new BadRequest('Phone numbers do not match');
- return deeplinkInfo;
- }
- static async removeInvitationPhone(chatId, userPhone) {
- const phonesToInvite = (await fb.ref(`chats/${chatId}/phonesToInvite`).once('value')).val();
- if (phonesToInvite && phonesToInvite[userPhone]) {
- return fb.ref(`chats/${chatId}/phonesToInvite/${userPhone}`).set(null);
- }
- // legacy code for old chat in mongo
- let chatKey = await ChatKey.findById(chatId);
- if (!chatKey)
- throw new BadRequest('Key entry not found for chat ' + chatId);
- let chatKeyUserKey = chatKey.getKeysByPhone(userPhone);
- if (!chatKeyUserKey)
- throw new BadRequest(`Key entry not found for chat ${chatId} and phone ${userPhone}`);
- chatKeyUserKey.phone = null;
- await chatKey.save();
- }
- static async addNewUserToChat(chatId, userId, deeplinkInfo, publicKey) {
- const promises = [];
- promises.push(fb.ref(`chats/${chatId}/users/${userId}`).set({
- type: deeplinkInfo.userType,
- publicKey,
- }));
- // Fetch chat info
- let chatInfo = (await fb.ref(`chats/${chatId}/info`).once('value')).val();
- // Add the chat to the new user
- promises.push(fb.ref(`users/${userId}/chats/${chatInfo.propertyId}/${chatId}`).set({
- type: 'property',
- updated: -_.get(chatInfo, 'lastMessage.created', chatInfo.created)
- }));
- return Promise.all(promises);
- }
- static async addUserToBlockchain({ web3, settings, chatId, contract, walletAddress, deeplinkInfo }) {
- // Unlock source Ethereum account
- web3.personal.unlockAccount(
- settings('ethereum.shelterZoomWallet.address'),
- settings('ethereum.shelterZoomWallet.passPhrase'));
- // Add new participant to document, get first revision
- const docs = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
- const firstRevisionDoc = docs[Object.keys(docs)[0]];
- const binderObj = web3.eth.contract(contract).at(firstRevisionDoc.address);
- binderObj.addUser.sendTransaction(
- walletAddress, deeplinkInfo.userType,
- { from: settings('ethereum.shelterZoomWallet.address') });
- const eventDoc = binderObj.eventDoc({
- _from: '0x' + settings('ethereum.shelterZoomWallet.address'),
- _binder: firstRevisionDoc.address
- });
- return { eventDoc, binderObj };
- }
- static async removeUserFromBlockchain({ web3, settings, chatId, contract, walletAddress, deeplinkInfo }) {
- // Unlock source Ethereum account
- web3.personal.unlockAccount(
- settings('ethereum.shelterZoomWallet.address'),
- settings('ethereum.shelterZoomWallet.passPhrase'));
- // Add new participant to document, get first revision
- const docs = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
- const firstRevisionDoc = docs[Object.keys(docs)[0]];
- const binderObj = web3.eth.contract(contract).at(firstRevisionDoc.address);
- binderObj.addUser.sendTransaction(
- walletAddress, deeplinkInfo.userType,
- { from: settings('ethereum.shelterZoomWallet.address') });
- const eventDoc = binderObj.eventDoc({
- _from: '0x' + settings('ethereum.shelterZoomWallet.address'),
- _binder: firstRevisionDoc.address
- });
- return { eventDoc, binderObj };
- }
- /**
- * Смотрим когда добавил юзер в блокчейн,
- * вызываем функцию которая добавляет его во все ревизии документа.
- * @param eventDoc
- * @param binderObj
- * @param chatId
- * @param storage
- * @param settings
- * @param walletAddress
- */
- static watchingForAddUser({ eventDoc, binderObj, chatId, storage, settings, walletAddress, userId }) {
- // awaiting when transaction was complete
- eventDoc.watch(async function (err, result) {
- if (err)
- return console.error(err);
- try {
- let _action = bytes16ToString(_.get(result, 'args._action', ''));
- if (_action === 'userAdded') {
- // check added user address
- const addrList = binderObj.getUsers.call();
- if (!addrList.includes(walletAddress)) return;
- // Unbind the watcher
- eventDoc.stopWatching();
- await Helper.saveReEncryptedBinder(chatId, storage, userId, settings);
- }
- } catch (ex) {
- console.error(ex);
- }
- });
- }
- /**
- * Перешифровываем ревизии документов, тк добавили нового юзера.
- * Рекурсивно перевызываем если за время шифрования пригласили еще одного юзера.
- * @param chatId
- * @param storage
- * @param settings
- * @returns {Promise.<void>}
- */
- static async saveReEncryptedBinder(chatId, storage, userId, settings) {
- // get actual data for ChatKey
- const chatKey = await ChatKey.findById(chatId);
- await Helper.updateChatKeyToVersion3(chatKey, settings);
- const keystoreObj = keystore.deserialize(chatKey.keystore);
- keystoreObj.pKeyFromPassword = promisify(keystoreObj.keyFromPassword);
- // Prepare keys needed
- const pwDerivedKey = await keystoreObj.pKeyFromPassword(settings('ethereum.lightwallet.password'));
- const publicKeys = await Helper.getPublicKeys(chatId);
- if (publicKeys.length === 0)
- throw new InternalError('No public keys found for chat ' + chatId);
- const serverPublicKey = (await fb.ref(`chats/${chatId}/info/serverPublicKey`).once('value')).val();
- const documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
- _.forOwn(documents, async (doc, docId) => {
- // Use a binder from the firebase
- let encryptedBinder = await Storage.get(storage, chatId, docId);
- const address = keystoreObj.getAddresses()[0];
- // Decrypt the binder
- let decryptedBinder = encryption.multiDecryptString(
- keystoreObj,
- pwDerivedKey,
- JSON.parse(encryptedBinder),
- (doc.publicKey === serverPublicKey) ? publicKeys[0] : doc.publicKey,
- address);
- if (!decryptedBinder)
- throw new AccessDenied(`Cannot decrypt document ${docId} for chat ${chatId}`);
- // Re-encrypt the binder
- let reEncryptedBinder = encryption.multiEncryptString(
- keystoreObj,
- pwDerivedKey,
- decryptedBinder,
- address,
- publicKeys);
- const newPublicKeys = await Helper.getPublicKeys(chatId);
- if (publicKeys.length !== newPublicKeys.length) {
- await Helper.saveReEncryptedBinder(chatId, storage, userId, settings);
- return;
- }
- let docRef = fb.ref(`chats/${chatId}/documents/${docId}`);
- await Storage.put(storage, chatId, docId, JSON.stringify(reEncryptedBinder));
- await docRef.child('updated').set(firebase.database.ServerValue.TIMESTAMP);
- await docRef.child('publicKey').set(serverPublicKey);
- await docRef.child('readers/' + userId).set({
- status: 'canView',
- updated: firebase.database.ServerValue.TIMESTAMP
- });
- });
- }
- /**
- * Updated chatKey keystore to new version #3
- * @param chatKey
- * @returns {Promise.<void>}
- */
- static async updateChatKeyToVersion3(chatKey, settings) {
- // change keystore for actual version
- let parsedKeystore = JSON.parse(chatKey.keystore);
- if (parsedKeystore.version !== 3) {
- const oldHDPathString = "m/0'/0'/0'";
- delete parsedKeystore.ksData[oldHDPathString];
- const pUpgradeOldSerialized = promisify(upgrade.upgradeOldSerialized);
- chatKey.keystore = await pUpgradeOldSerialized(JSON.stringify(parsedKeystore), settings('ethereum.lightwallet.password'));
- // Save keys to the database
- await chatKey.save();
- }
- }
- static async getPublicKeys(chatId, withServerPublicKey = false) {
- const publicKeys = _.map((await fb.ref(`chats/${chatId}/users`).once('value')).val(), 'publicKey');
- if (withServerPublicKey) {
- const serverPublicKey = (await fb.ref(`chats/${chatId}/info/serverPublicKey`).once('value')).val();
- publicKeys.push(serverPublicKey);
- }
- return publicKeys;
- }
- static EthereumUnlockAccount(web3, settings) {
- web3.personal.unlockAccount(
- settings('ethereum.shelterZoomWallet.address'),
- settings('ethereum.shelterZoomWallet.passPhrase'));
- }
- static watchingForActionCreated({ eventDoc, chatId, userId, invitations, revokeAccess }) {
- eventDoc.watch(async function (err, result) {
- if (err)
- return console.error(err);
- try {
- let _action = bytes16ToString(_.get(result, 'args._action', ''));
- if (_action == 'created' || _action == 'createdSigned') {
- // Unbind the watcher
- eventDoc.stopWatching();
- // Update document.revision field at Firebase
- _.forOwn((await fb.ref(`chats/${chatId}/documents`).once('value')).val(),
- async function (doc, docId) {
- // Find user's document w/o revision
- if (doc.creator == userId && !doc.revision) {
- // Write a revision to this document
- await fb.ref(`chats/${chatId}/documents/${docId}/revision`).set(result.args._result.toNumber());
- // Stop iteration
- return false;
- }
- });
- if (revokeAccess && revokeAccess.length) {
- await Helper.revokeUsersAccess(revokeAccess, chatId);
- }
- if (invitations && invitations.length) {
- // Send deeplinks to requested users
- await Promise.all(invitations.map(user => sendDeeplinkAsSMS(chatId, user)));
- }
- }
- }
- catch (ex) {
- console.error(ex);
- }
- });
- }
- static setTransactionsInChatKey(stripeTrx, etherTrx, serviceTariff, chatKey) {
- if (_.isArray(chatKey.transactions)) {
- chatKey.transactions.push({ stripeTrx, etherTrx, serviceTariff });
- } else {
- chatKey.transactions = [{ stripeTrx, etherTrx, serviceTariff }];
- }
- }
- static addInvitationsPhone(invitations, chatId) {
- const phonesToInvite = {};
- _.map(invitations, 'phone')
- .forEach(phone => {
- phonesToInvite[phone] = true;
- });
- return fb.ref(`chats/${chatId}/phonesToInvite`).update(phonesToInvite);
- }
- static async getUserIdByPhone(phone) {
- const usersRef = fb.ref('users');
- const query = usersRef.orderByChild('info/phone').equalTo(phone);
- const user = (await query.once('value')).val();
- const userId = user ? Object.keys(chat)[0] : null;
- if (!userId) {
- throw new InternalError(`User not found with phone ${phone}`);
- }
- return userId;
- }
- static async getChatIdIfExists(deeplinkId) {
- const chatsRef = fb.ref('chats');
- const query = chatsRef.orderByChild('info/deeplinkId').equalTo(deeplinkId);
- const chat = (await query.once('value')).val();
- const chatId = chat ? Object.keys(chat)[0] : null;
- // show error is we are have chat with document
- if (chatId && chat && _.isObject(chat.documents) && !_.isEmpty(chat.documents)) {
- throw new InternalError(`Chat ${chatId} with documents already exists`);
- }
- return chatId;
- }
- static async revokeUsersAccess(revokeAccess, chatId) {
- await Promise.all(revokeAccess.map(async (r) => {
- const userId = await Helper.getUserIdByPhone(r.phone);
- const documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
- _.forOwn(documents, async (doc, docId) => {
- await fb.ref(`chats/${chatId}/documents/${docId}/readers/${userId}`).set(null);
- });
- return fb.ref(`chats/${chatId}/users/${userId}`).set(null);
- }));
- }
- }
- module.exports = Helper;
Add Comment
Please, Sign In to add comment