Merge pull request #487 from shocknet/bug/deprecate-rsa-encryption

Deprecate RSA Encryption
This commit is contained in:
CapDog 2021-10-15 08:19:59 -04:00 committed by GitHub
commit 9bbd2a14fe
6 changed files with 45 additions and 370 deletions

View file

@ -20,7 +20,6 @@ const path = require('path')
const getListPage = require('../utils/paginate') const getListPage = require('../utils/paginate')
const auth = require('../services/auth/auth') const auth = require('../services/auth/auth')
const FS = require('../utils/fs') const FS = require('../utils/fs')
const Encryption = require('../utils/encryptionStore')
const ECC = require('../utils/ECC') const ECC = require('../utils/ECC')
const LightningServices = require('../utils/lightningServices') const LightningServices = require('../utils/lightningServices')
const lndErrorManager = require('../utils/lightningServices/errors') const lndErrorManager = require('../utils/lightningServices/errors')
@ -215,98 +214,12 @@ module.exports = async (
next() next()
}) })
app.use((req, res, next) => {
const legacyDeviceId = req.headers['x-shockwallet-device-id']
const deviceId = req.headers['encryption-device-id']
try {
if (
nonEncryptedRoutes.includes(req.path) ||
process.env.DISABLE_SHOCK_ENCRYPTION === 'true' ||
(deviceId && !legacyDeviceId)
) {
return next()
}
if (!legacyDeviceId) {
const error = {
field: 'deviceId',
message: 'Please specify a device ID'
}
logger.error('Please specify a device ID')
return res.status(401).json(error)
}
if (!Encryption.isAuthorizedDevice({ deviceId: legacyDeviceId })) {
const error = {
field: 'deviceId',
message: 'Please specify a device ID'
}
logger.error('Unknown Device')
return res.status(401).json(error)
}
if (
!req.body.encryptionKey &&
!req.body.iv &&
!req.headers['x-shock-encryption-token']
) {
return next()
}
let reqData = null
let IV = null
let encryptedKey = null
let encryptedToken = null
if (req.method === 'GET' || req.method === 'DELETE') {
if (req.headers['x-shock-encryption-token']) {
encryptedToken = req.headers['x-shock-encryption-token']
encryptedKey = req.headers['x-shock-encryption-key']
IV = req.headers['x-shock-encryption-iv']
}
} else {
encryptedToken = req.body.token
encryptedKey = req.body.encryptionKey || req.body.encryptedKey
IV = req.body.iv
reqData = req.body.data || req.body.encryptedData
}
const decryptedKey = Encryption.decryptKey({
deviceId: legacyDeviceId,
message: encryptedKey
})
if (reqData) {
const decryptedMessage = Encryption.decryptMessage({
message: reqData,
key: decryptedKey,
iv: IV
})
req.body = JSON.parse(decryptedMessage)
}
const decryptedToken = encryptedToken
? Encryption.decryptMessage({
message: encryptedToken,
key: decryptedKey,
iv: IV
})
: null
if (decryptedToken) {
req.headers.authorization = decryptedToken
}
return next()
} catch (err) {
logger.error(err)
return res.status(401).json(err)
}
})
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const legacyDeviceId = req.headers['x-shockwallet-device-id']
const deviceId = req.headers['encryption-device-id'] const deviceId = req.headers['encryption-device-id']
try { try {
if ( if (
nonEncryptedRoutes.includes(req.path) || nonEncryptedRoutes.includes(req.path) ||
process.env.DISABLE_SHOCK_ENCRYPTION === 'true' || process.env.DISABLE_SHOCK_ENCRYPTION === 'true'
(legacyDeviceId && !deviceId)
) { ) {
return next() return next()
} }
@ -514,44 +427,6 @@ module.exports = async (
res.json({ msg: 'OK' }) res.json({ msg: 'OK' })
}) })
app.post('/api/security/exchangeKeys', async (req, res) => {
try {
const { publicKey, deviceId } = req.body
if (!publicKey) {
return res.status(400).json({
field: 'publicKey',
message: 'Please provide a valid public key'
})
}
if (
!deviceId ||
!/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/iu.test(
deviceId
)
) {
return res.status(400).json({
field: 'deviceId',
message: 'Please provide a valid device ID'
})
}
const authorizedDevice = await Encryption.authorizeDevice({
deviceId,
publicKey
})
logger.info(authorizedDevice)
return res.json(authorizedDevice)
} catch (err) {
logger.error(err)
return res.status(401).json({
field: 'unknown',
message: err
})
}
})
app.post('/api/encryption/exchange', async (req, res) => { app.post('/api/encryption/exchange', async (req, res) => {
try { try {
const { publicKey, deviceId } = req.body const { publicKey, deviceId } = req.body

View file

@ -1,5 +1,3 @@
const { generateRandomString } = require('../utils/encryptionStore')
/** /**
* @prettier * @prettier
*/ */
@ -22,7 +20,6 @@ const server = program => {
const ECC = require('../utils/ECC') const ECC = require('../utils/ECC')
const LightningServices = require('../utils/lightningServices') const LightningServices = require('../utils/lightningServices')
const Encryption = require('../utils/encryptionStore')
const app = Express() const app = Express()
const compression = require('compression') const compression = require('compression')
@ -124,7 +121,6 @@ const server = program => {
* @param {(() => void)} next * @param {(() => void)} next
*/ */
const modifyResponseBody = (req, res, next) => { const modifyResponseBody = (req, res, next) => {
const legacyDeviceId = req.headers['x-shockwallet-device-id']
const deviceId = req.headers['encryption-device-id'] const deviceId = req.headers['encryption-device-id']
const oldSend = res.send const oldSend = res.send
@ -136,39 +132,6 @@ const server = program => {
return return
} }
if (legacyDeviceId) {
res.send = (...args) => {
if (args[0] && args[0].encryptedData && args[0].encryptionKey) {
logger.warn('Response loop detected!')
oldSend.apply(res, args)
return
}
const { cached, hash } = cacheCheck({ req, res, args, send: oldSend })
if (cached) {
return
}
// arguments[0] (or `data`) contains the response body
const authorized = Encryption.isAuthorizedDevice({
deviceId: legacyDeviceId
})
const encryptedMessage = authorized
? Encryption.encryptMessage({
message: args[0] ? args[0] : {},
deviceId: legacyDeviceId,
metadata: {
hash
}
})
: args[0]
args[0] = JSON.stringify(encryptedMessage)
oldSend.apply(res, args)
}
}
if (deviceId) {
res.send = (...args) => { res.send = (...args) => {
if (args[0] && args[0].ciphertext && args[0].iv) { if (args[0] && args[0].ciphertext && args[0].iv) {
logger.warn('Response loop detected!') logger.warn('Response loop detected!')
@ -197,7 +160,6 @@ const server = program => {
oldSend.apply(res, args) oldSend.apply(res, args)
} }
} }
}
next() next()
} }
@ -288,7 +250,7 @@ const server = program => {
return randomField return randomField
} }
const newValue = await Encryption.generateRandomString(length) const newValue = await ECC.generateRandomString(length)
await Storage.setItem(fieldName, newValue) await Storage.setItem(fieldName, newValue)
return newValue return newValue
} }
@ -440,10 +402,11 @@ const server = program => {
} }
}) })
} }
if(process.env.ALLOW_UNLOCKED_LND === 'true'){ if(process.env.ALLOW_UNLOCKED_LND === 'true'){
const codes = await Storage.valuesWithKeyMatch(/^UnlockedAccessSecrets\//u) const codes = await Storage.valuesWithKeyMatch(/^UnlockedAccessSecrets\//u)
if(codes.length === 0){ if(codes.length === 0){
const code = await generateRandomString(12) const code = ECC.generateRandomString(12)
await Storage.setItem(`UnlockedAccessSecrets/${code}`, false) await Storage.setItem(`UnlockedAccessSecrets/${code}`, false)
await Storage.setItem(`FirstAccessSecret`, code) await Storage.setItem(`FirstAccessSecret`, code)
logger.info("the access code is:"+code) logger.info("the access code is:"+code)

View file

@ -47,7 +47,7 @@
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@ -60,5 +60,6 @@
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"useUnknownInCatchVariables": false
} }
} }

View file

@ -1,4 +1,5 @@
const { Buffer } = require("buffer"); const { Buffer } = require("buffer");
const Crypto = require("crypto");
const FieldError = require("../fieldError") const FieldError = require("../fieldError")
/** /**
@ -17,6 +18,19 @@ const FieldError = require("../fieldError")
* @prop {string} ephemPublicKey * @prop {string} ephemPublicKey
*/ */
const generateRandomString = (length = 16) =>
new Promise((resolve, reject) => {
Crypto.randomBytes(length, (err, buffer) => {
if (err) {
reject(err)
return
}
const token = buffer.toString('hex')
resolve(token)
})
})
/** /**
* @param {string} value * @param {string} value
*/ */
@ -101,10 +115,11 @@ const convertToEncryptedMessage = (encryptedMessage) => {
}; };
module.exports = { module.exports = {
generateRandomString,
convertUTF8ToBuffer, convertUTF8ToBuffer,
convertBase64ToBuffer, convertBase64ToBuffer,
convertBufferToBase64, convertBufferToBase64,
convertToEncryptedMessage, convertToEncryptedMessage,
convertToEncryptedMessageResponse, convertToEncryptedMessageResponse,
processKey processKey,
} }

View file

@ -4,6 +4,7 @@ const Storage = require('node-persist')
const FieldError = require('../fieldError') const FieldError = require('../fieldError')
const logger = require('../../config/log') const logger = require('../../config/log')
const { const {
generateRandomString,
convertBufferToBase64, convertBufferToBase64,
processKey, processKey,
convertToEncryptedMessageResponse, convertToEncryptedMessageResponse,
@ -182,5 +183,6 @@ module.exports = {
generateKeyPair, generateKeyPair,
encryptMessage, encryptMessage,
decryptMessage, decryptMessage,
authorizeDevice authorizeDevice,
generateRandomString
} }

View file

@ -1,181 +0,0 @@
/**
* @prettier
*/
const Crypto = require('crypto')
const { Buffer } = require('buffer')
const logger = require('../config/log')
const APIKeyPair = new Map()
const authorizedDevices = new Map()
const nonEncryptedEvents = [
'ping',
'disconnect',
'IS_GUN_AUTH',
'SET_LAST_SEEN_APP'
]
const Encryption = {
/**
* @param {string} event
* @returns {boolean}
*/
isNonEncrypted: event => nonEncryptedEvents.includes(event),
/**
* @param {{ deviceId: string , message: string }} arg0
*/
encryptKey: ({ deviceId, message }) => {
if (!authorizedDevices.has(deviceId)) {
throw { field: 'deviceId', message: 'Unknown Device ID' }
}
const devicePublicKey = authorizedDevices.get(deviceId)
const data = Buffer.from(message)
const encryptedData = Crypto.publicEncrypt(
{
key: devicePublicKey,
padding: Crypto.constants.RSA_PKCS1_PADDING
},
data
)
return encryptedData.toString('base64')
},
/**
* @param {{ deviceId: string , message: string }} arg0
*/
decryptKey: ({ deviceId, message }) => {
if (!authorizedDevices.has(deviceId)) {
throw { field: 'deviceId', message: 'Unknown Device ID' }
}
const data = Buffer.from(message, 'base64')
const encryptedData = Crypto.privateDecrypt(
{
key: APIKeyPair.get(deviceId).privateKey,
padding: Crypto.constants.RSA_PKCS1_PADDING
},
data
)
return encryptedData.toString()
},
/**
* @param {{ deviceId: string , message: any , metadata?: any}} arg0
*/
encryptMessage: ({ deviceId, message, metadata = {} }) => {
const parsedMessage =
typeof message === 'object' ? JSON.stringify(message) : message
const data = Buffer.from(parsedMessage)
const key = Crypto.randomBytes(32)
const iv = Crypto.randomBytes(16)
const encryptedKey = Encryption.encryptKey({
deviceId,
message: key.toString('hex')
})
const cipher = Crypto.createCipheriv('aes-256-cbc', key, iv)
const encryptedCipher = cipher.update(data)
const encryptedBuffer = Buffer.concat([
Buffer.from(encryptedCipher),
Buffer.from(cipher.final())
])
const encryptedData = encryptedBuffer.toString('base64')
const encryptedMessage = {
encryptedData,
encryptedKey,
iv: iv.toString('hex'),
metadata
}
return encryptedMessage
},
/**
* @param {{ message: string , key: string , iv: string }} arg0
*/
decryptMessage: ({ message, key, iv }) => {
const data = Buffer.from(message, 'base64')
const cipher = Crypto.createDecipheriv(
'aes-256-cbc',
Buffer.from(key, 'hex'),
Buffer.from(iv, 'hex')
)
const decryptedCipher = cipher.update(data)
const decryptedBuffer = Buffer.concat([
Buffer.from(decryptedCipher),
Buffer.from(cipher.final())
])
const decryptedData = decryptedBuffer.toString()
return decryptedData.toString()
},
/**
* @param {{ deviceId: string }} arg0
*/
isAuthorizedDevice: ({ deviceId }) => {
if (authorizedDevices.has(deviceId)) {
return true
}
return false
},
/**
* @param {{ deviceId: string , publicKey: string }} arg0
*/
authorizeDevice: ({ deviceId, publicKey }) =>
new Promise((resolve, reject) => {
authorizedDevices.set(deviceId, publicKey)
Crypto.generateKeyPair(
'rsa',
{
modulusLength: 2048,
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
}
},
(err, publicKey, privateKey) => {
if (err) {
// @ts-ignore
logger.error(err)
reject(err)
return
}
const exportedKey = {
publicKey,
privateKey
}
APIKeyPair.set(deviceId, exportedKey)
resolve({
success: true,
APIPublicKey: exportedKey.publicKey
})
}
)
}),
/**
* @param {{ deviceId: string }} arg0
*/
unAuthorizeDevice: ({ deviceId }) => {
authorizedDevices.delete(deviceId)
},
generateRandomString: (length = 16) =>
new Promise((resolve, reject) => {
Crypto.randomBytes(length, (err, buffer) => {
if (err) {
reject(err)
return
}
const token = buffer.toString('hex')
resolve(token)
})
})
}
module.exports = Encryption