package com.intel.mtwilson.security.http;
import com.intel.dcsg.cpg.crypto.HmacCredential;
import com.intel.dcsg.cpg.iso8601.Iso8601Date;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Symmetric/shared-secret signatures for Mt Wilson 0.5.1 in Authorization header using custom "MtWilson" scheme
* This class requires the following libraries:
* org.apache.commons.codec.binary.Base64 from commons-codec
* org.apache.commons.lang.StringUtils from commons-lang
* @since 0.5.1
* @author jbuhacoff
*/
public class HmacAuthorization {
private final Logger log = LoggerFactory.getLogger(getClass());
private HmacCredential credentials;
private String signatureMethod;
private String realm = null;
/*
public RequestAuthorization(Credentials credentials) {
this(new String(credentials.identity()), "", "SHA256");
}*/
public HmacAuthorization(HmacCredential credentials) {
this(credentials,"HmacSHA256");
}
public HmacAuthorization(HmacCredential credentials, String signatureMethod) {
this.credentials = credentials;
// this just accommodates some common variations on the names of known algorithms
HashMap<String,String> algorithms = new HashMap<String,String>();
algorithms.put("HmacSHA256", "HmacSHA256");
algorithms.put("HMAC-SHA256", "HmacSHA256");
if( algorithms.containsKey(signatureMethod) ) {
this.signatureMethod = algorithms.get(signatureMethod);
}
else {
this.signatureMethod = signatureMethod;
}
}
public void setRealm(String realmName) {
realm = realmName;
}
/**
* Use this method to sign requests that do not have a message body,
* such as GET and DELETE.
* @param httpMethod the string "GET" or "DELETE"
* @param requestUrl the URL to access
* @return content of the Authorization header to add to the request
*/
public String getAuthorizationQuietly(String httpMethod, String requestUrl) {
return getAuthorizationQuietly(httpMethod, requestUrl, null, null);
}
/**
* Use this method to sign requests that have a message body,
* such as PUT and POST. You can also use it to sign requests with
* no message body by passing null for the requestBody parameter.
* @param httpMethod the string "GET" or "DELETE"
* @param requestUrl the URL to access
* @param requestBody the body of the request
* @return content of the Authorization header to add to the request
*/
public String getAuthorizationQuietly(String httpMethod, String requestUrl, String requestBody) {
return getAuthorizationQuietly(httpMethod, requestUrl, null, requestBody);
}
/**
* Use this method to sign requests that do have a message body and URL
* parameters.
* @param httpMethod the string "GET" or "DELETE"
* @param requestUrl the URL to access
* @return content of the Authorization header to add to the request
*/
public String getAuthorizationQuietly(String httpMethod, String requestUrl, Map<String,Object> urlParams, String requestBody) {
try {
return getAuthorization(httpMethod, requestUrl, urlParams, requestBody);
}
catch(NoSuchAlgorithmException e) {
log.error("Algorithm not available: "+e.getMessage(), e);
}
catch(InvalidKeyException e) {
log.error("Password not suitable for signature: "+e.getMessage(), e);
}
catch(IOException e) {
log.error("Error creating signature: "+e.getMessage(), e);
}
return null;
}
/**
* Generates the content for the Authorization header, using the
* Signature format: client-token : nonce : signature
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl complete URL such as https://example.com/some/path
* @return
* @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm
* @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256
* @throws IOException if there was a problem generating the nonce
*/
public String getAuthorization(String httpMethod, String requestUrl) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
return getAuthorization(httpMethod, requestUrl, null, null);
}
/**
* Generates the content for the Authorization header, using the
* Signature format: client-token : nonce : signature
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl complete URL such as https://example.com/some/path
* @param requestBody only required if you are sending POST or PUT message body; can be null (will be converted to empty string)
* @return
* @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm
* @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256
* @throws IOException if there was a problem generating the nonce
*/
public String getAuthorization(String httpMethod, String requestUrl, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
return getAuthorization(httpMethod, requestUrl, null, requestBody);
}
/**
* Generates the content for the Authorization header, using the
* Signature format: client-token : nonce : signature
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl complete URL such as https://example.com/some/path
* @param urlParams a key-value map representing url parameters; the value can be null, or a single string value, a string array, or a list of strings
* @return
* @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm
* @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256
* @throws IOException if there was a problem generating the nonce
*/
public String getAuthorization(String httpMethod, String requestUrl, Map<String,Object> urlParams) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
return getAuthorization(httpMethod, requestUrl, urlParams, null);
}
/**
* Generates the content for the Authorization header, using the
* Signature format: client-token : nonce : signature
*
* If the urlParams are given, they will be added to the request URL in
* ascending alphabetical order. If any parameter is multi-valued, its values
* will be listed in ascending alphabetical order.
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl complete URL such as https://example.com/some/path or /some/path
* @param urlParams a Map containing keys of type String and values of either type String or String[]
* @param requestBody only required if you are sending POST or PUT message body; can be null (will be converted to empty string)
* @return
* @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm
* @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256
* @throws IOException if there was a problem generating the nonce
*/
public String getAuthorization(String httpMethod, String requestUrl, Map<String,Object> urlParams, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
String nonce = new String(Base64.encodeBase64(nonce()));
String username = new String(Base64.encodeBase64(credentials.identity()));
String timestamp = new Iso8601Date(new Date(System.currentTimeMillis())).toString();
HmacSignatureInput signatureBlock = new HmacSignatureInput();
signatureBlock.httpMethod = httpMethod;
signatureBlock.absoluteUrl = new HttpRequestURL(requestUrl,urlParams).toString();
signatureBlock.fromToken = username;
signatureBlock.nonce = nonce;
signatureBlock.body = requestBody;
signatureBlock.signatureMethod = signatureMethod;
signatureBlock.timestamp = timestamp;
String content = signatureBlock.toString();
//log.debug("signed content follows... ("+content.length()+") \n"+content);
String signature = sign(content);
String authorization = String.format("MtWilson %s", headerParams( httpMethod, signatureBlock.absoluteUrl, username, nonce, signatureMethod, timestamp, realm, signature));
//log.debug("authorization: "+authorization);
return authorization;
}
/**
* Generates a 24-byte nonce comprised of 8 bytes current time (milliseconds) and 16 bytes random data.
* @return
* @throws IOException if there was a problem generating the nonce
*/
private byte[] nonce() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
long currentTime = System.currentTimeMillis();
dos.writeLong(currentTime);
SecureRandom r = new SecureRandom();
byte[] nonce = new byte[16];
r.nextBytes(nonce);
dos.write(nonce);
dos.flush();
//byte[] noncedata = bos.toByteArray(); // should be 8 bytes timestamp + 16 bytes random numbers
//System.out.println("nonce data length = "+noncedata.length);
//assert noncedata.length == 24;
dos.close();
return bos.toByteArray();
}
/**
* Generates the parameters of the Authorization header.
* Sample output (with newlines for clarity - actual output is one line)
*
username="0685bd9184jfhq22",
httpMethod="GET",
uri="/reports/trust?hostName=example",
timestamp="2012-02-14T08:15:00PST",
nonce="4572616e48616d6d65724c61686176",
signature_method="HMAC-SHA256",
signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D"
*
* If any parameter is null, then it is not included in the output. The
* sample output above was generated with realm=null so the realm attribute
* was not included.
*
* @param httpMethod
* @param absoluteUrl
* @param fromToken
* @param nonce
* @param signatureMethod
* @param timestamp
* @return
*/
private String headerParams(String httpMethod, String absoluteUrl, String fromToken, String nonce, String signatureMethod, String timestamp, String realm, String signature) {
String[] input = new String[] { httpMethod, absoluteUrl, fromToken, nonce, signatureMethod, timestamp, realm, signature };
String[] label = new String[] {"http_method","uri", "username", "nonce","signature_method","timestamp","realm","signature"};
ArrayList<String> errors = new ArrayList<String>();
ArrayList<String> params = new ArrayList<String>();
for(int i=0; i<input.length; i++) {
if( input[i] != null && input[i].contains("\"") ) { errors.add(String.format("%s contains quotes", label[i])); }
if( input[i] != null ) { params.add(String.format("%s=\"%s\"", label[i], encodeHeaderAttributeValue(input[i]))); }
}
if( !errors.isEmpty() ) { throw new IllegalArgumentException("Cannot create authorization header: "+StringUtils.join(errors, ", ")); }
return StringUtils.join(params, ", ");
}
/**
* Encodes a string for use as an attribute value in the Authorization header.
* None of the values should include quotes. URL should be URL-encoded, and
* none of the other values allow quotes in their formats with the exception
* of "realm" and "username" which are application dependent but which we
* define as not including quotes.
* So, instead of encoding here, we throw errors (see headerParams function)
* when a value contains a quote or newline character.
* @param value
* @return
*/
private String encodeHeaderAttributeValue(String value) {
return value;
}
/**
* Given arbitrary text, returns a base64-encoded version of the text's HMAC using the
* previously specified secret key.
*
* The text is decoded as UTF-8 for hashing, and the result is base64-encoded.
*
* @param text
* @return
* @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm
* @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256
*/
private String sign(String text) throws NoSuchAlgorithmException,InvalidKeyException, UnsupportedEncodingException {
return new String(Base64.encodeBase64(credentials.signature(text.getBytes("UTF-8"))));
}
}