Skip to content

Commit

Permalink
fix(keystone): remove converting to hex; avoid semgrep; update readme;
Browse files Browse the repository at this point in the history
  • Loading branch information
YEgorLu committed Nov 25, 2024
1 parent 03197c0 commit c342ded
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 67 deletions.
45 changes: 16 additions & 29 deletions packages/keystone/crypto/EncryptionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let DEFAULT_VERSION_ID
// \u{E003B} - TAG SEMICOLON
// \u{2800} - BRAILLE PATTERN BLANK

// since data is converted in hex, ':' shouldn't be in it
// ':' should not be presented in version id or encryption prefix
const SEPARATOR = ':'
const ENCRYPTION_PREFIX = ['\u{200B}', '\u{034F}', '\u{180C}', '\u{1D175}', '\u{E003B}', '\u{2800}'].join('')
const SUPPORTED_MODES = ['cbc', 'ctr', 'cfb', 'ofb', 'gcm']
Expand All @@ -46,13 +46,13 @@ const SUGGESTIONS = {
/**
* Used for versioning secrets for encryption / decryption
* @example use defaults
* const manager = new EncryptManager() // use versions from .env DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_CONFIG and encrypt in DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_VERSION_ID
* const manager = new EncryptManager() // use versions from .env DATA_ENCRYPTION_CONFIG and encrypt in DATA_ENCRYPTION_VERSION_ID
*
* @example override version for encryption
* const manager = new EncryptManager({ encryptionVersionId: 'version id - key of version in default config' })
*
* @example provide custom versions
* const versions = { 'versionId': { algorithm: 'aes-256-cbc', secret: '...' } }
* const versions = { 'versionId': { algorithm: 'aes-256-cbc', secret: '...', compressor: 'open-condo_brotli', keyDeriver: 'open-condo_pbkdf2-sha512 } }
* const encryptionVersionId = 'versionId'
* const manager = new EncryptManager({ versions, encryptionVersionId })
*
Expand All @@ -71,8 +71,7 @@ const SUGGESTIONS = {
* manager.decrypt(encryptedDataNull) // this throws error. You should not put null / undefined here
*
* const encryptedWithVersionWhichNotPresent = 'this-data-was-encrypted-using-version-which-is-not-present-in-Encryption-Manager'
* const decryptedData = manager.decrypt(encryptedWithVersionWhichNotPresent)
* expect(decryptedData).toBe(null)
* expect(() => manager.decrypt(encryptedWithVersionWhichNotPresent))).toThrow()
*/
class EncryptionManager {

Expand All @@ -83,7 +82,7 @@ class EncryptionManager {
/**
* @param config
* @param {EncryptionManagerConfig?} config.versions - override default versions for encryption and decryption
* @param {string?} config.encryptionVersionId - add default versions from .env. Defaults to true
* @param {string?} config.encryptionVersionId - override default version to encrypt data with
* */
constructor ({ versions = null, encryptionVersionId } = {}) {
if (isNil(versions)) {
Expand All @@ -101,20 +100,9 @@ class EncryptionManager {
const version = this._config[this._encryptionVersionId]
return [
ENCRYPTION_PREFIX,
Buffer.from(this._encryptionVersionId).toString('hex'),
this._encryptionVersionId,
version.encrypt(data),
].join(SEPARATOR)
// const { algorithm, ivLength, secret } = this._config[this._encryptionVersionId]
// const iv = crypto.randomBytes(ivLength)
//
// const cipheriv = crypto.createCipheriv(algorithm, secret, iv)
// const encryptedValue = Buffer.concat([cipheriv.update(data), cipheriv.final()])
// return [
// ENCRYPTION_PREFIX,
// Buffer.from(this._encryptionVersionId).toString('hex'),
// encryptedValue.toString('hex'),
// iv.toString('hex'),
// ].join(SEPARATOR)
}

/** @param {string} encrypted
Expand All @@ -129,11 +117,10 @@ class EncryptionManager {
throw new Error('Invalid format of encrypted data')
}

const [encryptionPrefix, versionIdHex, encryptedPayload] = parts
const [encryptionPrefix, versionId, encryptedPayload] = parts
if (encryptionPrefix !== ENCRYPTION_PREFIX) {
throw new Error('Invalid encrypted data. It was not encrypted with EncryptionManager')
}
const versionId = Buffer.from(versionIdHex, 'hex').toString()
const version = this._config[versionId]
if (isNil(version)) {
throw new Error('Invalid encrypted data. Versions id is not present in EncryptionManager')
Expand All @@ -157,8 +144,7 @@ class EncryptionManager {
if (parts[0] !== ENCRYPTION_PREFIX) {
return false
}
const decodedFromHexVersion = Buffer.from(parts[1], 'hex').toString()
return !!this._config[decodedFromHexVersion]
return !!this._config[parts[1]]
}

_initializeDefaults (overrideEncryptionVersionId) {
Expand All @@ -168,12 +154,12 @@ class EncryptionManager {
return
}

const defaultVersionsJSON = conf.DEFAULT_KEYSTONE_ENCRYPTION_CONFIG
const defaultVersionsJSON = conf.DATA_ENCRYPTION_CONFIG
if (isNil(defaultVersionsJSON)) {
throw new Error('env DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_CONFIG is not present')
throw new Error('env DATA_ENCRYPTION_CONFIG is not present')
}
const defaultVersions = JSON.parse(defaultVersionsJSON)
this._encryptionVersionId = isNil(overrideEncryptionVersionId) ? conf.DEFAULT_KEYSTONE_ENCRYPTION_VERSION_ID : overrideEncryptionVersionId
this._encryptionVersionId = isNil(overrideEncryptionVersionId) ? conf.DATA_ENCRYPTION_VERSION_ID : overrideEncryptionVersionId

this._validateVersions(defaultVersions)
this._config = this._parseVersions(defaultVersions)
Expand All @@ -199,14 +185,14 @@ class EncryptionManager {
_checkAtLeastOneVersionPresent () {
const atLeastOneVersionPresent = Object.keys(this._config).length > 0
if (!atLeastOneVersionPresent) {
throw new Error('Zero versions were provided. Add version in env.DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_CONFIG or provide in constructor')
throw new Error('Zero versions were provided. Add version in env.DATA_ENCRYPTION_CONFIG or provide in constructor')
}
}

_checkEncryptionVersionPresent () {
if (isNil(this._encryptionVersionId)) {
throw new Error(`Invalid encryption version id, received: ${this._encryptionVersionId}. You must add it in
env.DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_VERSION_ID or provide in constructor`)
env.DATA_ENCRYPTION_VERSION_ID or provide in constructor`)
}

if (isNil(this._config[this._encryptionVersionId])) {
Expand Down Expand Up @@ -235,6 +221,9 @@ class EncryptionManager {
if (versionId.length === 0) {
throw new Error('Invalid version id. Empty string is forbidden')
}
if (versionId.includes(SEPARATOR)) {
throw new Error(`${SEPARATOR} is forbidden to use in version id at ${versionId}`)
}

const {
algorithm,
Expand Down Expand Up @@ -275,6 +264,4 @@ class EncryptionManager {

module.exports = {
EncryptionManager,
SUPPORTED_MODES,
ENCRYPTION_PREFIX,
}
51 changes: 29 additions & 22 deletions packages/keystone/crypto/EncryptionManager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ const crypto = require('crypto')
const { faker } = require('@faker-js/faker')
const { groupBy } = require('lodash')

const { EncryptionManager, SUPPORTED_MODES, ENCRYPTION_PREFIX } = require('@open-condo/keystone/crypto/EncryptionManager')
const { EncryptionManager } = require('@open-condo/keystone/crypto/EncryptionManager')
const { catchErrorFrom, getRandomString } = require('@open-condo/keystone/test.utils')

const SUPPORTED_MODES = ['cbc', 'ctr', 'cfb', 'ofb', 'gcm']
const ENCRYPTION_PREFIX = ['\u{200B}', '\u{034F}', '\u{180C}', '\u{1D175}', '\u{E003B}', '\u{2800}'].join('')

function getSecretKey (len) {
return crypto.randomBytes(len)
}

function generateVersions () {
const cipherAlgorithms = crypto.getCiphers()
const cipherInfos = cipherAlgorithms.map(alg => crypto.getCipherInfo(alg))
const modes = new Set(cipherInfos.map(info => info.mode))

const secrets = cipherInfos.map((info) => faker.random.alphaNumeric(info.keyLength || 0))
const secrets = cipherInfos.map((info) => getSecretKey(info.keyLength || 0))
const versions = cipherAlgorithms.map((algorithm, i) => ({
mode: cipherInfos[i].mode,
id: faker.random.alphaNumeric(10),
id: getRandomString(),
algorithm: algorithm,
secret: secrets[i],
}))
Expand All @@ -29,9 +36,9 @@ function generateVersionsInMode (mode, count = 1) {
const infosForVersions = faker.helpers.arrayElements(cipherInfos, count)
const versionsArray = infosForVersions.map(info => {
return {
id: faker.random.alphaNumeric(10),
id: getRandomString(),
algorithm: info.name,
secret: faker.random.alphaNumeric(info.keyLength || 0),
secret: getSecretKey(info.keyLength || 0),
}
})
return versionsArray.reduce((versions, currentVersion) => {
Expand All @@ -52,13 +59,13 @@ describe('EncryptionManager', () => {
test.each(versions)('$algorithm', (version) => {
const manager = new EncryptionManager({ versions: { [version.id]: version }, encryptionVersionId: version.id })

const initialString = faker.random.alphaNumeric(20)
const initialString = getRandomString()
const encrypted = manager.encrypt(initialString)
expect(encrypted).not.toEqual(initialString)
const parts = encrypted.split(':')
expect(parts).toHaveLength(3)
expect(parts[0]).toEqual(ENCRYPTION_PREFIX)
expect(parts[1]).toEqual(Buffer.from(version.id).toString('hex'))
expect(parts[1]).toEqual(version.id)

const decrypted = manager.decrypt(encrypted)
expect(decrypted).toEqual(initialString)
Expand Down Expand Up @@ -104,7 +111,7 @@ describe('EncryptionManager', () => {
},
}
const manager = new EncryptionManager({ versions, encryptionVersionId: versionId })
const exampleString = faker.random.alphaNumeric(10)
const exampleString = getRandomString()
const encrypted = manager.encrypt(exampleString)
expect(manager.decrypt(encrypted)).toEqual(exampleString)
})
Expand All @@ -127,15 +134,15 @@ describe('EncryptionManager', () => {
},
}
const manager = new EncryptionManager({ versions, encryptionVersionId: versionId })
const exampleString = faker.random.alphaNumeric(10)
const exampleString = getRandomString()
const encrypted = manager.encrypt(exampleString)
expect(manager.decrypt(encrypted)).toEqual(exampleString)
})
})

test('Can decrypt from multiple versions', () => {
const differentVersionsCount = 3
const exampleString = faker.random.alphaNumeric(10)
const exampleString = getRandomString()
const encryptedInDifferentVersionsStrings = []
const versions = {}

Expand All @@ -160,7 +167,7 @@ describe('EncryptionManager', () => {
describe('Unsuccessful decryption', () => {
const manager = new EncryptionManager({
versions: {
'1': { algorithm: 'aes-256-cbc', secret: faker.random.alphaNumeric(32) },
'1': { algorithm: 'aes-256-cbc', secret: getSecretKey(32) },
},
encryptionVersionId: '1',
})
Expand All @@ -180,30 +187,30 @@ describe('EncryptionManager', () => {
})

test('Is not encrypted', () => {
const notEncrypted = faker.random.alphaNumeric(10)
expect(manager.decrypt(notEncrypted)).toBeNull()
const notEncrypted = getRandomString()
expect(() => manager.decrypt(notEncrypted)).toThrow()
})

test('Is encrypted in version, which not provided', () => {
const anotherManager = new EncryptionManager({
versions: { '2': { algorithm: 'aes-256-cbc', secret: faker.random.alphaNumeric(32) } },
versions: { '2': { algorithm: 'aes-256-cbc', secret: getSecretKey(32) } },
encryptionVersionId: '2',
})
const exampleString = faker.random.alphaNumeric(10)
const exampleString = getRandomString()
const encryptedString = anotherManager.encrypt(exampleString)
expect(manager.decrypt(encryptedString)).toBeNull()
expect(() => manager.decrypt(encryptedString)).toThrow()
})

test('Versions are same, but secret or algorithm differs', () => {
const managerAnotherAlgorithm = new EncryptionManager({
versions: { '1': { algorithm: 'aes-128-cbc', secret: faker.random.alphaNumeric(16) } },
versions: { '1': { algorithm: 'aes-128-cbc', secret: getSecretKey(16) } },
encryptionVersionId: '1',
})
const managerAnotherSecret = new EncryptionManager({
versions: { '1': { algorithm: 'aes-256-cbc', secret: faker.random.alphaNumeric(32) } },
versions: { '1': { algorithm: 'aes-256-cbc', secret: getSecretKey(32) } },
encryptionVersionId: '1',
})
const exampleValue = faker.random.alphaNumeric(10)
const exampleValue = getRandomString()
const encryptedWithDifferentAlgorithm = managerAnotherAlgorithm.encrypt(exampleValue)
const encryptedWithDifferentSecret = managerAnotherSecret.encrypt(exampleValue)

Expand All @@ -215,17 +222,17 @@ describe('EncryptionManager', () => {
describe('isEncrypted', () => {
const manager = new EncryptionManager({
versions: {
'1': { algorithm: 'aes-256-cbc', secret: faker.random.alphaNumeric(32) },
'1': { algorithm: 'aes-256-cbc', secret: getSecretKey(32) },
},
encryptionVersionId: '1',
})

test('Checks, that value was encrypted with one of provided versions', () => {
const exampleValue = faker.random.alphaNumeric(10)
const exampleValue = getRandomString()
const encrypted = manager.encrypt(exampleValue)

const anotherManager = new EncryptionManager({
versions: { '2': { algorithm: 'aes-256-cbc', secret: faker.random.alphaNumeric(32) } },
versions: { '2': { algorithm: 'aes-256-cbc', secret: getSecretKey(32) } },
encryptionVersionId: '2',
})
const anotherEncrypted = anotherManager.encrypt(exampleValue)
Expand Down
2 changes: 1 addition & 1 deletion packages/keystone/crypto/keyDerivers/pbkdf2.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function derive (secretKey, { keyLen, salt = null, iterations = null }) {
salt = crypto.randomBytes(SALT_LENGTH)
}
if (isNil(iterations)) {
iterations = Math.floor(Math.random() * (10000)) + BRUTE_FORCE_STRENGTH
iterations = crypto.randomInt(10000) + BRUTE_FORCE_STRENGTH
}
const masterKey = crypto.pbkdf2Sync(secretKey, salt, iterations, keyLen, 'sha512')
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/keystone/fields/EncryptedText/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ class EncryptedTextImplementation extends Text.implementation {
* @param {string} listKey
*/
constructor (path, options = {}, { listKey }) {
// provide encryptionManager in options for fieldAdapter for database
const errorStart = _getErrorStart(listKey, path)
const encryptionManager = _getEncryptionManager(errorStart, options)
set(options, 'encryptionManager', encryptionManager)

super(...arguments)

this.encryptionManager = encryptionManager
Expand Down
33 changes: 18 additions & 15 deletions packages/keystone/fields/EncryptedText/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,24 @@ Create EncryptionManager separately, so you can use it elsewhere to decrypt data
const { EncryptionManager } = require('@open-condo/keystone/crypto/EncryptionManager');
const conf = require('@open-condo/config')

const UserCrypto = {
nonPublicData: new EncryptionManager()
}
const manager = new EncryptionManager()

keystone.createList('User', {
fields: {
email: { type: 'Text' },
nonPublicData: {
type: 'EncryptedText',
encryptionManager: UserCrypto.nonPublicData,
encryptionManager: manager,
hooks: {
resolveInput({ existingItem }) {
const decryptedNonPublicData = UserCrypto.nonPublicData.decrypt(existingItem.nonPublicData)
const decryptedNonPublicData = manager.decrypt(existingItem.nonPublicData)
}
},
},
},
hooks: {
afterChanges: async ({ updatedItem }) => {
const decryptedNonPublicData = UserCrypto.nonPublicData.decrypt(updatedItem.nonPublicData)
const decryptedNonPublicData = manager.decrypt(updatedItem.nonPublicData)
},
},
});
Expand All @@ -56,19 +54,24 @@ If you want to configure EncryptionManager yourself
const { EncryptionManager } = require('@open-condo/keystone/crypto/EncryptionManager');
const conf = require('@open-condo/config')

const UserCrypto = {
nonPublicData: new EncryptionManager({
versions: { 'versionId': { algorithm: 'crypto algorithm', secret: 'your secret key' } },
const manager = new EncryptionManager({
versions: {
'versionId': {
algorithm: 'crypto algorithm',
secret: 'your secret key',
compressor: 'open-condo_brotli', // defaults to noop
keyDeriver: 'open-condo_pbkdf2-sha512', // defaults to noop
}
},
encryptionVersionId: 'versionId'
})
}

keystone.createList('User', {
fields: {
email: {type: 'Text'},
nonPublicData: {
type: 'EncryptedText',
encryptionManager: UserCrypto.nonPublicData,
encryptionManager: manager,
},
}
});
Expand Down Expand Up @@ -97,7 +100,7 @@ The value stored is string containing provided version, iv and, encrypted with p

## Service information

| Env | Format | Description |
|----------------------------------------------------|-----------------------------------------------------------|-------------------------------------------------------------------|
| `DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_CONFIG` | `{ [id: string]: { algorithm: string, secret: string } }` | Default versions |
| `DEFAULT_KEYSTONE_SYMMETRIC_ENCRYPTION_VERSION_ID` | `string` | Default version id from default versions to encrypt new data with |
| Env | Format | Description |
|------------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| `DATA_ENCRYPTION_CONFIG` | `{ [id: string]: { algorithm: string, secret: string, compressor?: string, keyDeriver?: string } }` | Default versions |
| `DATA_ENCRYPTION_VERSION_ID` | `string` | Default version id from default versions to encrypt new data with |

0 comments on commit c342ded

Please sign in to comment.