Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -134,7 +134,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 @@ -56,7 +56,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, passphrase: string | undefined) {
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.2",
"@qnighy/marshal": "0.1.3",
"@renovatebot/detect-tools": "1.1.0",
"@renovatebot/kbpgp": "4.0.1",
"@renovatebot/osv-offline": "1.6.9",
"@renovatebot/pep440": "4.2.0",
"@renovatebot/ruby-semver": "4.1.0",
Expand Down
Loading
Loading