2023-03-28 21:05:36 +00:00
|
|
|
import crypto from 'node:crypto';
|
2020-05-01 16:03:48 +00:00
|
|
|
import is from '@sindresorhus/is';
|
2021-09-16 10:11:13 +00:00
|
|
|
import * as openpgp from 'openpgp';
|
2019-08-23 13:46:31 +00:00
|
|
|
import { logger } from '../logger';
|
|
|
|
import { maskToken } from '../util/mask';
|
2021-10-19 12:53:34 +00:00
|
|
|
import { regEx } from '../util/regex';
|
2022-01-26 09:57:21 +00:00
|
|
|
import { addSecretForSanitizing } from '../util/sanitize';
|
2023-03-25 09:14:33 +00:00
|
|
|
import { ensureTrailingSlash } from '../util/url';
|
2021-11-23 20:10:45 +00:00
|
|
|
import { GlobalConfig } from './global';
|
2023-03-12 05:52:19 +00:00
|
|
|
import { DecryptedObject } from './schema';
|
2021-03-02 20:44:55 +00:00
|
|
|
import type { RenovateConfig } from './types';
|
2019-07-17 08:14:56 +00:00
|
|
|
|
2021-09-16 10:11:13 +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
|
2021-10-19 12:53:34 +00:00
|
|
|
armoredKey: privateKey.replace(regEx(/\n[ \t]+/g), '\n'), // little massage to help a common problem
|
2021-09-16 10:11:13 +00:00
|
|
|
});
|
|
|
|
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 {
|
2022-02-11 10:02:30 +00:00
|
|
|
let decryptedStr: string | null = null;
|
2021-09-10 10:47:33 +00:00
|
|
|
try {
|
|
|
|
decryptedStr = crypto
|
|
|
|
.privateDecrypt(privateKey, Buffer.from(encryptedStr, 'base64'))
|
|
|
|
.toString();
|
|
|
|
logger.debug('Decrypted config using default padding');
|
|
|
|
} catch (err) {
|
2021-09-16 10:11:13 +00:00
|
|
|
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 {
|
2022-02-11 10:02:30 +00:00
|
|
|
let decryptedStr: string | null = null;
|
2021-09-10 10:47:33 +00:00
|
|
|
try {
|
|
|
|
decryptedStr = crypto
|
|
|
|
.privateDecrypt(
|
|
|
|
{
|
|
|
|
key: privateKey,
|
|
|
|
padding: crypto.constants.RSA_PKCS1_PADDING,
|
|
|
|
},
|
|
|
|
Buffer.from(encryptedStr, 'base64')
|
|
|
|
)
|
|
|
|
.toString();
|
|
|
|
} catch (err) {
|
2021-09-16 10:11:13 +00:00
|
|
|
logger.debug('Could not decrypt using PKCS1 padding');
|
2021-09-10 10:47:33 +00:00
|
|
|
}
|
|
|
|
return decryptedStr;
|
|
|
|
}
|
|
|
|
|
2021-09-16 10:11:13 +00:00
|
|
|
export async function tryDecrypt(
|
2021-09-10 10:47:33 +00:00
|
|
|
privateKey: string,
|
2021-09-16 10:11:13 +00:00
|
|
|
encryptedStr: string,
|
|
|
|
repository: string
|
|
|
|
): Promise<string | null> {
|
2022-02-11 10:02:30 +00:00
|
|
|
let decryptedStr: string | null = null;
|
2021-09-16 10:11:13 +00:00
|
|
|
if (privateKey?.startsWith('-----BEGIN PGP PRIVATE KEY BLOCK-----')) {
|
|
|
|
const decryptedObjStr = await tryDecryptPgp(privateKey, encryptedStr);
|
|
|
|
if (decryptedObjStr) {
|
|
|
|
try {
|
2023-08-21 15:29:41 +00:00
|
|
|
const decryptedObj = DecryptedObject.safeParse(decryptedObjStr);
|
2023-03-12 05:52:19 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (!decryptedObj.success) {
|
|
|
|
const error = new Error('config-validation');
|
|
|
|
error.validationError = `Could not parse decrypted config.`;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { o: org, r: repo, v: value } = decryptedObj.data;
|
2021-09-16 10:11:13 +00:00
|
|
|
if (is.nonEmptyString(value)) {
|
|
|
|
if (is.nonEmptyString(org)) {
|
2023-03-25 09:14:33 +00:00
|
|
|
const orgPrefixes = org
|
|
|
|
.split(',')
|
|
|
|
.map((o) => o.trim())
|
|
|
|
.map((o) => o.toUpperCase())
|
|
|
|
.map((o) => ensureTrailingSlash(o));
|
2021-09-16 10:11:13 +00:00
|
|
|
if (is.nonEmptyString(repo)) {
|
2023-03-25 09:14:33 +00:00
|
|
|
const scopedRepos = orgPrefixes.map((orgPrefix) =>
|
|
|
|
`${orgPrefix}${repo}`.toUpperCase()
|
|
|
|
);
|
|
|
|
if (scopedRepos.some((r) => r === repository.toUpperCase())) {
|
2021-09-16 10:11:13 +00:00
|
|
|
decryptedStr = value;
|
|
|
|
} else {
|
2021-09-16 13:05:11 +00:00
|
|
|
logger.debug(
|
2023-03-25 09:14:33 +00:00
|
|
|
{ scopedRepos },
|
2021-09-16 10:11:13 +00:00
|
|
|
'Secret is scoped to a different repository'
|
|
|
|
);
|
2021-09-16 13:05:11 +00:00
|
|
|
const error = new Error('config-validation');
|
2023-03-25 09:14:33 +00:00
|
|
|
error.validationError = `Encrypted secret is scoped to a different repository: "${scopedRepos.join(
|
|
|
|
','
|
|
|
|
)}".`;
|
2021-09-16 13:05:11 +00:00
|
|
|
throw error;
|
2021-09-16 10:11:13 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-11-18 17:32:44 +00:00
|
|
|
if (
|
2023-03-25 09:14:33 +00:00
|
|
|
orgPrefixes.some((orgPrefix) =>
|
|
|
|
repository.toUpperCase().startsWith(orgPrefix)
|
|
|
|
)
|
2021-11-18 17:32:44 +00:00
|
|
|
) {
|
2021-09-16 10:11:13 +00:00
|
|
|
decryptedStr = value;
|
|
|
|
} else {
|
2021-09-16 13:05:11 +00:00
|
|
|
logger.debug(
|
2023-03-25 09:14:33 +00:00
|
|
|
{ orgPrefixes },
|
2021-09-16 10:11:13 +00:00
|
|
|
'Secret is scoped to a different org'
|
|
|
|
);
|
2021-09-16 13:05:11 +00:00
|
|
|
const error = new Error('config-validation');
|
2023-03-25 09:14:33 +00:00
|
|
|
error.validationError = `Encrypted secret is scoped to a different org: "${orgPrefixes.join(
|
|
|
|
','
|
|
|
|
)}".`;
|
2021-09-16 13:05:11 +00:00
|
|
|
throw error;
|
2021-09-16 10:11:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2021-09-16 13:05:11 +00:00
|
|
|
const error = new Error('config-validation');
|
|
|
|
error.validationError = `Encrypted value in config is missing a scope.`;
|
|
|
|
throw error;
|
2021-09-16 10:11:13 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-09-16 13:05:11 +00:00
|
|
|
const error = new Error('config-validation');
|
|
|
|
error.validationError = `Encrypted value in config is missing a value.`;
|
|
|
|
throw error;
|
2021-09-16 10:11:13 +00:00
|
|
|
}
|
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
2021-09-16 10:11:13 +00:00
|
|
|
export async function decryptConfig(
|
|
|
|
config: RenovateConfig,
|
|
|
|
repository: string
|
|
|
|
): Promise<RenovateConfig> {
|
2018-03-27 19:57:02 +00:00
|
|
|
logger.trace({ config }, 'decryptConfig()');
|
2017-11-03 06:51:44 +00:00
|
|
|
const decryptedConfig = { ...config };
|
2023-06-25 19:34:42 +00:00
|
|
|
const privateKey = GlobalConfig.get('privateKey');
|
|
|
|
const privateKeyOld = GlobalConfig.get('privateKeyOld');
|
2017-11-10 12:46:16 +00:00
|
|
|
for (const [key, val] of Object.entries(config)) {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (key === 'encrypted' && is.object(val)) {
|
2017-09-01 04:45:51 +00:00
|
|
|
logger.debug({ config: val }, 'Found encrypted config');
|
|
|
|
if (privateKey) {
|
2017-11-10 12:46:16 +00:00
|
|
|
for (const [eKey, eVal] of Object.entries(val)) {
|
2021-09-10 10:47:33 +00:00
|
|
|
logger.debug('Trying to decrypt ' + eKey);
|
2021-09-16 10:11:13 +00:00
|
|
|
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`);
|
2021-09-16 10:11:13 +00:00
|
|
|
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') {
|
2021-12-29 06:26:13 +00:00
|
|
|
const token = decryptedStr.replace(regEx(/\n$/), '');
|
2022-01-26 09:57:21 +00:00
|
|
|
addSecretForSanitizing(token);
|
2021-09-10 10:47:33 +00:00
|
|
|
logger.debug(
|
|
|
|
{ decryptedToken: maskToken(token) },
|
|
|
|
'Migrating npmToken to npmrc'
|
|
|
|
);
|
|
|
|
if (is.string(decryptedConfig.npmrc)) {
|
2021-11-09 06:50:25 +00:00
|
|
|
/* eslint-disable no-template-curly-in-string */
|
2021-09-10 10:47:33 +00:00
|
|
|
if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) {
|
|
|
|
logger.debug('Replacing ${NPM_TOKEN} with decrypted token');
|
|
|
|
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
|
2021-10-19 12:53:34 +00:00
|
|
|
regEx(/\${NPM_TOKEN}/g),
|
2021-09-10 10:47:33 +00:00
|
|
|
token
|
|
|
|
);
|
2018-07-05 13:44:42 +00:00
|
|
|
} else {
|
2021-09-10 10:47:33 +00:00
|
|
|
logger.debug('Appending _authToken= to end of existing npmrc');
|
|
|
|
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
|
2021-12-29 06:26:13 +00:00
|
|
|
regEx(/\n?$/),
|
2021-09-10 10:47:33 +00:00
|
|
|
`\n_authToken=${token}\n`
|
|
|
|
);
|
2018-07-05 13:44:42 +00:00
|
|
|
}
|
2021-09-10 10:47:33 +00:00
|
|
|
/* eslint-enable no-template-curly-in-string */
|
2017-09-01 05:43:49 +00:00
|
|
|
} else {
|
2021-09-10 10:47:33 +00:00
|
|
|
logger.debug('Adding npmrc to config');
|
|
|
|
decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
|
2017-09-01 05:43:49 +00:00
|
|
|
}
|
2021-09-10 10:47:33 +00:00
|
|
|
} else {
|
|
|
|
decryptedConfig[eKey] = decryptedStr;
|
2022-01-26 09:57:21 +00:00
|
|
|
addSecretForSanitizing(decryptedStr);
|
2017-09-01 04:45:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.error('Found encrypted data but no privateKey');
|
|
|
|
}
|
|
|
|
delete decryptedConfig.encrypted;
|
2018-06-04 18:07:22 +00:00
|
|
|
} else if (is.array(val)) {
|
2017-09-01 04:45:51 +00:00
|
|
|
decryptedConfig[key] = [];
|
2021-09-16 10:11:13 +00:00
|
|
|
for (const item of val) {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (is.object(item) && !is.array(item)) {
|
2020-03-02 11:06:16 +00:00
|
|
|
(decryptedConfig[key] as RenovateConfig[]).push(
|
2021-09-16 10:11:13 +00:00
|
|
|
await decryptConfig(item as RenovateConfig, repository)
|
2020-03-02 11:06:16 +00:00
|
|
|
);
|
2017-09-01 04:45:51 +00:00
|
|
|
} else {
|
2020-07-30 04:54:20 +00:00
|
|
|
(decryptedConfig[key] as unknown[]).push(item);
|
2017-09-01 04:45:51 +00:00
|
|
|
}
|
2021-09-16 10:11:13 +00:00
|
|
|
}
|
2018-06-04 18:07:22 +00:00
|
|
|
} else if (is.object(val) && key !== 'content') {
|
2021-09-16 10:11:13 +00:00
|
|
|
decryptedConfig[key] = await decryptConfig(
|
|
|
|
val as RenovateConfig,
|
|
|
|
repository
|
|
|
|
);
|
2017-09-01 04:45:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
delete decryptedConfig.encrypted;
|
2017-09-01 05:43:49 +00:00
|
|
|
logger.trace({ config: decryptedConfig }, 'decryptedConfig');
|
2017-09-01 04:45:51 +00:00
|
|
|
return decryptedConfig;
|
|
|
|
}
|