diff --git a/.prettierrc b/.prettierrc index adbf2efe..b6283b83 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,5 @@ "requirePragma": true, "semi": false, "singleQuote": true, - "endOfLine": "lf" + "endOfLine": "auto" } diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index 46735819..2aa3e827 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -4,6 +4,7 @@ const Gun = require('gun') const debounce = require('lodash/debounce') const once = require('lodash/once') +const Encryption = require('../../../utils/encryptionStore') /** @type {import('../contact-api/SimpleGUN').ISEA} */ // @ts-ignore @@ -96,6 +97,7 @@ const Action = require('../action-constants.js') const API = require('../contact-api/index') const Config = require('../config') const Event = require('../event-constants') +// const { nonEncryptedRoutes } = require('../../../utils/protectedRoutes') /** * @typedef {import('../contact-api/SimpleGUN').GUNNode} GUNNode @@ -281,37 +283,116 @@ class Mediator { * @param {Readonly} socket */ constructor(socket) { - this.socket = socket + this.socket = this.encryptSocketInstance(socket) this.connected = true - socket.on('disconnect', this.onDisconnect) + this.socket.on('disconnect', this.onDisconnect) - socket.on(Action.ACCEPT_REQUEST, this.acceptRequest) - socket.on(Action.BLACKLIST, this.blacklist) - socket.on(Action.GENERATE_NEW_HANDSHAKE_NODE, this.generateHandshakeNode) - socket.on(Action.SEND_HANDSHAKE_REQUEST, this.sendHandshakeRequest) - socket.on( + this.socket.on(Action.ACCEPT_REQUEST, this.acceptRequest) + this.socket.on(Action.BLACKLIST, this.blacklist) + this.socket.on( + Action.GENERATE_NEW_HANDSHAKE_NODE, + this.generateHandshakeNode + ) + this.socket.on(Action.SEND_HANDSHAKE_REQUEST, this.sendHandshakeRequest) + this.socket.on( Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG, this.sendHRWithInitialMsg ) - socket.on(Action.SEND_MESSAGE, this.sendMessage) - socket.on(Action.SEND_PAYMENT, this.sendPayment) - socket.on(Action.SET_AVATAR, this.setAvatar) - socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName) - socket.on(Action.SET_BIO, this.setBio) + this.socket.on(Action.SEND_MESSAGE, this.sendMessage) + this.socket.on(Action.SET_AVATAR, this.setAvatar) + this.socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName) + this.socket.on(Action.SEND_PAYMENT, this.sendPayment) + this.socket.on(Action.SET_BIO, this.setBio) - socket.on(Event.ON_AVATAR, this.onAvatar) - socket.on(Event.ON_BLACKLIST, this.onBlacklist) - socket.on(Event.ON_CHATS, this.onChats) - socket.on(Event.ON_DISPLAY_NAME, this.onDisplayName) - socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress) - socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests) - socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests) - socket.on(Event.ON_BIO, this.onBio) - socket.on(Event.ON_SEED_BACKUP, this.onSeedBackup) + this.socket.on(Event.ON_AVATAR, this.onAvatar) + this.socket.on(Event.ON_BLACKLIST, this.onBlacklist) + this.socket.on(Event.ON_CHATS, this.onChats) + this.socket.on(Event.ON_DISPLAY_NAME, this.onDisplayName) + this.socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress) + this.socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests) + this.socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests) + this.socket.on(Event.ON_BIO, this.onBio) + this.socket.on(Event.ON_SEED_BACKUP, this.onSeedBackup) - socket.on(IS_GUN_AUTH, this.isGunAuth) + this.socket.on(IS_GUN_AUTH, this.isGunAuth) + } + + encryptSocketInstance = socket => { + return { + on: (eventName, cb) => { + const deviceId = socket.handshake.query['x-shockwallet-device-id'] + socket.on(eventName, data => { + try { + // if (nonEncryptedEvents.includes(eventName)) { + // return cb(data) + // } + + if (!data) { + return cb(data) + } + + if (!deviceId) { + const error = { + field: 'deviceId', + message: 'Please specify a device ID' + } + console.error(error) + return false + } + + if (!Encryption.isAuthorizedDevice({ deviceId })) { + const error = { + field: 'deviceId', + message: 'Please specify a device ID' + } + console.error('Unknown Device', error) + return false + } + if(typeof data === 'string'){ + data = JSON.parse(data) + } + console.log('Event:', eventName) + console.log('Data:', data) + console.log('Decrypt params:', { + deviceId, + message: data.encryptedKey + }) + const decryptedKey = Encryption.decryptKey({ + deviceId, + message: data.encryptedKey + }) + const decryptedMessage = Encryption.decryptMessage({ + message: data.encryptedData, + key: decryptedKey, + iv: data.iv + }) + const decryptedData = JSON.parse(decryptedMessage) + return cb(decryptedData) + } catch (err) { + console.error(err) + return false + } + }) + }, + emit: (eventName, data) => { + try { + const deviceId = socket.handshake.query['x-shockwallet-device-id'] + const authorized = Encryption.isAuthorizedDevice({ deviceId }) + const encryptedMessage = authorized + ? Encryption.encryptMessage({ + message: data, + deviceId + }) + : data + console.log('Sending Message...', eventName, data, encryptedMessage) + socket.emit(eventName, encryptedMessage) + } catch (err) { + console.error(err) + } + } + } } isGunAuth = () => { @@ -725,6 +806,8 @@ class Mediator { try { const { token } = body + console.log('ON_CHATS', body) + await throwOnInvalidToken(token) API.Events.onChats( diff --git a/src/routes.js b/src/routes.js index 4dbfb316..7a07a8d2 100644 --- a/src/routes.js +++ b/src/routes.js @@ -5,27 +5,37 @@ */ "use strict"; -const Http = require("axios"); +const Axios = require("axios"); const Crypto = require("crypto"); const logger = require("winston"); +const httpsAgent = require("https"); const responseTime = require("response-time"); +const uuid = require("uuid/v4"); const getListPage = require("../utils/paginate"); const auth = require("../services/auth/auth"); const FS = require("../utils/fs"); +const Encryption = require("../utils/encryptionStore"); const LightningServices = require("../utils/lightningServices"); const GunDB = require("../services/gunDB/Mediator"); +const { unprotectedRoutes, nonEncryptedRoutes } = require("../utils/protectedRoutes"); const GunActions = require("../services/gunDB/contact-api/actions") -const { unprotectedRoutes } = require("../utils/protectedRoutes"); const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10; +const SESSION_ID = uuid(); // module.exports = (app) => { -module.exports = ( +module.exports = async ( app, config, mySocketsEvents, - { serverPort } + { serverPort, CA, CA_KEY, usetls } ) => { + const Http = Axios.create({ + httpsAgent: new httpsAgent.Agent({ + ca: await FS.readFile(CA) + }) + }) + const sanitizeLNDError = (message = "") => message.toLowerCase().includes("unknown") ? message @@ -82,7 +92,7 @@ module.exports = ( const serviceStatus = await getAvailableService(); const LNDStatus = serviceStatus; try { - const APIHealth = await Http.get(`http://localhost:${serverPort}/ping`); + const APIHealth = await Http.get(`${usetls ? 'https' : 'http'}://localhost:${serverPort}/ping`); const APIStatus = { message: APIHealth.data, responseTime: APIHealth.headers["x-response-time"], @@ -93,6 +103,7 @@ module.exports = ( APIStatus }; } catch (err) { + console.error(err); const APIStatus = { message: err.response.data, responseTime: err.response.headers["x-response-time"], @@ -109,7 +120,7 @@ module.exports = ( const health = await checkHealth(); if (health.LNDStatus.success) { if (err) { - res.send({ + res.json({ errorMessage: sanitizeLNDError(err.message) }); } else { @@ -117,7 +128,7 @@ module.exports = ( } } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } }; @@ -185,6 +196,58 @@ module.exports = ( } }; + app.use((req, res, next) => { + res.setHeader("x-session-id", SESSION_ID) + next() + }) + + app.use((req, res, next) => { + const deviceId = req.headers["x-shockwallet-device-id"]; + try { + if (nonEncryptedRoutes.includes(req.path)) { + return next(); + } + + if (!deviceId) { + const error = { + field: "deviceId", + message: "Please specify a device ID" + }; + console.error(error) + return res.status(401).json(error); + } + + if (!Encryption.isAuthorizedDevice({ deviceId })) { + const error = { + field: "deviceId", + message: "Please specify a device ID" + }; + console.error("Unknown Device", error) + return res.status(401).json(error); + } + + if (req.method === "GET") { + console.log("Method:", req.method); + return next(); + } + + console.log("Body:", req.body) + console.log("Decrypt params:", { deviceId, message: req.body.encryptionKey }) + const decryptedKey = Encryption.decryptKey({ deviceId, message: req.body.encryptionKey }); + console.log("decryptedKey", decryptedKey) + const decryptedMessage = Encryption.decryptMessage({ message: req.body.data, key: decryptedKey, iv: req.body.iv }) + req.body = JSON.parse(decryptedMessage); + return next(); + } catch (err) { + console.error(err); + return res + .status(401) + .json( + err + ); + } + }) + app.use(async (req, res, next) => { try { console.log("Route:", req.path) @@ -240,7 +303,7 @@ module.exports = ( */ app.get("/health", async (req, res) => { const health = await checkHealth(); - res.send(health); + res.json(health); }); /** @@ -248,11 +311,11 @@ module.exports = ( */ app.get("/healthz", async (req, res) => { const health = await checkHealth(); - res.send(health); + res.json(health); }); app.get("/ping", (req, res) => { - res.send("OK"); + res.json({ message: "OK" }); }); app.post("/api/mobile/error", (req, res) => { @@ -260,6 +323,38 @@ module.exports = ( res.json({ msg: "OK" }); }); + app.post("/api/security/exchangeKeys", async (req, res) => { + try { + const { publicKey, deviceId } = req.body; + + if (!publicKey || publicKey.length < 600) { + return res.status(400).json({ + field: 'publicKey', + message: "Please provide a valid public key" + }) + } + + if (!deviceId || + !/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/iu + .test(deviceId)) { + return res.status(400).json({ + field: 'deviceId', + message: "Please provide a valid device ID" + }) + } + + const authorizedDevice = await Encryption.authorizeDevice({ deviceId, publicKey }) + console.log(authorizedDevice) + return res.status(200).json(authorizedDevice) + } catch (err) { + console.error(err) + return res.status(401).json({ + field: 'unknown', + message: err + }) + } + }) + app.get("/api/lnd/wallet/status", async (req, res) => { try { const walletStatus = await walletExists(); @@ -281,6 +376,7 @@ module.exports = ( app.post("/api/lnd/auth", async (req, res) => { try { + console.log("/api/lnd/auth Body:", req.body) const health = await checkHealth(); const walletInitialized = await walletExists(); // If we're connected to lnd, unlock the wallet using the password supplied @@ -326,7 +422,7 @@ module.exports = ( } res.status(500); - res.send({ + res.json({ field: "health", errorMessage: sanitizeLNDError(health.LNDStatus.message), success: false @@ -335,7 +431,7 @@ module.exports = ( } catch (err) { logger.debug("Unlock Error:", err); res.status(400); - res.send({ field: "user", errorMessage: sanitizeLNDError(err.message), success: false }); + res.json({ field: "user", errorMessage: sanitizeLNDError(err.message), success: false }); return err; } }); @@ -357,10 +453,10 @@ module.exports = ( const health = await checkHealth(); if (health.LNDStatus.success) { res.status(400); - res.send({ field: "WalletUnlocker", errorMessage: unlockErr.message }); + res.json({ field: "WalletUnlocker", errorMessage: unlockErr.message }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } else { await recreateLnServices(); @@ -424,12 +520,12 @@ module.exports = ( const message = genSeedErr.details; return res .status(400) - .send({ field: "GenSeed", errorMessage: message, success: false }); + .json({ field: "GenSeed", errorMessage: message, success: false }); } return res .status(500) - .send({ field: "health", errorMessage: "LND is down", success: false }); + .json({ field: "health", errorMessage: "LND is down", success: false }); } logger.debug("GenSeed:", genSeedResponse); @@ -608,13 +704,13 @@ module.exports = ( logger.error("GetInfo Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "getInfo", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.info("GetInfo:", response); @@ -639,13 +735,13 @@ module.exports = ( const health = await checkHealth(); if (health.LNDStatus.success) { res.status(400); - res.send({ + res.json({ field: "getNodeInfo", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("GetNodeInfo:", response); @@ -661,13 +757,13 @@ module.exports = ( logger.debug("GetNetworkInfo Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "getNodeInfo", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("GetNetworkInfo:", response); @@ -683,13 +779,13 @@ module.exports = ( logger.debug("ListPeers Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "listPeers", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("ListPeers:", response); @@ -705,13 +801,13 @@ module.exports = ( logger.debug("NewAddress Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "newAddress", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("NewAddress:", response); @@ -726,13 +822,13 @@ module.exports = ( const health = await checkHealth(); if (health.LNDStatus.success) { res.status(403); - return res.send({ + return res.json({ field: "limituser", errorMessage: "User limited" }); } res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } const connectRequest = { addr: { pubkey: req.body.pubkey, host: req.body.host }, @@ -742,7 +838,7 @@ module.exports = ( lightning.connectPeer(connectRequest, (err, response) => { if (err) { logger.debug("ConnectPeer Error:", err); - res.status(500).send({ field: "connectPeer", errorMessage: sanitizeLNDError(err.message) }); + res.status(500).json({ field: "connectPeer", errorMessage: sanitizeLNDError(err.message) }); } else { logger.debug("ConnectPeer:", response); res.json(response); @@ -757,20 +853,20 @@ module.exports = ( const health = await checkHealth(); if (health.LNDStatus.success) { res.status(403); - return res.send({ + return res.json({ field: "limituser", errorMessage: "User limited" }); } res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } const disconnectRequest = { pub_key: req.body.pubkey }; logger.debug("DisconnectPeer Request:", disconnectRequest); lightning.disconnectPeer(disconnectRequest, (err, response) => { if (err) { logger.debug("DisconnectPeer Error:", err); - res.status(400).send({ field: "disconnectPeer", errorMessage: sanitizeLNDError(err.message) }); + res.status(400).json({ field: "disconnectPeer", errorMessage: sanitizeLNDError(err.message) }); } else { logger.debug("DisconnectPeer:", response); res.json(response); @@ -786,13 +882,13 @@ module.exports = ( logger.debug("ListChannels Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "listChannels", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("ListChannels:", response); @@ -808,13 +904,13 @@ module.exports = ( logger.debug("PendingChannels Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "pendingChannels", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("PendingChannels:", response); @@ -901,10 +997,10 @@ module.exports = ( logger.debug("ListInvoices Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ errorMessage: sanitizeLNDError(err.message), success: false }); + res.status(400).json({ errorMessage: sanitizeLNDError(err.message), success: false }); } else { res.status(500); - res.send({ errorMessage: health.LNDStatus.message, success: false }); + res.json({ errorMessage: health.LNDStatus.message, success: false }); } } else { // logger.debug("ListInvoices:", response); @@ -927,13 +1023,13 @@ module.exports = ( logger.debug("ForwardingHistory Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "forwardingHistory", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("ForwardingHistory:", response); @@ -949,13 +1045,13 @@ module.exports = ( logger.debug("WalletBalance Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "walletBalance", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("WalletBalance:", response); @@ -971,13 +1067,13 @@ module.exports = ( if (err) { logger.debug("WalletBalance Error:", err); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "walletBalance", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send(health.LNDStatus); + res.json(health.LNDStatus); } return err; } @@ -986,13 +1082,13 @@ module.exports = ( if (err) { logger.debug("ChannelBalance Error:", err); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "channelBalance", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send(health.LNDStatus); + res.json(health.LNDStatus); } return err; } @@ -1015,11 +1111,11 @@ module.exports = ( logger.debug("DecodePayReq Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(500).send({ + res.status(500).json({ errorMessage: sanitizeLNDError(err.message) }); } else { - res.status(500).send({ errorMessage: "LND is down" }); + res.status(500).json({ errorMessage: "LND is down" }); } } else { logger.info("DecodePayReq:", paymentRequest); @@ -1037,13 +1133,13 @@ module.exports = ( logger.debug("ChannelBalance Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "channelBalance", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("ChannelBalance:", response); @@ -1060,7 +1156,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } return; } @@ -1084,10 +1180,10 @@ module.exports = ( logger.info("OpenChannelRequest Error:", err); const health = await checkHealth(); if (health.LNDStatus.success && !res.headersSent) { - res.status(500).send({ field: "openChannelRequest", errorMessage: sanitizeLNDError(err.message) }); + res.status(500).json({ field: "openChannelRequest", errorMessage: sanitizeLNDError(err.message) }); } else if (!res.headersSent) { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } }); openedChannel.write(openChannelRequest) @@ -1103,7 +1199,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } const { channelPoint, outputIndex } = req.body; @@ -1131,13 +1227,13 @@ module.exports = ( if (!res.headersSent) { if (health.LNDStatus.success) { logger.debug("CloseChannelRequest Error:", err); - res.status(400).send({ + res.status(400).json({ field: "closeChannel", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } }); @@ -1152,7 +1248,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } const paymentRequest = { payment_request: req.body.payreq }; @@ -1184,12 +1280,12 @@ module.exports = ( logger.error("SendPayment Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(500).send({ + res.status(500).json({ errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } }); @@ -1205,7 +1301,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } return false; } @@ -1221,13 +1317,13 @@ module.exports = ( logger.debug("AddInvoice Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "addInvoice", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } return err; } @@ -1245,7 +1341,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } lightning.signMessage( @@ -1255,10 +1351,10 @@ module.exports = ( logger.debug("SignMessage Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ field: "signMessage", errorMessage: sanitizeLNDError(err.message) }); + res.status(400).json({ field: "signMessage", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("SignMessage:", response); @@ -1277,10 +1373,10 @@ module.exports = ( logger.debug("VerifyMessage Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ field: "verifyMessage", errorMessage: sanitizeLNDError(err.message) }); + res.status(400).json({ field: "verifyMessage", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("VerifyMessage:", response); @@ -1298,7 +1394,7 @@ module.exports = ( res.sendStatus(403); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } const sendCoinsRequest = { addr: req.body.addr, amount: req.body.amount }; @@ -1308,13 +1404,13 @@ module.exports = ( logger.debug("SendCoins Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ field: "sendCoins", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("SendCoins:", response); @@ -1334,10 +1430,10 @@ module.exports = ( logger.debug("QueryRoute Error:", err); const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ field: "queryRoute", errorMessage: sanitizeLNDError(err.message) }); + res.status(400).json({ field: "queryRoute", errorMessage: sanitizeLNDError(err.message) }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } logger.debug("QueryRoute:", response); @@ -1360,12 +1456,12 @@ module.exports = ( if (err) { const health = await checkHealth(); if (health.LNDStatus.success) { - res.status(400).send({ + res.status(400).json({ error: err.message }); } else { res.status(500); - res.send({ errorMessage: "LND is down" }); + res.json({ errorMessage: "LND is down" }); } } else { logger.debug("EstimateFee:", fee); diff --git a/src/server.js b/src/server.js index b52f0210..f0f1dbe7 100644 --- a/src/server.js +++ b/src/server.js @@ -1,78 +1,119 @@ -"use strict"; +/** + * @prettier + */ /** * Module dependencies. */ const server = program => { - const Https = require("https"); - const Http = require("http"); - const Express = require("express"); - const LightningServices = require("../utils/lightningServices"); - const app = Express(); - - const FS = require("../utils/fs"); - const bodyParser = require("body-parser"); - const session = require("express-session"); - const methodOverride = require("method-override"); - const { unprotectedRoutes, sensitiveRoutes } = require("../utils/protectedRoutes"); + const Https = require('https') + const Http = require('http') + const Express = require('express') + const LightningServices = require('../utils/lightningServices') + const Encryption = require('../utils/encryptionStore') + const app = Express() + + const FS = require('../utils/fs') + const bodyParser = require('body-parser') + const session = require('express-session') + const methodOverride = require('method-override') + const { + unprotectedRoutes, + sensitiveRoutes, + nonEncryptedRoutes + } = require('../utils/protectedRoutes') // load app default configuration data - const defaults = require("../config/defaults")(program.mainnet); + const defaults = require('../config/defaults')(program.mainnet) // define useful global variables ====================================== - module.useTLS = program.usetls; - module.serverPort = program.serverport || defaults.serverPort; - module.httpsPort = module.serverPort; - module.serverHost = program.serverhost || defaults.serverHost; + module.useTLS = program.usetls + module.serverPort = program.serverport || defaults.serverPort + module.httpsPort = module.serverPort + module.serverHost = program.serverhost || defaults.serverHost // setup winston logging ========== - const logger = require("../config/log")( + const logger = require('../config/log')( program.logfile || defaults.logfile, program.loglevel || defaults.loglevel - ); + ) // utilities functions ================= - require("../utils/server-utils")(module); + require('../utils/server-utils')(module) - logger.info("Mainnet Mode:", !!program.mainnet); + logger.info('Mainnet Mode:', !!program.mainnet) + + const modifyResponseBody = (req, res, next) => { + const deviceId = req.headers['x-shockwallet-device-id'] + const oldSend = res.send + + if (!nonEncryptedRoutes.includes(req.path)) { + res.send = (...args) => { + if (args[0] && args[0].encryptedData && args[0].encryptionKey) { + console.log('Response loop detected', req.path, args[0]) + oldSend.apply(res, args) + } else { + // arguments[0] (or `data`) contains the response body + const authorized = Encryption.isAuthorizedDevice({ deviceId }) + const encryptedMessage = authorized + ? Encryption.encryptMessage({ + message: args[0], + deviceId + }) + : args[0] + args[0] = JSON.stringify(encryptedMessage) + oldSend.apply(res, args) + } + } + } + next() + } const wait = seconds => new Promise(resolve => { - const timer = setTimeout(() => resolve(timer), seconds * 1000); - }); + const timer = setTimeout(() => resolve(timer), seconds * 1000) + }) // eslint-disable-next-line consistent-return const startServer = async () => { try { - LightningServices.setDefaults(program); - await LightningServices.init(); + LightningServices.setDefaults(program) + await LightningServices.init() // init lnd module ================= - const lnd = require("../services/lnd/lnd")(LightningServices.services.lightning); - const auth = require("../services/auth/auth"); + const lnd = require('../services/lnd/lnd')( + LightningServices.services.lightning + ) + const auth = require('../services/auth/auth') app.use(async (req, res, next) => { - console.log("Route:", req.path) + console.log('Route:', req.path) if (unprotectedRoutes[req.method][req.path]) { - next(); + next() } else { try { const response = await auth.validateToken( - req.headers.authorization.replace("Bearer ", "") - ); + req.headers.authorization.replace('Bearer ', '') + ) if (response.valid) { - next(); + next() } else { - res.status(401).json({ field: "authorization", errorMessage: "The authorization token you've supplied is invalid" }); + res.status(401).json({ + field: 'authorization', + errorMessage: + "The authorization token you've supplied is invalid" + }) } } catch (err) { logger.error( - !req.headers.authorization - ? "Please add an Authorization header" + !req.headers.authorization + ? 'Please add an Authorization header' : err - ); - res.status(401).json({ field: "authorization", errorMessage: "Please log in" }); + ) + res + .status(401) + .json({ field: 'authorization', errorMessage: 'Please log in' }) } } - }); + }) app.use((req, res, next) => { if (sensitiveRoutes[req.method][req.path]) { @@ -84,7 +125,7 @@ const server = program => { path: req.path, sessionId: req.sessionId }) - ); + ) } else { console.log( JSON.stringify({ @@ -96,10 +137,10 @@ const server = program => { query: req.query, sessionId: req.sessionId }) - ); + ) } - next(); - }); + next() + }) app.use( session({ secret: defaults.sessionSecret, @@ -108,79 +149,84 @@ const server = program => { rolling: true, saveUninitialized: true }) - ); - app.use(bodyParser.urlencoded({ extended: "true" })); - app.use(bodyParser.json()); - app.use(bodyParser.json({ type: "application/vnd.api+json" })); - app.use(methodOverride()); + ) + app.use(bodyParser.urlencoded({ extended: 'true' })) + app.use(bodyParser.json()) + app.use(bodyParser.json({ type: 'application/vnd.api+json' })) + app.use(methodOverride()) // WARNING // error handler middleware, KEEP 4 parameters as express detects the // arity of the function to treat it as a err handling middleware // eslint-disable-next-line no-unused-vars app.use((err, _, res, __) => { // Do logging and user-friendly error message display - logger.error(err); - res - .status(500) - .send({ status: 500, errorMessage: "internal error" }); - }); + logger.error(err) + res.status(500).send({ status: 500, errorMessage: 'internal error' }) + }) + + const CA = LightningServices.servicesConfig.lndCertPath + const CA_KEY = CA.replace('cert', 'key') const createServer = async () => { try { - if (program.usetls) { + if (LightningServices.servicesConfig.lndCertPath && program.usetls) { const [key, cert] = await Promise.all([ - FS.readFile(program.usetls + "/key.pem"), - FS.readFile(program.usetls + "/cert.pem") - ]); - const httpsServer = Https.createServer({ key, cert }, app); + FS.readFile(CA_KEY), + FS.readFile(CA) + ]) + const httpsServer = Https.createServer({ key, cert }, app) - return httpsServer; + return httpsServer } - const httpServer = Http.Server(app); - return httpServer; + const httpServer = Http.Server(app) + return httpServer } catch (err) { - logger.error(err.message); - throw err; + logger.error(err.message) + logger.error( + 'An error has occurred while finding an LND cert to use to open an HTTPS server' + ) + logger.warn('Falling back to opening an HTTP server...') + const httpServer = Http.Server(app) + return httpServer } - }; + } - const serverInstance = await createServer(); + const serverInstance = await createServer() - const io = require("socket.io")(serverInstance); + const io = require('socket.io')(serverInstance) - const Sockets = require("./sockets")( + const Sockets = require('./sockets')( io, lnd, program.user, program.pwd, program.limituser, program.limitpwd - ); + ) - require("./routes")( - app, - defaults, - Sockets, - { - serverHost: module.serverHost, - serverPort: module.serverPort - } - ); + require('./routes')(app, defaults, Sockets, { + serverHost: module.serverHost, + serverPort: module.serverPort, + usetls: program.usetls, + CA, + CA_KEY + }) // enable CORS headers - app.use(require("./cors")); + app.use(require('./cors')) // app.use(bodyParser.json({limit: '100000mb'})); - app.use(bodyParser.json({ limit: "50mb" })); - app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); + app.use(bodyParser.json({ limit: '50mb' })) + app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })) + app.use(modifyResponseBody) - serverInstance.listen(module.serverPort, module.serverhost); + serverInstance.listen(module.serverPort, module.serverhost) logger.info( - "App listening on " + module.serverHost + " port " + module.serverPort - ); + 'App listening on ' + module.serverHost + ' port ' + module.serverPort + ) - module.server = serverInstance; + module.server = serverInstance // const localtunnel = require('localtunnel'); // @@ -189,15 +235,15 @@ const server = program => { // console.log('t', t.url); // }); } catch (err) { - logger.info(err); - logger.info("Restarting server in 30 seconds..."); - await wait(30); - startServer(); - return false; + logger.info(err) + logger.info('Restarting server in 30 seconds...') + await wait(30) + startServer() + return false } - }; + } - startServer(); -}; + startServer() +} -module.exports = server; +module.exports = server diff --git a/utils/encryptionStore.js b/utils/encryptionStore.js new file mode 100644 index 00000000..a661de65 --- /dev/null +++ b/utils/encryptionStore.js @@ -0,0 +1,125 @@ +/** + * @prettier + */ +const Crypto = require('crypto') +const { Buffer } = require('buffer') +const APIKeyPair = new Map() +const authorizedDevices = new Map() + +const Encryption = { + encryptKey: ({ deviceId, message }) => { + if (!authorizedDevices.has(deviceId)) { + throw { field: 'deviceId', message: 'Unknown Device ID' } + } + + const devicePublicKey = authorizedDevices.get(deviceId) + const data = Buffer.from(message) + const encryptedData = Crypto.publicEncrypt( + { + key: devicePublicKey, + padding: Crypto.constants.RSA_PKCS1_PADDING + }, + data + ) + return encryptedData.toString('base64') + }, + decryptKey: ({ deviceId, message }) => { + if (!authorizedDevices.has(deviceId)) { + throw { field: 'deviceId', message: 'Unknown Device ID' } + } + + const data = Buffer.from(message, 'base64') + const encryptedData = Crypto.privateDecrypt( + { + key: APIKeyPair.get(deviceId).privateKey, + padding: Crypto.constants.RSA_PKCS1_PADDING + }, + data + ) + console.log('Decrypted Data:', encryptedData) + return encryptedData.toString() + }, + encryptMessage: ({ deviceId, message }) => { + const parsedMessage = + typeof message === 'object' ? JSON.stringify(message) : message + const data = Buffer.from(parsedMessage) + const key = Crypto.randomBytes(32) + const iv = Crypto.randomBytes(16) + const encryptedKey = Encryption.encryptKey({ + deviceId, + message: key.toString('hex') + }) + const cipher = Crypto.createCipheriv('aes-256-cbc', key, iv) + const encryptedCipher = cipher.update(data) + const encryptedBuffer = Buffer.concat([ + Buffer.from(encryptedCipher), + Buffer.from(cipher.final()) + ]) + const encryptedData = encryptedBuffer.toString('base64') + return { encryptedData, encryptedKey, iv: iv.toString('hex') } + }, + decryptMessage: ({ message, key, iv }) => { + const data = Buffer.from(message, 'base64') + const cipher = Crypto.createDecipheriv( + 'aes-256-cbc', + Buffer.from(key, 'hex'), + Buffer.from(iv, 'hex') + ) + const decryptedCipher = cipher.update(data) + const decryptedBuffer = Buffer.concat([ + Buffer.from(decryptedCipher), + Buffer.from(cipher.final()) + ]) + const decryptedData = decryptedBuffer.toString() + console.log('Decrypted Data:', decryptedData) + return decryptedData.toString() + }, + isAuthorizedDevice: ({ deviceId }) => { + if (authorizedDevices.has(deviceId)) { + return true + } + + return false + }, + authorizeDevice: ({ deviceId, publicKey }) => + new Promise((resolve, reject) => { + authorizedDevices.set(deviceId, publicKey) + Crypto.generateKeyPair( + 'rsa', + { + modulusLength: 4096, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }, + (err, publicKey, privateKey) => { + if (err) { + console.error(err) + reject(err) + return err + } + + const exportedKey = { + publicKey, + privateKey + } + + APIKeyPair.set(deviceId, exportedKey) + resolve({ + success: true, + APIPublicKey: exportedKey.publicKey + }) + } + ) + }), + unAuthorizeDevice: ({ deviceId }) => { + authorizedDevices.delete(deviceId) + } +} + +module.exports = Encryption diff --git a/utils/protectedRoutes.js b/utils/protectedRoutes.js index a0b53ca7..8ece4f12 100644 --- a/utils/protectedRoutes.js +++ b/utils/protectedRoutes.js @@ -13,7 +13,8 @@ module.exports = { "/api/lnd/connect": true, "/api/lnd/wallet": true, "/api/lnd/wallet/existing": true, - "/api/lnd/auth": true + "/api/lnd/auth": true, + "/api/security/exchangeKeys": true }, PUT: {}, DELETE: {} @@ -26,5 +27,6 @@ module.exports = { }, PUT: {}, DELETE: {} - } + }, + nonEncryptedRoutes: ['/api/security/exchangeKeys', '/healthz', '/ping', '/api/lnd/wallet/status'] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 77699c0c..7abb87b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -873,7 +873,7 @@ ascli@~1: colour "~0.7.1" optjs "~3.2.2" -asn1@~0.2.3: +asn1@^0.2.4, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -4341,6 +4341,13 @@ node-pre-gyp@^0.13.0: semver "^5.3.0" tar "^4" +node-rsa@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.7.tgz#85b7a6d6fa8ee624be6402a6b41be49272d58055" + integrity sha512-idwRXma6scFufZmbaKkHpJoLL93yynRefP6yur13wZ5i9FR35ex451KCoF2OORDeJanyRVahmjjiwmUlCnTqJA== + dependencies: + asn1 "^0.2.4" + nodemon@^1.19.3: version "1.19.3" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.3.tgz#db71b3e62aef2a8e1283a9fa00164237356102c0"