/* ==================================================================
* AuthenticationData.java - 1/03/2017 5:23:56 PM
*
* Copyright 2007-2017 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.central.security.web;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.springframework.security.authentication.BadCredentialsException;
/**
* Abstract base class for parsing and exposing the authentication data included
* in a HTTP authentication header.
*
* @author matt
* @version 1.0
* @since 1.8
*/
public abstract class AuthenticationData {
/** The fixed length of a SolarNetwork authentication token ID. */
public static final int AUTH_TOKEN_ID_LENGTH = 20;
private final Date date;
private final long dateSkew;
private final AuthenticationScheme scheme;
/**
* Constructor.
*
* @param scheme
* The scheme associated with the data.
* @param request
* The request.
* @param headerValue
* The authentication HTTP header value.
* @throws BadCredentialsException
* if the request date is not available or no data is associated
* with the authentication header
*/
public AuthenticationData(AuthenticationScheme scheme, SecurityHttpServletRequestWrapper request,
String headerValue) {
this.scheme = scheme;
String dateHeader = WebConstants.HEADER_DATE;
long dateValue = request.getDateHeader(dateHeader);
if ( dateValue < 0 ) {
dateHeader = "Date";
dateValue = request.getDateHeader(dateHeader);
if ( dateValue < 0 ) {
throw new BadCredentialsException("Missing or invalid HTTP Date header value");
}
}
this.date = new Date(dateValue);
this.dateSkew = Math.abs(System.currentTimeMillis() - date.getTime());
}
/**
* Validate a digest header value presented in a request against the request
* body content.
*
* @param request
* The request.
* @throws IOException
* If an IO error occurs.
* @throws BadCredentialsException
* If a digest does not match.
*/
public static void validateContentDigest(SecurityHttpServletRequestWrapper request)
throws IOException {
// try Digest HTTP header first
String headerValue = request.getHeader("Digest");
if ( headerValue != null ) {
final String delimitedString = headerValue + ",";
int prevDelimIdx = 0;
int delimIdx = 0;
for ( delimIdx = delimitedString.indexOf(','); delimIdx >= 0; prevDelimIdx = delimIdx
+ 1, delimIdx = delimitedString.indexOf(',', prevDelimIdx) ) {
String oneDigest = headerValue.substring(prevDelimIdx, delimIdx);
int splitIdx = oneDigest.indexOf('=');
if ( splitIdx > 0 ) {
String algName = oneDigest.substring(0, splitIdx);
try {
DigestAlgorithm digestAlg = DigestAlgorithm.forAlgorithmName(algName);
String providedDigest = (oneDigest.length() > splitIdx + 1
? oneDigest.substring(splitIdx + 1) : null);
validateContentDigest(digestAlg, providedDigest, request);
// if we get past this, we have validated a digest so can stop looking for more
return;
} catch ( IllegalArgumentException e ) {
// ignore and move on
}
}
}
} else {
// try the Content-MD5 HTTP header
headerValue = request.getHeader("Content-MD5");
if ( headerValue != null ) {
validateContentDigest(DigestAlgorithm.MD5, headerValue, request);
}
}
}
private static void validateContentDigest(DigestAlgorithm alg, String providedDigestString,
SecurityHttpServletRequestWrapper request) throws IOException {
byte[] computedDigest = null;
byte[] providedDigest = null;
int hexLength = 0;
try {
switch (alg) {
case MD5:
hexLength = 32;
computedDigest = request.getContentMD5();
break;
case SHA1:
hexLength = 40;
computedDigest = request.getContentSHA1();
break;
case SHA256:
hexLength = 64;
computedDigest = request.getContentSHA256();
break;
}
} catch ( net.solarnetwork.central.security.SecurityException e ) {
throw new BadCredentialsException("Content too large", e);
}
try {
if ( providedDigestString.length() == hexLength ) {
// treat as hex
providedDigest = Hex.decodeHex(providedDigestString.toCharArray());
} else {
// treat as Base64
providedDigest = Base64.decodeBase64(providedDigestString);
}
} catch ( DecoderException e ) {
throw new BadCredentialsException("Invalid Digest SHA-256 encoding");
}
if ( !Arrays.equals(computedDigest, providedDigest) ) {
throw new BadCredentialsException(
"Content " + alg.getAlgorithmName() + " digest value mismatch");
}
}
/**
* Get a string value of a specific HTTP header, returning an empty string
* if not available.
*
* @param request
* The request.
* @param headerName
* The name of the HTTP header to get.
* @return The header value, or an empty string if not found.
*/
public static String nullSafeHeaderValue(HttpServletRequest request, String headerName) {
final String result = request.getHeader(headerName);
return (result == null ? "" : result);
}
/**
* Get a HTTP formatted date.
*
* @param date
* The date to format.
* @return The formatted date.
*/
public static String httpDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(date);
}
/**
* AWS style implementation of "uri encoding" using UTF-8 encoding.
*
* @param input
* The text input to encode.
* @return The URI escaped string.
*/
public static String uriEncode(CharSequence input) {
StringBuilder result = new StringBuilder();
byte[] tmpByteArray = new byte[1];
for ( int i = 0; i < input.length(); i++ ) {
char ch = input.charAt(i);
if ( (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|| ch == '_' || ch == '-' || ch == '~' || ch == '.' ) {
result.append(ch);
} else {
try {
byte[] bytes = String.valueOf(ch).getBytes("UTF-8");
for ( byte b : bytes ) {
tmpByteArray[0] = b;
result.append('%').append(Hex.encodeHex(tmpByteArray, false));
}
} catch ( UnsupportedEncodingException e ) {
// ignore, should never be here
}
}
}
return result.toString();
}
/**
* Get an ISO8601 formatted date.
*
* @param date
* The date to format.
* @return The formatted date.
*/
public static String iso8601Date(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(date);
}
/**
* Get the date associated with the request.
*
* @return The date.
*/
public Date getDate() {
return date;
}
/**
* Get the date skew (in milliseconds) associated with the request (from the
* system date).
*
* @return The date skew.
*/
public long getDateSkew() {
return dateSkew;
}
/**
* The scheme of the authentication data.
*
* @return The scheme.
*/
public AuthenticationScheme getScheme() {
return scheme;
}
/**
* Test if the date skew is less than a maximum date skew.
*
* @param maxDateSkew
* The maximum allowed date skew.
* @return {@code true} if the date skew is within the allowed skew
*/
public boolean isDateValid(long maxDateSkew) {
return dateSkew < maxDateSkew;
}
/**
* Compute a Base64 MAC digest from the signature data.
*
* @param secretKey
* the secret key
* @param macAlgorithm
* @return The base64 encoded digest.
* @throws SecurityException
* if any error occurs
*/
protected final byte[] computeMACDigest(final String secretKey, String macAlgorithm) {
return computeMACDigest(secretKey, getSignatureData(), macAlgorithm);
}
/**
* Compute a Base64 MAC digest from signature data.
*
* @param secretKey
* the secret key
* @param data
* the data to sign
* @param macAlgorithm
* @return The base64 encoded digest.
* @throws SecurityException
* if any error occurs
*/
public static final byte[] computeMACDigest(final byte[] secretKey, final String data,
String macAlgorithm) {
try {
return computeMACDigest(secretKey, data.getBytes("UTF-8"), macAlgorithm);
} catch ( UnsupportedEncodingException e ) {
throw new SecurityException("Error loading " + macAlgorithm + " crypto function", e);
}
}
/**
* Compute a Base64 MAC digest from signature data.
*
* @param secretKey
* the secret key
* @param data
* the data to sign
* @param macAlgorithm
* @return The base64 encoded digest.
* @throws SecurityException
* if any error occurs
*/
public static final byte[] computeMACDigest(final String secretKey, final String data,
String macAlgorithm) {
try {
return computeMACDigest(secretKey.getBytes("UTF-8"), data.getBytes("UTF-8"), macAlgorithm);
} catch ( UnsupportedEncodingException e ) {
throw new SecurityException("Error loading " + macAlgorithm + " crypto function", e);
}
}
/**
* Compute a Base64 MAC digest from signature data.
*
* @param secretKey
* the secret key
* @param data
* the data to sign
* @param macAlgorithm
* @return The base64 encoded digest.
* @throws SecurityException
* if any error occurs
*/
public static final byte[] computeMACDigest(final byte[] secretKey, final byte[] data,
String macAlgorithm) {
Mac mac;
try {
mac = Mac.getInstance(macAlgorithm);
mac.init(new SecretKeySpec(secretKey, macAlgorithm));
byte[] result = mac.doFinal(data);
return result;
} catch ( NoSuchAlgorithmException e ) {
throw new SecurityException("Error loading " + macAlgorithm + " crypto function", e);
} catch ( InvalidKeyException e ) {
throw new SecurityException("Error loading " + macAlgorithm + " crypto function", e);
}
}
/**
* Compute the signature digest from the request data and a given secret
* key.
*
* @param secretKey
* The secret key.
* @return The computed digest, as a Base64 encoded string.
*/
public abstract String computeSignatureDigest(String secretKey);
/**
* Get the authentication token ID.
*
* @return The authentication token ID.
*/
public abstract String getAuthTokenId();
/**
* Get the signature digest as presented in the HTTP header value.
*
* @return The presented signature digest.
*/
public abstract String getSignatureDigest();
/**
* Get the extracted signature data from this request.
*
* @return The raw signature data.
*/
public abstract String getSignatureData();
}