From e46b07a4ddf1898caf1fb399e53164c27e420d93 Mon Sep 17 00:00:00 2001 From: subbudvk <115633743+subbudvk@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:58:47 +0530 Subject: [PATCH 1/4] Validate bounds check on Argon2 memorySizeExponent in S2K wire parser --- pg/src/main/java/org/bouncycastle/bcpg/S2K.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java index 772f10bd09..0db742d6c9 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java @@ -50,6 +50,14 @@ public class S2K { private static final int EXPBIAS = 6; + // Cap memorySizeExponent read from untrusted wire data. + // Default 21 matches RFC 9580 recommended level (2 GiB) and GnuPG defaults. + // Lower this on heap-constrained deployments: + // -Dorg.bouncycastle.openpgp.argon2.max_memory_exp=N (1 <= N <= 30) + private static final int MAX_ARGON2_MEMORY_EXP = + Math.min(30, Math.max(1, + Integer.getInteger("org.bouncycastle.openpgp.argon2.max_memory_exp", 21))); + /** * Simple key generation. A single non-salted iteration of a hash function. * This method is deprecated to use, since it can be brute-forced when used @@ -157,6 +165,11 @@ public class S2K passes = dIn.read(); parallelism = dIn.read(); memorySizeExponent = dIn.read(); + if (memorySizeExponent < 1 || memorySizeExponent > MAX_ARGON2_MEMORY_EXP) + { + throw new IOException("Argon2 memorySizeExponent out of safe range: " + memorySizeExponent + + " (max=" + MAX_ARGON2_MEMORY_EXP + ")"); + } break; case GNU_DUMMY_S2K: From b6e666be1a4f021e631789633c53a9cc96a361f1 Mon Sep 17 00:00:00 2001 From: subbudvk <115633743+subbudvk@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:59:58 +0530 Subject: [PATCH 2/4] add bounds check on Argon2 memorySizeExponent in S2K wire parser --- pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java b/pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java index 6b48a2d86b..4a80ecfe76 100644 --- a/pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java +++ b/pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java @@ -54,6 +54,7 @@ public static Test suite() TestSuite suite = new TestSuite("OpenPGP Packet Tests"); suite.addTestSuite(AllTests.class); + suite.addTest(new junit.framework.JUnit4TestAdapter(Argon2S2KMemExpPocTest.class)); return new BCPacketTests(suite); } From fc87865aec9a8cebd678cef12bf456501944e719 Mon Sep 17 00:00:00 2001 From: subbudvk <115633743+subbudvk@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:01:12 +0530 Subject: [PATCH 3/4] Validate bounds check on Argon2 memorySizeExponent in S2K wire parser --- .../bcpg/test/Argon2S2KMemExpPocTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 pg/src/test/java/org/bouncycastle/bcpg/test/Argon2S2KMemExpPocTest.java diff --git a/pg/src/test/java/org/bouncycastle/bcpg/test/Argon2S2KMemExpPocTest.java b/pg/src/test/java/org/bouncycastle/bcpg/test/Argon2S2KMemExpPocTest.java new file mode 100644 index 0000000000..90e71b7c91 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/bcpg/test/Argon2S2KMemExpPocTest.java @@ -0,0 +1,75 @@ +package org.bouncycastle.bcpg.test; + +import java.io.IOException; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; +import org.junit.Assert; +import org.junit.Test; + +/** + * PGP S2K Argon2 memorySizeExponent OOM. + * + * poc_argon2_s2k.pgp is a crafted 24-byte PGP SKESK v4 packet with + * memorySizeExponent=22 (4 GiB). Without the fix, S2K wire parsing + * (S2K.java) accepted any byte value (0-255), allowing + * Argon2BytesGenerator.init() to allocate 4,194,304 Block objects + * (each long[128] = 1 KB) => OutOfMemoryError. + * + * Stack trace (pre-fix): + * java.lang.OutOfMemoryError: Java heap space + * at Argon2BytesGenerator$Block. + * at Argon2BytesGenerator.init + * + * Fix: wire parser enforces MAX_ARGON2_MEMORY_EXP (default 21, configurable via + * -Dorg.bouncycastle.openpgp.argon2.max_memory_exp). The crafted packet + * (memExp=22) is rejected at parse time with IOException => no memory allocated. + */ +public class Argon2S2KMemExpPocTest { + + /** + * Confirms the pre-fix behaviour: passing memExp=22 directly to + * Argon2BytesGenerator (bypassing the wire parser) causes OutOfMemoryError. + * Reproduces the vulnerability without the wire-parse guard. + */ + @Test + public void withoutFix_craftedPacketCausesOom() { + Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) + .withSalt(new byte[16]) + .withIterations(1) + .withParallelism(1) + .withMemoryPowOfTwo(22) // 1 << 22 KiB = 4 GiB, no guard + .withVersion(Argon2Parameters.ARGON2_VERSION_13) + .build(); + + try { + new Argon2BytesGenerator().init(params); // 4,194,304 × 1 KB blocks => OOM + Assert.fail("expected OutOfMemoryError"); + } catch (OutOfMemoryError e) { + // confirmed: heap exhausted before any key derivation runs + } + } + + /** + * Fix verification — the same crafted packet that caused OOM is now + * rejected at wire-parse time with IOException. No Argon2 memory is allocated. + * + * Default cap: 21 (configurable via -Dorg.bouncycastle.openpgp.argon2.max_memory_exp). + * poc_argon2_s2k.pgp has memorySizeExponent=22, which exceeds the default cap. + */ + @Test + public void withFix_samePacketThrowsIOExceptionNotOom() throws Exception { + BCPGInputStream pgpIn = new BCPGInputStream( + getClass().getResourceAsStream("poc_argon2_s2k.pgp")); + + try { + pgpIn.readPacket(); // throws IOException at parse time, no Argon2 allocation + Assert.fail("expected IOException for memorySizeExponent=22"); + } catch (IOException e) { + Assert.assertTrue( + "expected message to contain 'memorySizeExponent', got: " + e.getMessage(), + e.getMessage().contains("memorySizeExponent")); + } + } +} From 85b8a638cc0f7aa51667f22e0aa0d7d1e3e71363 Mon Sep 17 00:00:00 2001 From: subbudvk <115633743+subbudvk@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:06:20 +0530 Subject: [PATCH 4/4] Validate bounds check on Argon2 memorySizeExponent in S2K wire parser --- .../org/bouncycastle/bcpg/test/poc_argon2_s2k.pgp | Bin 0 -> 24 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pg/src/test/java/org/bouncycastle/bcpg/test/poc_argon2_s2k.pgp diff --git a/pg/src/test/java/org/bouncycastle/bcpg/test/poc_argon2_s2k.pgp b/pg/src/test/java/org/bouncycastle/bcpg/test/poc_argon2_s2k.pgp new file mode 100644 index 0000000000000000000000000000000000000000..2dcd0dba7293b0aac5befd619c7f83e7f5da0d23 GIT binary patch literal 24 RcmeAXW8q|BKm&}7VgL}b0L%aY literal 0 HcmV?d00001