Java tutorial
// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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 // // http://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 com.cloud.bridge.util; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.SignatureException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collection; import java.util.Iterator; import java.util.TreeMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; public class EC2RestAuth { protected final static Logger logger = Logger.getLogger(RestAuth.class); // TreeMap: used to Sort the UTF-8 query string components by parameter name with natural byte ordering protected TreeMap<String, String> queryParts = null; // used to generate a CanonicalizedQueryString protected String canonicalizedQueryString = null; protected String hostHeader = null; protected String httpRequestURI = null; public EC2RestAuth() { // these must be lexicographically sorted queryParts = new TreeMap<String, String>(); } public static Calendar parseDateString(String created) { DateFormat formatter = null; Calendar cal = Calendar.getInstance(); // -> for some unknown reason SimpleDateFormat does not properly handle the 'Z' timezone if (created.endsWith("Z")) created = created.replace("Z", "+0000"); try { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } try { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } // -> the time zone is GMT if not defined try { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); cal.setTime(formatter.parse(created)); created = created + "+0000"; formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } // -> including millseconds? try { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.Sz"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } try { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SZ"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } // -> the CloudStack API used to return this format for some calls try { formatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); cal.setTime(formatter.parse(created)); return cal; } catch (Exception e) { } return null; } /** * Assuming that a port number is to be included. * * @param header - contents of the "Host:" header, skipping the 'Host:' preamble. */ public void setHostHeader(String hostHeader) { if (null == hostHeader) this.hostHeader = null; else this.hostHeader = hostHeader.trim().toLowerCase(); } public void setHTTPRequestURI(String uri) { if (null == uri || 0 == uri.length()) this.httpRequestURI = new String("/"); else this.httpRequestURI = uri.trim(); } /** * The given query string needs to be pulled apart, sorted by paramter name, and reconstructed. * We sort the query string values via a TreeMap. * * @param query - this string still has all URL encoding in place. */ public void setQueryString(String query) { String parameter = null; if (null == query) { this.canonicalizedQueryString = null; return; } // -> sort by paramter name String[] parts = query.split("&"); if (null != parts) { for (int i = 0; i < parts.length; i++) { parameter = parts[i]; if (parameter.startsWith("?")) parameter = parameter.substring(1); // -> don't include a 'Signature=' parameter if (parameter.startsWith("Signature=")) continue; int offset = parameter.indexOf("="); if (-1 == offset) queryParts.put(parameter, parameter + "="); else queryParts.put(parameter.substring(0, offset), parameter); } } // -> reconstruct into a canonicalized format Collection<String> headers = queryParts.values(); Iterator<String> itr = headers.iterator(); StringBuffer reconstruct = new StringBuffer(); int count = 0; while (itr.hasNext()) { if (0 < count) reconstruct.append("&"); reconstruct.append(itr.next()); count++; } canonicalizedQueryString = reconstruct.toString(); } /** * The request is authenticated if we can regenerate the same signature given * on the request. Before calling this function make sure to set the header values * defined by the public values above. * * @param httpVerb - the type of HTTP request (e.g., GET, PUT) * @param secretKey - value obtained from the AWSAccessKeyId * @param signature - the signature we are trying to recreate, note can be URL-encoded * @param method - { "HmacSHA1", "HmacSHA256" } * * @throws SignatureException * * @return true if request has been authenticated, false otherwise * @throws UnsupportedEncodingException */ public boolean verifySignature(String httpVerb, String secretKey, String signature, String method) throws SignatureException, UnsupportedEncodingException { if (null == httpVerb || null == secretKey || null == signature) return false; httpVerb = httpVerb.trim(); secretKey = secretKey.trim(); signature = signature.trim(); // -> first calculate the StringToSign after the caller has initialized all the header values String StringToSign = genStringToSign(httpVerb); String calSig = calculateRFC2104HMAC(StringToSign, secretKey, method.equalsIgnoreCase("HmacSHA1")); // -> the passed in signature is defined to be URL encoded? (and it must be base64 encoded) int offset = signature.indexOf("%"); if (-1 != offset) signature = URLDecoder.decode(signature, "UTF-8"); boolean match = signature.equals(calSig); if (!match) logger.error("Signature mismatch, [" + signature + "] [" + calSig + "] over [" + StringToSign + "]"); return match; } /** * This function generates the single string that will be used to sign with a users * secret key. * * StringToSign = HTTP-Verb + "\n" + * ValueOfHostHeaderInLowercase + "\n" + * HTTPRequestURI + "\n" + * CanonicalizedQueryString * * @return The single StringToSign or null. */ private String genStringToSign(String httpVerb) { StringBuffer stringToSign = new StringBuffer(); stringToSign.append(httpVerb).append("\n"); if (null != this.hostHeader) stringToSign.append(this.hostHeader); stringToSign.append("\n"); if (null != this.httpRequestURI) stringToSign.append(this.httpRequestURI); stringToSign.append("\n"); if (null != this.canonicalizedQueryString) stringToSign.append(this.canonicalizedQueryString); if (0 == stringToSign.length()) return null; else return stringToSign.toString(); } /** * Create a signature by the following method: * new String( Base64( SHA1 or SHA256 ( key, byte array ))) * * @param signIt - the data to generate a keyed HMAC over * @param secretKey - the user's unique key for the HMAC operation * @param useSHA1 - if false use SHA256 * @return String - the recalculated string * @throws SignatureException */ private String calculateRFC2104HMAC(String signIt, String secretKey, boolean useSHA1) throws SignatureException { SecretKeySpec key = null; Mac hmacShaAlg = null; String result = null; try { if (useSHA1) { key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); hmacShaAlg = Mac.getInstance("HmacSHA1"); } else { key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); hmacShaAlg = Mac.getInstance("HmacSHA256"); } hmacShaAlg.init(key); byte[] rawHmac = hmacShaAlg.doFinal(signIt.getBytes()); result = new String(Base64.encodeBase64(rawHmac)); } catch (Exception e) { throw new SignatureException("Failed to generate keyed HMAC on REST request: " + e.getMessage()); } return result.trim(); } }