Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
71 changes: 71 additions & 0 deletions lib/config/decrypt/gnupg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import os from 'node:os';
import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is';
import { mkdtemp, outputFile, rm } from 'fs-extra';
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 ${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 ${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",
Copy link
Contributor

@jamietanna jamietanna Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth then deprecating this library? Looks like it's only Renovate using it: https://deps.dev/npm/%40renovatebot%2Fkbpgp/4.0.3/dependents

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, but i'm thinking of using https://github.com/renovatebot/pgp (dotnet wasm variant) instead of the cli 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've raised renovatebot/kbpgp#216 for the deprecation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking of using https://github.com/renovatebot/pgp (dotnet wasm variant) instead of the cli

Sounds like a plan - is that something that needs much validation ahead of next week's major release?

"@renovatebot/osv-offline": "1.6.8",
"@renovatebot/pep440": "4.1.0",
"@renovatebot/ruby-semver": "4.0.0",
Expand Down
Loading