Skip to content

Commit 452ade3

Browse files
authored
feat(config/encrypt): replace pgp library (#28312)
1 parent 61ee26f commit 452ade3

File tree

7 files changed

+353
-2
lines changed

7 files changed

+353
-2
lines changed

docs/usage/self-hosted-experimental.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ Don't combine with `redisUrl`, Redis would be preferred over SQlite.
185185

186186
Suppress the pre-commit support warning in PR bodies.
187187

188+
## `RENOVATE_X_USE_OPENPGP`
189+
190+
Use `openpgp` instead of `kbpgp` for `PGP` decryption.
191+
188192
## `RENOVATE_X_YARN_PROXY`
189193

190194
Configure global Yarn proxy settings if HTTP proxy environment variables are detected.

lib/config/decrypt.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { maskToken } from '../util/mask';
44
import { regEx } from '../util/regex';
55
import { addSecretForSanitizing } from '../util/sanitize';
66
import { ensureTrailingSlash } from '../util/url';
7+
import { tryDecryptKbPgp } from './decrypt/kbpgp';
78
import {
89
tryDecryptPublicKeyDefault,
910
tryDecryptPublicKeyPKCS1,
@@ -21,7 +22,10 @@ export async function tryDecrypt(
2122
): Promise<string | null> {
2223
let decryptedStr: string | null = null;
2324
if (privateKey?.startsWith('-----BEGIN PGP PRIVATE KEY BLOCK-----')) {
24-
const decryptedObjStr = await tryDecryptOpenPgp(privateKey, encryptedStr);
25+
const decryptedObjStr =
26+
process.env.RENOVATE_X_USE_OPENPGP === 'true'
27+
? await tryDecryptOpenPgp(privateKey, encryptedStr)
28+
: await tryDecryptKbPgp(privateKey, encryptedStr);
2529
if (decryptedObjStr) {
2630
decryptedStr = validateDecryptedValue(decryptedObjStr, repository);
2731
}

lib/config/decrypt/kbpgp.spec.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Fixtures } from '../../../test/fixtures';
2+
import { CONFIG_VALIDATION } from '../../constants/error-messages';
3+
import { decryptConfig } from '../decrypt';
4+
import { GlobalConfig } from '../global';
5+
import type { RenovateConfig } from '../types';
6+
import { tryDecryptKbPgp } from './kbpgp';
7+
8+
const privateKey = Fixtures.get('private-pgp.pem', '..');
9+
const repository = 'abc/def';
10+
11+
describe('config/decrypt/kbpgp', () => {
12+
describe('decryptConfig()', () => {
13+
let config: RenovateConfig;
14+
15+
beforeEach(() => {
16+
config = {};
17+
GlobalConfig.reset();
18+
});
19+
20+
it('returns null for invalid key', async () => {
21+
expect(
22+
await tryDecryptKbPgp(
23+
'invalid-key',
24+
'wcFMAw+4H7SgaqGOAQ/+Lz6RlbEymbnmMhrktuaGiDPWRNPEQFuMRwwYM6/B/r0JMZa9tskAA5RpyYKxGmJJeuRtlA8GkTw02GoZomlJf/KXJZ95FwSbkXMSRJRD8LJ2402Hw2TaOTaSvfamESnm8zhNo8cok627nkKQkyrpk64heVlU5LIbO2+UgYgbiSQjuXZiW+QuJ1hVRjx011FQgEYc59+22yuKYqd8rrni7TrVqhGRlHCAqvNAGjBI4H7uTFh0sP4auunT/JjxTeTkJoNu8KgS/LdrvISpO67TkQziZo9XD5FOzSN7N3e4f8vO4N4fpjgkIDH/9wyEYe0zYz34xMAFlnhZzqrHycRqzBJuMxGqlFQcKWp9IisLMoVJhLrnvbDLuwwcjeqYkhvODjSs7UDKwTE4X4WmvZr0x4kOclOeAAz/pM6oNVnjgWJd9SnYtoa67bZVkne0k6mYjVhosie8v8icijmJ4OyLZUGWnjZCRd/TPkzQUw+B0yvsop9FYGidhCI+4MVx6W5w7SRtCctxVfCjLpmU4kWaBUUJ5YIQ5xm55yxEYuAsQkxOAYDCMFlV8ntWStYwIG1FsBgJX6VPevXuPPMjWiPNedIpJwBH2PLB4blxMfzDYuCeaIqU4daDaEWxxpuFTTK9fLdJKuipwFG6rwE3OuijeSN+2SLszi834DXtUjQdikHSTQG392+oTmZCFPeffLk/OiV2VpdXF3gGL7sr5M9hOWIZ783q0vW1l6nAElZ7UA//kW+L6QRxbnBVTJK5eCmMY6RJmL76zjqC1jQ0FC10',
25+
),
26+
).toBeNull();
27+
});
28+
29+
it('rejects invalid PGP message', async () => {
30+
GlobalConfig.set({ privateKey });
31+
config.encrypted = {
32+
token:
33+
'long-but-wrong-wcFMAw+4H7SgaqGOAQ//ZNPgHJ4RQBdfFoDX8Ywe9UxqMlc8k6VasCszQ2JULh/BpEdKdgRUGNaKaeZ+oBKYDBmDwAD5V5FEMlsg+KO2gykp/p2BAwvKGtYK0MtxLh4h9yJbN7TrVnGO3/cC+Inp8exQt0gD6f1Qo/9yQ9NE4/BIbaSs2b2DgeIK7Ed8N675AuSo73UOa6o7t+9pKeAAK5TQwgSvolihbUs8zjnScrLZD+nhvL3y5gpAqK9y//a+bTu6xPA1jdLjsswoCUq/lfVeVsB2GWV2h6eex/0fRKgN7xxNgdMn0a7msrvumhTawP8mPisPY2AAsHRIgQ9vdU5HbOPdGoIwI9n9rMdIRn9Dy7/gcX9Ic+RP2WwS/KnPHLu/CveY4W5bYqYoikWtJs9HsBCyWFiHIRrJF+FnXwtKdoptRfxTfJIkBoLrV6fDIyKo79iL+xxzgrzWs77KEJUJfexZBEGBCnrV2o7mo3SU197S0qx7HNvqrmeCj8CLxq8opXC71TNa+XE6BQUVyhMFxtW9LNxZUHRiNzrTSikArT4hzjyr3f9cb0kZVcs6XJQsm1EskU3WXo7ETD7nsukS9GfbwMn7tfYidB/yHSHl09ih871BcgByDmEKKdmamcNilW2bmTAqB5JmtaYT5/H8jRQWo/VGrEqlmiA4KmwSv7SZPlDnaDFrmzmMZZDSRgHe5KWl283XLmSeE8J0NPqwFH3PeOv4fIbOjJrnbnFBwSAsgsMe2K4OyFDh2COfrho7s8EP1Kl5lBkYJ+VRreGRerdSu24',
34+
};
35+
await expect(decryptConfig(config, repository)).rejects.toThrow(
36+
CONFIG_VALIDATION,
37+
);
38+
config.encrypted = {
39+
// Missing value
40+
token:
41+
'wcFMAw+4H7SgaqGOAQ//ZNPgHJ4RQBdfFoDX8Ywe9UxqMlc8k6VasCszQ2JULh/BpEdKdgRUGNaKaeZ+oBKYDBmDwAD5V5FEMlsg+KO2gykp/p2BAwvKGtYK0MtxLh4h9yJbN7TrVnGO3/cC+Inp8exQt0gD6f1Qo/9yQ9NE4/BIbaSs2b2DgeIK7Ed8N675AuSo73UOa6o7t+9pKeAAK5TQwgSvolihbUs8zjnScrLZD+nhvL3y5gpAqK9y//a+bTu6xPA1jdLjsswoCUq/lfVeVsB2GWV2h6eex/0fRKgN7xxNgdMn0a7msrvumhTawP8mPisPY2AAsHRIgQ9vdU5HbOPdGoIwI9n9rMdIRn9Dy7/gcX9Ic+RP2WwS/KnPHLu/CveY4W5bYqYoikWtJs9HsBCyWFiHIRrJF+FnXwtKdoptRfxTfJIkBoLrV6fDIyKo79iL+xxzgrzWs77KEJUJfexZBEGBCnrV2o7mo3SU197S0qx7HNvqrmeCj8CLxq8opXC71TNa+XE6BQUVyhMFxtW9LNxZUHRiNzrTSikArT4hzjyr3f9cb0kZVcs6XJQsm1EskU3WXo7ETD7nsukS9GfbwMn7tfYidB/yHSHl09ih871BcgByDmEKKdmamcNilW2bmTAqB5JmtaYT5/H8jRQWo/VGrEqlmiA4KmwSv7SZPlDnaDFrmzmMZZDSRgHe5KWl283XLmSeE8J0NPqwFH3PeOv4fIbOjJrnbnFBwSAsgsMe2K4OyFDh2COfrho7s8EP1Kl5lBkYJ+VRreGRerdSu24',
42+
};
43+
await expect(decryptConfig(config, repository)).rejects.toThrow(
44+
CONFIG_VALIDATION,
45+
);
46+
config.encrypted = {
47+
// Missing org scope
48+
token:
49+
'wcFMAw+4H7SgaqGOAQ//W38A3PmaZnE9XTCHGDQFD52Kz78UYnaiYeAT13cEqYWTwEvQ57B7D7I6i4jCLe7KwkUCS90kyoqd7twD75W/sO70MyIveKnMlqqnpkagQkFgmzMaXXNHaJXEkjzsflTELZu6UsUs/kZYmab7r14YLl9HbH/pqN9exil/9s3ym9URCPOyw/l04KWntdMAy0D+c5M4mE+obv6fz6nDb8tkdeT5Rt2uU+qw3gH1OsB2yu+zTWpI/xTGwDt5nB5txnNTsVrQ/ZK85MSktacGVcYuU9hsEDmSrShmtqlg6Myq+Hjb7cYAp2g4n13C/I3gGGaczl0PZaHD7ALMjI7p6O1q+Ix7vMxipiKMVjS3omJoqBCz3FKc6DVhyX4tfhxgLxFo0DpixNwGbBRbMBO8qZfUk7bicAl/oCRc2Ijmay5DDYuvtkw3G3Ou+sZTe6DNpWUFy6VA4ai7hhcLvcAuiYmLdwPISRR/X4ePa8ZrmSVPyVOvbmmwLhcDYSDlC9Mw4++7ELomlve5kvjVSHvPv9BPVb5sJF7gX4vOT4FrcKalQRPmhNCZrE8tY2lvlrXwV2EEhya8EYv4QTd3JUYEYW5FXiJrORK5KDTnISw+U02nFZjFlnoz9+R6h+aIT1crS3/+YjCHE/EIKvSftOnieYb02Gk7M9nqU19EYL9ApYw4+IjSRgFM3DShIrvuDwDkAwUfaq8mKtr9Vjg/r+yox//GKS3u3r4I3+dfCljA3OwskTPfbSD+huBk4mylIvaL5v8Fngxo979wiLw',
50+
};
51+
await expect(decryptConfig(config, repository)).rejects.toThrow(
52+
CONFIG_VALIDATION,
53+
);
54+
config.encrypted = {
55+
// Impossible to parse
56+
token:
57+
'wcFMAw+4H7SgaqGOAQ//Wa/gHgQdH7tj3LQdW6rWKjzmkYVKZW9EbexJExu4WLaMgEKodlRMilcqCKfQZpjzoiC31J8Ly/x6Soury+lQnLVbtIQ4KWa/uCIz4lXCpPpGNgN2jPfOmdwWBMOcXIT+BgAMxRu3rAmvTtunrkACJ3J92eYNwJhTzp2Azn9LpT7kHnZ64z2SPhbdUgMMhCBwBG5BPArPzF5fdaqa8uUSbKhY0GMiqPXq6Zeq+EBNoPc/RJp2urpYTknO+nRb39avKjihd9MCZ/1d3QYymbRj7SZC3LJhenVF0hil3Uk8TBASnGQiDmBcIXQFhJ0cxavXqKjx+AEALq+kTdwGu5vuE2+2B820/o3lAXR9OnJHr8GodJ2ZBpzOaPrQe5zvxL0gLEeUUPatSOwuLhdo/6+bRCl2wNz23jIjDEFFTmsLqfEHcdVYVTH2QqvLjnUYcCRRuM32vS4rCMOEe0l6p0CV2rk22UZDIPcxqXjKucxse2Sow8ATWiPoIw7zWj7XBLqUKHFnMpPV2dCIKFKBsOKYgLjF4BvKzZJyhmVEPgMcKQLYqeT/2uWDR77NSWH0Cyiwk9M3KbOIMmV3pWh9PiXk6CvumECELbJHYH0Mc+P//BnbDq2Ie9dHdmKhFgRyHU7gWvkPhic9BX36xyldPcnhTgr1XWRoVe0ETGLDPCcqrQ/SUQGrLiujSOgxGu2K/6LDJhi4IKz1/nf7FUSj5eTIDqQiSPP5pXDjlH7oYxXXrHI/aYOCZ5sBx7mOzlEcENIrYblCHO/CYMTWdCJ4Wrftqk7K/A=',
58+
};
59+
await expect(decryptConfig(config, repository)).rejects.toThrow(
60+
CONFIG_VALIDATION,
61+
);
62+
config.encrypted = {
63+
token: 'too-short',
64+
};
65+
await expect(decryptConfig(config, repository)).rejects.toThrow(
66+
CONFIG_VALIDATION,
67+
);
68+
});
69+
70+
it('handles PGP org constraint', async () => {
71+
GlobalConfig.set({ privateKey });
72+
config.encrypted = {
73+
token:
74+
'wcFMAw+4H7SgaqGOAQ/+Lz6RlbEymbnmMhrktuaGiDPWRNPEQFuMRwwYM6/B/r0JMZa9tskAA5RpyYKxGmJJeuRtlA8GkTw02GoZomlJf/KXJZ95FwSbkXMSRJRD8LJ2402Hw2TaOTaSvfamESnm8zhNo8cok627nkKQkyrpk64heVlU5LIbO2+UgYgbiSQjuXZiW+QuJ1hVRjx011FQgEYc59+22yuKYqd8rrni7TrVqhGRlHCAqvNAGjBI4H7uTFh0sP4auunT/JjxTeTkJoNu8KgS/LdrvISpO67TkQziZo9XD5FOzSN7N3e4f8vO4N4fpjgkIDH/9wyEYe0zYz34xMAFlnhZzqrHycRqzBJuMxGqlFQcKWp9IisLMoVJhLrnvbDLuwwcjeqYkhvODjSs7UDKwTE4X4WmvZr0x4kOclOeAAz/pM6oNVnjgWJd9SnYtoa67bZVkne0k6mYjVhosie8v8icijmJ4OyLZUGWnjZCRd/TPkzQUw+B0yvsop9FYGidhCI+4MVx6W5w7SRtCctxVfCjLpmU4kWaBUUJ5YIQ5xm55yxEYuAsQkxOAYDCMFlV8ntWStYwIG1FsBgJX6VPevXuPPMjWiPNedIpJwBH2PLB4blxMfzDYuCeaIqU4daDaEWxxpuFTTK9fLdJKuipwFG6rwE3OuijeSN+2SLszi834DXtUjQdikHSTQG392+oTmZCFPeffLk/OiV2VpdXF3gGL7sr5M9hOWIZ783q0vW1l6nAElZ7UA//kW+L6QRxbnBVTJK5eCmMY6RJmL76zjqC1jQ0FC10',
75+
};
76+
const res = await decryptConfig(config, repository);
77+
expect(res.encrypted).toBeUndefined();
78+
expect(res.token).toBe('123');
79+
await expect(decryptConfig(config, 'wrong/org')).rejects.toThrow(
80+
CONFIG_VALIDATION,
81+
);
82+
});
83+
84+
it('handles PGP multi-org constraint', async () => {
85+
GlobalConfig.set({ privateKey });
86+
config.encrypted = {
87+
token:
88+
'wcFMAw+4H7SgaqGOAQ//Yk4RTQoLEhO0TKxN2IUBrCi88ts+CG1SXKeL06sJ2qikN/3n2JYAGGKgkHRICfu5dOnsjyFdLJ1XWUrbsM3XgVWikMbrmzD1Xe7N5DsoZXlt4Wa9pZ+IkZuE6XcKKu9whIJ22ciEwCzFwDmk/CBshdCCVVQ3IYuM6uibEHn/AHQ8K15XhraiSzF6DbJpevs5Cy7b5YHFyE936H25CVnouUQnMPsirpQq3pYeMq/oOtV/m4mfRUUQ7MUxvtrwE4lq4hLjFu5n9rwlcqaFPl7I7BEM++1c9LFpYsP5mTS7hHCZ9wXBqER8fa3fKYx0bK1ihCpjP4zUkR7P/uhWDArXamv7gHX2Kj/Qsbegn7KjTdZlggAmaJl/CuSgCbhySy+E55g3Z1QFajiLRpQ5+RsWFDbbI08YEgzyQ0yNCaRvrkgo7kZ1D95rEGRfY96duOQbjzOEqtvYmFChdemZ2+f9Kh/JH1+X9ynxY/zYe/0p/U7WD3QNTYN18loc4aXiB1adXD5Ka2QfNroLudQBmLaJpJB6wASFfuxddsD5yRnO32NSdRaqIWC1x6ti3ZYJZ2RsNwJExPDzjpQTuMOH2jtpu3q7NHmW3snRKy2YAL2UjI0YdeKIlhc/qLCJt9MRcOxWYvujTMD/yGprhG44qf0jjMkJBu7NjuVIMONujabl9b7SUQGfO/t+3rMuC68bQdCGLlO8gf3hvtD99utzXphi6idjC0HKSW/9KzuMkm+syGmIAYq/0L3EFvpZ38uq7z8KzwFFQHI3sBA34bNEr5zpU5OMWg',
89+
};
90+
let res = await decryptConfig(config, repository);
91+
expect(res.encrypted).toBeUndefined();
92+
expect(res.token).toBe('123');
93+
res = await decryptConfig(config, 'def/ghi');
94+
expect(res.encrypted).toBeUndefined();
95+
expect(res.token).toBe('123');
96+
await expect(decryptConfig(config, 'wrong/org')).rejects.toThrow(
97+
CONFIG_VALIDATION,
98+
);
99+
});
100+
101+
it('handles PGP org/repo constraint', async () => {
102+
GlobalConfig.set({ privateKey });
103+
config.encrypted = {
104+
token:
105+
'wcFMAw+4H7SgaqGOAQ//Wp7N0PaDZp0uOdwsc1CuqAq0UPcq+IQdHyKpJs3tHiCecXBHogy4P+rY9nGaUrVneCr4HexuKGuyJf1yl0ZqFffAUac5PjF8eDvjukQGOUq4aBlOogJCEefnuuVxVJx+NRR5iF1P6v57bmI1c+zoqZI/EQB30KU6O1BsdGPLUA/+R3dwCZd5Mbd36s34eYBasqcY9/QbqFcpElXMEPMse3kMCsVXPbZ+UMjtPJiBPUmtJq+ifnu1LzDrfshusSQMwgd/QNk7nEsijiYKllkWhHTP6g7zigvJ46x0h6AYS108YiuK3B9XUhXN9m05Ac6KTEEUdRI3E/dK2dQuRkLjXC8wceQm4A19Gm0uHoMIJYOCbiVoBCH6ayvKbZWZV5lZ4D1JbDNGmKeIj6OX9XWEMKiwTx0Xe89V7BdJzwIGrL0TCLtXuYWZ/R2k+UuBqtgzr44BsBqMpKUA0pcGBoqsEou1M05Ae9fJMF6ADezF5UQZPxT1hrMldiTp3p9iHGfWN2tKHeoW/8CqlIqg9JEkTc+Pl/L9E6ndy5Zjf097PvcmSGhxUQBE7XlrZoIlGhiEU/1HPMen0UUIs0LUu1ywpjCex2yTWnU2YmEwy0MQI1sekSr96QFxDDz9JcynYOYbqR/X9pdxEWyzQ+NJ3n6K97nE1Dj9Sgwu7mFGiUdNkf/SUAF0eZi/eXg71qumpMGBd4eWPtgkeMPLHjvMSYw9vBUfcoKFz6RJ4woG0dw5HOFkPnIjXKWllnl/o01EoBp/o8uswsIS9Nb8i+bp27U6tAHE',
106+
};
107+
const res = await decryptConfig(config, repository);
108+
expect(res.encrypted).toBeUndefined();
109+
expect(res.token).toBe('123');
110+
await expect(decryptConfig(config, 'abc/defg')).rejects.toThrow(
111+
CONFIG_VALIDATION,
112+
);
113+
});
114+
115+
it('handles PGP multi-org/repo constraint', async () => {
116+
GlobalConfig.set({ privateKey });
117+
config.encrypted = {
118+
token:
119+
'wcFMAw+4H7SgaqGOARAAibXL3zr0KZawiND868UGdPpGRo1aVZfn0NUBHpm8mXfgB1rBHaLsP7qa8vxDHpwH9DRD1IyB4vvPUwtu7wmuv1Vtr596tD40CCcCZYB5JjZLWRF0O0xaZFCOi7Z9SqqdaOQoMScyvPO+3/lJkS7zmLllJFH0mQoX5Cr+owUAMSWqbeCQ9r/KAXpnhmpraDjTav48WulcdTMc8iQ/DHimcdzHErLOAjtiQi4OUe1GnDCcN76KQ+c+ZHySnkXrYi/DhOOu9qB4glJ5n68NueFja+8iR39z/wqCI6V6TIUiOyjFN86iVyNPQ4Otem3KuNwrnwSABLDqP491eUNjT8DUDffsyhNC9lnjQLmtViK0EN2yLVpMdHq9cq8lszBChB7gobD9rm8nUHnTuLf6yJvZOj6toD5Yqj8Ibj58wN90Q8CUsBp9/qp0J+hBVUPOx4sT6kM2p6YarlgX3mrIW5c1U+q1eDbCddLjHiU5cW7ja7o+cqlA6mbDRu3HthjBweiXTicXZcRu1o/wy/+laQQ95x5FzAXDnOwQUHBmpTDI3tUJvQ+oy8XyBBbyC0LsBye2c2SLkPJ4Ai3IMR+Mh8puSzVywTbneiAQNBzJHlj5l85nCF2tUjvNo3dWC+9mU5sfXg11iEC6LRbg+icjpqRtTjmQURtciKDUbibWacwU5T/SVAGPXnW7adBOS0PZPIZQcSwjchOdOl0IjzBy6ofu7ODdn2CXZXi8zbevTICXsHvjnW4MAj5oXrStxK3LkWyM3YBOLe7sOfWvWz7n9TM3dHg032navQ',
120+
};
121+
let res = await decryptConfig(config, repository);
122+
expect(res.encrypted).toBeUndefined();
123+
expect(res.token).toBe('123');
124+
res = await decryptConfig(config, 'def/def');
125+
expect(res.encrypted).toBeUndefined();
126+
expect(res.token).toBe('123');
127+
await expect(decryptConfig(config, 'abc/defg')).rejects.toThrow(
128+
CONFIG_VALIDATION,
129+
);
130+
});
131+
});
132+
});

lib/config/decrypt/kbpgp.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import kbpgp from 'kbpgp';
2+
import { logger } from '../../logger';
3+
import { regEx } from '../../util/regex';
4+
5+
declare module 'kbpgp' {
6+
export class KeyManager {
7+
static import_from_armored_pgp(
8+
opts: { armored: string },
9+
cb: (err: Error, pk: KeyManager) => void,
10+
): void;
11+
}
12+
13+
// eslint-disable-next-line @typescript-eslint/no-namespace
14+
export namespace keyring {
15+
export class KeyRing {
16+
add_key_manager(pk: KeyManager): void;
17+
}
18+
}
19+
20+
export class Literal {
21+
toString(): string;
22+
}
23+
24+
export function unbox(
25+
opts: { keyfetch: keyring.KeyRing; armored: string },
26+
cb: (err: Error, literals: Literal[]) => void,
27+
): void;
28+
}
29+
30+
export async function tryDecryptKbPgp(
31+
privateKey: string,
32+
encryptedStr: string,
33+
): Promise<string | null> {
34+
if (encryptedStr.length < 500) {
35+
// optimization during transition of public key -> pgp
36+
return null;
37+
}
38+
try {
39+
const pk = await new Promise<kbpgp.KeyManager>((resolve, reject) => {
40+
kbpgp.KeyManager.import_from_armored_pgp(
41+
{
42+
armored: privateKey.replace(regEx(/\n[ \t]+/g), '\n'),
43+
},
44+
(err: Error, pk) => {
45+
if (err) {
46+
reject(err);
47+
} else {
48+
resolve(pk);
49+
}
50+
},
51+
);
52+
});
53+
54+
const ring = new kbpgp.keyring.KeyRing();
55+
ring.add_key_manager(pk);
56+
57+
const startBlock = '-----BEGIN PGP MESSAGE-----\n\n';
58+
const endBlock = '\n-----END PGP MESSAGE-----';
59+
let armoredMessage = encryptedStr.trim();
60+
if (!armoredMessage.startsWith(startBlock)) {
61+
armoredMessage = `${startBlock}${armoredMessage}`;
62+
}
63+
if (!armoredMessage.endsWith(endBlock)) {
64+
armoredMessage = `${armoredMessage}${endBlock}`;
65+
}
66+
67+
const data = await new Promise<kbpgp.Literal>((resolve, reject) => {
68+
kbpgp.unbox(
69+
{
70+
keyfetch: ring,
71+
armored: armoredMessage,
72+
},
73+
(err: Error, literals: any) => {
74+
if (err) {
75+
reject(err);
76+
} else {
77+
resolve(literals[0].toString());
78+
}
79+
},
80+
);
81+
});
82+
logger.debug('Decrypted config using kppgp');
83+
return data as string;
84+
} catch (err) {
85+
logger.debug({ err }, 'Could not decrypt using kppgp');
86+
return null;
87+
}
88+
}

lib/config/decrypt/openpgp.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ describe('config/decrypt/openpgp', () => {
1111
describe('decryptConfig()', () => {
1212
let config: RenovateConfig;
1313

14+
beforeAll(() => {
15+
process.env.RENOVATE_X_USE_OPENPGP = 'true';
16+
});
17+
1418
beforeEach(() => {
1519
jest.resetModules();
1620
config = {};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
"json-stringify-pretty-compact": "3.0.0",
213213
"json5": "2.2.3",
214214
"jsonata": "2.0.4",
215+
"kbpgp": "2.1.15",
215216
"klona": "2.0.6",
216217
"lru-cache": "10.2.0",
217218
"luxon": "3.4.4",

0 commit comments

Comments
 (0)