renovate/lib/config/decrypt.ts

231 lines
7.9 KiB
TypeScript
Raw Normal View History

2019-08-23 13:46:31 +00:00
import crypto from 'crypto';
2020-05-01 16:03:48 +00:00
import is from '@sindresorhus/is';
import * as openpgp from 'openpgp';
2019-08-23 13:46:31 +00:00
import { logger } from '../logger';
import { maskToken } from '../util/mask';
import { add } from '../util/sanitize';
import { getGlobalConfig } from './global';
2021-03-02 20:44:55 +00:00
import type { RenovateConfig } from './types';
2019-07-17 08:14:56 +00:00
export async function tryDecryptPgp(
privateKey: string,
encryptedStr: string
): Promise<string | null> {
if (encryptedStr.length < 500) {
// optimization during transition of public key -> pgp
return null;
}
try {
const pk = await openpgp.readPrivateKey({
// prettier-ignore
armoredKey: privateKey.replace(/\n[ \t]+/g, '\n'), // little massage to help a common problem
});
const startBlock = '-----BEGIN PGP MESSAGE-----\n\n';
const endBlock = '\n-----END PGP MESSAGE-----';
let armoredMessage = encryptedStr.trim();
if (!armoredMessage.startsWith(startBlock)) {
armoredMessage = `${startBlock}${armoredMessage}`;
}
if (!armoredMessage.endsWith(endBlock)) {
armoredMessage = `${armoredMessage}${endBlock}`;
}
const message = await openpgp.readMessage({
armoredMessage,
});
const { data } = await openpgp.decrypt({
message,
decryptionKeys: pk,
});
logger.debug('Decrypted config using openpgp');
return data;
} catch (err) {
logger.debug({ err }, 'Could not decrypt using openpgp');
return null;
}
}
2021-09-10 10:47:33 +00:00
export function tryDecryptPublicKeyDefault(
privateKey: string,
encryptedStr: string
): string | null {
let decryptedStr: string = null;
try {
decryptedStr = crypto
.privateDecrypt(privateKey, Buffer.from(encryptedStr, 'base64'))
.toString();
logger.debug('Decrypted config using default padding');
} catch (err) {
logger.debug('Could not decrypt using default padding');
2021-09-10 10:47:33 +00:00
}
return decryptedStr;
}
export function tryDecryptPublicKeyPKCS1(
privateKey: string,
encryptedStr: string
): string | null {
let decryptedStr: string = null;
try {
decryptedStr = crypto
.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
Buffer.from(encryptedStr, 'base64')
)
.toString();
} catch (err) {
logger.debug('Could not decrypt using PKCS1 padding');
2021-09-10 10:47:33 +00:00
}
return decryptedStr;
}
export async function tryDecrypt(
2021-09-10 10:47:33 +00:00
privateKey: string,
encryptedStr: string,
repository: string
): Promise<string | null> {
let decryptedStr: string = null;
if (privateKey?.startsWith('-----BEGIN PGP PRIVATE KEY BLOCK-----')) {
const decryptedObjStr = await tryDecryptPgp(privateKey, encryptedStr);
if (decryptedObjStr) {
try {
const decryptedObj = JSON.parse(decryptedObjStr);
const { o: org, r: repo, v: value } = decryptedObj;
if (is.nonEmptyString(value)) {
if (is.nonEmptyString(org)) {
const orgName = org.replace(/\/$/, ''); // Strip trailing slash
if (is.nonEmptyString(repo)) {
const scopedRepository = `${orgName}/${repo}`;
if (scopedRepository === repository) {
decryptedStr = value;
} else {
logger.debug(
{ scopedRepository },
'Secret is scoped to a different repository'
);
const error = new Error('config-validation');
error.validationError = `Encrypted secret is scoped to a different repository: ${scopedRepository}.`;
throw error;
}
} else {
const scopedOrg = `${orgName}/`;
if (repository.startsWith(scopedOrg)) {
decryptedStr = value;
} else {
logger.debug(
{ scopedOrg },
'Secret is scoped to a different org'
);
const error = new Error('config-validation');
error.validationError = `Encrypted secret is scoped to a different org" ${scopedOrg}.`;
throw error;
}
}
} else {
const error = new Error('config-validation');
error.validationError = `Encrypted value in config is missing a scope.`;
throw error;
}
} else {
const error = new Error('config-validation');
error.validationError = `Encrypted value in config is missing a value.`;
throw error;
}
} catch (err) {
logger.warn({ err }, 'Could not parse decrypted string');
}
}
} else {
decryptedStr = tryDecryptPublicKeyDefault(privateKey, encryptedStr);
if (!is.string(decryptedStr)) {
decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr);
}
2021-09-10 10:47:33 +00:00
}
return decryptedStr;
}
export async function decryptConfig(
config: RenovateConfig,
repository: string
): Promise<RenovateConfig> {
2018-03-27 19:57:02 +00:00
logger.trace({ config }, 'decryptConfig()');
const decryptedConfig = { ...config };
2021-09-10 10:47:33 +00:00
const { privateKey, privateKeyOld } = getGlobalConfig();
for (const [key, val] of Object.entries(config)) {
if (key === 'encrypted' && is.object(val)) {
logger.debug({ config: val }, 'Found encrypted config');
if (privateKey) {
for (const [eKey, eVal] of Object.entries(val)) {
2021-09-10 10:47:33 +00:00
logger.debug('Trying to decrypt ' + eKey);
let decryptedStr = await tryDecrypt(privateKey, eVal, repository);
2021-09-10 10:47:33 +00:00
if (privateKeyOld && !is.nonEmptyString(decryptedStr)) {
logger.debug(`Trying to decrypt with old private key`);
decryptedStr = await tryDecrypt(privateKeyOld, eVal, repository);
2021-09-10 10:47:33 +00:00
}
if (!is.nonEmptyString(decryptedStr)) {
const error = new Error('config-validation');
error.validationError = `Failed to decrypt field ${eKey}. Please re-encrypt and try again.`;
throw error;
}
logger.debug(`Decrypted ${eKey}`);
if (eKey === 'npmToken') {
const token = decryptedStr.replace(/\n$/, '');
add(token);
logger.debug(
{ decryptedToken: maskToken(token) },
'Migrating npmToken to npmrc'
);
if (is.string(decryptedConfig.npmrc)) {
/* eslint-disable no-template-curly-in-string */
if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) {
logger.debug('Replacing ${NPM_TOKEN} with decrypted token');
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
/\${NPM_TOKEN}/g,
token
);
} else {
2021-09-10 10:47:33 +00:00
logger.debug('Appending _authToken= to end of existing npmrc');
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
/\n?$/,
`\n_authToken=${token}\n`
);
}
2021-09-10 10:47:33 +00:00
/* eslint-enable no-template-curly-in-string */
} else {
2021-09-10 10:47:33 +00:00
logger.debug('Adding npmrc to config');
decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
}
2021-09-10 10:47:33 +00:00
} else {
decryptedConfig[eKey] = decryptedStr;
add(decryptedStr);
}
}
} else {
logger.error('Found encrypted data but no privateKey');
}
delete decryptedConfig.encrypted;
} else if (is.array(val)) {
decryptedConfig[key] = [];
for (const item of val) {
if (is.object(item) && !is.array(item)) {
(decryptedConfig[key] as RenovateConfig[]).push(
await decryptConfig(item as RenovateConfig, repository)
);
} else {
(decryptedConfig[key] as unknown[]).push(item);
}
}
} else if (is.object(val) && key !== 'content') {
decryptedConfig[key] = await decryptConfig(
val as RenovateConfig,
repository
);
}
}
delete decryptedConfig.encrypted;
logger.trace({ config: decryptedConfig }, 'decryptedConfig');
return decryptedConfig;
}