From e224949e9ff05985eff77b71ab7f09cd35dc4c4f Mon Sep 17 00:00:00 2001 From: emad-salah Date: Fri, 15 Oct 2021 09:21:36 +0100 Subject: [PATCH 1/4] Add missing functionality to new ECC encryption --- utils/ECC/crypto.js | 17 ++++++++++++++++- utils/ECC/index.js | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/utils/ECC/crypto.js b/utils/ECC/crypto.js index 51acbdbe..2a734cb2 100644 --- a/utils/ECC/crypto.js +++ b/utils/ECC/crypto.js @@ -1,4 +1,5 @@ const { Buffer } = require("buffer"); +const Crypto = require("crypto"); const FieldError = require("../fieldError") /** @@ -17,6 +18,19 @@ const FieldError = require("../fieldError") * @prop {string} ephemPublicKey */ +const generateRandomString = (length = 16) => + new Promise((resolve, reject) => { + Crypto.randomBytes(length, (err, buffer) => { + if (err) { + reject(err) + return + } + + const token = buffer.toString('hex') + resolve(token) + }) + }) + /** * @param {string} value */ @@ -101,10 +115,11 @@ const convertToEncryptedMessage = (encryptedMessage) => { }; module.exports = { + generateRandomString, convertUTF8ToBuffer, convertBase64ToBuffer, convertBufferToBase64, convertToEncryptedMessage, convertToEncryptedMessageResponse, - processKey + processKey, } \ No newline at end of file diff --git a/utils/ECC/index.js b/utils/ECC/index.js index 4ddac5d4..960067f5 100644 --- a/utils/ECC/index.js +++ b/utils/ECC/index.js @@ -4,6 +4,7 @@ const Storage = require('node-persist') const FieldError = require('../fieldError') const logger = require('../../config/log') const { + generateRandomString, convertBufferToBase64, processKey, convertToEncryptedMessageResponse, @@ -182,5 +183,6 @@ module.exports = { generateKeyPair, encryptMessage, decryptMessage, - authorizeDevice + authorizeDevice, + generateRandomString } From 485ec104654fce9f078214a079f8dd7e2f390a43 Mon Sep 17 00:00:00 2001 From: emad-salah Date: Fri, 15 Oct 2021 09:24:07 +0100 Subject: [PATCH 2/4] Disable "useUnknownInCatchVariables" in newer TS versions --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index da9874a9..04b0caa9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,7 +47,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -60,5 +60,6 @@ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "useUnknownInCatchVariables": false } } From 7b1e7ae97a064874224b41ec2a6aee2c9d16b30b Mon Sep 17 00:00:00 2001 From: emad-salah Date: Fri, 15 Oct 2021 09:24:49 +0100 Subject: [PATCH 3/4] Deprecate RSA Encryption --- src/routes.js | 126 +-------------------------- src/server.js | 54 +++--------- utils/encryptionStore.js | 181 --------------------------------------- 3 files changed, 11 insertions(+), 350 deletions(-) delete mode 100644 utils/encryptionStore.js diff --git a/src/routes.js b/src/routes.js index e67a7c69..14d4c4f5 100644 --- a/src/routes.js +++ b/src/routes.js @@ -20,7 +20,6 @@ const path = require('path') const getListPage = require('../utils/paginate') const auth = require('../services/auth/auth') const FS = require('../utils/fs') -const Encryption = require('../utils/encryptionStore') const ECC = require('../utils/ECC') const LightningServices = require('../utils/lightningServices') const lndErrorManager = require('../utils/lightningServices/errors') @@ -214,98 +213,13 @@ module.exports = async ( next() }) - app.use((req, res, next) => { - const legacyDeviceId = req.headers['x-shockwallet-device-id'] - const deviceId = req.headers['encryption-device-id'] - try { - if ( - nonEncryptedRoutes.includes(req.path) || - process.env.DISABLE_SHOCK_ENCRYPTION === 'true' || - (deviceId && !legacyDeviceId) - ) { - return next() - } - - if (!legacyDeviceId) { - const error = { - field: 'deviceId', - message: 'Please specify a device ID' - } - logger.error('Please specify a device ID') - return res.status(401).json(error) - } - - if (!Encryption.isAuthorizedDevice({ deviceId: legacyDeviceId })) { - const error = { - field: 'deviceId', - message: 'Please specify a device ID' - } - logger.error('Unknown Device') - return res.status(401).json(error) - } - if ( - !req.body.encryptionKey && - !req.body.iv && - !req.headers['x-shock-encryption-token'] - ) { - return next() - } - let reqData = null - let IV = null - let encryptedKey = null - let encryptedToken = null - if (req.method === 'GET' || req.method === 'DELETE') { - if (req.headers['x-shock-encryption-token']) { - encryptedToken = req.headers['x-shock-encryption-token'] - encryptedKey = req.headers['x-shock-encryption-key'] - IV = req.headers['x-shock-encryption-iv'] - } - } else { - encryptedToken = req.body.token - encryptedKey = req.body.encryptionKey || req.body.encryptedKey - IV = req.body.iv - reqData = req.body.data || req.body.encryptedData - } - const decryptedKey = Encryption.decryptKey({ - deviceId: legacyDeviceId, - message: encryptedKey - }) - if (reqData) { - const decryptedMessage = Encryption.decryptMessage({ - message: reqData, - key: decryptedKey, - iv: IV - }) - req.body = JSON.parse(decryptedMessage) - } - - const decryptedToken = encryptedToken - ? Encryption.decryptMessage({ - message: encryptedToken, - key: decryptedKey, - iv: IV - }) - : null - - if (decryptedToken) { - req.headers.authorization = decryptedToken - } - - return next() - } catch (err) { - logger.error(err) - return res.status(401).json(err) - } - }) - app.use(async (req, res, next) => { - const legacyDeviceId = req.headers['x-shockwallet-device-id'] const deviceId = req.headers['encryption-device-id'] try { if ( nonEncryptedRoutes.includes(req.path) || process.env.DISABLE_SHOCK_ENCRYPTION === 'true' || - (legacyDeviceId && !deviceId) + !deviceId ) { return next() } @@ -499,44 +413,6 @@ module.exports = async ( res.json({ msg: 'OK' }) }) - app.post('/api/security/exchangeKeys', async (req, res) => { - try { - const { publicKey, deviceId } = req.body - - if (!publicKey) { - 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 - }) - logger.info(authorizedDevice) - return res.json(authorizedDevice) - } catch (err) { - logger.error(err) - return res.status(401).json({ - field: 'unknown', - message: err - }) - } - }) - app.post('/api/encryption/exchange', async (req, res) => { try { const { publicKey, deviceId } = req.body diff --git a/src/server.js b/src/server.js index b62c0d34..5cd84b0a 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,3 @@ -const { generateRandomString } = require('../utils/encryptionStore') - /** * @prettier */ @@ -22,7 +20,6 @@ const server = program => { const ECC = require('../utils/ECC') const LightningServices = require('../utils/lightningServices') - const Encryption = require('../utils/encryptionStore') const app = Express() const compression = require('compression') @@ -124,7 +121,6 @@ const server = program => { * @param {(() => void)} next */ const modifyResponseBody = (req, res, next) => { - const legacyDeviceId = req.headers['x-shockwallet-device-id'] const deviceId = req.headers['encryption-device-id'] const oldSend = res.send @@ -136,38 +132,6 @@ const server = program => { return } - if (legacyDeviceId) { - res.send = (...args) => { - if (args[0] && args[0].encryptedData && args[0].encryptionKey) { - logger.warn('Response loop detected!') - oldSend.apply(res, args) - return - } - - const { cached, hash } = cacheCheck({ req, res, args, send: oldSend }) - - if (cached) { - return - } - - // arguments[0] (or `data`) contains the response body - const authorized = Encryption.isAuthorizedDevice({ - deviceId: legacyDeviceId - }) - const encryptedMessage = authorized - ? Encryption.encryptMessage({ - message: args[0] ? args[0] : {}, - deviceId: legacyDeviceId, - metadata: { - hash - } - }) - : args[0] - args[0] = JSON.stringify(encryptedMessage) - oldSend.apply(res, args) - } - } - if (deviceId) { res.send = (...args) => { if (args[0] && args[0].ciphertext && args[0].iv) { @@ -288,7 +252,7 @@ const server = program => { return randomField } - const newValue = await Encryption.generateRandomString(length) + const newValue = await ECC.generateRandomString(length) await Storage.setItem(fieldName, newValue) return newValue } @@ -440,14 +404,16 @@ const server = program => { }) } - if(process.env.ALLOW_UNLOCKED_LND === 'true'){ - const codes = await Storage.valuesWithKeyMatch(/^UnlockedAccessSecrets\//u) - if(codes.length === 0){ - const code = generateRandomString(12) + if (process.env.ALLOW_UNLOCKED_LND === 'true') { + const codes = await Storage.valuesWithKeyMatch( + /^UnlockedAccessSecrets\//u + ) + if (codes.length === 0) { + const code = ECC.generateRandomString(12) await Storage.setItem(`UnlockedAccessSecrets/${code}`, false) - logger.info("the access code is:"+code) - } else if(codes.length === 1 || codes[0] === false){ - logger.info("the access code is:"+codes[0]) + logger.info('the access code is:' + code) + } else if (codes.length === 1 || codes[0] === false) { + logger.info('the access code is:' + codes[0]) } } serverInstance.listen(serverPort, serverHost) diff --git a/utils/encryptionStore.js b/utils/encryptionStore.js deleted file mode 100644 index eb32f369..00000000 --- a/utils/encryptionStore.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @prettier - */ -const Crypto = require('crypto') -const { Buffer } = require('buffer') -const logger = require('../config/log') - -const APIKeyPair = new Map() -const authorizedDevices = new Map() - -const nonEncryptedEvents = [ - 'ping', - 'disconnect', - 'IS_GUN_AUTH', - 'SET_LAST_SEEN_APP' -] - -const Encryption = { - /** - * @param {string} event - * @returns {boolean} - */ - isNonEncrypted: event => nonEncryptedEvents.includes(event), - /** - * @param {{ deviceId: string , message: string }} arg0 - */ - 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') - }, - /** - * @param {{ deviceId: string , message: string }} arg0 - */ - 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 - ) - - return encryptedData.toString() - }, - /** - * @param {{ deviceId: string , message: any , metadata?: any}} arg0 - */ - encryptMessage: ({ deviceId, message, metadata = {} }) => { - 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') - const encryptedMessage = { - encryptedData, - encryptedKey, - iv: iv.toString('hex'), - metadata - } - - return encryptedMessage - }, - /** - * @param {{ message: string , key: string , iv: string }} arg0 - */ - 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() - - return decryptedData.toString() - }, - /** - * @param {{ deviceId: string }} arg0 - */ - isAuthorizedDevice: ({ deviceId }) => { - if (authorizedDevices.has(deviceId)) { - return true - } - - return false - }, - /** - * @param {{ deviceId: string , publicKey: string }} arg0 - */ - authorizeDevice: ({ deviceId, publicKey }) => - new Promise((resolve, reject) => { - authorizedDevices.set(deviceId, publicKey) - Crypto.generateKeyPair( - 'rsa', - { - modulusLength: 2048, - privateKeyEncoding: { - type: 'pkcs1', - format: 'pem' - }, - publicKeyEncoding: { - type: 'pkcs1', - format: 'pem' - } - }, - (err, publicKey, privateKey) => { - if (err) { - // @ts-ignore - logger.error(err) - reject(err) - return - } - - const exportedKey = { - publicKey, - privateKey - } - - APIKeyPair.set(deviceId, exportedKey) - resolve({ - success: true, - APIPublicKey: exportedKey.publicKey - }) - } - ) - }), - /** - * @param {{ deviceId: string }} arg0 - */ - unAuthorizeDevice: ({ deviceId }) => { - authorizedDevices.delete(deviceId) - }, - generateRandomString: (length = 16) => - new Promise((resolve, reject) => { - Crypto.randomBytes(length, (err, buffer) => { - if (err) { - reject(err) - return - } - - const token = buffer.toString('hex') - resolve(token) - }) - }) -} - -module.exports = Encryption From 281c7f7ac26296a984a8daa0e6de60debeefb9c3 Mon Sep 17 00:00:00 2001 From: emad-salah Date: Fri, 15 Oct 2021 09:27:22 +0100 Subject: [PATCH 4/4] Remove legacy encryption fallback code --- src/routes.js | 3 +-- src/server.js | 48 +++++++++++++++++++++++------------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/routes.js b/src/routes.js index 14d4c4f5..8d865b57 100644 --- a/src/routes.js +++ b/src/routes.js @@ -218,8 +218,7 @@ module.exports = async ( try { if ( nonEncryptedRoutes.includes(req.path) || - process.env.DISABLE_SHOCK_ENCRYPTION === 'true' || - !deviceId + process.env.DISABLE_SHOCK_ENCRYPTION === 'true' ) { return next() } diff --git a/src/server.js b/src/server.js index 5cd84b0a..5ab400a8 100644 --- a/src/server.js +++ b/src/server.js @@ -132,34 +132,32 @@ const server = program => { return } - if (deviceId) { - res.send = (...args) => { - if (args[0] && args[0].ciphertext && args[0].iv) { - logger.warn('Response loop detected!') - oldSend.apply(res, args) - return - } + res.send = (...args) => { + if (args[0] && args[0].ciphertext && args[0].iv) { + logger.warn('Response loop detected!') + oldSend.apply(res, args) + return + } - const authorized = ECC.isAuthorizedDevice({ - deviceId + const authorized = ECC.isAuthorizedDevice({ + deviceId + }) + + // Using classic promises syntax to avoid + // modifying res.send's return type + if (authorized && process.env.SHOCK_ENCRYPTION_ECC !== 'false') { + ECC.encryptMessage({ + deviceId, + message: args[0] + }).then(encryptedMessage => { + args[0] = JSON.stringify(encryptedMessage) + oldSend.apply(res, args) }) + } - // Using classic promises syntax to avoid - // modifying res.send's return type - if (authorized && process.env.SHOCK_ENCRYPTION_ECC !== 'false') { - ECC.encryptMessage({ - deviceId, - message: args[0] - }).then(encryptedMessage => { - args[0] = JSON.stringify(encryptedMessage) - oldSend.apply(res, args) - }) - } - - if (!authorized || process.env.SHOCK_ENCRYPTION_ECC === 'false') { - args[0] = JSON.stringify(args[0]) - oldSend.apply(res, args) - } + if (!authorized || process.env.SHOCK_ENCRYPTION_ECC === 'false') { + args[0] = JSON.stringify(args[0]) + oldSend.apply(res, args) } }