View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.auth;
28  
29  import java.io.IOException;
30  import java.io.ObjectInputStream;
31  import java.io.ObjectOutputStream;
32  import java.io.Serializable;
33  import java.nio.charset.Charset;
34  import java.nio.charset.StandardCharsets;
35  import java.security.MessageDigest;
36  import java.security.Principal;
37  import java.security.SecureRandom;
38  import java.util.ArrayList;
39  import java.util.Formatter;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.StringTokenizer;
47  
48  import org.apache.hc.client5.http.auth.AuthChallenge;
49  import org.apache.hc.client5.http.auth.AuthScheme;
50  import org.apache.hc.client5.http.auth.AuthScope;
51  import org.apache.hc.client5.http.auth.AuthenticationException;
52  import org.apache.hc.client5.http.auth.Credentials;
53  import org.apache.hc.client5.http.auth.CredentialsProvider;
54  import org.apache.hc.client5.http.auth.MalformedChallengeException;
55  import org.apache.hc.client5.http.auth.StandardAuthScheme;
56  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
57  import org.apache.hc.client5.http.protocol.HttpClientContext;
58  import org.apache.hc.client5.http.utils.ByteArrayBuilder;
59  import org.apache.hc.core5.annotation.Internal;
60  import org.apache.hc.core5.http.ClassicHttpRequest;
61  import org.apache.hc.core5.http.HttpEntity;
62  import org.apache.hc.core5.http.HttpHost;
63  import org.apache.hc.core5.http.HttpRequest;
64  import org.apache.hc.core5.http.NameValuePair;
65  import org.apache.hc.core5.http.message.BasicHeaderValueFormatter;
66  import org.apache.hc.core5.http.message.BasicNameValuePair;
67  import org.apache.hc.core5.http.protocol.HttpContext;
68  import org.apache.hc.core5.net.PercentCodec;
69  import org.apache.hc.core5.util.Args;
70  import org.apache.hc.core5.util.CharArrayBuffer;
71  import org.apache.hc.core5.util.TextUtils;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * Digest authentication scheme.
77   * Both MD5 (default) and MD5-sess are supported.
78   * Currently only qop=auth or no qop is supported. qop=auth-int
79   * is unsupported. If auth and auth-int are provided, auth is
80   * used.
81   * <p>
82   * Since the digest username is included as clear text in the generated
83   * Authentication header, the charset of the username must be compatible
84   * with the HTTP element charset used by the connection.
85   * </p>
86   *
87   * @since 4.0
88   */
89  public class DigestScheme implements AuthScheme, Serializable {
90  
91      private static final long serialVersionUID = 3883908186234566916L;
92  
93      private static final Logger LOG = LoggerFactory.getLogger(DigestScheme.class);
94  
95      /**
96       * Hexa values used when creating 32 character long digest in HTTP DigestScheme
97       * in case of authentication.
98       *
99       * @see #formatHex(byte[])
100      */
101     private static final char[] HEXADECIMAL = {
102         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
103         'e', 'f'
104     };
105 
106     /**
107      * Represent the possible values of quality of protection.
108      */
109     private enum QualityOfProtection {
110         UNKNOWN, MISSING, AUTH_INT, AUTH
111     }
112 
113     private transient Charset defaultCharset;
114     private final Map<String, String> paramMap;
115     private boolean complete;
116     private transient ByteArrayBuilder buffer;
117 
118     /**
119      * Flag indicating whether username hashing is supported.
120      * <p>
121      * This flag is used to determine if the server supports hashing of the username
122      * as part of the Digest Access Authentication process. When set to {@code true},
123      * the client is expected to hash the username using the same algorithm used for
124      * hashing the credentials. This is in accordance with Section 3.4.4 of RFC 7616.
125      * </p>
126      * <p>
127      * The default value is {@code false}, indicating that username hashing is not
128      * supported. If the server requires username hashing (indicated by the
129      * {@code userhash} parameter in the  a header set to {@code true}),
130      * this flag should be set to {@code true} to comply with the server's requirements.
131      * </p>
132      */
133     private boolean userhashSupported = false;
134 
135 
136     private String lastNonce;
137     private long nounceCount;
138     private String cnonce;
139     private byte[] a1;
140     private byte[] a2;
141 
142     private UsernamePasswordCredentials credentials;
143 
144     public DigestScheme() {
145         this.defaultCharset = StandardCharsets.UTF_8;
146         this.paramMap = new HashMap<>();
147         this.complete = false;
148     }
149 
150     /**
151      * @deprecated This constructor is deprecated to enforce the use of {@link StandardCharsets#UTF_8} encoding
152      * in compliance with RFC 7616 for HTTP Digest Access Authentication. Use the default constructor {@link #DigestScheme()} instead.
153      *
154      * @param charset the {@link Charset} set to be used for encoding credentials. This parameter is ignored as UTF-8 is always used.
155      */
156     @Deprecated
157     public DigestScheme(final Charset charset) {
158         this();
159     }
160 
161     public void initPreemptive(final Credentials credentials, final String cnonce, final String realm) {
162         Args.notNull(credentials, "Credentials");
163         Args.check(credentials instanceof UsernamePasswordCredentials,
164                 "Unsupported credential type: " + credentials.getClass());
165         this.credentials = (UsernamePasswordCredentials) credentials;
166         this.paramMap.put("cnonce", cnonce);
167         this.paramMap.put("realm", realm);
168     }
169 
170     @Override
171     public String getName() {
172         return StandardAuthScheme.DIGEST;
173     }
174 
175     @Override
176     public boolean isConnectionBased() {
177         return false;
178     }
179 
180     @Override
181     public String getRealm() {
182         return this.paramMap.get("realm");
183     }
184 
185     @Override
186     public void processChallenge(
187             final AuthChallenge authChallenge,
188             final HttpContext context) throws MalformedChallengeException {
189         Args.notNull(authChallenge, "AuthChallenge");
190         this.paramMap.clear();
191         final List<NameValuePair> params = authChallenge.getParams();
192         if (params != null) {
193             for (final NameValuePair param: params) {
194                 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
195             }
196         }
197         if (this.paramMap.isEmpty()) {
198             throw new MalformedChallengeException("Missing digest auth parameters");
199         }
200 
201         final String userHashValue = this.paramMap.get("userhash");
202         this.userhashSupported = "true".equalsIgnoreCase(userHashValue);
203 
204         this.complete = true;
205     }
206 
207     @Override
208     public boolean isChallengeComplete() {
209         final String s = this.paramMap.get("stale");
210         return !"true".equalsIgnoreCase(s) && this.complete;
211     }
212 
213     @Override
214     public boolean isResponseReady(
215             final HttpHost host,
216             final CredentialsProvider credentialsProvider,
217             final HttpContext context) throws AuthenticationException {
218 
219         Args.notNull(host, "Auth host");
220         Args.notNull(credentialsProvider, "CredentialsProvider");
221 
222         final AuthScope authScope = new AuthScope(host, getRealm(), getName());
223         final Credentials credentials = credentialsProvider.getCredentials(
224                 authScope, context);
225         if (credentials instanceof UsernamePasswordCredentials) {
226             this.credentials = (UsernamePasswordCredentials) credentials;
227             return true;
228         }
229 
230         if (LOG.isDebugEnabled()) {
231             final HttpClientContext clientContext = HttpClientContext.cast(context);
232             final String exchangeId = clientContext.getExchangeId();
233             LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
234         }
235         this.credentials = null;
236         return false;
237     }
238 
239     @Override
240     public Principal getPrincipal() {
241         return null;
242     }
243 
244     @Override
245             public String generateAuthResponse(
246             final HttpHost host,
247             final HttpRequest request,
248             final HttpContext context) throws AuthenticationException {
249 
250         Args.notNull(request, "HTTP request");
251         if (this.paramMap.get("realm") == null) {
252             throw new AuthenticationException("missing realm");
253         }
254         if (this.paramMap.get("nonce") == null) {
255             throw new AuthenticationException("missing nonce");
256         }
257 
258         if (context != null) {
259             final HttpClientContext clientContext = HttpClientContext.cast(context);
260             final String nextNonce = clientContext.getNextNonce();
261             if (!TextUtils.isBlank(nextNonce)) {
262                 this.paramMap.put("nonce", nextNonce);
263                 clientContext.setNextNonce(null);
264             }
265         }
266 
267         return createDigestResponse(request);
268     }
269 
270     private static MessageDigest createMessageDigest(
271             final String digAlg) throws UnsupportedDigestAlgorithmException {
272         try {
273             return MessageDigest.getInstance(digAlg);
274         } catch (final Exception e) {
275             throw new UnsupportedDigestAlgorithmException(
276               "Unsupported algorithm in HTTP Digest authentication: "
277                + digAlg);
278         }
279     }
280 
281     private String createDigestResponse(final HttpRequest request) throws AuthenticationException {
282         if (credentials == null) {
283             throw new AuthenticationException("User credentials have not been provided");
284         }
285         final String uri = request.getRequestUri();
286         final String method = request.getMethod();
287         final String realm = this.paramMap.get("realm");
288         final String nonce = this.paramMap.get("nonce");
289         final String opaque = this.paramMap.get("opaque");
290         final String algorithm = this.paramMap.get("algorithm");
291 
292         final Set<String> qopset = new HashSet<>(8);
293         QualityOfProtection qop = QualityOfProtection.UNKNOWN;
294         final String qoplist = this.paramMap.get("qop");
295         if (qoplist != null) {
296             final StringTokenizer tok = new StringTokenizer(qoplist, ",");
297             while (tok.hasMoreTokens()) {
298                 final String variant = tok.nextToken().trim();
299                 qopset.add(variant.toLowerCase(Locale.ROOT));
300             }
301             final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
302             if (entity != null && qopset.contains("auth-int")) {
303                 qop = QualityOfProtection.AUTH_INT;
304             } else if (qopset.contains("auth")) {
305                 qop = QualityOfProtection.AUTH;
306             } else if (qopset.contains("auth-int")) {
307                 qop = QualityOfProtection.AUTH_INT;
308             }
309         } else {
310             qop = QualityOfProtection.MISSING;
311         }
312 
313         if (qop == QualityOfProtection.UNKNOWN) {
314             throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
315         }
316 
317         final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
318 
319         // If an algorithm is not specified, default to MD5.
320 
321         DigestAlgorithm digAlg = null;
322 
323         final MessageDigest digester;
324         try {
325             digAlg = DigestAlgorithm.fromString(algorithm == null ? "MD5" : algorithm);
326             digester = createMessageDigest(digAlg.getBaseAlgorithm());
327         } catch (final UnsupportedDigestAlgorithmException ex) {
328             throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
329         }
330 
331         if (nonce.equals(this.lastNonce)) {
332             nounceCount++;
333         } else {
334             nounceCount = 1;
335             cnonce = null;
336             lastNonce = nonce;
337         }
338 
339         final StringBuilder sb = new StringBuilder(8);
340         try (final Formatter formatter = new Formatter(sb, Locale.ROOT)) {
341             formatter.format("%08x", nounceCount);
342         }
343         final String nc = sb.toString();
344 
345         if (cnonce == null) {
346             cnonce = formatHex(createCnonce(digAlg));
347         }
348 
349         if (buffer == null) {
350             buffer = new ByteArrayBuilder(128);
351         } else {
352             buffer.reset();
353         }
354         buffer.charset(charset);
355 
356         a1 = null;
357         a2 = null;
358 
359 
360         // Extract username and username*
361         String username = credentials.getUserName();
362         String encodedUsername = null;
363         // Check if 'username' has invalid characters and use 'username*'
364         if (username != null && containsInvalidABNFChars(username)) {
365             encodedUsername = "UTF-8''" + PercentCodec.RFC5987.encode(username);
366         }
367 
368         final String usernameForDigest;
369         if (this.userhashSupported) {
370             final String usernameRealm = username + ":" + realm;
371             final byte[] hashedBytes = digester.digest(usernameRealm.getBytes(StandardCharsets.UTF_8));
372             usernameForDigest = formatHex(hashedBytes); // Use hashed username for digest
373             username = usernameForDigest;
374         } else if (encodedUsername != null) {
375             usernameForDigest = encodedUsername; // Use encoded username for digest
376         } else {
377             usernameForDigest = username; // Use regular username for digest
378         }
379 
380         // 3.2.2.2: Calculating digest
381         if (digAlg.isSessionBased()) {
382             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
383             //      ":" unq(nonce-value)
384             //      ":" unq(cnonce-value)
385 
386             // calculated one per session
387             buffer.append(username).append(":").append(realm).append(":").append(credentials.getUserPassword());
388             final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
389             buffer.reset();
390             buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
391         } else {
392             // unq(username-value) ":" unq(realm-value) ":" passwd
393             buffer.append(username).append(":").append(realm).append(":").append(credentials.getUserPassword());
394         }
395         a1 = buffer.toByteArray();
396 
397         final String hasha1 = formatHex(digester.digest(a1));
398         buffer.reset();
399 
400         if (qop == QualityOfProtection.AUTH) {
401             // Method ":" digest-uri-value
402             a2 = buffer.append(method).append(":").append(uri).toByteArray();
403         } else if (qop == QualityOfProtection.AUTH_INT) {
404             // Method ":" digest-uri-value ":" H(entity-body)
405             final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
406             if (entity != null && !entity.isRepeatable()) {
407                 // If the entity is not repeatable, try falling back onto QOP_AUTH
408                 if (qopset.contains("auth")) {
409                     qop = QualityOfProtection.AUTH;
410                     a2 = buffer.append(method).append(":").append(uri).toByteArray();
411                 } else {
412                     throw new AuthenticationException("Qop auth-int cannot be used with " +
413                             "a non-repeatable entity");
414                 }
415             } else {
416                 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
417                 try {
418                     if (entity != null) {
419                         entity.writeTo(entityDigester);
420                     }
421                     entityDigester.close();
422                 } catch (final IOException ex) {
423                     throw new AuthenticationException("I/O error reading entity content", ex);
424                 }
425                 a2 = buffer.append(method).append(":").append(uri)
426                         .append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
427             }
428         } else {
429             a2 = buffer.append(method).append(":").append(uri).toByteArray();
430         }
431 
432         final String hasha2 = formatHex(digester.digest(a2));
433         buffer.reset();
434 
435         // 3.2.2.1
436 
437         final byte[] digestInput;
438         if (qop == QualityOfProtection.MISSING) {
439             buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
440         } else {
441             buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
442                 .append(cnonce).append(":").append(qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth")
443                 .append(":").append(hasha2);
444         }
445         digestInput = buffer.toByteArray();
446         buffer.reset();
447 
448         final String digest = formatHex(digester.digest(digestInput));
449 
450         final CharArrayBuffer buffer = new CharArrayBuffer(128);
451         buffer.append(StandardAuthScheme.DIGEST + " ");
452 
453         final List<BasicNameValuePair> params = new ArrayList<>(20);
454         if (this.userhashSupported) {
455             // Use hashed username for the 'username' parameter
456             params.add(new BasicNameValuePair("username", usernameForDigest));
457             params.add(new BasicNameValuePair("userhash", "true"));
458         } else if (encodedUsername != null) {
459             // Use encoded 'username*' parameter
460             params.add(new BasicNameValuePair("username*", encodedUsername));
461         } else {
462             // Use regular 'username' parameter
463             params.add(new BasicNameValuePair("username", username));
464         }
465         params.add(new BasicNameValuePair("realm", realm));
466         params.add(new BasicNameValuePair("nonce", nonce));
467         params.add(new BasicNameValuePair("uri", uri));
468         params.add(new BasicNameValuePair("response", digest));
469 
470         if (qop != QualityOfProtection.MISSING) {
471             params.add(new BasicNameValuePair("qop", qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth"));
472             params.add(new BasicNameValuePair("nc", nc));
473             params.add(new BasicNameValuePair("cnonce", cnonce));
474             params.add(new BasicNameValuePair("rspauth", hasha2));
475         }
476         if (algorithm != null) {
477             params.add(new BasicNameValuePair("algorithm", algorithm));
478         }
479         if (opaque != null) {
480             params.add(new BasicNameValuePair("opaque", opaque));
481         }
482 
483         for (int i = 0; i < params.size(); i++) {
484             final BasicNameValuePair param = params.get(i);
485             if (i > 0) {
486                 buffer.append(", ");
487             }
488             final String name = param.getName();
489             final boolean noQuotes = "nc".equals(name) || "qop".equals(name)
490                     || "algorithm".equals(name);
491             BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
492         }
493         return buffer.toString();
494     }
495 
496     @Internal
497     public String getNonce() {
498         return lastNonce;
499     }
500 
501     @Internal
502     public long getNounceCount() {
503         return nounceCount;
504     }
505 
506     @Internal
507     public String getCnonce() {
508         return cnonce;
509     }
510 
511     String getA1() {
512         return a1 != null ? new String(a1, StandardCharsets.US_ASCII) : null;
513     }
514 
515     String getA2() {
516         return a2 != null ? new String(a2, StandardCharsets.US_ASCII) : null;
517     }
518 
519     /**
520      * Encodes a byte array digest into a hexadecimal string.
521      * <p>
522      * This method supports digests of various lengths, such as 16 bytes (128-bit) for MD5,
523      * 32 bytes (256-bit) for SHA-256, and SHA-512/256. Each byte is converted to two
524      * hexadecimal characters, so the resulting string length is twice the byte array length.
525      * </p>
526      *
527      * @param binaryData the array containing the digest bytes
528      * @return encoded hexadecimal string, or {@code null} if encoding failed
529      */
530     static String formatHex(final byte[] binaryData) {
531         final int n = binaryData.length;
532         final char[] buffer = new char[n * 2];
533         for (int i = 0; i < n; i++) {
534             final int low = binaryData[i] & 0x0f;
535             final int high = (binaryData[i] & 0xf0) >> 4;
536             buffer[i * 2] = HEXADECIMAL[high];
537             buffer[(i * 2) + 1] = HEXADECIMAL[low];
538         }
539         return new String(buffer);
540     }
541 
542 
543     /**
544      * Creates a random cnonce value based on the specified algorithm's expected entropy.
545      * Adjusts the length of the byte array based on the algorithm to ensure sufficient entropy.
546      *
547      * @param algorithm the algorithm for which the cnonce is being generated (e.g., "MD5", "SHA-256", "SHA-512-256").
548      * @return The cnonce value as a byte array.
549      * @since 5.5
550      */
551     static byte[] createCnonce(final DigestAlgorithm algorithm) {
552         final SecureRandom rnd = new SecureRandom();
553         final int length;
554         switch (algorithm.name().toUpperCase()) {
555             case "SHA-256":
556             case "SHA-512/256":
557                 length = 32;
558                 break;
559             case "MD5":
560             default:
561                 length = 16;
562                 break;
563         }
564         final byte[] tmp = new byte[length];
565         rnd.nextBytes(tmp);
566         return tmp;
567     }
568 
569 
570     private void writeObject(final ObjectOutputStream out) throws IOException {
571         out.defaultWriteObject();
572         out.writeUTF(defaultCharset.name());
573     }
574 
575     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
576         in.defaultReadObject();
577         this.defaultCharset = Charset.forName(in.readUTF());
578     }
579 
580     @Override
581     public String toString() {
582         return getName() + this.paramMap;
583     }
584 
585     /**
586      * Checks if a given string contains characters that are not allowed
587      * in an ABNF quoted-string as per standard specifications.
588      * <p>
589      * The method checks for:
590      * - Control characters (ASCII 0x00 to 0x1F and 0x7F).
591      * - Characters outside the printable ASCII range (above 0x7E).
592      * - Double quotes (&quot;) and backslashes (\), which are not allowed.
593      * </p>
594      *
595      * @param value The string to be checked for invalid ABNF characters.
596      * @return {@code true} if invalid characters are found, {@code false} otherwise.
597      * @throws IllegalArgumentException if the input string is null.
598      */
599     private boolean containsInvalidABNFChars(final String value) {
600         if (value == null) {
601             throw new IllegalArgumentException("Input string should not be null.");
602         }
603 
604         for (int i = 0; i < value.length(); i++) {
605             final char c = value.charAt(i);
606 
607             // Check for control characters and DEL
608             if (c <= 0x1F || c == 0x7F) {
609                 return true;
610             }
611 
612             // Check for characters outside the range 0x20 to 0x7E
613             if (c > 0x7E) {
614                 return true;
615             }
616 
617             // Exclude double quotes and backslash
618             if (c == '"' || c == '\\') {
619                 return true;
620             }
621         }
622         return false;
623     }
624 
625     /**
626      * Enum representing supported digest algorithms for HTTP Digest Authentication,
627      * including session-based variants.
628      */
629     private enum DigestAlgorithm {
630 
631         /**
632          * MD5 digest algorithm.
633          */
634         MD5("MD5", false),
635 
636         /**
637          * MD5 digest algorithm with session-based variant.
638          */
639         MD5_SESS("MD5", true),
640 
641         /**
642          * SHA-256 digest algorithm.
643          */
644         SHA_256("SHA-256", false),
645 
646         /**
647          * SHA-256 digest algorithm with session-based variant.
648          */
649         SHA_256_SESS("SHA-256", true),
650 
651         /**
652          * SHA-512/256 digest algorithm.
653          */
654         SHA_512_256("SHA-512/256", false),
655 
656         /**
657          * SHA-512/256 digest algorithm with session-based variant.
658          */
659         SHA_512_256_SESS("SHA-512/256", true);
660 
661         private final String baseAlgorithm;
662         private final boolean sessionBased;
663 
664         /**
665          * Constructor for {@code DigestAlgorithm}.
666          *
667          * @param baseAlgorithm the base name of the algorithm, e.g., "MD5" or "SHA-256"
668          * @param sessionBased indicates if the algorithm is session-based (i.e., includes the "-sess" suffix)
669          */
670         DigestAlgorithm(final String baseAlgorithm, final boolean sessionBased) {
671             this.baseAlgorithm = baseAlgorithm;
672             this.sessionBased = sessionBased;
673         }
674 
675         /**
676          * Retrieves the base algorithm name without session suffix.
677          *
678          * @return the base algorithm name
679          */
680         private String getBaseAlgorithm() {
681             return baseAlgorithm;
682         }
683 
684         /**
685          * Checks if the algorithm is session-based.
686          *
687          * @return {@code true} if the algorithm includes the "-sess" suffix, otherwise {@code false}
688          */
689         private boolean isSessionBased() {
690             return sessionBased;
691         }
692 
693         /**
694          * Maps a string representation of an algorithm to the corresponding enum constant.
695          *
696          * @param algorithm the algorithm name, e.g., "SHA-256" or "SHA-512-256-sess"
697          * @return the corresponding {@code DigestAlgorithm} constant
698          * @throws UnsupportedDigestAlgorithmException if the algorithm is unsupported
699          */
700         private static DigestAlgorithm fromString(final String algorithm) {
701             switch (algorithm.toUpperCase(Locale.ROOT)) {
702                 case "MD5":
703                     return MD5;
704                 case "MD5-SESS":
705                     return MD5_SESS;
706                 case "SHA-256":
707                     return SHA_256;
708                 case "SHA-256-SESS":
709                     return SHA_256_SESS;
710                 case "SHA-512/256":
711                 case "SHA-512-256":
712                     return SHA_512_256;
713                 case "SHA-512-256-SESS":
714                     return SHA_512_256_SESS;
715                 default:
716                     throw new UnsupportedDigestAlgorithmException("Unsupported digest algorithm: " + algorithm);
717             }
718         }
719     }
720 
721 
722 }