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
13 changes: 13 additions & 0 deletions pg/src/main/java/org/bouncycastle/bcpg/S2K.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pg/src/test/java/org/bouncycastle/bcpg/test/AllTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.<init>
* 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"));
}
}
}
Binary file not shown.