diff --git a/.eslintrc.json b/.eslintrc.json index 6d35aae7..d18a89c7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -95,7 +95,10 @@ "no-warning-comments": "off", // broken - "sort-imports": "off" + "sort-imports": "off", + + // Would require to needlessly split code into too many files. + "mocha/max-top-level-suites": "off" }, "parser": "babel-eslint", "env": { diff --git a/.vscode/settings.json b/.vscode/settings.json index 5c0e42b5..aa72f34d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,10 +16,15 @@ "falsey", "GUNRPC", "ISEA", + "LNDRPC", + "lndstreaming", "PUBKEY", "radata", "Reqs", + "shockping", + "SHOCKWALLET", "thenables", + "unsubscription", "uuidv" ] } diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index c6ea9731..e89b0894 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -178,7 +178,7 @@ mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => { if (recipientOrSenderSEA === null) { throw new TypeError( - 'sea has to be nont null, args: ' + + 'sea has to be non null, args: ' + `${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify( recipientOrSenderSEA )}` @@ -187,7 +187,7 @@ mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => { if (recipientOrSenderEpub === recipientOrSenderSEA.pub) { throw new Error( - 'Do not use pub for mysecret, args: ' + + 'Do not use pub for mySecret, args: ' + `${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify( recipientOrSenderSEA )}` @@ -287,7 +287,7 @@ const getGun = () => { const getUser = () => { if (!user.is) { - logger.warn('called getUser() without being authed') + logger.warn('called getUser() without being authenticated') throw new Error(Constants.ErrorCode.NOT_AUTH) } return user diff --git a/utils/ECC/ECC.js b/utils/ECC/ECC.js new file mode 100644 index 00000000..990eac62 --- /dev/null +++ b/utils/ECC/ECC.js @@ -0,0 +1,207 @@ +/** @format */ +const ECCrypto = require('eccrypto') +const Storage = require('node-persist') +const FieldError = require('../fieldError') +const logger = require('../../config/log') +const { + generateRandomString, + convertBufferToBase64, + processKey, + convertToEncryptedMessageResponse, + convertUTF8ToBuffer, + convertToEncryptedMessage, + convertBase64ToBuffer +} = require('./crypto') + +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 && + message.iv && + message.mac && + message.ephemPublicKey + +/** + * @typedef {object} Pair + * @prop {Buffer} privateKey + * @prop {Buffer} publicKey + * @prop {string} privateKeyBase64 + * @prop {string} publicKeyBase64 + */ + +/** + * Generates a new encryption key pair that will be used + * when communicating with the deviceId specified + * @param {string} deviceId + * @returns {Pair} + */ +const generateKeyPair = deviceId => { + try { + const existingKey = nodeKeyPairs.get(deviceId) + + if (existingKey) { + logger.info('Device ID is already trusted') + return { + ...existingKey, + publicKeyBase64: convertBufferToBase64(existingKey.publicKey), + privateKeyBase64: convertBufferToBase64(existingKey.privateKey) + } + } + + const privateKey = ECCrypto.generatePrivate() + const publicKey = ECCrypto.getPublic(privateKey) + const privateKeyBase64 = convertBufferToBase64(privateKey) + const publicKeyBase64 = convertBufferToBase64(publicKey) + + if (!Buffer.isBuffer(privateKey) || !Buffer.isBuffer(publicKey)) { + throw new Error('Invalid KeyPair Generated') + } + + nodeKeyPairs.set(deviceId, { + privateKey, + publicKey + }) + + return { + privateKey, + publicKey, + privateKeyBase64, + publicKeyBase64 + } + } catch (err) { + logger.error( + '[ENCRYPTION] An error has occurred while generating a new KeyPair', + err + ) + logger.error('Device ID:', deviceId) + + throw err + } +} + +/** + * 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)) + const keyPair = generateKeyPair(deviceId) + + return { + success: true, + APIPublicKey: keyPair.publicKeyBase64, + hostId + } +} + +/** + * Encrypts the specified message using the specified deviceId's + * public key + * @param {{ deviceId: string, message: string | number | boolean }} arg0 + * @returns {Promise} + */ +const encryptMessage = async ({ message = '', deviceId }) => { + const parsedMessage = message.toString() + // decryptMessage checks for known devices while this one checks for + // authorized ones instead, why? + const publicKey = devicePublicKeys.get(deviceId) + + if (!publicKey) { + throw new FieldError({ + field: 'deviceId', + message: 'encryptMessage() -> Unauthorized Device ID detected' + }) + } + + const processedPublicKey = processKey(publicKey) + const messageBuffer = convertUTF8ToBuffer(parsedMessage) + const encryptedMessage = await ECCrypto.encrypt( + processedPublicKey, + messageBuffer + ) + const encryptedMessageResponse = { + ciphertext: encryptedMessage.ciphertext, + iv: encryptedMessage.iv, + mac: encryptedMessage.mac, + ephemPublicKey: encryptedMessage.ephemPublicKey, + metadata: { + _deviceId: deviceId, + _publicKey: publicKey + } + } + + return convertToEncryptedMessageResponse(encryptedMessageResponse) +} + +/** + * Decrypts the specified message using the API keypair + * associated with the specified deviceId + * @param {{ encryptedMessage: import('./crypto').EncryptedMessageResponse, deviceId: string }} arg0 + */ +const decryptMessage = async ({ encryptedMessage, deviceId }) => { + // encryptMessages checks for authorized devices while this one checks for + // known ones, why? + const keyPair = nodeKeyPairs.get(deviceId) + try { + if (!keyPair) { + throw new FieldError({ + field: 'deviceId', + message: 'decryptMessage() -> Unknown Device ID detected' + }) + } + + const processedPrivateKey = processKey(keyPair.privateKey) + const decryptedMessage = await ECCrypto.decrypt( + processedPrivateKey, + convertToEncryptedMessage(encryptedMessage) + ) + const parsedMessage = decryptedMessage.toString('utf8') + + return parsedMessage + } catch (err) { + logger.error(err) + if (err.message?.toLowerCase() === 'bad mac') { + logger.error( + 'Bad Mac!', + err, + convertToEncryptedMessage(encryptedMessage), + !!keyPair + ) + } + throw err + } +} + +module.exports = { + isAuthorizedDevice, + isEncryptedMessage, + generateKeyPair, + encryptMessage, + decryptMessage, + authorizeDevice, + generateRandomString, + nodeKeyPairs, + devicePublicKeys +} diff --git a/utils/ECC/ECC.spec.js b/utils/ECC/ECC.spec.js new file mode 100644 index 00000000..e4fbb9f7 --- /dev/null +++ b/utils/ECC/ECC.spec.js @@ -0,0 +1,114 @@ +/** + * @format + */ +// @ts-check +const Path = require('path') +const Storage = require('node-persist') +const expect = require('expect') +const words = require('random-words') + +const { + authorizeDevice, + decryptMessage, + encryptMessage, + generateKeyPair, + isAuthorizedDevice +} = require('./ECC') + +const uuid = () => words({ exactly: 24 }).join('-') + +const storageDirectory = Path.resolve(__dirname, `./.test-storage`) + +console.log(`Storage directory: ${storageDirectory}`) + +describe('generateKeyPair()', () => { + it('generates a keypair', () => { + const pair = generateKeyPair(uuid()) + + expect(pair.privateKey).toBeInstanceOf(Buffer) + expect(typeof pair.privateKeyBase64 === 'string').toBeTruthy() + expect(pair.publicKey).toBeInstanceOf(Buffer) + expect(typeof pair.publicKeyBase64 === 'string').toBeTruthy() + }) + it('returns the same pair for the same device', () => { + const id = uuid() + const pair = generateKeyPair(id) + const pairAgain = generateKeyPair(id) + + expect(pairAgain).toStrictEqual(pair) + }) +}) + +describe('authorizeDevice()/isAuthorizedDevice()', () => { + it('authorizes a device given its ID', async () => { + expect.hasAssertions() + await Storage.init({ + dir: storageDirectory + }) + const deviceId = uuid() + const pair = generateKeyPair(deviceId) + await authorizeDevice({ deviceId, publicKey: pair.publicKeyBase64 }) + expect(isAuthorizedDevice({ deviceId })).toBeTruthy() + }) +}) + +describe('encryptMessage()/decryptMessage()', () => { + before(() => + Storage.init({ + dir: storageDirectory + }) + ) + it('throws if provided with an unauthorized device id when encrypting', async () => { + expect.hasAssertions() + const deviceId = uuid() + + try { + await encryptMessage({ + message: uuid(), + deviceId + }) + throw new Error('encryptMessage() did not throw') + } catch (_) { + expect(true).toBeTruthy() + } + }) + it('throws if provided with an unknown device id when decrypting', async () => { + expect.hasAssertions() + const deviceId = uuid() + + try { + await decryptMessage({ + deviceId, + encryptedMessage: { + ciphertext: uuid(), + ephemPublicKey: uuid(), + iv: uuid(), + mac: uuid(), + metadata: uuid() + } + }) + throw new Error('decryptMessage() did not throw') + } catch (_) { + expect(true).toBeTruthy() + } + }) + it('encrypts and decrypts messages when given a known device id', async () => { + expect.hasAssertions() + const deviceId = uuid() + + const pair = generateKeyPair(deviceId) + + await authorizeDevice({ deviceId, publicKey: pair.publicKeyBase64 }) + + const message = 'Bitcoin fixes this' + + const encryptedMessage = await encryptMessage({ deviceId, message }) + + const decrypted = await decryptMessage({ + deviceId, + encryptedMessage + }) + + expect(decrypted).toEqual(message) + }) +}) diff --git a/utils/ECC/crypto.js b/utils/ECC/crypto.js index 5fe5b56a..6d329eb7 100644 --- a/utils/ECC/crypto.js +++ b/utils/ECC/crypto.js @@ -1,6 +1,9 @@ -const { Buffer } = require("buffer"); -const Crypto = require("crypto"); -const FieldError = require("../fieldError") +/** + * @format + */ +const { Buffer } = require('buffer') +const Crypto = require('crypto') +const FieldError = require('../fieldError') /** * @typedef {object} EncryptedMessageBuffer @@ -21,102 +24,109 @@ const FieldError = require("../fieldError") */ const generateRandomString = (length = 16) => - new Promise((resolve, reject) => { - Crypto.randomBytes(length, (err, buffer) => { - if (err) { - reject(err) - return - } + new Promise((resolve, reject) => { + // Gotta halve because randomBytes returns a sequence twice the size + Crypto.randomBytes(length / 2, (err, buffer) => { + if (err) { + reject(err) + return + } - const token = buffer.toString('hex') - resolve(token) - }) + const token = buffer.toString('hex') + resolve(token) }) + }) /** * @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) => { - if (Buffer.isBuffer(key)) { - return key; - } - const convertedKey = convertBase64ToBuffer(key); - return convertedKey; -}; +const processKey = key => { + if (Buffer.isBuffer(key)) { + return key + } + const convertedKey = convertBase64ToBuffer(key) + return convertedKey +} /** * @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage * @returns {EncryptedMessageResponse} */ -const convertToEncryptedMessageResponse = (encryptedMessage) => { - if (Buffer.isBuffer(encryptedMessage.ciphertext) && - Buffer.isBuffer(encryptedMessage.iv) && - Buffer.isBuffer(encryptedMessage.mac) && - Buffer.isBuffer(encryptedMessage.ephemPublicKey)) { - return { - ciphertext: convertBufferToBase64(encryptedMessage.ciphertext), - iv: convertBufferToBase64(encryptedMessage.iv), - mac: convertBufferToBase64(encryptedMessage.mac), - ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey), - metadata: encryptedMessage.metadata - }; +const convertToEncryptedMessageResponse = encryptedMessage => { + if ( + Buffer.isBuffer(encryptedMessage.ciphertext) && + Buffer.isBuffer(encryptedMessage.iv) && + Buffer.isBuffer(encryptedMessage.mac) && + Buffer.isBuffer(encryptedMessage.ephemPublicKey) + ) { + return { + ciphertext: convertBufferToBase64(encryptedMessage.ciphertext), + iv: convertBufferToBase64(encryptedMessage.iv), + mac: convertBufferToBase64(encryptedMessage.mac), + ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey), + metadata: encryptedMessage.metadata } + } - if (typeof encryptedMessage.ciphertext === "string") { - // @ts-ignore - return encryptedMessage; - } + if (typeof encryptedMessage.ciphertext === 'string') { + // @ts-ignore + return encryptedMessage + } - throw new FieldError({ - field: "encryptedMessage", - message: "Unknown encrypted message format" - }); -}; + 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 && - encryptedMessage.mac instanceof Buffer && - encryptedMessage.ephemPublicKey instanceof Buffer) { - // @ts-ignore - return encryptedMessage; +const convertToEncryptedMessage = encryptedMessage => { + if ( + encryptedMessage.ciphertext instanceof Buffer && + encryptedMessage.iv instanceof Buffer && + encryptedMessage.mac instanceof Buffer && + encryptedMessage.ephemPublicKey instanceof Buffer + ) { + // @ts-ignore + return encryptedMessage + } + if ( + typeof encryptedMessage.ciphertext === 'string' && + typeof encryptedMessage.iv === 'string' && + typeof encryptedMessage.mac === 'string' && + typeof encryptedMessage.ephemPublicKey === 'string' + ) { + return { + ciphertext: convertBase64ToBuffer(encryptedMessage.ciphertext), + iv: convertBase64ToBuffer(encryptedMessage.iv), + mac: convertBase64ToBuffer(encryptedMessage.mac), + ephemPublicKey: convertBase64ToBuffer(encryptedMessage.ephemPublicKey), + metadata: encryptedMessage.metadata } - if (typeof encryptedMessage.ciphertext === "string" && - typeof encryptedMessage.iv === "string" && - typeof encryptedMessage.mac === "string" && - typeof encryptedMessage.ephemPublicKey === "string") { - return { - ciphertext: convertBase64ToBuffer(encryptedMessage.ciphertext), - iv: convertBase64ToBuffer(encryptedMessage.iv), - mac: convertBase64ToBuffer(encryptedMessage.mac), - ephemPublicKey: convertBase64ToBuffer(encryptedMessage.ephemPublicKey), - metadata: encryptedMessage.metadata - }; - } - throw new FieldError({ - field: "encryptedMessage", - message: "Unknown encrypted message format" - }); -}; + } + throw new FieldError({ + field: 'encryptedMessage', + message: 'Unknown encrypted message format' + }) +} module.exports = { generateRandomString, @@ -125,5 +135,5 @@ module.exports = { convertBufferToBase64, convertToEncryptedMessage, convertToEncryptedMessageResponse, - processKey, -} \ No newline at end of file + processKey +} diff --git a/utils/ECC/crypto.spec.js b/utils/ECC/crypto.spec.js new file mode 100644 index 00000000..9b9904a0 --- /dev/null +++ b/utils/ECC/crypto.spec.js @@ -0,0 +1,33 @@ +/** + * @format + */ +// @ts-check +const expect = require('expect') + +const { + generateRandomString, + convertBase64ToBuffer, + convertBufferToBase64 +} = require('./crypto') + +describe('generateRandomString()', () => { + it('creates a random string of the specified length', async () => { + expect.hasAssertions() + const len = Math.ceil(Math.random() * 100) + const result = await generateRandomString(len) + + expect(result.length).toEqual(len) + }) +}) + +describe('Buffer <> String <> Buffer', () => { + it('preserves values', async () => { + const rnd = await generateRandomString(24) + + const asBuffer = convertBase64ToBuffer(rnd) + + const asStringAgain = convertBufferToBase64(asBuffer) + + expect(asStringAgain).toEqual(rnd) + }) +}) diff --git a/utils/ECC/index.js b/utils/ECC/index.js index c06d7b08..3d30c2b9 100644 --- a/utils/ECC/index.js +++ b/utils/ECC/index.js @@ -1,194 +1 @@ -/** @format */ -const ECCrypto = require('eccrypto') -const Storage = require('node-persist') -const FieldError = require('../fieldError') -const logger = require('../../config/log') -const { - generateRandomString, - convertBufferToBase64, - processKey, - convertToEncryptedMessageResponse, - convertUTF8ToBuffer, - convertToEncryptedMessage, - convertBase64ToBuffer -} = require('./crypto') - -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 && - message.iv && - 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 => { - try { - const existingKey = nodeKeyPairs.get(deviceId) - - if (existingKey) { - logger.warn('Device ID is already trusted') - return { - ...existingKey, - publicKeyBase64: convertBufferToBase64(existingKey.publicKey), - privateKeyBase64: convertBufferToBase64(existingKey.privateKey) - } - } - - const privateKey = ECCrypto.generatePrivate() - const publicKey = ECCrypto.getPublic(privateKey) - const privateKeyBase64 = convertBufferToBase64(privateKey) - const publicKeyBase64 = convertBufferToBase64(publicKey) - - if (!Buffer.isBuffer(privateKey) || !Buffer.isBuffer(publicKey)) { - throw new Error('Invalid KeyPair Generated') - } - - nodeKeyPairs.set(deviceId, { - privateKey, - publicKey - }) - - return { - privateKey, - publicKey, - privateKeyBase64, - publicKeyBase64 - } - } catch (err) { - logger.error( - '[ENCRYPTION] An error has occurred while generating a new KeyPair', - err - ) - logger.error('Device ID:', deviceId) - - throw err - } -} - -/** - * 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)) - const keyPair = generateKeyPair(deviceId) - - return { - success: true, - APIPublicKey: keyPair.publicKeyBase64, - hostId - } -} - -/** - * Encrypts the specified message using the specified deviceId's - * public key - * @param {{ deviceId: string, message: string | number | boolean }} arg0 - * @returns {Promise} - */ -const encryptMessage = async ({ message = '', deviceId }) => { - const parsedMessage = message.toString() - const publicKey = devicePublicKeys.get(deviceId) - - if (!publicKey) { - throw new FieldError({ - field: 'deviceId', - message: 'Unauthorized Device ID detected' - }) - } - - const processedPublicKey = processKey(publicKey) - const messageBuffer = convertUTF8ToBuffer(parsedMessage) - const encryptedMessage = await ECCrypto.encrypt( - processedPublicKey, - messageBuffer - ) - const encryptedMessageResponse = { - ciphertext: encryptedMessage.ciphertext, - iv: encryptedMessage.iv, - mac: encryptedMessage.mac, - ephemPublicKey: encryptedMessage.ephemPublicKey, - metadata: { - _deviceId: deviceId, - _publicKey: publicKey - } - } - - return convertToEncryptedMessageResponse(encryptedMessageResponse) -} - -/** - * Decrypts the specified message using the API keypair - * associated with the specified deviceId - * @param {{ encryptedMessage: import('./crypto').EncryptedMessageResponse, deviceId: string }} arg0 - */ -const decryptMessage = async ({ encryptedMessage, deviceId }) => { - const keyPair = nodeKeyPairs.get(deviceId) - try { - if (!keyPair) { - throw new FieldError({ - field: 'deviceId', - message: 'Unauthorized Device ID detected' - }) - } - - const processedPrivateKey = processKey(keyPair.privateKey) - const decryptedMessage = await ECCrypto.decrypt( - processedPrivateKey, - convertToEncryptedMessage(encryptedMessage) - ) - const parsedMessage = decryptedMessage.toString('utf8') - - return parsedMessage - } catch (err) { - logger.error(err) - if (err.message?.toLowerCase() === 'bad mac') { - logger.error( - 'Bad Mac!', - err, - convertToEncryptedMessage(encryptedMessage), - !!keyPair - ) - } - throw err - } -} - -module.exports = { - isAuthorizedDevice, - isEncryptedMessage, - generateKeyPair, - encryptMessage, - decryptMessage, - authorizeDevice, - generateRandomString, - nodeKeyPairs, - devicePublicKeys -} +module.exports = require('./ECC') \ No newline at end of file