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