Skip to content

Commit 6335381

Browse files
authored
PBES2 decryption maximum iterations (#911)
Ensured there is an upper bound (maximum) iterations enforced for PBES2 decryption to help mitigate potential DoS attacks. Many thanks to Jingcheng Yang and Jianjun Chen from Sichuan University and Zhongguancun Lab for their work on this!
1 parent 2884eb7 commit 6335381

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ This release also:
6767
[Issue 901](https://github.com/jwtk/jjwt/issues/901).
6868
* Ensures that Secret JWKs for HMAC-SHA algorithms with `k` sizes larger than the algorithm minimum can
6969
be parsed/used as expected. See [Issue #905](https://github.com/jwtk/jjwt/issues/905)
70+
* Ensures there is an upper bound (maximum) iterations enforced for PBES2 decryption to help mitigate potential DoS
71+
attacks. Many thanks to Jingcheng Yang and Jianjun Chen from Sichuan University and Zhongguancun Lab for their
72+
work on this. See [PR 911](https://github.com/jwtk/jjwt/pull/911).
7073
* Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these!
7174

7275
### 0.12.3

impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package io.jsonwebtoken.impl.security;
1717

1818
import io.jsonwebtoken.JweHeader;
19+
import io.jsonwebtoken.UnsupportedJwtException;
1920
import io.jsonwebtoken.impl.DefaultJweHeader;
2021
import io.jsonwebtoken.impl.lang.Bytes;
2122
import io.jsonwebtoken.impl.lang.CheckedFunction;
23+
import io.jsonwebtoken.impl.lang.Parameter;
2224
import io.jsonwebtoken.impl.lang.ParameterReadable;
2325
import io.jsonwebtoken.impl.lang.RequiredParameterReader;
2426
import io.jsonwebtoken.lang.Assert;
@@ -50,11 +52,13 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm
5052
"[JWA RFC 7518, Section 4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2) " +
5153
"recommends password-based-encryption iterations be greater than or equal to " +
5254
MIN_RECOMMENDED_ITERATIONS + ". Provided: ";
55+
private static final double MAX_ITERATIONS_FACTOR = 2.5;
5356

5457
private final int HASH_BYTE_LENGTH;
5558
private final int DERIVED_KEY_BIT_LENGTH;
5659
private final byte[] SALT_PREFIX;
5760
private final int DEFAULT_ITERATIONS;
61+
private final int MAX_ITERATIONS;
5862
private final KeyAlgorithm<SecretKey, SecretKey> wrapAlg;
5963

6064
private static byte[] toRfcSaltPrefix(byte[] bytes) {
@@ -106,6 +110,7 @@ protected Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm<SecretKey, SecretK
106110
} else {
107111
DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS;
108112
}
113+
MAX_ITERATIONS = (int) (DEFAULT_ITERATIONS * MAX_ITERATIONS_FACTOR); // upper bound to help mitigate DoS attacks
109114

110115
// https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8, 2nd paragraph, last sentence:
111116
// "Their derived-key lengths respectively are 16, 24, and 32 octets." :
@@ -184,7 +189,16 @@ public SecretKey getDecryptionKey(DecryptionKeyRequest<Password> request) throws
184189
final Password key = Assert.notNull(request.getKey(), "Decryption Password cannot be null.");
185190
ParameterReadable reader = new RequiredParameterReader(header);
186191
final byte[] inputSalt = reader.get(DefaultJweHeader.P2S);
187-
final int iterations = reader.get(DefaultJweHeader.P2C);
192+
193+
Parameter<Integer> param = DefaultJweHeader.P2C;
194+
final int iterations = reader.get(param);
195+
if (iterations > MAX_ITERATIONS) {
196+
String msg = "JWE Header " + param + " value " + iterations + " exceeds " + getId() + " maximum " +
197+
"allowed value " + MAX_ITERATIONS + ". The larger value is rejected to help mitigate " +
198+
"potential Denial of Service attacks.";
199+
throw new UnsupportedJwtException(msg);
200+
}
201+
188202
final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt);
189203
final char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next:
190204
final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);

impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
package io.jsonwebtoken.impl.security
1717

1818
import io.jsonwebtoken.Jwts
19+
import io.jsonwebtoken.UnsupportedJwtException
1920
import io.jsonwebtoken.impl.DefaultJweHeaderMutator
2021
import io.jsonwebtoken.impl.DefaultMutableJweHeader
22+
import io.jsonwebtoken.io.Encoders
23+
import io.jsonwebtoken.lang.Strings
2124
import io.jsonwebtoken.security.KeyRequest
2225
import io.jsonwebtoken.security.Keys
2326
import io.jsonwebtoken.security.Password
@@ -50,6 +53,36 @@ class Pbes2HsAkwAlgorithmTest {
5053
}
5154
}
5255

56+
@Test
57+
void testExceedsMaxIterations() {
58+
for (Pbes2HsAkwAlgorithm alg : ALGS) {
59+
def password = Keys.password('correct horse battery staple'.toCharArray())
60+
def iterations = alg.MAX_ITERATIONS + 1
61+
// we make the JWE string directly from JSON here (instead of using Jwts.builder()) to avoid
62+
// the computational time it would take to create such JWEs with excessive iterations as well as
63+
// avoid the builder throwing any exceptions (and this is what a potential attacker would do anyway):
64+
def headerJson = """
65+
{
66+
"p2c": ${iterations},
67+
"p2s": "831BG_z_ZxkN7Rnt5v1iYm1A0bn6VEuxpW4gV7YBMoE",
68+
"alg": "${alg.id}",
69+
"enc": "A256GCM"
70+
}"""
71+
def jwe = Encoders.BASE64URL.encode(Strings.utf8(headerJson)) +
72+
'.OSAhMk3FtaCeZ5v1c8bWBgssEVqx2mCPUEnJUsg4hwIQyrUP-LCYkg.' +
73+
'K4R_-zb4qaZ3R0W8.sGS4mcT_xBhZC1d7G-g.kWqd_4sEsaKrWE_hMZ5HmQ'
74+
try {
75+
Jwts.parser().decryptWith(password).build().parse(jwe)
76+
} catch (UnsupportedJwtException expected) {
77+
String msg = "JWE Header 'p2c' (PBES2 Count) value ${iterations} exceeds ${alg.id} maximum allowed " +
78+
"value ${alg.MAX_ITERATIONS}. The larger value is rejected to help mitigate potential " +
79+
"Denial of Service attacks."
80+
//println msg
81+
assertEquals msg, expected.message
82+
}
83+
}
84+
}
85+
5386
// for manual/developer testing only. Takes a long time and there is no deterministic output to assert
5487
/*
5588
@Test

0 commit comments

Comments
 (0)