From 6c0d50703f394e5ac528da179455de2b7d745f41 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Fri, 1 Sep 2017 06:45:51 +0200 Subject: [PATCH] feat: encrypted configuration strings (#759) A new config object `encrypted` can be defined at any level and contain encrypted configuration strings. Initial use is for encrypting an npm token for use with the hosted renovate app. Closes #650 --- lib/config/decrypt.js | 55 ++++++++++++++++++++++++++++++ lib/config/definitions.js | 16 +++++++++ lib/logger/config-serializer.js | 1 + lib/workers/repository/index.js | 3 ++ test/_fixtures/keys/private.pem | 27 +++++++++++++++ test/config/decrypt.spec.js | 59 +++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 lib/config/decrypt.js create mode 100644 test/_fixtures/keys/private.pem create mode 100644 test/config/decrypt.spec.js diff --git a/lib/config/decrypt.js b/lib/config/decrypt.js new file mode 100644 index 0000000000..45a5ff94cc --- /dev/null +++ b/lib/config/decrypt.js @@ -0,0 +1,55 @@ +const crypto = require('crypto'); + +module.exports = { + decryptConfig, +}; + +function decryptConfig( + config, + logger = config.logger, + privateKey = config.privateKey +) { + const decryptedConfig = { ...config }; + logger.trace({ config }, 'decryptConfig'); + for (const key of Object.keys(config)) { + const val = config[key]; + if (key === 'encrypted' && isObject(val)) { + logger.debug({ config: val }, 'Found encrypted config'); + if (privateKey) { + for (const encryptedKey of Object.keys(val)) { + try { + decryptedConfig[encryptedKey] = crypto + .privateDecrypt( + privateKey, + new Buffer(val[encryptedKey], 'base64') + ) + .toString(); + logger.debug(`Decrypted ${encryptedKey}`); + } catch (err) { + logger.warn({ err }, `Error decrypting ${encryptedKey}`); + } + } + } else { + logger.error('Found encrypted data but no privateKey'); + } + delete decryptedConfig.encrypted; + } else if (isObject(val) && key !== 'content' && key !== 'logger') { + decryptedConfig[key] = decryptConfig(val, logger, privateKey); + } else if (Array.isArray(val)) { + decryptedConfig[key] = []; + val.forEach(item => { + if (isObject(item)) { + decryptedConfig[key].push(decryptConfig(item, logger, privateKey)); + } else { + decryptedConfig[key].push(item); + } + }); + } + } + delete decryptedConfig.encrypted; + return decryptedConfig; +} + +function isObject(obj) { + return Object.prototype.toString.call(obj) === '[object Object]'; +} diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 2dca42af88..4f7193ef5b 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -71,6 +71,22 @@ const options = [ stage: 'repository', type: 'boolean', }, + // encryption + { + name: 'privateKey', + description: 'Server-side private key', + stage: 'repository', + type: 'string', + replaceLineReturns: true, + }, + { + name: 'encrypted', + description: + 'A configuration object containing configuration encrypted with project key', + stage: 'repository', + type: 'json', + default: null, + }, // Scheduling { name: 'timezone', diff --git a/lib/logger/config-serializer.js b/lib/logger/config-serializer.js index fff9cee8f2..c8e51cd9ee 100644 --- a/lib/logger/config-serializer.js +++ b/lib/logger/config-serializer.js @@ -9,6 +9,7 @@ function configSerializer(config) { 'npmToken', 'npmrc', 'yarnrc', + 'privateKey', ]; const functionFields = ['api', 'logger']; const templateFields = ['commitMessage', 'prTitle', 'prBody']; diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js index fd603979b4..61f8c6899b 100644 --- a/lib/workers/repository/index.js +++ b/lib/workers/repository/index.js @@ -7,6 +7,7 @@ const apis = require('./apis'); const onboarding = require('./onboarding'); const upgrades = require('./upgrades'); const cleanup = require('./cleanup'); +const { decryptConfig } = require('../../config/decrypt'); module.exports = { renovateRepository, @@ -100,6 +101,8 @@ async function renovateRepository(repoConfig, token) { config.logger = logger; logger.trace({ config }, 'onboarding config'); } + config = decryptConfig(config); + logger.trace({ config }, 'post-decrypt config'); const allUpgrades = await upgrades.determineRepoUpgrades(config); const res = await upgrades.branchifyUpgrades(allUpgrades, logger); config.errors = config.errors.concat(res.errors); diff --git a/test/_fixtures/keys/private.pem b/test/_fixtures/keys/private.pem new file mode 100644 index 0000000000..efee57e904 --- /dev/null +++ b/test/_fixtures/keys/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAzVLc1KmhWxcLnoPTPpQwxVHySFx4vY+0Sk+2AdnlJvrTFlNR +8XgBPdgU9SDwHFFTYyXRQ/msm0YMOBRoIdz4/psz6IzRV80aOtDIkUPAU+cdqnje +3rusrPnqCykDuIHGoBZ8zlt57t/8OVNOduUflrQRqMFors8iLWcNjuGJfZAusI5B +M8KYYDPL2oUo/8AV0bWN8MdNnxDSwGtan4bbzrtG6dpNzyntsG5DkL1h8OZRki4S +kHf+71ZzJFz1KcyrzBVJ9DBxdiJdmLLkv9caLLc2QS0VFmtfLnlrga3Sahy6YNwc +rN33aaNOXwFPSErsKWIgIf16eOaSUQCFusXY9QIDAQABAoIBAQCkltMM6nm9Ikkf +JX9V/8bkth7o4K+tDSAyHZnB/CBUUeaaU+oxDci5AZkzMtcnbA3TQcJxohg6VDmB +TuJ2msNCnblLpm492t023pyYzd3DpFXEjKXjmEAAXUm+7n7cDbPiKoSbivrAgO6Y +KW6RonPjA6/QPlIjJ0m3aY+VxLfJXTYVfFBk+0HGEAvmrSSSEezXej8Qzs9CRKtz +cQkR5Bs749SS509MHxaslP7n366EvJBJkqrjUxA6kbxOxUkuuOWF9jfduDAG+NYc +pe4IXFOYMpK/w29wqvkNulKYs+FHXr1sGjkztGjyNnjP0NnX+r0EYU07xLJ26krD +KMWQG8sBAoGBAO93iFrPnEBnnKojI8b+u3YowVCz0HVuHHIStHd9uUgfsn/Bxfls +HRIlg8l1MJd69TT0knOhkJxBCP+b/qBvid7YLbTxIVELCAfLAzsmGgY+DEN/TTUb +FDvHGa/drCnkSR/O/RbtHvMQISzTja1siYrdwwY+wpwzR2tB7ZH/9iO1AoGBANt/ +3Srpv4BbP7JZ+cNte4JfI2gZq41mo+DF5ryuFZzsP3R5SZ1BOapQGyLEATQaoxsJ +QZsI73KPBzab0/+E75qJuXckIGmFftHXgoQpClGvPaEDF9M21QW5Y3vKVa/45qhy +3wpb8gEYqrt1x0rmzyumCtCD8J470Er64gE1eKhBAoGBALW6ScFYwqRhvROkvTb8 +A7mE7kfXXgBwAqhTJ59yytRAMc8gd6R0do9Z5uxQwgKDLmj0ndugpcTe2fxZHuAU +JVX3SqCBSZ5eN8bqOtZ9cMyB8/6ZMjd2CGHhE85R9KCJ/TBlfc4TPySIfhStq1wL +/UlkR+eKY1f01mNAUhE1ZU7tAoGBAKwS/B51KsSERFYcRToYbRfSX55vaVarnVNL +scw+qQDhEAnOP5CBHqTOscc6YzsmmrFKO10/zv8+80ezN6n73B6JU5T8BFDU74uv +6EiVJ9rLh4PfOeFB/hPDtyLHhw8yEBkEHKgxVnHXlZjqBzdH5Cdyvs2icZKKj4sI +TP7nnVRBAoGAPZBjjTn9HgB9et5Kdovp+7nr842WboxwklVyTpbNGAjIxNXIXex+ +TST21UbquTIpIrpYRk1WMIu9T5PndAagDWVEsmUYQZeuhmuY64K+iviRsfdGthws +nbbR8sMEkn4XEtPZfrEBq27g01RNmIIMW5Es2O5N2AjLlyeBOh099Fw= +-----END RSA PRIVATE KEY----- diff --git a/test/config/decrypt.spec.js b/test/config/decrypt.spec.js new file mode 100644 index 0000000000..95508e4b15 --- /dev/null +++ b/test/config/decrypt.spec.js @@ -0,0 +1,59 @@ +const { decryptConfig } = require('../../lib/config/decrypt.js'); +const defaultConfig = require('../../lib/config/defaults').getConfig(); +const logger = require('../_fixtures/logger'); +const fs = require('fs'); + +const privateKey = fs.readFileSync('test/_fixtures/keys/private.pem'); + +describe('config/massage', () => { + describe('massageConfig', () => { + let config; + beforeEach(() => { + config = { ...defaultConfig, logger }; + }); + it('returns empty with no privateKey', () => { + delete config.encrypted; + const res = decryptConfig(config); + expect(res).toMatchObject(config); + }); + it('warns if no privateKey found', () => { + config.encrypted = { a: '1' }; + const res = decryptConfig(config); + expect(res.encrypted).not.toBeDefined(); + expect(res.a).not.toBeDefined(); + }); + it('handles invalid encrypted type', () => { + config.encrypted = 1; + config.privateKey = privateKey; + const res = decryptConfig(config); + expect(res.encrypted).not.toBeDefined(); + }); + it('handles invalid encrypted value', () => { + config.encrypted = { a: 1 }; + config.privateKey = privateKey; + const res = decryptConfig(config); + expect(res.encrypted).not.toBeDefined(); + expect(res.a).not.toBeDefined(); + }); + it('decrypts nested', () => { + config.privateKey = privateKey; + config.packageFiles = [ + { + packageFile: 'package.json', + devDependencies: { + encrypted: { + branchPrefix: + 'FLA9YHIzpE7YetAg/P0X46npGRCMqn7hgyzwX5ZQ9wYgu9BRRbTiBVsUIFTyM5BuP1Q22slT2GkWvFvum7GU236Y6QiT7Nr8SLvtsJn2XUuq8H7REFKzdy3+wqyyWbCErYTFyY1dcPM7Ht+CaGDWdd8u/FsoX7AdMRs/X1jNUo6iSmlUiyGlYDKF+QMnCJom1VPVgZXWsGKdjI2MLny991QMaiv0VajmFIh4ENv4CtXOl/1twvIl/6XTXAaqpJJKDTPZEuydi+PHDZmal2RAOfrkH4m0UURa7SlfpUlIg+EaqbNGp85hCYXLwRcEET1OnYr3rH1oYkcYJ40any1tvQ==', + }, + }, + }, + ]; + const res = decryptConfig(config); + expect(res.encrypted).not.toBeDefined(); + expect(res.packageFiles[0].devDependencies.encrypted).not.toBeDefined(); + expect(res.packageFiles[0].devDependencies.branchPrefix).toEqual( + 'abcdef-ghijklm-nopqf-stuvwxyz' + ); + }); + }); +}); -- GitLab