Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/usage/self-hosted-experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Suppress the pre-commit support warning in PR bodies.

## `RENOVATE_X_USE_OPENPGP`

Use `openpgp` instead of `kbpgp` for `PGP` decryption.
Use `openpgp` instead of `gnugp` for `PGP` decryption.

## `RENOVATE_X_YARN_PROXY`

Expand Down
4 changes: 2 additions & 2 deletions lib/config/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getEnv } from '../util/env';
import { regEx } from '../util/regex';
import { addSecretForSanitizing } from '../util/sanitize';
import { ensureTrailingSlash, parseUrl, trimSlashes } from '../util/url';
import { tryDecryptKbPgp } from './decrypt/kbpgp';
import { tryDecryptGnupg } from './decrypt/gnupg';
import {
tryDecryptPublicKeyDefault,
tryDecryptPublicKeyPKCS1,
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function tryDecrypt(
const decryptedObjStr =
getEnv().RENOVATE_X_USE_OPENPGP === 'true'
? await tryDecryptOpenPgp(key, encryptedStr)
: await tryDecryptKbPgp(key, encryptedStr);
: await tryDecryptGnupg(key, encryptedStr);
if (decryptedObjStr) {
decryptedStr = validateDecryptedValue(decryptedObjStr, repository);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,88 @@
import { codeBlock } from 'common-tags';
import { CONFIG_VALIDATION } from '../../constants/error-messages';
import { decryptConfig, setPrivateKeys } from '../decrypt';
import { GlobalConfig } from '../global';
import type { RenovateConfig } from '../types';
import { tryDecryptKbPgp } from './kbpgp';
import { tryDecryptGnupg } from './gnupg';
import { Fixtures } from '~test/fixtures';
import { logger } from '~test/util';

const privateKey = Fixtures.get('private-pgp.pem', '..');
const privateKeyEcc = codeBlock`
-----BEGIN PGP PRIVATE KEY BLOCK-----

lFgEaIInBxYJKwYBBAHaRw8BAQdA3sIP1X2sD3ZhqCfsDK8XxYcIXWX69X/3/GNx
5CaBOoEAAQDDWad/QZsw8kb+Mgay806FAAz0UAgxAlZWUSavqp5zxA4RtDdXaGl0
ZVNvdXJjZSBSZW5vdmF0ZSA8cmVub3ZhdGVAd2hpdGVzb3VyY2Vzb2Z0d2FyZS5j
b20+iJMEExYKADsWIQT2bRiAlIgt3xD8h1s7X4hIOZTAnAUCaIInBwIbAwULCQgH
AgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRA7X4hIOZTAnNsAAQCZEdlHC7bVp0jX
bleru7PkdWHLJMrM3xrsiYgmOhvMNAD/dMnoeuUq2JpTMOTGouTsFkY5yq+ue672
/VaWKUAgSwGcXQRogicHEgorBgEEAZdVAQUBAQdASRmOaEd461jnRjjMNYfPPU3t
zwgd1afFG+Yp9w7+yA8DAQgHAAD/Rk411EVr2OoJf6Xd0zoIs8E/VeZIJftG0bsY
HkRtD0gPk4h4BBgWCgAgFiEE9m0YgJSILd8Q/IdbO1+ISDmUwJwFAmiCJwcCGwwA
CgkQO1+ISDmUwJzUuAD/dHGdjs4fR3PsbjnR7Xi5j0LcOE5Q9dMjvSFQ9fBJzesB
ANU4vFbrMABZjdOelzYSj+Z/DRQ4UfK40J8qkQKekbYG
=iXr1
-----END PGP PRIVATE KEY BLOCK-----
`;
const repository = 'abc/def';

describe('config/decrypt/kbpgp', () => {
vi.unmock('../../util/exec/common');

describe('config/decrypt/gnupg', () => {
describe('decryptConfig()', () => {
let config: RenovateConfig;

beforeEach(() => {
config = {};
GlobalConfig.reset();
setPrivateKeys(undefined, undefined);
// reset();
});

it('returns null for invalid key', async () => {
expect(
await tryDecryptKbPgp(
await tryDecryptGnupg(
'invalid-key',
'wcFMAw+4H7SgaqGOAQ/+Lz6RlbEymbnmMhrktuaGiDPWRNPEQFuMRwwYM6/B/r0JMZa9tskAA5RpyYKxGmJJeuRtlA8GkTw02GoZomlJf/KXJZ95FwSbkXMSRJRD8LJ2402Hw2TaOTaSvfamESnm8zhNo8cok627nkKQkyrpk64heVlU5LIbO2+UgYgbiSQjuXZiW+QuJ1hVRjx011FQgEYc59+22yuKYqd8rrni7TrVqhGRlHCAqvNAGjBI4H7uTFh0sP4auunT/JjxTeTkJoNu8KgS/LdrvISpO67TkQziZo9XD5FOzSN7N3e4f8vO4N4fpjgkIDH/9wyEYe0zYz34xMAFlnhZzqrHycRqzBJuMxGqlFQcKWp9IisLMoVJhLrnvbDLuwwcjeqYkhvODjSs7UDKwTE4X4WmvZr0x4kOclOeAAz/pM6oNVnjgWJd9SnYtoa67bZVkne0k6mYjVhosie8v8icijmJ4OyLZUGWnjZCRd/TPkzQUw+B0yvsop9FYGidhCI+4MVx6W5w7SRtCctxVfCjLpmU4kWaBUUJ5YIQ5xm55yxEYuAsQkxOAYDCMFlV8ntWStYwIG1FsBgJX6VPevXuPPMjWiPNedIpJwBH2PLB4blxMfzDYuCeaIqU4daDaEWxxpuFTTK9fLdJKuipwFG6rwE3OuijeSN+2SLszi834DXtUjQdikHSTQG392+oTmZCFPeffLk/OiV2VpdXF3gGL7sr5M9hOWIZ783q0vW1l6nAElZ7UA//kW+L6QRxbnBVTJK5eCmMY6RJmL76zjqC1jQ0FC10',
),
).toBeNull();

expect(logger.logger.debug).toHaveBeenCalledWith(
expect.stringMatching(/^Private key import failed: Command failed/),
);
});

it('works broken PGP message', async () => {
expect(
await tryDecryptGnupg(
privateKey,
'wcFMAw+4H7SgaqGOAQ/+Lz6RlbEymbnmMhrktuaGiDPWRNPEQFuMRwwYM6/B/r0JMZa9tskAA5RpyYKxGmJJeuRtlA8GkTw02GoZomlJf/KXJZ95FwSbkXMSRJRD8LJ2402Hw2TaOTaSvfamESnm8zhNo8cok627nkKQkyrpk64heVlU5LIbO2+UgYgbiSQjuXZiW+QuJ1hVRjx011FQgEYc59+22yuKYqd8rrni7TrVqhGRlHCAqvNAGjBI4H7uTFh0sP4auunT/JjxTeTkJoNu8KgS/LdrvISpO67TkQziZo9XD5FOzSN7N3e4f8vO4N4fpjgkIDH/9wyEYe0zYz34xMAFlnhZzqrHycRqzBJuMxGqlFQcKWp9IisLMoVJhLrnvbDLuwwcjeqYkhvODjSs7UDKwTE4X4WmvZr0x4kOclOeAAz/pM6oNVnjgWJd9SnYtoa67bZVkne0k6mYjVhosie8v8icijmJ4OyLZUGWnjZCRd/TPkzQUw+B0yvsop9FYGidhCI+4MVx6W5w7SRtCctxVfCjLpmU4kWaBUUJ5YIQ5xm55yxEYuAsQkxOAYDCMFlV8ntWStYwIG1FsBgJX6VPevXuPPMjWiPNedIpJwBH2PLB4blxMfzDYuCeaIqU4daDaEWxxpuFTTK9fLdJKuipwFG6rwE3OuijeSN+2SLszi834DXtUjQdikHSTQG392+oTmZCFPeffLk/OiV2VpdXF3gGL7sr5M9hOWIZ783q0vW1l6nAElZ7UA//kW+L6QRxbnBVTJK5eCmMY6RJmL76zjqC1jQ0FC10',
),
).toBe('{"o":"abc","r":"","v":"123"}');

expect(logger.logger.debug).toHaveBeenCalledWith(
expect.objectContaining({
stdout: '',
stderr: expect.any(String),
}),
'Private key import result',
);

expect(logger.logger.debug).toHaveBeenCalledWith(
expect.stringMatching(/^Decryption failed, but stdout is available: /),
);
});

it('works with ECC and AEAD', async () => {
expect(
await tryDecryptGnupg(
privateKeyEcc,
'hF4DdO67WRkDWjwSAQdAmRs+snKu04B3aKLNCF1ePqnXDQskj/Mj+neZbd0ucQgw' +
'TvchqMgVWv20RqhLKEdhyCp/iqnhCzDTRpbPyqjqPZ49kxDZqq9EhwvmBldiSBb5' +
'1F0BCQIQsycgt62mxOWtYITs3GGBnDS5s7iMxbxgOg5BlEMu2EQvgxvGETdz6n76' +
'h7t+FpU4y1ljrsNSLY36QPD4Jg2cGR48vMLVnPS6+eg3gFz3WfP5BAX3c6jQIOA=' +
'=C3oS',
),
).toBe('{"o":"abc","r":"","v":"123"}');
});

it('rejects invalid PGP message', async () => {
Expand Down
72 changes: 72 additions & 0 deletions lib/config/decrypt/gnupg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os from 'node:os';
import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is';
import { mkdtemp, outputFile, rm } from 'fs-extra';
import { quote } from 'shlex';
import upath from 'upath';
import { logger } from '../../logger';
import { exec } from '../../util/exec';

const keyImported = new Set<string>();

export async function tryDecryptGnupg(
privateKey: string,
encryptedStr: string,
): Promise<string | null> {
const tmpDir = await mkdtemp(upath.join(os.tmpdir(), 'renovate-gpg-'));

if (!keyImported.has(privateKey)) {
try {
const keyFilePath = upath.join(tmpDir, 'key.pem');
await outputFile(keyFilePath, privateKey);
const { stdout, stderr } = await exec(
`gpg --batch --no-tty --yes --import ${quote(keyFilePath)}`,
);
keyImported.add(privateKey);

logger.debug({ stdout, stderr }, 'Private key import result');
} catch (err) {
logger.debug(`Private key import failed: ${err.message}`);
// cleanup temp dir
await rm(tmpDir, { recursive: true, force: true });
return null;
}
}
try {
const startBlock = '-----BEGIN PGP MESSAGE-----\n\n';
const endBlock = '\n-----END PGP MESSAGE-----\n';
let armoredMessage = encryptedStr.trim();
if (!armoredMessage.startsWith(startBlock)) {
armoredMessage = `${startBlock}${armoredMessage}`;
}
if (!armoredMessage.endsWith(endBlock)) {
armoredMessage = `${armoredMessage}${endBlock}`;
}
const encryptedFilePath = upath.join(tmpDir, 'msg.pem');
await outputFile(encryptedFilePath, armoredMessage);

const { stdout, stderr } = await exec(
`gpg --batch --no-tty --yes --decrypt ${quote(encryptedFilePath)}`,
);

logger.debug({ stderr }, 'Decrypted config using gnupg');
return stdout;
} catch (err) {
if (
'exitCode' in err &&
err.exitCode === 2 &&
isNonEmptyStringAndNotWhitespace(err.stdout)
) {
// gpg returns exit code 2 when it cannot fully decrypt the message, but stdout may contain what we need
logger.debug(
`Decryption failed, but stdout is available: ${err.message}`,
);
return err.stdout;
}
logger.debug(`Decryption failed using gnupg: ${err.message}`);
return null;
/* v8 ignore next -- coverage bug */
} finally {
// cleanup temp dir
await rm(tmpDir, { recursive: true, force: true });
}
}
63 changes: 0 additions & 63 deletions lib/config/decrypt/kbpgp.ts

This file was deleted.

2 changes: 1 addition & 1 deletion lib/util/git/private-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract class PrivateKey {
protected abstract importKey(): Promise<string | undefined>;
}

class GPGKey extends PrivateKey {
export class GPGKey extends PrivateKey {
protected readonly gpgFormat = 'openpgp';

constructor(key: string) {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@
"@pnpm/parse-overrides": "1001.0.0",
"@qnighy/marshal": "0.1.3",
"@renovatebot/detect-tools": "1.1.0",
"@renovatebot/kbpgp": "4.0.1",
"@renovatebot/osv-offline": "1.6.8",
"@renovatebot/pep440": "4.1.0",
"@renovatebot/ruby-semver": "4.0.0",
Expand Down
Loading