Added encryption support throughout the API, fixed bugs and added typings for ECC encryption

This commit is contained in:
emad-salah 2021-03-30 17:41:11 +01:00
parent 29640c9b59
commit 062a7f4a77
10 changed files with 318 additions and 109 deletions

View file

@ -66,12 +66,14 @@
"@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@types/bluebird": "^3.5.32", "@types/bluebird": "^3.5.32",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/eccrypto": "^1.1.2",
"@types/express": "^4.17.1", "@types/express": "^4.17.1",
"@types/gun": "^0.9.2", "@types/gun": "^0.9.2",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.7", "@types/jsonwebtoken": "^8.3.7",
"@types/lodash": "^4.14.141", "@types/lodash": "^4.14.141",
"@types/node-fetch": "^2.5.8", "@types/node-fetch": "^2.5.8",
"@types/node-persist": "^3.1.1",
"@types/ramda": "types/npm-ramda#dist", "@types/ramda": "types/npm-ramda#dist",
"@types/react": "16.x.x", "@types/react": "16.x.x",
"@types/socket.io": "^2.1.11", "@types/socket.io": "^2.1.11",

View file

@ -12,8 +12,8 @@ require('gun/lib/open')
// @ts-ignore // @ts-ignore
require('gun/lib/load') require('gun/lib/load')
const debounce = require('lodash/debounce') const debounce = require('lodash/debounce')
//@ts-ignore
const Encryption = require('../../../utils/encryptionStore') const { encryptedEmit, encryptedOn } = require('../../../utils/ECC/socket')
const Key = require('../contact-api/key') const Key = require('../contact-api/key')
/** @type {import('../contact-api/SimpleGUN').ISEA} */ /** @type {import('../contact-api/SimpleGUN').ISEA} */
@ -237,18 +237,26 @@ const Config = require('../config')
*/ */
/** /**
* @typedef {object} EncryptedEmission * @typedef {object} EncryptedEmissionLegacy
* @prop {string} encryptedData * @prop {string} encryptedData
* @prop {string} encryptedKey * @prop {string} encryptedKey
* @prop {string} iv * @prop {string} iv
*/ */
/**
* @typedef {object} EncryptedEmission
* @prop {string} ciphertext
* @prop {string} mac
* @prop {string} iv
* @prop {string} ephemPublicKey
*/
// TO DO: move to common repo // TO DO: move to common repo
/** /**
* @typedef {object} SimpleSocket * @typedef {object} SimpleSocket
* @prop {(eventName: string, data?: Emission|EncryptedEmission) => void} emit * @prop {(eventName: string, data?: Emission|EncryptedEmissionLegacy|EncryptedEmission) => void} emit
* @prop {(eventName: string, handler: (data: any) => void) => void} on * @prop {(eventName: string, handler: (data: any) => void) => void} on
* @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake * @prop {{ query: { 'x-shockwallet-device-id': string, encryptionId: string }}} handshake
*/ */
/* eslint-disable init-declarations */ /* eslint-disable init-declarations */
@ -566,84 +574,16 @@ class Mediator {
/** @param {SimpleSocket} socket */ /** @param {SimpleSocket} socket */
encryptSocketInstance = socket => { encryptSocketInstance = socket => {
const emit = encryptedEmit(socket)
const on = encryptedOn(socket)
return { return {
/** /**
* @type {SimpleSocket['on']} * @type {SimpleSocket['on']}
*/ */
on: (eventName, cb) => { on,
const deviceId = socket.handshake.query['x-shockwallet-device-id']
socket.on(eventName, _data => {
try {
if (Encryption.isNonEncrypted(eventName)) {
return cb(_data)
}
if (!_data) {
return cb(_data)
}
let data = _data
if (!deviceId) {
const error = {
field: 'deviceId',
message: 'Please specify a device ID'
}
logger.error(JSON.stringify(error))
return false
}
if (!Encryption.isAuthorizedDevice({ deviceId })) {
const error = {
field: 'deviceId',
message: 'Please specify a device ID'
}
logger.error('Unknown Device', error)
return false
}
if (typeof data === 'string') {
data = JSON.parse(data)
}
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) {
logger.error(err)
return false
}
})
},
/** @type {SimpleSocket['emit']} */ /** @type {SimpleSocket['emit']} */
emit: (eventName, data) => { emit
try {
if (Encryption.isNonEncrypted(eventName)) {
socket.emit(eventName, data)
return
}
const deviceId = socket.handshake.query['x-shockwallet-device-id']
const authorized = Encryption.isAuthorizedDevice({ deviceId })
const encryptedMessage = authorized
? Encryption.encryptMessage({
message: data,
deviceId
})
: data
socket.emit(eventName, encryptedMessage)
} catch (err) {
logger.error(err.message)
logger.error(err)
}
}
} }
} }

View file

@ -336,6 +336,10 @@ module.exports = async (
return res.status(401).json(error) return res.status(401).json(error)
} }
if (req.method === 'GET') {
return next()
}
if (!ECC.isEncryptedMessage(req.body)) { if (!ECC.isEncryptedMessage(req.body)) {
logger.warn('Message not encrypted!', req.body) logger.warn('Message not encrypted!', req.body)
return next() return next()

View file

@ -18,6 +18,7 @@ const {
const { deepDecryptIfNeeded } = require('../services/gunDB/rpc') const { deepDecryptIfNeeded } = require('../services/gunDB/rpc')
const GunEvents = require('../services/gunDB/contact-api/events') const GunEvents = require('../services/gunDB/contact-api/events')
const SchemaManager = require('../services/schema') const SchemaManager = require('../services/schema')
const { encryptedEmit, encryptedOn } = require('../utils/ECC/socket')
/** /**
* @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket * @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket
* @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue * @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue
@ -28,7 +29,7 @@ module.exports = (
io io
) => { ) => {
// This should be used for encrypting and emitting your data // This should be used for encrypting and emitting your data
const emitEncryptedEvent = ({ eventName, data, socket }) => { const encryptedEmitLegacy = ({ eventName, data, socket }) => {
try { try {
if (Encryption.isNonEncrypted(eventName)) { if (Encryption.isNonEncrypted(eventName)) {
return socket.emit(eventName, data) return socket.emit(eventName, data)
@ -73,7 +74,7 @@ module.exports = (
const stream = lightning.subscribeInvoices({}) const stream = lightning.subscribeInvoices({})
stream.on('data', data => { stream.on('data', data => {
logger.info('[SOCKET] New invoice data:', data) logger.info('[SOCKET] New invoice data:', data)
emitEncryptedEvent({ eventName: 'invoice:new', data, socket }) encryptedEmitLegacy({ eventName: 'invoice:new', data, socket })
if (!data.settled) { if (!data.settled) {
return return
} }
@ -158,7 +159,7 @@ module.exports = (
//buddy needs to manage this //buddy needs to manage this
} else { } else {
//business as usual //business as usual
emitEncryptedEvent({ eventName: 'transaction:new', data, socket }) encryptedEmitLegacy({ eventName: 'transaction:new', data, socket })
} }
} }
) )
@ -258,6 +259,8 @@ module.exports = (
return return
} }
const emit = encryptedEmit(socket)
const { $shock, publicKeyForDecryption } = socket.handshake.query const { $shock, publicKeyForDecryption } = socket.handshake.query
const [root, path, method] = $shock.split('::') const [root, path, method] = $shock.split('::')
@ -293,9 +296,9 @@ module.exports = (
publicKeyForDecryption publicKeyForDecryption
) )
socket.emit('$shock', decData, key) emit('$shock', decData)
} else { } else {
socket.emit('$shock', data, key) emit('$shock', data)
} }
} catch (err) { } catch (err) {
logger.error( logger.error(
@ -335,6 +338,9 @@ module.exports = (
return return
} }
const on = encryptedOn(socket)
const emit = encryptedEmit(socket)
const { services } = LightningServices const { services } = LightningServices
const { service, method, args: unParsed } = socket.handshake.query const { service, method, args: unParsed } = socket.handshake.query
@ -359,24 +365,24 @@ module.exports = (
}) })
})() })()
socket.emit('data', data) emit('data', data)
}) })
call.on('status', status => { call.on('status', status => {
socket.emit('status', status) emit('status', status)
}) })
call.on('end', () => { call.on('end', () => {
socket.emit('end') emit('end')
}) })
call.on('error', err => { call.on('error', err => {
// 'error' is a reserved event name we can't use it // 'error' is a reserved event name we can't use it
socket.emit('$error', err) emit('$error', err)
}) })
// Possibly allow streaming writes such as sendPaymentV2 // Possibly allow streaming writes such as sendPaymentV2
socket.on('write', args => { on('write', args => {
call.write(args) call.write(args)
}) })
} catch (err) { } catch (err) {
@ -467,12 +473,15 @@ module.exports = (
let chatsUnsub = emptyUnsub let chatsUnsub = emptyUnsub
io.of('chats').on('connect', async socket => { io.of('chats').on('connect', async socket => {
const on = encryptedOn(socket)
const emit = encryptedEmit(socket)
try { try {
if (!isAuthenticated()) { if (!isAuthenticated()) {
logger.info( logger.info(
'not authenticated in gun for chats socket, will send NOT_AUTH' 'not authenticated in gun for chats socket, will send NOT_AUTH'
) )
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
@ -483,7 +492,7 @@ module.exports = (
if (!isAuth) { if (!isAuth) {
logger.warn('invalid token for chats socket') logger.warn('invalid token for chats socket')
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
@ -522,30 +531,33 @@ module.exports = (
} }
) )
socket.emit('$shock', processed) emit('$shock', processed)
} }
chatsUnsub = GunEvents.onChats(onChats) chatsUnsub = GunEvents.onChats(onChats)
socket.on('disconnect', () => { on('disconnect', () => {
chatsUnsub() chatsUnsub()
chatsUnsub = emptyUnsub chatsUnsub = emptyUnsub
}) })
} catch (e) { } catch (e) {
logger.error('Error inside chats socket connect: ' + e.message) logger.error('Error inside chats socket connect: ' + e.message)
socket.emit('$error', e.message) emit('$error', e.message)
} }
}) })
let sentReqsUnsub = emptyUnsub let sentReqsUnsub = emptyUnsub
io.of('sentReqs').on('connect', async socket => { io.of('sentReqs').on('connect', async socket => {
const on = encryptedOn(socket)
const emit = encryptedEmit(socket)
try { try {
if (!isAuthenticated()) { if (!isAuthenticated()) {
logger.info( logger.info(
'not authenticated in gun for sentReqs socket, will send NOT_AUTH' 'not authenticated in gun for sentReqs socket, will send NOT_AUTH'
) )
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
@ -556,13 +568,13 @@ module.exports = (
if (!isAuth) { if (!isAuth) {
logger.warn('invalid token for sentReqs socket') logger.warn('invalid token for sentReqs socket')
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
if (sentReqsUnsub !== emptyUnsub) { if (sentReqsUnsub !== emptyUnsub) {
logger.error( logger.error(
'Tried to set sentReqs socket twice, this might be due to an app restart and the old socket not being recycled by socket.io in time, will disable the older subscription, which means the old socket wont work and data will be sent to this new socket instead' 'Tried to set sentReqs socket twice, this might be due to an app restart and the old socket not being recycled by io in time, will disable the older subscription, which means the old socket wont work and data will be sent to this new socket instead'
) )
sentReqsUnsub() sentReqsUnsub()
sentReqsUnsub = emptyUnsub sentReqsUnsub = emptyUnsub
@ -594,30 +606,32 @@ module.exports = (
return stripped return stripped
} }
) )
socket.emit('$shock', processed) emit('$shock', processed)
} }
sentReqsUnsub = GunEvents.onSimplerSentRequests(onSentReqs) sentReqsUnsub = GunEvents.onSimplerSentRequests(onSentReqs)
socket.on('disconnect', () => { on('disconnect', () => {
sentReqsUnsub() sentReqsUnsub()
sentReqsUnsub = emptyUnsub sentReqsUnsub = emptyUnsub
}) })
} catch (e) { } catch (e) {
logger.error('Error inside sentReqs socket connect: ' + e.message) logger.error('Error inside sentReqs socket connect: ' + e.message)
socket.emit('$error', e.message) emit('$error', e.message)
} }
}) })
let receivedReqsUnsub = emptyUnsub let receivedReqsUnsub = emptyUnsub
io.of('receivedReqs').on('connect', async socket => { io.of('receivedReqs').on('connect', async socket => {
const on = encryptedOn(socket)
const emit = encryptedEmit(socket)
try { try {
if (!isAuthenticated()) { if (!isAuthenticated()) {
logger.info( logger.info(
'not authenticated in gun for receivedReqs socket, will send NOT_AUTH' 'not authenticated in gun for receivedReqs socket, will send NOT_AUTH'
) )
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
@ -628,7 +642,7 @@ module.exports = (
if (!isAuth) { if (!isAuth) {
logger.warn('invalid token for receivedReqs socket') logger.warn('invalid token for receivedReqs socket')
socket.emit(Common.Constants.ErrorCode.NOT_AUTH) emit(Common.Constants.ErrorCode.NOT_AUTH)
return return
} }
@ -657,18 +671,18 @@ module.exports = (
return stripped return stripped
}) })
socket.emit('$shock', processed) emit('$shock', processed)
} }
receivedReqsUnsub = GunEvents.onSimplerReceivedRequests(onReceivedReqs) receivedReqsUnsub = GunEvents.onSimplerReceivedRequests(onReceivedReqs)
socket.on('disconnect', () => { on('disconnect', () => {
receivedReqsUnsub() receivedReqsUnsub()
receivedReqsUnsub = emptyUnsub receivedReqsUnsub = emptyUnsub
}) })
} catch (e) { } catch (e) {
logger.error('Error inside receivedReqs socket connect: ' + e.message) logger.error('Error inside receivedReqs socket connect: ' + e.message)
socket.emit('$error', e.message) emit('$error', e.message)
} }
}) })

View file

@ -1,12 +1,40 @@
const { Buffer } = require("buffer"); const { Buffer } = require("buffer");
const FieldError = require("../fieldError") const FieldError = require("../fieldError")
/**
* @typedef {object} EncryptedMessageBuffer
* @prop {Buffer} ciphertext
* @prop {Buffer} iv
* @prop {Buffer} mac
* @prop {Buffer} ephemPublicKey
*/
/**
* @typedef {object} EncryptedMessageResponse
* @prop {string} ciphertext
* @prop {string} iv
* @prop {string} mac
* @prop {string} ephemPublicKey
*/
/**
* @param {string} value
*/
const convertUTF8ToBuffer = (value) => Buffer.from(value, 'utf-8'); const convertUTF8ToBuffer = (value) => Buffer.from(value, 'utf-8');
/**
* @param {string} value
*/
const convertBase64ToBuffer = (value) => Buffer.from(value, 'base64'); const convertBase64ToBuffer = (value) => Buffer.from(value, 'base64');
/**
* @param {Buffer} buffer
*/
const convertBufferToBase64 = (buffer) => buffer.toString("base64"); const convertBufferToBase64 = (buffer) => buffer.toString("base64");
/**
* @param {Buffer | string} key
*/
const processKey = (key) => { const processKey = (key) => {
if (Buffer.isBuffer(key)) { if (Buffer.isBuffer(key)) {
return key; return key;
@ -15,11 +43,11 @@ const processKey = (key) => {
return convertedKey; return convertedKey;
}; };
/**
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
* @returns {EncryptedMessageResponse}
*/
const convertToEncryptedMessageResponse = (encryptedMessage) => { const convertToEncryptedMessageResponse = (encryptedMessage) => {
if (typeof encryptedMessage.ciphertext === "string") {
return encryptedMessage;
}
if (Buffer.isBuffer(encryptedMessage.ciphertext) && if (Buffer.isBuffer(encryptedMessage.ciphertext) &&
Buffer.isBuffer(encryptedMessage.iv) && Buffer.isBuffer(encryptedMessage.iv) &&
Buffer.isBuffer(encryptedMessage.mac) && Buffer.isBuffer(encryptedMessage.mac) &&
@ -31,12 +59,22 @@ const convertToEncryptedMessageResponse = (encryptedMessage) => {
ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey) ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey)
}; };
} }
if (typeof encryptedMessage.ciphertext === "string") {
// @ts-ignore
return encryptedMessage;
}
throw new FieldError({ throw new FieldError({
field: "encryptedMessage", field: "encryptedMessage",
message: "Unknown encrypted message format" message: "Unknown encrypted message format"
}); });
}; };
/**
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
* @returns {EncryptedMessageBuffer}
*/
const convertToEncryptedMessage = (encryptedMessage) => { const convertToEncryptedMessage = (encryptedMessage) => {
if (encryptedMessage.ciphertext instanceof Buffer && if (encryptedMessage.ciphertext instanceof Buffer &&
encryptedMessage.iv instanceof Buffer && encryptedMessage.iv instanceof Buffer &&

View file

@ -14,6 +14,18 @@ const {
const nodeKeyPairs = new Map() const nodeKeyPairs = new Map()
const devicePublicKeys = new Map() const devicePublicKeys = new Map()
/**
* @typedef {object} EncryptedMessage
* @prop {string} ciphertext
* @prop {string} iv
* @prop {string} mac
* @prop {string} ephemPublicKey
*/
/**
* Checks if the message supplied is encrypted or not
* @param {EncryptedMessage} message
*/
const isEncryptedMessage = message => const isEncryptedMessage = message =>
message && message &&
message.ciphertext && message.ciphertext &&
@ -21,6 +33,11 @@ const isEncryptedMessage = message =>
message.mac && message.mac &&
message.ephemPublicKey message.ephemPublicKey
/**
* Generates a new encryption key pair that will be used
* when communicating with the deviceId specified
* @param {string} deviceId
*/
const generateKeyPair = deviceId => { const generateKeyPair = deviceId => {
const privateKey = ECCrypto.generatePrivate() const privateKey = ECCrypto.generatePrivate()
const publicKey = ECCrypto.getPublic(privateKey) const publicKey = ECCrypto.getPublic(privateKey)
@ -40,8 +57,17 @@ const generateKeyPair = deviceId => {
} }
} }
/**
* Checks if the specified device has a keypair generated
* @param {{ deviceId: string }} arg0
*/
const isAuthorizedDevice = ({ deviceId }) => devicePublicKeys.has(deviceId) const isAuthorizedDevice = ({ deviceId }) => devicePublicKeys.has(deviceId)
/**
* Generates a new keypair for the deviceId specified and
* saves its publicKey locally
* @param {{ deviceId: string, publicKey: string }} arg0
*/
const authorizeDevice = async ({ deviceId, publicKey }) => { const authorizeDevice = async ({ deviceId, publicKey }) => {
const hostId = await Storage.get('encryption/hostId') const hostId = await Storage.get('encryption/hostId')
devicePublicKeys.set(deviceId, convertBase64ToBuffer(publicKey)) devicePublicKeys.set(deviceId, convertBase64ToBuffer(publicKey))
@ -54,6 +80,12 @@ const authorizeDevice = async ({ deviceId, publicKey }) => {
} }
} }
/**
* Encrypts the specified message using the specified deviceId's
* public key
* @param {{ deviceId: string, message: string }} arg0
* @returns {Promise<import('./crypto').EncryptedMessageResponse>}
*/
const encryptMessage = async ({ message = '', deviceId }) => { const encryptMessage = async ({ message = '', deviceId }) => {
const publicKey = devicePublicKeys.get(deviceId) const publicKey = devicePublicKeys.get(deviceId)
@ -80,6 +112,11 @@ const encryptMessage = async ({ message = '', deviceId }) => {
return convertToEncryptedMessageResponse(encryptedMessageResponse) return convertToEncryptedMessageResponse(encryptedMessageResponse)
} }
/**
* Decrypts the specified message using the API keypair
* associated with the specified deviceId
* @param {{ encryptedMessage: EncryptedMessage, deviceId: string }} arg0
*/
const decryptMessage = async ({ encryptedMessage, deviceId }) => { const decryptMessage = async ({ encryptedMessage, deviceId }) => {
try { try {
const keyPair = nodeKeyPairs.get(deviceId) const keyPair = nodeKeyPairs.get(deviceId)
@ -92,7 +129,6 @@ const decryptMessage = async ({ encryptedMessage, deviceId }) => {
} }
const processedPrivateKey = processKey(keyPair.privateKey) const processedPrivateKey = processKey(keyPair.privateKey)
const processedPublicKey = processKey(keyPair.publicKey)
const decryptedMessage = await ECCrypto.decrypt( const decryptedMessage = await ECCrypto.decrypt(
processedPrivateKey, processedPrivateKey,
convertToEncryptedMessage(encryptedMessage) convertToEncryptedMessage(encryptedMessage)

140
utils/ECC/socket.js Normal file
View file

@ -0,0 +1,140 @@
/**
* @format
*/
const Common = require('shock-common')
const logger = require('winston')
const { safeParseJSON } = require('../json')
const ECC = require('./index')
const nonEncryptedEvents = [
'ping',
'disconnect',
'IS_GUN_AUTH',
'SET_LAST_SEEN_APP',
Common.Constants.ErrorCode.NOT_AUTH
]
/**
* @typedef {import('../../services/gunDB/Mediator').SimpleSocket} SimpleSocket
* @typedef {import('../../services/gunDB/Mediator').Emission} Emission
* @typedef {import('../../services/gunDB/Mediator').EncryptedEmission} EncryptedEmission
* @typedef {import('../../services/gunDB/Mediator').EncryptedEmissionLegacy} EncryptedEmissionLegacy
*/
/**
* @param {string} eventName
*/
const isNonEncrypted = eventName => nonEncryptedEvents.includes(eventName)
/**
* @param {SimpleSocket} socket
* @returns {(eventName: string, args?: Emission | EncryptedEmission | EncryptedEmissionLegacy) => Promise<void>}
*/
const encryptedEmit = socket => async (eventName, ...args) => {
try {
if (isNonEncrypted(eventName)) {
return socket.emit(eventName, ...args)
}
const deviceId = socket.handshake.query.encryptionId
if (!deviceId) {
throw {
field: 'deviceId',
message: 'Please specify a device ID'
}
}
const authorized = ECC.isAuthorizedDevice({ deviceId })
if (!authorized) {
throw {
field: 'deviceId',
message: 'Please exchange keys with the API before using the socket'
}
}
const encryptedArgs = await Promise.all(
args.map(data => {
if (!data) {
return data
}
return ECC.encryptMessage({
message: typeof data === 'object' ? JSON.stringify(data) : data,
deviceId
})
})
)
console.log('Encrypted args:', encryptedArgs)
return socket.emit(eventName, ...encryptedArgs)
} catch (err) {
logger.error(
`[SOCKET] An error has occurred while encrypting an event (${eventName}):`,
err
)
return socket.emit('encryption:error', err)
}
}
/**
* @param {SimpleSocket} socket
* @returns {(eventName: string, callback: (data: any) => void) => void}
*/
const encryptedOn = socket => (eventName, callback) => {
try {
if (isNonEncrypted(eventName)) {
return socket.on(eventName, callback)
}
const deviceId = socket.handshake.query.encryptionId
if (!deviceId) {
throw {
field: 'deviceId',
message: 'Please specify a device ID'
}
}
const authorized = ECC.isAuthorizedDevice({ deviceId })
if (!authorized) {
throw {
field: 'deviceId',
message: 'Please exchange keys with the API before using the socket'
}
}
socket.on(eventName, async data => {
if (isNonEncrypted(eventName)) {
callback(data)
return
}
if (data) {
const decryptedMessage = await ECC.decryptMessage({
deviceId,
encryptedMessage: data
})
callback(safeParseJSON(decryptedMessage))
}
})
} catch (err) {
logger.error(
`[SOCKET] An error has occurred while decrypting an event (${eventName}):`,
err
)
return socket.emit('encryption:error', err)
}
}
module.exports = {
isNonEncrypted,
encryptedOn,
encryptedEmit
}

14
utils/JSON.js Normal file
View file

@ -0,0 +1,14 @@
/** @param {any} data */
const safeParseJSON = (data) => {
try {
const parsedJSON = JSON.parse(data);
return parsedJSON;
} catch (err) {
console.error(err);
return data;
}
};
module.exports = {
safeParseJSON
}

View file

@ -1,4 +1,5 @@
class FieldError extends Error { class FieldError extends Error {
/** @param {any} error */
constructor(error) { constructor(error) {
super(); super();
this.message = error?.message ?? "An unknown error has occurred"; this.message = error?.message ?? "An unknown error has occurred";

View file

@ -634,6 +634,14 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/eccrypto@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/eccrypto/-/eccrypto-1.1.2.tgz#49c452e78c02f890036b8cbee7c18bd77aa16ce1"
integrity sha512-qmB/iGIoqDdCMHAcJiOKI4ZBI1Z3kBQGYQCkgNP/Z9ge5w/EVx5uxQYkGOFpllm6l2N/B3qXcn9vjqXGaV1vRQ==
dependencies:
"@types/expect" "^1.20.4"
"@types/node" "*"
"@types/engine.io@*": "@types/engine.io@*":
version "3.1.4" version "3.1.4"
resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.4.tgz#3d9472711d179daa7c95c051e50ad411e18a9bdc" resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.4.tgz#3d9472711d179daa7c95c051e50ad411e18a9bdc"
@ -641,6 +649,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/expect@^1.20.4":
version "1.20.4"
resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
"@types/express-serve-static-core@*": "@types/express-serve-static-core@*":
version "4.16.9" version "4.16.9"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558"
@ -730,6 +743,13 @@
"@types/node" "*" "@types/node" "*"
form-data "^3.0.0" form-data "^3.0.0"
"@types/node-persist@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/node-persist/-/node-persist-3.1.1.tgz#de444e87561e8ad022e1f31ad4fee377d9db1b13"
integrity sha512-4PRvgEWkKrWWCZR5Hp/aoAu13z3+e99d9KtbXEDbcJPe84yDoRz3d2IEhiVcSe8fJubYMpXJhyGOdqJ8yoGZsA==
dependencies:
"@types/node" "*"
"@types/node@*": "@types/node@*":
version "12.7.4" version "12.7.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04"