Skip to content

Commit 867f50e

Browse files
committed
RFC 7616: honour charset=UTF-8 in Digest auth #2068
1 parent 6222d4b commit 867f50e

File tree

3 files changed

+35
-12
lines changed

3 files changed

+35
-12
lines changed

client/src/main/java/org/asynchttpclient/Realm.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424

2525
import java.nio.charset.Charset;
2626
import java.security.MessageDigest;
27+
import java.util.Arrays;
2728
import java.util.Map;
2829
import java.util.concurrent.ThreadLocalRandom;
2930

3031
import static java.nio.charset.StandardCharsets.ISO_8859_1;
3132
import static java.nio.charset.StandardCharsets.UTF_8;
3233
import static java.util.Objects.requireNonNull;
3334
import static org.asynchttpclient.util.HttpConstants.Methods.GET;
34-
import static org.asynchttpclient.util.MessageDigestUtils.pooledMd5MessageDigest;
3535
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
3636
import static org.asynchttpclient.util.StringUtils.appendBase16;
3737
import static org.asynchttpclient.util.StringUtils.toHexString;
@@ -272,17 +272,19 @@ public static class Builder {
272272
private String methodName = GET;
273273
private boolean usePreemptive;
274274
private String ntlmDomain = System.getProperty("http.auth.ntlm.domain");
275-
private Charset charset = UTF_8;
275+
private static Charset charset = UTF_8;
276276
private String ntlmHost = "localhost";
277277
private boolean useAbsoluteURI;
278278
private boolean omitQuery;
279+
private Charset digestCharset = ISO_8859_1; // RFC default
279280
/**
280281
* Kerberos/Spnego properties
281282
*/
282283
private @Nullable Map<String, String> customLoginConfig;
283284
private @Nullable String servicePrincipalName;
284285
private boolean useCanonicalHostname;
285286
private @Nullable String loginContextName;
287+
private @Nullable String cs;
286288

287289
public Builder() {
288290
principal = null;
@@ -425,6 +427,10 @@ public Builder parseWWWAuthenticateHeader(String headerLine) {
425427
.setOpaque(match(headerLine, "opaque"))
426428
.setScheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC);
427429
String algorithm = match(headerLine, "algorithm");
430+
String cs = match(headerLine, "charset");
431+
if ("UTF-8".equalsIgnoreCase(cs)) {
432+
this.digestCharset = UTF_8;
433+
}
428434
if (isNonEmpty(algorithm)) {
429435
setAlgorithm(algorithm);
430436
}
@@ -471,17 +477,20 @@ public Builder parseProxyAuthenticateHeader(String headerLine) {
471477
private void newCnonce(MessageDigest md) {
472478
byte[] b = new byte[8];
473479
ThreadLocalRandom.current().nextBytes(b);
474-
b = md.digest(b);
475-
cnonce = toHexString(b);
480+
byte[] full = md.digest(b);
481+
// trim to first 8 bytes → 16 hex chars
482+
byte[] small = Arrays.copyOf(full, Math.min(8, full.length));
483+
cnonce = toHexString(small);
476484
}
477485

478-
private static byte[] digestFromRecycledStringBuilder(StringBuilder sb, MessageDigest md) {
479-
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
486+
private static byte[] digestFromRecycledStringBuilder(StringBuilder sb, MessageDigest md, Charset enc) {
487+
md.update(StringUtils.charSequence2ByteBuffer(sb, enc));
480488
sb.setLength(0);
481489
return md.digest();
482490
}
483491

484492
private static MessageDigest getDigestInstance(String algorithm) {
493+
if ("SHA-512/256".equalsIgnoreCase(algorithm)) algorithm = "SHA-512-256";
485494
if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "MD5-sess".equalsIgnoreCase(algorithm)) {
486495
return MessageDigestUtils.pooledMd5MessageDigest();
487496
} else if ("SHA-256".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm)) {
@@ -500,7 +509,7 @@ private byte[] ha1(StringBuilder sb, MessageDigest md) {
500509
// passwd ) ":" nonce-value ":" cnonce-value
501510

502511
sb.append(principal).append(':').append(realmName).append(':').append(password);
503-
byte[] core = digestFromRecycledStringBuilder(sb, md);
512+
byte[] core = digestFromRecycledStringBuilder(sb, md, digestCharset);
504513

505514
if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "SHA-256".equalsIgnoreCase(algorithm) || "SHA-512-256".equalsIgnoreCase(algorithm)) {
506515
// A1 = username ":" realm-value ":" passwd
@@ -510,7 +519,7 @@ private byte[] ha1(StringBuilder sb, MessageDigest md) {
510519
// A1 = HASH(username ":" realm-value ":" passwd ) ":" nonce ":" cnonce
511520
appendBase16(sb, core);
512521
sb.append(':').append(nonce).append(':').append(cnonce);
513-
return digestFromRecycledStringBuilder(sb, md);
522+
return digestFromRecycledStringBuilder(sb, md, digestCharset);
514523
}
515524
throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm);
516525
}
@@ -530,7 +539,7 @@ private byte[] ha2(StringBuilder sb, String digestUri, MessageDigest md) {
530539
throw new UnsupportedOperationException("Digest qop not supported: " + qop);
531540
}
532541

533-
return digestFromRecycledStringBuilder(sb, md);
542+
return digestFromRecycledStringBuilder(sb, md, digestCharset);
534543
}
535544

536545
private void appendMiddlePart(StringBuilder sb) {
@@ -557,7 +566,7 @@ private void newResponse(MessageDigest md) {
557566
appendMiddlePart(sb);
558567
appendBase16(sb, ha2);
559568

560-
byte[] responseDigest = digestFromRecycledStringBuilder(sb, md);
569+
byte[] responseDigest = digestFromRecycledStringBuilder(sb, md, digestCharset);
561570
response = toHexString(responseDigest);
562571
}
563572
}
@@ -591,7 +600,7 @@ public Realm build() {
591600
cnonce,
592601
uri,
593602
usePreemptive,
594-
charset,
603+
(scheme == AuthScheme.DIGEST ? digestCharset : charset),
595604
ntlmDomain,
596605
ntlmHost,
597606
useAbsoluteURI,

client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,20 @@ private static String computeDigestAuthentication(Realm realm, Uri uri) {
8282
if (realm.getOpaque() != null) {
8383
append(builder, "opaque", realm.getOpaque(), true);
8484
}
85+
if (realm.getScheme() == Realm.AuthScheme.DIGEST && realm.getCharset() == StandardCharsets.UTF_8) {
86+
append(builder, "charset", "UTF-8", false);
87+
}
8588
if (realm.getQop() != null) {
8689
append(builder, "qop", realm.getQop(), false);
8790
append(builder, "nc", realm.getNc(), false);
8891
append(builder, "cnonce", realm.getCnonce(), true);
8992
}
9093
// RFC7616: userhash parameter (optional, not implemented yet)
9194
builder.setLength(builder.length() - 2); // remove tailing ", "
92-
return new String(StringUtils.charSequence2Bytes(builder, ISO_8859_1), StandardCharsets.UTF_8);
95+
Charset wireCs = (realm.getCharset() == StandardCharsets.UTF_8)
96+
? StandardCharsets.UTF_8
97+
: ISO_8859_1;
98+
return new String(StringUtils.charSequence2Bytes(builder, wireCs), wireCs);
9399
}
94100

95101
private static void append(StringBuilder builder, String name, @Nullable String value, boolean quoted) {

client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
import java.security.MessageDigest;
1919
import java.security.NoSuchAlgorithmException;
2020

21+
/**
22+
* Thread-safety: Each digest is kept in a ThreadLocal. This
23+
* class is intended for use on long-lived threads (e.g., Netty event loops).
24+
* If you call it from a short-lived or unbounded thread pool, you may
25+
* inadvertently retain one MessageDigest instance per thread, leading
26+
* to memory leaks.
27+
*/
2128
public final class MessageDigestUtils {
2229

2330
private static final ThreadLocal<MessageDigest> MD5_MESSAGE_DIGESTS = ThreadLocal.withInitial(() -> {
@@ -68,6 +75,7 @@ private MessageDigestUtils() {
6875
public static MessageDigest pooledMessageDigest(String algorithm) {
6976
String alg = algorithm.replace("_", "-").toUpperCase();
7077
MessageDigest md;
78+
if ("SHA-512-256".equals(alg)) alg = "SHA-512/256";
7179
switch (alg) {
7280
case "MD5":
7381
md = MD5_MESSAGE_DIGESTS.get();

0 commit comments

Comments
 (0)