commit
4596b9bbe2
8 changed files with 446 additions and 267 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -16,10 +16,15 @@
|
|||
"falsey",
|
||||
"GUNRPC",
|
||||
"ISEA",
|
||||
"LNDRPC",
|
||||
"lndstreaming",
|
||||
"PUBKEY",
|
||||
"radata",
|
||||
"Reqs",
|
||||
"shockping",
|
||||
"SHOCKWALLET",
|
||||
"thenables",
|
||||
"unsubscription",
|
||||
"uuidv"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
207
utils/ECC/ECC.js
Normal file
207
utils/ECC/ECC.js
Normal file
|
|
@ -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<import('./crypto').EncryptedMessageResponse>}
|
||||
*/
|
||||
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
|
||||
}
|
||||
114
utils/ECC/ECC.spec.js
Normal file
114
utils/ECC/ECC.spec.js
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
processKey
|
||||
}
|
||||
33
utils/ECC/crypto.spec.js
Normal file
33
utils/ECC/crypto.spec.js
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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<import('./crypto').EncryptedMessageResponse>}
|
||||
*/
|
||||
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')
|
||||
Loading…
Add table
Add a link
Reference in a new issue