Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): privateKeyOld #11653

Merged
merged 9 commits into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/usage/self-hosted-configuration.md
Expand Up @@ -337,6 +337,11 @@ echo 'actual-secret' | openssl rsautl -encrypt -pubin -inkey rsa_pub.pem | base6

Replace `actual-secret` with the secret to encrypt.

## privateKeyOld

Use this field if you need to perform a "key rotation" and support more than one keypair at a time.
Decryption with this key will be attempted after `privateKey`.

## privateKeyPath

Used as an alternative to `privateKey`, if you wish for the key to be read from disk instead.
Expand Down
4 changes: 2 additions & 2 deletions lib/config/decrypt.spec.ts
Expand Up @@ -31,11 +31,11 @@ describe('config/decrypt', () => {
});
it('handles invalid encrypted value', () => {
config.encrypted = { a: 1 };
setGlobalConfig({ privateKey });
setGlobalConfig({ privateKey, privateKeyOld: 'invalid-key' });
expect(() => decryptConfig(config)).toThrow(Error('config-validation'));
});
it('replaces npm token placeholder in npmrc', () => {
setGlobalConfig({ privateKey });
setGlobalConfig({ privateKey: 'invalid-key', privateKeyOld: privateKey }); // test old key failover
config.npmrc =
'//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n'; // eslint-disable-line no-template-curly-in-string
config.encrypted = {
Expand Down
147 changes: 87 additions & 60 deletions lib/config/decrypt.ts
Expand Up @@ -6,78 +6,105 @@ import { add } from '../util/sanitize';
import { getGlobalConfig } from './global';
import type { RenovateConfig } from './types';

export function tryDecryptPublicKeyDefault(
privateKey: string,
rarkins marked this conversation as resolved.
Show resolved Hide resolved
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('Failed to decrypt using default padding');
}
return decryptedStr;
}

export function tryDecryptPublicKeyPKCS1(
privateKey: string,
rarkins marked this conversation as resolved.
Show resolved Hide resolved
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('Failed to decrypt using PKCS1 padding');
}
return decryptedStr;
}

export function tryDecrypt(
privateKey: string,
rarkins marked this conversation as resolved.
Show resolved Hide resolved
encryptedStr: string
): string | null {
let decryptedStr = tryDecryptPublicKeyDefault(privateKey, encryptedStr);
if (!is.string(decryptedStr)) {
decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr);
}
return decryptedStr;
}

export function decryptConfig(config: RenovateConfig): RenovateConfig {
logger.trace({ config }, 'decryptConfig()');
const decryptedConfig = { ...config };
const { privateKey } = getGlobalConfig();
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)) {
try {
let decryptedStr: string;
try {
logger.debug('Trying default padding for ' + eKey);
decryptedStr = crypto
.privateDecrypt(privateKey, Buffer.from(eVal, 'base64'))
.toString();
logger.debug('Decrypted config using default padding');
} catch (err) {
logger.debug('Trying RSA_PKCS1_PADDING for ' + eKey);
decryptedStr = crypto
.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
Buffer.from(eVal, 'base64')
)
.toString();
// let it throw if the above fails
}
// istanbul ignore if
if (!decryptedStr.length) {
throw new Error('empty string');
}
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 {
logger.debug(
'Appending _authToken= to end of existing npmrc'
);
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
/\n?$/,
`\n_authToken=${token}\n`
);
}
/* eslint-enable no-template-curly-in-string */
logger.debug('Trying to decrypt ' + eKey);
let decryptedStr = tryDecrypt(privateKey, eVal);
if (privateKeyOld && !is.nonEmptyString(decryptedStr)) {
logger.debug(`Trying to decrypt with old private key`);
decryptedStr = tryDecrypt(privateKeyOld, eVal);
}
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 {
logger.debug('Adding npmrc to config');
decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
logger.debug('Appending _authToken= to end of existing npmrc');
decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
/\n?$/,
`\n_authToken=${token}\n`
);
}
/* eslint-enable no-template-curly-in-string */
} else {
decryptedConfig[eKey] = decryptedStr;
add(decryptedStr);
logger.debug('Adding npmrc to config');
decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
}
} catch (err) {
const error = new Error('config-validation');
error.validationError = `Failed to decrypt field ${eKey}. Please re-encrypt and try again.`;
throw error;
} else {
decryptedConfig[eKey] = decryptedStr;
add(decryptedStr);
}
}
} else {
Expand Down
1 change: 1 addition & 0 deletions lib/config/global.ts
Expand Up @@ -17,6 +17,7 @@ const repoGlobalOptions = [
'exposeAllEnv',
'migratePresets',
'privateKey',
'privateKeyOld',
'localDir',
'cacheDir',
];
Expand Down
8 changes: 8 additions & 0 deletions lib/config/options/index.ts
Expand Up @@ -452,6 +452,14 @@ const options: RenovateOptions[] = [
replaceLineReturns: true,
globalOnly: true,
},
{
name: 'privateKeyOld',
description: 'Secondary/old private key to try.',
stage: 'repository',
type: 'string',
replaceLineReturns: true,
globalOnly: true,
},
{
name: 'privateKeyPath',
description: 'Path to the Server-side private key.',
Expand Down
3 changes: 2 additions & 1 deletion lib/config/types.ts
Expand Up @@ -99,7 +99,8 @@ export interface RepoGlobalConfig {
dryRun?: boolean;
exposeAllEnv?: boolean;
migratePresets?: Record<string, string>;
privateKey?: string | Buffer;
privateKey?: string;
privateKeyOld?: string;
rarkins marked this conversation as resolved.
Show resolved Hide resolved
localDir?: string;
cacheDir?: string;
}
Expand Down
1 change: 1 addition & 0 deletions lib/util/sanitize.ts
Expand Up @@ -7,6 +7,7 @@ export const redactedFields = [
'npmToken',
'npmrc',
'privateKey',
'privateKeyOld',
'gitPrivateKey',
'forkToken',
'password',
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/global/config/parse/index.ts
Expand Up @@ -39,7 +39,7 @@ export async function parseConfigs(
}

if (!config.privateKey && config.privateKeyPath) {
config.privateKey = await readFile(config.privateKeyPath);
config.privateKey = (await readFile(config.privateKeyPath)).toString();
rarkins marked this conversation as resolved.
Show resolved Hide resolved
delete config.privateKeyPath;
}

Expand Down