| /* | |
| * Copyright (C) 2011 Citrix Systems, Inc. All rights reserved. | |
| * | |
| * 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 | |
| * | |
| * 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(); | |
| } | |
| } |