| // 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(); |
| } |
| } |