From c342dedbfe1829c88f6887faf7a651d4f1bfde39 Mon Sep 17 00:00:00 2001 From: YEgorLu Date: Mon, 25 Nov 2024 14:40:19 +0500 Subject: [PATCH] fix(keystone): remove converting to hex; avoid semgrep; update readme; --- packages/keystone/crypto/EncryptionManager.js | 45 ++++++---------- .../keystone/crypto/EncryptionManager.spec.js | 51 +++++++++++-------- .../keystone/crypto/keyDerivers/pbkdf2.js | 2 +- .../fields/EncryptedText/Implementation.js | 2 + .../keystone/fields/EncryptedText/README.md | 33 ++++++------ 5 files changed, 66 insertions(+), 67 deletions(-) diff --git a/packages/keystone/crypto/EncryptionManager.js b/packages/keystone/crypto/EncryptionManager.js index 8b9cd0e4ffa..14fa915c83d 100644 --- a/packages/keystone/crypto/EncryptionManager.js +++ b/packages/keystone/crypto/EncryptionManager.js @@ -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'] @@ -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 }) * @@ -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 { @@ -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)) { @@ -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 @@ -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') @@ -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) { @@ -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) @@ -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])) { @@ -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, @@ -275,6 +264,4 @@ class EncryptionManager { module.exports = { EncryptionManager, - SUPPORTED_MODES, - ENCRYPTION_PREFIX, } \ No newline at end of file diff --git a/packages/keystone/crypto/EncryptionManager.spec.js b/packages/keystone/crypto/EncryptionManager.spec.js index 25e85f9ed27..2c71820cc11 100644 --- a/packages/keystone/crypto/EncryptionManager.spec.js +++ b/packages/keystone/crypto/EncryptionManager.spec.js @@ -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], })) @@ -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) => { @@ -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) @@ -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) }) @@ -127,7 +134,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) }) @@ -135,7 +142,7 @@ describe('EncryptionManager', () => { test('Can decrypt from multiple versions', () => { const differentVersionsCount = 3 - const exampleString = faker.random.alphaNumeric(10) + const exampleString = getRandomString() const encryptedInDifferentVersionsStrings = [] const versions = {} @@ -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', }) @@ -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) @@ -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) diff --git a/packages/keystone/crypto/keyDerivers/pbkdf2.js b/packages/keystone/crypto/keyDerivers/pbkdf2.js index cb861e8a1f2..e86d93598d4 100644 --- a/packages/keystone/crypto/keyDerivers/pbkdf2.js +++ b/packages/keystone/crypto/keyDerivers/pbkdf2.js @@ -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 { diff --git a/packages/keystone/fields/EncryptedText/Implementation.js b/packages/keystone/fields/EncryptedText/Implementation.js index 7153d1c136f..25132d87ef0 100644 --- a/packages/keystone/fields/EncryptedText/Implementation.js +++ b/packages/keystone/fields/EncryptedText/Implementation.js @@ -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 diff --git a/packages/keystone/fields/EncryptedText/README.md b/packages/keystone/fields/EncryptedText/README.md index e613803fed6..3a4af0a81ff 100644 --- a/packages/keystone/fields/EncryptedText/README.md +++ b/packages/keystone/fields/EncryptedText/README.md @@ -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) }, }, }); @@ -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, }, } }); @@ -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 |