Guest User

Untitled

a guest
Mar 27th, 2018
253
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 22.26 KB | None | 0 0
  1. const firebase = require('firebase-admin');
  2. const _ = require('lodash');
  3. const keccak256 = require('js-sha3').keccak256;
  4. const EC = require('elliptic').ec;
  5. const { promisify } = require('util');
  6. const { encryption, keystore, upgrade } = require('eth-lightwallet');
  7. const ChatKey = require('../../../model/ChatKey');
  8. const Storage = require('../../../lib/storage');
  9. const stripe = require('../../../api/stripe');
  10. const { sendDeeplinkAsSMS } = require('../../../lib/sms');
  11. const { BadRequest, AccessDenied, InternalError } = require('../../../lib/error');
  12.  
  13.  
  14. keystore.pCreateVault = promisify(keystore.createVault);
  15. // Convert Solidity bytes16 type to string
  16. const bytes16ToString = function (hex) {
  17. return Buffer.from(hex.replace(/^0x/, ''), 'hex').toString().replace(/\0+/, '')
  18. };
  19.  
  20. class Helper {
  21. static makeAddressWith0x(walletAddress) {
  22. return /^0x/.test(walletAddress) ? walletAddress : '0x' + walletAddress;
  23. }
  24.  
  25. static validateArguments(argumentsStr, req) {
  26. const args = argumentsStr.split(' ');
  27. args.forEach(field => req.checkBody(field, 'Missing ' + field).notEmpty());
  28. if (args.includes('userType')) {
  29. req.checkBody('userType', 'Invalid userType').isInt();
  30. }
  31.  
  32. let errors = req.validationErrors();
  33. if (errors)
  34. throw new BadRequest(errors.map(e => e.msg).join(', '));
  35. }
  36.  
  37. static async checkUserAccess(chatId, firebaseTokenID) {
  38. // Obtain user id from Firebase token
  39. let userId = (await firebase.auth().verifyIdToken(firebaseTokenID)).uid;
  40.  
  41. // Check user id vs chat users
  42. let chatUserIdList = _.keys((await fb.ref(`chats/${chatId}/users`).once('value')).val());
  43. if (!chatUserIdList.includes(userId))
  44. throw new AccessDenied('Access denied: chat');
  45. }
  46.  
  47. static async createKeystoreObj(settings) {
  48. // generate a new BIP32 12-word seed
  49. const seedPhrase = keystore.generateRandomSeed();
  50.  
  51. // Create new keystore
  52. const options = {
  53. password: settings('ethereum.lightwallet.password'),
  54. seedPhrase,
  55. hdPathString: settings('ethereum.lightwallet.hdPathString')
  56. };
  57. let keystoreObj = await keystore.pCreateVault(options);
  58. return keystoreObj;
  59. }
  60.  
  61. static async getNewChatKeyAndServerPublicKey(keystoreObj, settings) {
  62. // New ChatKey database entry
  63. let chatKey = new ChatKey();
  64.  
  65. keystoreObj.pKeyFromPassword = promisify(keystoreObj.keyFromPassword);
  66. const pwDerivedKey = await keystoreObj.pKeyFromPassword(settings('ethereum.lightwallet.password'));
  67. // generate new address/private key pairs
  68. // the corresponding private keys are also encrypted
  69. keystoreObj.generateNewAddress(pwDerivedKey, 1);
  70. // Create new public key for the server
  71. const address = keystoreObj.getAddresses()[0];
  72. const serverPublicKey = encryption.addressToPublicEncKey(keystoreObj, pwDerivedKey, address);
  73.  
  74. // Save the keystore after the keys were generated
  75. chatKey.keystore = keystoreObj.serialize();
  76. if (!chatKey.keystore)
  77. throw new InternalError('Keystore not found for chat ' + chatId);
  78. return { chatKey, serverPublicKey };
  79. }
  80.  
  81. static createChatInDB({ userId, propertyId, sourceId, serverPublicKey, deeplinkId, backlink, userType, publicKey }) {
  82. const created = firebase.database.ServerValue.TIMESTAMP;
  83. return fb.ref('chats').push({
  84. info: {
  85. created,
  86. propertyId,
  87. sourceId,
  88. serverPublicKey,
  89. type: 'property',
  90. creator: userId,
  91. deeplinkId: deeplinkId || null,
  92. backlink: backlink || null
  93. },
  94. users: {
  95. [userId]: {
  96. publicKey,
  97. type: userType
  98. }
  99. }
  100. });
  101. }
  102.  
  103. static getAdressFromPublicKey(pubKey){
  104. const ec = new EC('secp256k1');
  105. // Decode public key
  106. const key = ec.keyFromPublic(pubKey, 'hex');
  107. console.log("key", key);
  108.  
  109. // Convert to uncompressed format
  110. const publicKey = key.getPublic().encode('hex').slice(2);
  111.  
  112. return keccak256(Buffer.from(publicKey, 'hex')).slice(64 - 40);
  113. }
  114.  
  115. static async updateChatProperty({ chatId, userId, propertyId }) {
  116. const updated = (await fb.ref(`chats/${chatId}/info/created`).once('value')).val();
  117.  
  118. // Add new chat to the user
  119. return fb.ref(`users/${userId}/chats/${propertyId}/${chatId}`).set({
  120. type: 'property',
  121. updated: -updated
  122. });
  123. }
  124.  
  125. static async createStripeCharge({ serviceTariff, sourceId, stripeToken }) {
  126. // Get charge amount for Stripe, USD
  127. let stripeChargeAmt = (await fb.ref('serviceTariffs/' + serviceTariff).once('value')).val();
  128. if (!stripeChargeAmt)
  129. throw new BadRequest('Unknown serviceTariff value');
  130.  
  131. // Get CC charge settings
  132. let isCardChargeRequired = (await fb.ref(`sourceSystemIDs/${sourceId}/settings/creditCardCharging/${serviceTariff}`).once('value')).val();
  133. /*
  134. TODO когда появятся тарифы(или с ними станет все понятно) вернуть проверку
  135. if (isCardChargeRequired === null)
  136. throw new BadRequest(`No settings for ${sourceId}: tariff ${serviceTariff}`);*/
  137. if (isCardChargeRequired && !stripeToken)
  138. throw new BadRequest('Missing stripeToken');
  139.  
  140. if (isCardChargeRequired) {
  141. // Charge user's CC for the amount, using Stripe
  142. let stripeTrx = await stripe.charges.create({
  143. amount: stripeChargeAmt * 100,
  144. currency: 'usd',
  145. source: stripeToken,
  146. });
  147.  
  148. return stripeTrx.id;
  149. }
  150. return null;
  151. }
  152.  
  153. /**
  154. * Функция следит за созданим документа в блокчене.
  155. * После создания документа записывает данные в базу и рассылает смс приграшенным юзерам.
  156. * @param web3
  157. * @param contract
  158. * @param ShelterZoomContractAddress
  159. * @param walletAddress
  160. * @param chatId
  161. * @param invitations
  162. * @returns {Promise.<void>}
  163. */
  164. static async watchingForDocumentCreated({ web3, contract, ShelterZoomContractAddress, walletAddress, chatId, invitations }) {
  165. let shelterZoomObj = web3.eth.contract(contract).at(ShelterZoomContractAddress);
  166. let eventDocumentCreated = shelterZoomObj.documentCreated({ _from: walletAddress });
  167. eventDocumentCreated.watch(async function (err, result) {
  168. if (err)
  169. return console.error(err);
  170.  
  171. try {
  172. if (_.get(result, 'args._binder')) {
  173. // Unbind the watcher
  174. eventDocumentCreated.stopWatching();
  175.  
  176. // Locate first document
  177. let documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
  178. let docId = _.get(_.keys(documents), '0');
  179. if (!docId) {
  180. console.error(`Document for chat ${chatId} not found`);
  181. return;
  182. }
  183.  
  184. // Update document address at Firebase
  185. await fb.ref(`chats/${chatId}/documents/${docId}/address`).set(result.args._binder);
  186.  
  187. if (invitations && invitations.length) {
  188. // Send deeplinks to requested users
  189. await Promise.all(invitations.map(user => sendDeeplinkAsSMS(chatId, user)));
  190. }
  191. }
  192. else {
  193. console.error('Event result.args._binder is empty');
  194. }
  195. }
  196. catch (ex) {
  197. console.error(ex);
  198. }
  199. });
  200. }
  201.  
  202. /**
  203. * Obtain smart contract settings
  204. * @param web3
  205. * @param contract
  206. * @param settings
  207. * @returns {{ShelterZoomContractAddress: *, createBinderCharge, gasPrice: (number|*)}}
  208. */
  209. static getSmartContractSettings({ web3, contract, settings }) {
  210. let houseKeeperObj = web3.eth.contract(contract).at(settings('ethereum.HouseKeeper.v5.address'));
  211. let scSettings = houseKeeperObj.getSettings.call();
  212. let ShelterZoomContractAddress = scSettings[0];
  213. let createBinderCharge = scSettings[1].toString();
  214. let gasPrice = scSettings[2].toNumber();
  215. return { ShelterZoomContractAddress, createBinderCharge, gasPrice };
  216. }
  217.  
  218. static makeEthTransaction({ web3, settings, walletAddress, value, gasPrice }) {
  219. // Recharge client's Ethereum wallet
  220. return web3.eth.sendTransaction({
  221. from: settings('ethereum.shelterZoomWallet.address'),
  222. to: walletAddress,
  223. value,
  224. gasPrice
  225. });
  226. }
  227.  
  228. static getUserPhone(firebase) {
  229. let userPhone = _.get(firebase, 'identities.phone.0'); // +79967888864
  230. if (!userPhone)
  231. throw new BadRequest('Cannot obtain user phone from firebaseTokenID');
  232. return userPhone.replace(/^\+/, '');
  233. }
  234.  
  235. static async getDeepLinkInfo(deeplinkTokenID, userPhone) {
  236. // Fetch deeplink info
  237. let deeplinkInfo = (await fb.ref('invitationTokens/' + deeplinkTokenID).once('value')).val();
  238. if (!deeplinkInfo)
  239. throw new BadRequest('Given deeplinkTokenID not found');
  240.  
  241. // Check phone numbers
  242. if (deeplinkInfo.phone != userPhone)
  243. throw new BadRequest('Phone numbers do not match');
  244. return deeplinkInfo;
  245. }
  246.  
  247. static async removeInvitationPhone(chatId, userPhone) {
  248. const phonesToInvite = (await fb.ref(`chats/${chatId}/phonesToInvite`).once('value')).val();
  249. if (phonesToInvite && phonesToInvite[userPhone]) {
  250. return fb.ref(`chats/${chatId}/phonesToInvite/${userPhone}`).set(null);
  251. }
  252.  
  253. // legacy code for old chat in mongo
  254. let chatKey = await ChatKey.findById(chatId);
  255. if (!chatKey)
  256. throw new BadRequest('Key entry not found for chat ' + chatId);
  257.  
  258. let chatKeyUserKey = chatKey.getKeysByPhone(userPhone);
  259. if (!chatKeyUserKey)
  260. throw new BadRequest(`Key entry not found for chat ${chatId} and phone ${userPhone}`);
  261.  
  262. chatKeyUserKey.phone = null;
  263. await chatKey.save();
  264. }
  265.  
  266. static async addNewUserToChat(chatId, userId, deeplinkInfo, publicKey) {
  267. const promises = [];
  268. promises.push(fb.ref(`chats/${chatId}/users/${userId}`).set({
  269. type: deeplinkInfo.userType,
  270. publicKey,
  271. }));
  272.  
  273. // Fetch chat info
  274. let chatInfo = (await fb.ref(`chats/${chatId}/info`).once('value')).val();
  275. // Add the chat to the new user
  276. promises.push(fb.ref(`users/${userId}/chats/${chatInfo.propertyId}/${chatId}`).set({
  277. type: 'property',
  278. updated: -_.get(chatInfo, 'lastMessage.created', chatInfo.created)
  279. }));
  280.  
  281. return Promise.all(promises);
  282. }
  283.  
  284. static async addUserToBlockchain({ web3, settings, chatId, contract, walletAddress, deeplinkInfo }) {
  285. // Unlock source Ethereum account
  286. web3.personal.unlockAccount(
  287. settings('ethereum.shelterZoomWallet.address'),
  288. settings('ethereum.shelterZoomWallet.passPhrase'));
  289.  
  290. // Add new participant to document, get first revision
  291. const docs = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
  292. const firstRevisionDoc = docs[Object.keys(docs)[0]];
  293. const binderObj = web3.eth.contract(contract).at(firstRevisionDoc.address);
  294. binderObj.addUser.sendTransaction(
  295. walletAddress, deeplinkInfo.userType,
  296. { from: settings('ethereum.shelterZoomWallet.address') });
  297.  
  298. const eventDoc = binderObj.eventDoc({
  299. _from: '0x' + settings('ethereum.shelterZoomWallet.address'),
  300. _binder: firstRevisionDoc.address
  301. });
  302.  
  303. return { eventDoc, binderObj };
  304. }
  305.  
  306.  
  307. static async removeUserFromBlockchain({ web3, settings, chatId, contract, walletAddress, deeplinkInfo }) {
  308. // Unlock source Ethereum account
  309. web3.personal.unlockAccount(
  310. settings('ethereum.shelterZoomWallet.address'),
  311. settings('ethereum.shelterZoomWallet.passPhrase'));
  312.  
  313. // Add new participant to document, get first revision
  314. const docs = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
  315. const firstRevisionDoc = docs[Object.keys(docs)[0]];
  316. const binderObj = web3.eth.contract(contract).at(firstRevisionDoc.address);
  317. binderObj.addUser.sendTransaction(
  318. walletAddress, deeplinkInfo.userType,
  319. { from: settings('ethereum.shelterZoomWallet.address') });
  320.  
  321. const eventDoc = binderObj.eventDoc({
  322. _from: '0x' + settings('ethereum.shelterZoomWallet.address'),
  323. _binder: firstRevisionDoc.address
  324. });
  325.  
  326. return { eventDoc, binderObj };
  327. }
  328.  
  329. /**
  330. * Смотрим когда добавил юзер в блокчейн,
  331. * вызываем функцию которая добавляет его во все ревизии документа.
  332. * @param eventDoc
  333. * @param binderObj
  334. * @param chatId
  335. * @param storage
  336. * @param settings
  337. * @param walletAddress
  338. */
  339. static watchingForAddUser({ eventDoc, binderObj, chatId, storage, settings, walletAddress, userId }) {
  340. // awaiting when transaction was complete
  341. eventDoc.watch(async function (err, result) {
  342. if (err)
  343. return console.error(err);
  344. try {
  345. let _action = bytes16ToString(_.get(result, 'args._action', ''));
  346. if (_action === 'userAdded') {
  347. // check added user address
  348. const addrList = binderObj.getUsers.call();
  349. if (!addrList.includes(walletAddress)) return;
  350.  
  351. // Unbind the watcher
  352. eventDoc.stopWatching();
  353. await Helper.saveReEncryptedBinder(chatId, storage, userId, settings);
  354. }
  355. } catch (ex) {
  356. console.error(ex);
  357. }
  358. });
  359. }
  360.  
  361. /**
  362. * Перешифровываем ревизии документов, тк добавили нового юзера.
  363. * Рекурсивно перевызываем если за время шифрования пригласили еще одного юзера.
  364. * @param chatId
  365. * @param storage
  366. * @param settings
  367. * @returns {Promise.<void>}
  368. */
  369. static async saveReEncryptedBinder(chatId, storage, userId, settings) {
  370. // get actual data for ChatKey
  371. const chatKey = await ChatKey.findById(chatId);
  372. await Helper.updateChatKeyToVersion3(chatKey, settings);
  373. const keystoreObj = keystore.deserialize(chatKey.keystore);
  374. keystoreObj.pKeyFromPassword = promisify(keystoreObj.keyFromPassword);
  375.  
  376. // Prepare keys needed
  377. const pwDerivedKey = await keystoreObj.pKeyFromPassword(settings('ethereum.lightwallet.password'));
  378. const publicKeys = await Helper.getPublicKeys(chatId);
  379. if (publicKeys.length === 0)
  380. throw new InternalError('No public keys found for chat ' + chatId);
  381.  
  382. const serverPublicKey = (await fb.ref(`chats/${chatId}/info/serverPublicKey`).once('value')).val();
  383. const documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
  384. _.forOwn(documents, async (doc, docId) => {
  385. // Use a binder from the firebase
  386. let encryptedBinder = await Storage.get(storage, chatId, docId);
  387.  
  388. const address = keystoreObj.getAddresses()[0];
  389. // Decrypt the binder
  390. let decryptedBinder = encryption.multiDecryptString(
  391. keystoreObj,
  392. pwDerivedKey,
  393. JSON.parse(encryptedBinder),
  394. (doc.publicKey === serverPublicKey) ? publicKeys[0] : doc.publicKey,
  395. address);
  396.  
  397. if (!decryptedBinder)
  398. throw new AccessDenied(`Cannot decrypt document ${docId} for chat ${chatId}`);
  399.  
  400. // Re-encrypt the binder
  401. let reEncryptedBinder = encryption.multiEncryptString(
  402. keystoreObj,
  403. pwDerivedKey,
  404. decryptedBinder,
  405. address,
  406. publicKeys);
  407.  
  408. const newPublicKeys = await Helper.getPublicKeys(chatId);
  409. if (publicKeys.length !== newPublicKeys.length) {
  410. await Helper.saveReEncryptedBinder(chatId, storage, userId, settings);
  411. return;
  412. }
  413.  
  414. let docRef = fb.ref(`chats/${chatId}/documents/${docId}`);
  415.  
  416. await Storage.put(storage, chatId, docId, JSON.stringify(reEncryptedBinder));
  417. await docRef.child('updated').set(firebase.database.ServerValue.TIMESTAMP);
  418. await docRef.child('publicKey').set(serverPublicKey);
  419. await docRef.child('readers/' + userId).set({
  420. status: 'canView',
  421. updated: firebase.database.ServerValue.TIMESTAMP
  422. });
  423. });
  424. }
  425.  
  426. /**
  427. * Updated chatKey keystore to new version #3
  428. * @param chatKey
  429. * @returns {Promise.<void>}
  430. */
  431. static async updateChatKeyToVersion3(chatKey, settings) {
  432. // change keystore for actual version
  433. let parsedKeystore = JSON.parse(chatKey.keystore);
  434. if (parsedKeystore.version !== 3) {
  435. const oldHDPathString = "m/0'/0'/0'";
  436. delete parsedKeystore.ksData[oldHDPathString];
  437. const pUpgradeOldSerialized = promisify(upgrade.upgradeOldSerialized);
  438. chatKey.keystore = await pUpgradeOldSerialized(JSON.stringify(parsedKeystore), settings('ethereum.lightwallet.password'));
  439. // Save keys to the database
  440. await chatKey.save();
  441. }
  442. }
  443.  
  444. static async getPublicKeys(chatId, withServerPublicKey = false) {
  445. const publicKeys = _.map((await fb.ref(`chats/${chatId}/users`).once('value')).val(), 'publicKey');
  446. if (withServerPublicKey) {
  447. const serverPublicKey = (await fb.ref(`chats/${chatId}/info/serverPublicKey`).once('value')).val();
  448. publicKeys.push(serverPublicKey);
  449. }
  450. return publicKeys;
  451. }
  452.  
  453. static EthereumUnlockAccount(web3, settings) {
  454. web3.personal.unlockAccount(
  455. settings('ethereum.shelterZoomWallet.address'),
  456. settings('ethereum.shelterZoomWallet.passPhrase'));
  457. }
  458.  
  459. static watchingForActionCreated({ eventDoc, chatId, userId, invitations, revokeAccess }) {
  460. eventDoc.watch(async function (err, result) {
  461. if (err)
  462. return console.error(err);
  463.  
  464. try {
  465. let _action = bytes16ToString(_.get(result, 'args._action', ''));
  466. if (_action == 'created' || _action == 'createdSigned') {
  467. // Unbind the watcher
  468. eventDoc.stopWatching();
  469.  
  470. // Update document.revision field at Firebase
  471. _.forOwn((await fb.ref(`chats/${chatId}/documents`).once('value')).val(),
  472. async function (doc, docId) {
  473. // Find user's document w/o revision
  474. if (doc.creator == userId && !doc.revision) {
  475. // Write a revision to this document
  476. await fb.ref(`chats/${chatId}/documents/${docId}/revision`).set(result.args._result.toNumber());
  477.  
  478. // Stop iteration
  479. return false;
  480. }
  481. });
  482.  
  483. if (revokeAccess && revokeAccess.length) {
  484. await Helper.revokeUsersAccess(revokeAccess, chatId);
  485. }
  486.  
  487. if (invitations && invitations.length) {
  488. // Send deeplinks to requested users
  489. await Promise.all(invitations.map(user => sendDeeplinkAsSMS(chatId, user)));
  490. }
  491. }
  492. }
  493. catch (ex) {
  494. console.error(ex);
  495. }
  496. });
  497. }
  498.  
  499. static setTransactionsInChatKey(stripeTrx, etherTrx, serviceTariff, chatKey) {
  500. if (_.isArray(chatKey.transactions)) {
  501. chatKey.transactions.push({ stripeTrx, etherTrx, serviceTariff });
  502. } else {
  503. chatKey.transactions = [{ stripeTrx, etherTrx, serviceTariff }];
  504. }
  505. }
  506.  
  507. static addInvitationsPhone(invitations, chatId) {
  508. const phonesToInvite = {};
  509. _.map(invitations, 'phone')
  510. .forEach(phone => {
  511. phonesToInvite[phone] = true;
  512. });
  513.  
  514. return fb.ref(`chats/${chatId}/phonesToInvite`).update(phonesToInvite);
  515. }
  516.  
  517. static async getUserIdByPhone(phone) {
  518. const usersRef = fb.ref('users');
  519. const query = usersRef.orderByChild('info/phone').equalTo(phone);
  520. const user = (await query.once('value')).val();
  521. const userId = user ? Object.keys(chat)[0] : null;
  522. if (!userId) {
  523. throw new InternalError(`User not found with phone ${phone}`);
  524. }
  525. return userId;
  526. }
  527.  
  528. static async getChatIdIfExists(deeplinkId) {
  529. const chatsRef = fb.ref('chats');
  530. const query = chatsRef.orderByChild('info/deeplinkId').equalTo(deeplinkId);
  531. const chat = (await query.once('value')).val();
  532. const chatId = chat ? Object.keys(chat)[0] : null;
  533.  
  534. // show error is we are have chat with document
  535. if (chatId && chat && _.isObject(chat.documents) && !_.isEmpty(chat.documents)) {
  536. throw new InternalError(`Chat ${chatId} with documents already exists`);
  537. }
  538. return chatId;
  539. }
  540.  
  541. static async revokeUsersAccess(revokeAccess, chatId) {
  542. await Promise.all(revokeAccess.map(async (r) => {
  543. const userId = await Helper.getUserIdByPhone(r.phone);
  544. const documents = (await fb.ref(`chats/${chatId}/documents`).once('value')).val();
  545. _.forOwn(documents, async (doc, docId) => {
  546. await fb.ref(`chats/${chatId}/documents/${docId}/readers/${userId}`).set(null);
  547. });
  548. return fb.ref(`chats/${chatId}/users/${userId}`).set(null);
  549. }));
  550. }
  551. }
  552.  
  553. module.exports = Helper;
Add Comment
Please, Sign In to add comment