diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d02fedf3f3e707250bcf58cf9e9f56f4e6aee547..b04a619e166e801ff8f629c867ed27ea8f79a855 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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/
diff --git a/jest.config.ts b/jest.config.ts
index cf59e0deb91a898f5d70b9d011921a317ec8df2a..9aa56f29293e1be49cdf4120f8c4a2f82499336f 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -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';
@@ -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
@@ -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;
+}
diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts
index a9706d97805cfb9e6491d0cddef9a224fcf49c83..127301308e691a9038bf7bd4cf2122ba47b58849 100644
--- a/lib/config/decrypt.ts
+++ b/lib/config/decrypt.ts
@@ -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,
@@ -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);
     }
diff --git a/lib/config/decrypt/legacy.ts b/lib/config/decrypt/legacy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..caa1e8d4058824af3c7da62f45b1c1c92aeb02ac
--- /dev/null
+++ b/lib/config/decrypt/legacy.ts
@@ -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;
+}
diff --git a/lib/config/decrypt/openpgp.ts b/lib/config/decrypt/openpgp.ts
new file mode 100644
index 0000000000000000000000000000000000000000..570b0d696ea7bb6ce0f8e7d178ae24a0dbf72047
--- /dev/null
+++ b/lib/config/decrypt/openpgp.ts
@@ -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;
+  }
+}
diff --git a/package.json b/package.json
index a516b6aab8f6589729b83b38c8bb42daebf8be0f..567613ebe0afc5d60a4d96b498ebeccd41ea615a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tools/jest.mjs b/tools/jest.mjs
deleted file mode 100644
index 9a2c8830eb5a97ff7a3bf495a7cbabb6eaabdc10..0000000000000000000000000000000000000000
--- a/tools/jest.mjs
+++ /dev/null
@@ -1,32 +0,0 @@
-import { spawnSync } from 'node:child_process';
-import { argv, env, version } from 'node:process';
-import semver from 'semver';
-
-// needed for tests
-env.GIT_ALLOW_PROTOCOL = 'file';
-// reduce logging
-env.LOG_LEVEL = 'fatal';
-
-const args = ['--experimental-vm-modules'];
-
-/*
- * openpgp encryption is broken because it needs PKCS#1 v1.5
- * - #27375
- * - https://nodejs.org/en/blog/vulnerability/february-2024-security-releases#nodejs-is-vulnerable-to-the-marvin-attack-timing-variant-of-the-bleichenbacher-attack-against-pkcs1-v15-padding-cve-2023-46809---medium
- *
- * Sadly there is no way to suppress this warning: `SECURITY WARNING: Reverting CVE-2023-46809: Marvin attack on PKCS#1 padding`
- */
-if (semver.satisfies(version, '^18.19.1 || ^20.11.1 || >=21.6.2')) {
-  args.push('--security-revert=CVE-2023-46809');
-}
-
-args.push('node_modules/jest/bin/jest.js', '--logHeapUsage');
-
-// add other args after `node tools/jest.mjs`
-args.push(...argv.slice(2));
-
-const res = spawnSync('node', args, { stdio: 'inherit', env });
-
-if (res.status !== null && res.status !== 0) {
-  process.exit(res.status);
-}