org.springframework.security.web.authentication.www.DigestAuthUtils Maven / Gradle / Ivy
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.www;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
final class DigestAuthUtils {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private DigestAuthUtils() {
}
static String encodePasswordInA1Format(String username, String realm, String password) {
String a1 = username + ":" + realm + ":" + password;
return md5Hex(a1);
}
static String[] splitIgnoringQuotes(String str, char separatorChar) {
if (str == null) {
return null;
}
int len = str.length();
if (len == 0) {
return EMPTY_STRING_ARRAY;
}
List list = new ArrayList<>();
int i = 0;
int start = 0;
boolean match = false;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
break;
}
i++;
}
match = true;
continue;
}
if (str.charAt(i) == separatorChar) {
if (match) {
list.add(str.substring(start, i));
match = false;
}
start = ++i;
continue;
}
match = true;
i++;
}
if (match) {
list.add(str.substring(start, i));
}
return list.toArray(new String[0]);
}
/**
* Computes the response
portion of a Digest authentication header. Both
* the server and user agent should compute the response
independently.
* Provided as a static method to simplify the coding of user agents.
* @param passwordAlreadyEncoded true if the password argument is already encoded in
* the correct format. False if it is plain text.
* @param username the user's login name.
* @param realm the name of the realm.
* @param password the user's password in plaintext or ready-encoded.
* @param httpMethod the HTTP request method (GET, POST etc.)
* @param uri the request URI.
* @param qop the qop directive, or null if not set.
* @param nonce the nonce supplied by the server
* @param nc the "nonce-count" as defined in RFC 2617.
* @param cnonce opaque string supplied by the client when qop is set.
* @return the MD5 of the digest authentication response, encoded in hex
* @throws IllegalArgumentException if the supplied qop value is unsupported.
*/
static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
throws IllegalArgumentException {
String a2 = httpMethod + ":" + uri;
String a1Md5 = (!passwordAlreadyEncoded) ? DigestAuthUtils.encodePasswordInA1Format(username, realm, password)
: password;
String a2Md5 = md5Hex(a2);
if (qop == null) {
// as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
return md5Hex(a1Md5 + ":" + nonce + ":" + a2Md5);
}
if ("auth".equals(qop)) {
// As per RFC 2617 compliant clients
return md5Hex(a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5);
}
throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
}
/**
* Takes an array of String
s, and for each element removes any instances
* of removeCharacter
, and splits the element based on the
* delimiter
. A Map
is then generated, with the left of the
* delimiter providing the key, and the right of the delimiter providing the value.
*
* Will trim both the key and value before adding to the Map
.
*
* @param array the array to process
* @param delimiter to split each element using (typically the equals symbol)
* @param removeCharacters one or more characters to remove from each element prior to
* attempting the split operation (typically the quotation mark symbol) or
* null
if no removal should occur
* @return a Map
representing the array contents, or null
if
* the array to process was null or empty
*/
static Map splitEachArrayElementAndCreateMap(String[] array, String delimiter,
String removeCharacters) {
if ((array == null) || (array.length == 0)) {
return null;
}
Map map = new HashMap<>();
for (String s : array) {
String postRemove = (removeCharacters != null) ? StringUtils.replace(s, removeCharacters, "") : s;
String[] splitThisArrayElement = split(postRemove, delimiter);
if (splitThisArrayElement == null) {
continue;
}
map.put(splitThisArrayElement[0].trim(), splitThisArrayElement[1].trim());
}
return map;
}
/**
* Splits a String
at the first instance of the delimiter.
*
* Does not include the delimiter in the response.
*
* @param toSplit the string to split
* @param delimiter to split the string up with
* @return a two element array with index 0 being before the delimiter, and index 1
* being after the delimiter (neither element includes the delimiter)
* @throws IllegalArgumentException if an argument was invalid
*/
static String[] split(String toSplit, String delimiter) {
Assert.hasLength(toSplit, "Cannot split a null or empty string");
Assert.hasLength(delimiter, "Cannot use a null or empty delimiter to split a string");
Assert.isTrue(delimiter.length() == 1, "Delimiter can only be one character in length");
int offset = toSplit.indexOf(delimiter);
if (offset < 0) {
return null;
}
String beforeDelimiter = toSplit.substring(0, offset);
String afterDelimiter = toSplit.substring(offset + 1);
return new String[] { beforeDelimiter, afterDelimiter };
}
static String md5Hex(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("No MD5 algorithm available!");
}
}
}