Added encryption support throughout the API, fixed bugs and added typings for ECC encryption
This commit is contained in:
parent
29640c9b59
commit
062a7f4a77
10 changed files with 318 additions and 109 deletions
|
|
@ -66,12 +66,14 @@
|
|||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@types/bluebird": "^3.5.32",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/eccrypto": "^1.1.2",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/gun": "^0.9.2",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/jsonwebtoken": "^8.3.7",
|
||||
"@types/lodash": "^4.14.141",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/node-persist": "^3.1.1",
|
||||
"@types/ramda": "types/npm-ramda#dist",
|
||||
"@types/react": "16.x.x",
|
||||
"@types/socket.io": "^2.1.11",
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ require('gun/lib/open')
|
|||
// @ts-ignore
|
||||
require('gun/lib/load')
|
||||
const debounce = require('lodash/debounce')
|
||||
|
||||
const Encryption = require('../../../utils/encryptionStore')
|
||||
//@ts-ignore
|
||||
const { encryptedEmit, encryptedOn } = require('../../../utils/ECC/socket')
|
||||
const Key = require('../contact-api/key')
|
||||
|
||||
/** @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} encryptedKey
|
||||
* @prop {string} iv
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} EncryptedEmission
|
||||
* @prop {string} ciphertext
|
||||
* @prop {string} mac
|
||||
* @prop {string} iv
|
||||
* @prop {string} ephemPublicKey
|
||||
*/
|
||||
|
||||
// TO DO: move to common repo
|
||||
/**
|
||||
* @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 {{ query: { 'x-shockwallet-device-id': string }}} handshake
|
||||
* @prop {{ query: { 'x-shockwallet-device-id': string, encryptionId: string }}} handshake
|
||||
*/
|
||||
|
||||
/* eslint-disable init-declarations */
|
||||
|
|
@ -566,84 +574,16 @@ class Mediator {
|
|||
|
||||
/** @param {SimpleSocket} socket */
|
||||
encryptSocketInstance = socket => {
|
||||
const emit = encryptedEmit(socket)
|
||||
const on = encryptedOn(socket)
|
||||
|
||||
return {
|
||||
/**
|
||||
* @type {SimpleSocket['on']}
|
||||
*/
|
||||
on: (eventName, cb) => {
|
||||
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
|
||||
}
|
||||
})
|
||||
},
|
||||
on,
|
||||
/** @type {SimpleSocket['emit']} */
|
||||
emit: (eventName, data) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
emit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -336,6 +336,10 @@ module.exports = async (
|
|||
return res.status(401).json(error)
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!ECC.isEncryptedMessage(req.body)) {
|
||||
logger.warn('Message not encrypted!', req.body)
|
||||
return next()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const {
|
|||
const { deepDecryptIfNeeded } = require('../services/gunDB/rpc')
|
||||
const GunEvents = require('../services/gunDB/contact-api/events')
|
||||
const SchemaManager = require('../services/schema')
|
||||
const { encryptedEmit, encryptedOn } = require('../utils/ECC/socket')
|
||||
/**
|
||||
* @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket
|
||||
* @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue
|
||||
|
|
@ -28,7 +29,7 @@ module.exports = (
|
|||
io
|
||||
) => {
|
||||
// This should be used for encrypting and emitting your data
|
||||
const emitEncryptedEvent = ({ eventName, data, socket }) => {
|
||||
const encryptedEmitLegacy = ({ eventName, data, socket }) => {
|
||||
try {
|
||||
if (Encryption.isNonEncrypted(eventName)) {
|
||||
return socket.emit(eventName, data)
|
||||
|
|
@ -73,7 +74,7 @@ module.exports = (
|
|||
const stream = lightning.subscribeInvoices({})
|
||||
stream.on('data', data => {
|
||||
logger.info('[SOCKET] New invoice data:', data)
|
||||
emitEncryptedEvent({ eventName: 'invoice:new', data, socket })
|
||||
encryptedEmitLegacy({ eventName: 'invoice:new', data, socket })
|
||||
if (!data.settled) {
|
||||
return
|
||||
}
|
||||
|
|
@ -158,7 +159,7 @@ module.exports = (
|
|||
//buddy needs to manage this
|
||||
} else {
|
||||
//business as usual
|
||||
emitEncryptedEvent({ eventName: 'transaction:new', data, socket })
|
||||
encryptedEmitLegacy({ eventName: 'transaction:new', data, socket })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -258,6 +259,8 @@ module.exports = (
|
|||
return
|
||||
}
|
||||
|
||||
const emit = encryptedEmit(socket)
|
||||
|
||||
const { $shock, publicKeyForDecryption } = socket.handshake.query
|
||||
|
||||
const [root, path, method] = $shock.split('::')
|
||||
|
|
@ -293,9 +296,9 @@ module.exports = (
|
|||
publicKeyForDecryption
|
||||
)
|
||||
|
||||
socket.emit('$shock', decData, key)
|
||||
emit('$shock', decData)
|
||||
} else {
|
||||
socket.emit('$shock', data, key)
|
||||
emit('$shock', data)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
|
|
@ -335,6 +338,9 @@ module.exports = (
|
|||
return
|
||||
}
|
||||
|
||||
const on = encryptedOn(socket)
|
||||
const emit = encryptedEmit(socket)
|
||||
|
||||
const { services } = LightningServices
|
||||
|
||||
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 => {
|
||||
socket.emit('status', status)
|
||||
emit('status', status)
|
||||
})
|
||||
|
||||
call.on('end', () => {
|
||||
socket.emit('end')
|
||||
emit('end')
|
||||
})
|
||||
|
||||
call.on('error', err => {
|
||||
// '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
|
||||
socket.on('write', args => {
|
||||
on('write', args => {
|
||||
call.write(args)
|
||||
})
|
||||
} catch (err) {
|
||||
|
|
@ -467,12 +473,15 @@ module.exports = (
|
|||
let chatsUnsub = emptyUnsub
|
||||
|
||||
io.of('chats').on('connect', async socket => {
|
||||
const on = encryptedOn(socket)
|
||||
const emit = encryptedEmit(socket)
|
||||
|
||||
try {
|
||||
if (!isAuthenticated()) {
|
||||
logger.info(
|
||||
'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
|
||||
}
|
||||
|
|
@ -483,7 +492,7 @@ module.exports = (
|
|||
|
||||
if (!isAuth) {
|
||||
logger.warn('invalid token for chats socket')
|
||||
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -522,30 +531,33 @@ module.exports = (
|
|||
}
|
||||
)
|
||||
|
||||
socket.emit('$shock', processed)
|
||||
emit('$shock', processed)
|
||||
}
|
||||
|
||||
chatsUnsub = GunEvents.onChats(onChats)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
on('disconnect', () => {
|
||||
chatsUnsub()
|
||||
chatsUnsub = emptyUnsub
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Error inside chats socket connect: ' + e.message)
|
||||
socket.emit('$error', e.message)
|
||||
emit('$error', e.message)
|
||||
}
|
||||
})
|
||||
|
||||
let sentReqsUnsub = emptyUnsub
|
||||
|
||||
io.of('sentReqs').on('connect', async socket => {
|
||||
const on = encryptedOn(socket)
|
||||
const emit = encryptedEmit(socket)
|
||||
|
||||
try {
|
||||
if (!isAuthenticated()) {
|
||||
logger.info(
|
||||
'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
|
||||
}
|
||||
|
|
@ -556,13 +568,13 @@ module.exports = (
|
|||
|
||||
if (!isAuth) {
|
||||
logger.warn('invalid token for sentReqs socket')
|
||||
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
return
|
||||
}
|
||||
|
||||
if (sentReqsUnsub !== emptyUnsub) {
|
||||
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 = emptyUnsub
|
||||
|
|
@ -594,30 +606,32 @@ module.exports = (
|
|||
return stripped
|
||||
}
|
||||
)
|
||||
socket.emit('$shock', processed)
|
||||
emit('$shock', processed)
|
||||
}
|
||||
|
||||
sentReqsUnsub = GunEvents.onSimplerSentRequests(onSentReqs)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
on('disconnect', () => {
|
||||
sentReqsUnsub()
|
||||
sentReqsUnsub = emptyUnsub
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Error inside sentReqs socket connect: ' + e.message)
|
||||
socket.emit('$error', e.message)
|
||||
emit('$error', e.message)
|
||||
}
|
||||
})
|
||||
|
||||
let receivedReqsUnsub = emptyUnsub
|
||||
|
||||
io.of('receivedReqs').on('connect', async socket => {
|
||||
const on = encryptedOn(socket)
|
||||
const emit = encryptedEmit(socket)
|
||||
try {
|
||||
if (!isAuthenticated()) {
|
||||
logger.info(
|
||||
'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
|
||||
}
|
||||
|
|
@ -628,7 +642,7 @@ module.exports = (
|
|||
|
||||
if (!isAuth) {
|
||||
logger.warn('invalid token for receivedReqs socket')
|
||||
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -657,18 +671,18 @@ module.exports = (
|
|||
return stripped
|
||||
})
|
||||
|
||||
socket.emit('$shock', processed)
|
||||
emit('$shock', processed)
|
||||
}
|
||||
|
||||
receivedReqsUnsub = GunEvents.onSimplerReceivedRequests(onReceivedReqs)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
on('disconnect', () => {
|
||||
receivedReqsUnsub()
|
||||
receivedReqsUnsub = emptyUnsub
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Error inside receivedReqs socket connect: ' + e.message)
|
||||
socket.emit('$error', e.message)
|
||||
emit('$error', e.message)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,40 @@
|
|||
const { Buffer } = require("buffer");
|
||||
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');
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
const convertBase64ToBuffer = (value) => Buffer.from(value, 'base64');
|
||||
|
||||
/**
|
||||
* @param {Buffer} buffer
|
||||
*/
|
||||
const convertBufferToBase64 = (buffer) => buffer.toString("base64");
|
||||
|
||||
/**
|
||||
* @param {Buffer | string} key
|
||||
*/
|
||||
const processKey = (key) => {
|
||||
if (Buffer.isBuffer(key)) {
|
||||
return key;
|
||||
|
|
@ -15,11 +43,11 @@ const processKey = (key) => {
|
|||
return convertedKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
|
||||
* @returns {EncryptedMessageResponse}
|
||||
*/
|
||||
const convertToEncryptedMessageResponse = (encryptedMessage) => {
|
||||
if (typeof encryptedMessage.ciphertext === "string") {
|
||||
return encryptedMessage;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(encryptedMessage.ciphertext) &&
|
||||
Buffer.isBuffer(encryptedMessage.iv) &&
|
||||
Buffer.isBuffer(encryptedMessage.mac) &&
|
||||
|
|
@ -31,12 +59,22 @@ const convertToEncryptedMessageResponse = (encryptedMessage) => {
|
|||
ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey)
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof encryptedMessage.ciphertext === "string") {
|
||||
// @ts-ignore
|
||||
return encryptedMessage;
|
||||
}
|
||||
|
||||
throw new FieldError({
|
||||
field: "encryptedMessage",
|
||||
message: "Unknown encrypted message format"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
|
||||
* @returns {EncryptedMessageBuffer}
|
||||
*/
|
||||
const convertToEncryptedMessage = (encryptedMessage) => {
|
||||
if (encryptedMessage.ciphertext instanceof Buffer &&
|
||||
encryptedMessage.iv instanceof Buffer &&
|
||||
|
|
|
|||
|
|
@ -14,6 +14,18 @@ const {
|
|||
const nodeKeyPairs = 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 =>
|
||||
message &&
|
||||
message.ciphertext &&
|
||||
|
|
@ -21,6 +33,11 @@ const isEncryptedMessage = message =>
|
|||
message.mac &&
|
||||
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 privateKey = ECCrypto.generatePrivate()
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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 hostId = await Storage.get('encryption/hostId')
|
||||
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 publicKey = devicePublicKeys.get(deviceId)
|
||||
|
||||
|
|
@ -80,6 +112,11 @@ const encryptMessage = async ({ message = '', deviceId }) => {
|
|||
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 }) => {
|
||||
try {
|
||||
const keyPair = nodeKeyPairs.get(deviceId)
|
||||
|
|
@ -92,7 +129,6 @@ const decryptMessage = async ({ encryptedMessage, deviceId }) => {
|
|||
}
|
||||
|
||||
const processedPrivateKey = processKey(keyPair.privateKey)
|
||||
const processedPublicKey = processKey(keyPair.publicKey)
|
||||
const decryptedMessage = await ECCrypto.decrypt(
|
||||
processedPrivateKey,
|
||||
convertToEncryptedMessage(encryptedMessage)
|
||||
|
|
|
|||
140
utils/ECC/socket.js
Normal file
140
utils/ECC/socket.js
Normal 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
14
utils/JSON.js
Normal 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
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
class FieldError extends Error {
|
||||
/** @param {any} error */
|
||||
constructor(error) {
|
||||
super();
|
||||
this.message = error?.message ?? "An unknown error has occurred";
|
||||
|
|
|
|||
20
yarn.lock
20
yarn.lock
|
|
@ -634,6 +634,14 @@
|
|||
dependencies:
|
||||
"@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@*":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.4.tgz#3d9472711d179daa7c95c051e50ad411e18a9bdc"
|
||||
|
|
@ -641,6 +649,11 @@
|
|||
dependencies:
|
||||
"@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@*":
|
||||
version "4.16.9"
|
||||
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" "*"
|
||||
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@*":
|
||||
version "12.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue