commit
4596b9bbe2
8 changed files with 446 additions and 267 deletions
|
|
@ -95,7 +95,10 @@
|
||||||
"no-warning-comments": "off",
|
"no-warning-comments": "off",
|
||||||
|
|
||||||
// broken
|
// 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",
|
"parser": "babel-eslint",
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -16,10 +16,15 @@
|
||||||
"falsey",
|
"falsey",
|
||||||
"GUNRPC",
|
"GUNRPC",
|
||||||
"ISEA",
|
"ISEA",
|
||||||
|
"LNDRPC",
|
||||||
|
"lndstreaming",
|
||||||
"PUBKEY",
|
"PUBKEY",
|
||||||
"radata",
|
"radata",
|
||||||
"Reqs",
|
"Reqs",
|
||||||
|
"shockping",
|
||||||
|
"SHOCKWALLET",
|
||||||
"thenables",
|
"thenables",
|
||||||
|
"unsubscription",
|
||||||
"uuidv"
|
"uuidv"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => {
|
||||||
|
|
||||||
if (recipientOrSenderSEA === null) {
|
if (recipientOrSenderSEA === null) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'sea has to be nont null, args: ' +
|
'sea has to be non null, args: ' +
|
||||||
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
|
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
|
||||||
recipientOrSenderSEA
|
recipientOrSenderSEA
|
||||||
)}`
|
)}`
|
||||||
|
|
@ -187,7 +187,7 @@ mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => {
|
||||||
|
|
||||||
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
|
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Do not use pub for mysecret, args: ' +
|
'Do not use pub for mySecret, args: ' +
|
||||||
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
|
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
|
||||||
recipientOrSenderSEA
|
recipientOrSenderSEA
|
||||||
)}`
|
)}`
|
||||||
|
|
@ -287,7 +287,7 @@ const getGun = () => {
|
||||||
|
|
||||||
const getUser = () => {
|
const getUser = () => {
|
||||||
if (!user.is) {
|
if (!user.is) {
|
||||||
logger.warn('called getUser() without being authed')
|
logger.warn('called getUser() without being authenticated')
|
||||||
throw new Error(Constants.ErrorCode.NOT_AUTH)
|
throw new Error(Constants.ErrorCode.NOT_AUTH)
|
||||||
}
|
}
|
||||||
return user
|
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");
|
* @format
|
||||||
const FieldError = require("../fieldError")
|
*/
|
||||||
|
const { Buffer } = require('buffer')
|
||||||
|
const Crypto = require('crypto')
|
||||||
|
const FieldError = require('../fieldError')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} EncryptedMessageBuffer
|
* @typedef {object} EncryptedMessageBuffer
|
||||||
|
|
@ -21,102 +24,109 @@ const FieldError = require("../fieldError")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const generateRandomString = (length = 16) =>
|
const generateRandomString = (length = 16) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
Crypto.randomBytes(length, (err, buffer) => {
|
// Gotta halve because randomBytes returns a sequence twice the size
|
||||||
if (err) {
|
Crypto.randomBytes(length / 2, (err, buffer) => {
|
||||||
reject(err)
|
if (err) {
|
||||||
return
|
reject(err)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const token = buffer.toString('hex')
|
const token = buffer.toString('hex')
|
||||||
resolve(token)
|
resolve(token)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
const convertUTF8ToBuffer = (value) => Buffer.from(value, 'utf-8');
|
const convertUTF8ToBuffer = value => Buffer.from(value, 'utf-8')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
const convertBase64ToBuffer = (value) => Buffer.from(value, 'base64');
|
const convertBase64ToBuffer = value => Buffer.from(value, 'base64')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Buffer} buffer
|
* @param {Buffer} buffer
|
||||||
*/
|
*/
|
||||||
const convertBufferToBase64 = (buffer) => buffer.toString("base64");
|
const convertBufferToBase64 = buffer => buffer.toString('base64')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Buffer | string} key
|
* @param {Buffer | string} key
|
||||||
*/
|
*/
|
||||||
const processKey = (key) => {
|
const processKey = key => {
|
||||||
if (Buffer.isBuffer(key)) {
|
if (Buffer.isBuffer(key)) {
|
||||||
return key;
|
return key
|
||||||
}
|
}
|
||||||
const convertedKey = convertBase64ToBuffer(key);
|
const convertedKey = convertBase64ToBuffer(key)
|
||||||
return convertedKey;
|
return convertedKey
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
|
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
|
||||||
* @returns {EncryptedMessageResponse}
|
* @returns {EncryptedMessageResponse}
|
||||||
*/
|
*/
|
||||||
const convertToEncryptedMessageResponse = (encryptedMessage) => {
|
const convertToEncryptedMessageResponse = encryptedMessage => {
|
||||||
if (Buffer.isBuffer(encryptedMessage.ciphertext) &&
|
if (
|
||||||
Buffer.isBuffer(encryptedMessage.iv) &&
|
Buffer.isBuffer(encryptedMessage.ciphertext) &&
|
||||||
Buffer.isBuffer(encryptedMessage.mac) &&
|
Buffer.isBuffer(encryptedMessage.iv) &&
|
||||||
Buffer.isBuffer(encryptedMessage.ephemPublicKey)) {
|
Buffer.isBuffer(encryptedMessage.mac) &&
|
||||||
return {
|
Buffer.isBuffer(encryptedMessage.ephemPublicKey)
|
||||||
ciphertext: convertBufferToBase64(encryptedMessage.ciphertext),
|
) {
|
||||||
iv: convertBufferToBase64(encryptedMessage.iv),
|
return {
|
||||||
mac: convertBufferToBase64(encryptedMessage.mac),
|
ciphertext: convertBufferToBase64(encryptedMessage.ciphertext),
|
||||||
ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey),
|
iv: convertBufferToBase64(encryptedMessage.iv),
|
||||||
metadata: encryptedMessage.metadata
|
mac: convertBufferToBase64(encryptedMessage.mac),
|
||||||
};
|
ephemPublicKey: convertBufferToBase64(encryptedMessage.ephemPublicKey),
|
||||||
|
metadata: encryptedMessage.metadata
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof encryptedMessage.ciphertext === "string") {
|
if (typeof encryptedMessage.ciphertext === 'string') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return encryptedMessage;
|
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
|
* @param {EncryptedMessageBuffer | EncryptedMessageResponse} encryptedMessage
|
||||||
* @returns {EncryptedMessageBuffer}
|
* @returns {EncryptedMessageBuffer}
|
||||||
*/
|
*/
|
||||||
const convertToEncryptedMessage = (encryptedMessage) => {
|
const convertToEncryptedMessage = encryptedMessage => {
|
||||||
if (encryptedMessage.ciphertext instanceof Buffer &&
|
if (
|
||||||
encryptedMessage.iv instanceof Buffer &&
|
encryptedMessage.ciphertext instanceof Buffer &&
|
||||||
encryptedMessage.mac instanceof Buffer &&
|
encryptedMessage.iv instanceof Buffer &&
|
||||||
encryptedMessage.ephemPublicKey instanceof Buffer) {
|
encryptedMessage.mac instanceof Buffer &&
|
||||||
// @ts-ignore
|
encryptedMessage.ephemPublicKey instanceof Buffer
|
||||||
return encryptedMessage;
|
) {
|
||||||
|
// @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" &&
|
throw new FieldError({
|
||||||
typeof encryptedMessage.mac === "string" &&
|
field: 'encryptedMessage',
|
||||||
typeof encryptedMessage.ephemPublicKey === "string") {
|
message: 'Unknown encrypted message format'
|
||||||
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"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateRandomString,
|
generateRandomString,
|
||||||
|
|
@ -125,5 +135,5 @@ module.exports = {
|
||||||
convertBufferToBase64,
|
convertBufferToBase64,
|
||||||
convertToEncryptedMessage,
|
convertToEncryptedMessage,
|
||||||
convertToEncryptedMessageResponse,
|
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 */
|
module.exports = require('./ECC')
|
||||||
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
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue