Skip to content

Commit

Permalink
refactor(config): split decryption functions (#28571)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceice authored Apr 22, 2024
1 parent c8702b9 commit 96f9ad5
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 115 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
NODE_VERSION: 18
DRY_RUN: true
TEST_LEGACY_DECRYPTION: true
SPARSE_CHECKOUT: |-
.github/actions/
data/
Expand Down
16 changes: 11 additions & 5 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'node:crypto';
import os from 'node:os';
import { env } from 'node:process';
import v8 from 'node:v8';
import { minimatch } from 'minimatch';
import type { JestConfigWithTsJest } from 'ts-jest';
Expand Down Expand Up @@ -205,11 +206,7 @@ const config: JestConfig = {
'!lib/**/{__fixtures__,__mocks__,__testutil__,test}/**/*.{js,ts}',
'!lib/**/types.ts',
],
coveragePathIgnorePatterns: [
'/node_modules/',
'<rootDir>/test/',
'<rootDir>/tools/',
],
coveragePathIgnorePatterns: getCoverageIgnorePatterns(),
cacheDirectory: '.cache/jest',
collectCoverage: true,
coverageReporters: ci
Expand Down Expand Up @@ -450,3 +447,12 @@ process.stderr.write(`Host stats:
Memory: ${(mem / 1024 / 1024 / 1024).toFixed(2)} GB
HeapLimit: ${(stats.heap_size_limit / 1024 / 1024 / 1024).toFixed(2)} GB
`);
function getCoverageIgnorePatterns(): string[] | undefined {
const patterns = ['/node_modules/', '<rootDir>/test/', '<rootDir>/tools/'];

if (env.TEST_LEGACY_DECRYPTION !== 'true') {
patterns.push('<rootDir>/lib/config/decrypt/legacy.ts');
}

return patterns;
}
83 changes: 6 additions & 77 deletions lib/config/decrypt.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,18 @@
import crypto from 'node:crypto';
import is from '@sindresorhus/is';
import * as openpgp from 'openpgp';
import { logger } from '../logger';
import { maskToken } from '../util/mask';
import { regEx } from '../util/regex';
import { addSecretForSanitizing } from '../util/sanitize';
import { ensureTrailingSlash } from '../util/url';
import {
tryDecryptPublicKeyDefault,
tryDecryptPublicKeyPKCS1,
} from './decrypt/legacy';
import { tryDecryptOpenPgp } from './decrypt/openpgp';
import { GlobalConfig } from './global';
import { DecryptedObject } from './schema';
import type { RenovateConfig } from './types';

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(regEx(/\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;
}
}

export function tryDecryptPublicKeyDefault(
privateKey: string,
encryptedStr: string,
): string | null {
let decryptedStr: string | null = 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');
}
return decryptedStr;
}

export function tryDecryptPublicKeyPKCS1(
privateKey: string,
encryptedStr: string,
): string | null {
let decryptedStr: string | null = 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');
}
return decryptedStr;
}

export async function tryDecrypt(
privateKey: string,
encryptedStr: string,
Expand All @@ -92,7 +21,7 @@ export async function tryDecrypt(
): Promise<string | null> {
let decryptedStr: string | null = null;
if (privateKey?.startsWith('-----BEGIN PGP PRIVATE KEY BLOCK-----')) {
const decryptedObjStr = await tryDecryptPgp(privateKey, encryptedStr);
const decryptedObjStr = await tryDecryptOpenPgp(privateKey, encryptedStr);
if (decryptedObjStr) {
decryptedStr = validateDecryptedValue(decryptedObjStr, repository);
}
Expand Down
40 changes: 40 additions & 0 deletions lib/config/decrypt/legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/** istanbul ignore file */
import crypto from 'node:crypto';
import { logger } from '../../logger';

export function tryDecryptPublicKeyPKCS1(
privateKey: string,
encryptedStr: string,
): string | null {
let decryptedStr: string | null = 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');
}
return decryptedStr;
}

export function tryDecryptPublicKeyDefault(
privateKey: string,
encryptedStr: string,
): string | null {
let decryptedStr: string | null = 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');
}
return decryptedStr;
}
40 changes: 40 additions & 0 deletions lib/config/decrypt/openpgp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as openpgp from 'openpgp';
import { logger } from '../../logger';
import { regEx } from '../../util/regex';

export async function tryDecryptOpenPgp(
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(regEx(/\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;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"generate": "run-s 'generate:*'",
"generate:imports": "node tools/generate-imports.mjs",
"git-check": "node tools/check-git-version.mjs",
"jest": "node tools/jest.mjs",
"jest": "GIT_ALLOW_PROTOCOL=file LOG_LEVEL=fatal node --experimental-vm-modules node_modules/jest/bin/jest.js --logHeapUsage",
"lint": "run-s ls-lint type-check eslint prettier markdown-lint git-check doc-fence-check",
"lint-fix": "run-s eslint-fix prettier-fix markdown-lint-fix",
"ls-lint": "ls-lint",
Expand Down
32 changes: 0 additions & 32 deletions tools/jest.mjs

This file was deleted.

0 comments on commit 96f9ad5

Please sign in to comment.