Home | History | Annotate | Download | only in signature
      1 /*
      2  * Copyright 2007 Netflix, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package net.oauth.signature;
     18 
     19 import java.io.IOException;
     20 import java.net.URI;
     21 import java.net.URISyntaxException;
     22 import java.util.ArrayList;
     23 import java.util.Collection;
     24 import java.util.Collections;
     25 import java.util.List;
     26 import java.util.Map;
     27 import java.util.concurrent.ConcurrentHashMap;
     28 import net.oauth.OAuth;
     29 import net.oauth.OAuthAccessor;
     30 import net.oauth.OAuthConsumer;
     31 import net.oauth.OAuthException;
     32 import net.oauth.OAuthMessage;
     33 import net.oauth.OAuthProblemException;
     34 // BEGIN android-changed
     35 // import org.apache.commons.codec.binary.Base64;
     36 import android.util.Base64;
     37 // END android-changed
     38 
     39 /**
     40  * A pair of algorithms for computing and verifying an OAuth digital signature.
     41  *
     42  * @author John Kristian
     43  * @hide
     44  */
     45 public abstract class OAuthSignatureMethod {
     46 
     47     /** Add a signature to the message.
     48      * @throws URISyntaxException
     49      * @throws IOException */
     50     public void sign(OAuthMessage message)
     51     throws OAuthException, IOException, URISyntaxException {
     52         message.addParameter(new OAuth.Parameter("oauth_signature",
     53                 getSignature(message)));
     54     }
     55 
     56     /**
     57      * Check whether the message has a valid signature.
     58      * @throws URISyntaxException
     59      *
     60      * @throws OAuthProblemException
     61      *             the signature is invalid
     62      */
     63     public void validate(OAuthMessage message)
     64     throws IOException, OAuthException, URISyntaxException {
     65         message.requireParameters("oauth_signature");
     66         String signature = message.getSignature();
     67         String baseString = getBaseString(message);
     68         if (!isValid(signature, baseString)) {
     69             OAuthProblemException problem = new OAuthProblemException(
     70                     "signature_invalid");
     71             problem.setParameter("oauth_signature", signature);
     72             problem.setParameter("oauth_signature_base_string", baseString);
     73             problem.setParameter("oauth_signature_method", message
     74                     .getSignatureMethod());
     75             throw problem;
     76         }
     77     }
     78 
     79     protected String getSignature(OAuthMessage message)
     80     throws OAuthException, IOException, URISyntaxException {
     81         String baseString = getBaseString(message);
     82         String signature = getSignature(baseString);
     83         // Logger log = Logger.getLogger(getClass().getName());
     84         // if (log.isLoggable(Level.FINE)) {
     85         // log.fine(signature + "=getSignature(" + baseString + ")");
     86         // }
     87         return signature;
     88     }
     89 
     90     protected void initialize(String name, OAuthAccessor accessor)
     91             throws OAuthException {
     92         String secret = accessor.consumer.consumerSecret;
     93         if (name.endsWith(_ACCESSOR)) {
     94             // This code supports the 'Accessor Secret' extensions
     95             // described in http://oauth.pbwiki.com/AccessorSecret
     96             final String key = OAuthConsumer.ACCESSOR_SECRET;
     97             Object accessorSecret = accessor.getProperty(key);
     98             if (accessorSecret == null) {
     99                 accessorSecret = accessor.consumer.getProperty(key);
    100             }
    101             if (accessorSecret != null) {
    102                 secret = accessorSecret.toString();
    103             }
    104         }
    105         if (secret == null) {
    106             secret = "";
    107         }
    108         setConsumerSecret(secret);
    109     }
    110 
    111     public static final String _ACCESSOR = "-Accessor";
    112 
    113     /** Compute the signature for the given base string. */
    114     protected abstract String getSignature(String baseString) throws OAuthException;
    115 
    116     /** Decide whether the signature is valid. */
    117     protected abstract boolean isValid(String signature, String baseString)
    118             throws OAuthException;
    119 
    120     private String consumerSecret;
    121 
    122     private String tokenSecret;
    123 
    124     protected String getConsumerSecret() {
    125         return consumerSecret;
    126     }
    127 
    128     protected void setConsumerSecret(String consumerSecret) {
    129         this.consumerSecret = consumerSecret;
    130     }
    131 
    132     public String getTokenSecret() {
    133         return tokenSecret;
    134     }
    135 
    136     public void setTokenSecret(String tokenSecret) {
    137         this.tokenSecret = tokenSecret;
    138     }
    139 
    140     public static String getBaseString(OAuthMessage message)
    141             throws IOException, URISyntaxException {
    142         List<Map.Entry<String, String>> parameters;
    143         String url = message.URL;
    144         int q = url.indexOf('?');
    145         if (q < 0) {
    146             parameters = message.getParameters();
    147         } else {
    148             // Combine the URL query string with the other parameters:
    149             parameters = new ArrayList<Map.Entry<String, String>>();
    150             parameters.addAll(OAuth.decodeForm(message.URL.substring(q + 1)));
    151             parameters.addAll(message.getParameters());
    152             url = url.substring(0, q);
    153         }
    154         return OAuth.percentEncode(message.method.toUpperCase()) + '&'
    155                 + OAuth.percentEncode(normalizeUrl(url)) + '&'
    156                 + OAuth.percentEncode(normalizeParameters(parameters));
    157     }
    158 
    159     protected static String normalizeUrl(String url) throws URISyntaxException {
    160         URI uri = new URI(url);
    161         String scheme = uri.getScheme().toLowerCase();
    162         String authority = uri.getAuthority().toLowerCase();
    163         boolean dropPort = (scheme.equals("http") && uri.getPort() == 80)
    164                            || (scheme.equals("https") && uri.getPort() == 443);
    165         if (dropPort) {
    166             // find the last : in the authority
    167             int index = authority.lastIndexOf(":");
    168             if (index >= 0) {
    169                 authority = authority.substring(0, index);
    170             }
    171         }
    172         String path = uri.getRawPath();
    173         if (path == null || path.length() <= 0) {
    174             path = "/"; // conforms to RFC 2616 section 3.2.2
    175         }
    176         // we know that there is no query and no fragment here.
    177         return scheme + "://" + authority + path;
    178     }
    179 
    180     protected static String normalizeParameters(
    181             Collection<? extends Map.Entry> parameters) throws IOException {
    182         if (parameters == null) {
    183             return "";
    184         }
    185         List<ComparableParameter> p = new ArrayList<ComparableParameter>(
    186                 parameters.size());
    187         for (Map.Entry parameter : parameters) {
    188             if (!"oauth_signature".equals(parameter.getKey())) {
    189                 p.add(new ComparableParameter(parameter));
    190             }
    191         }
    192         Collections.sort(p);
    193         return OAuth.formEncode(getParameters(p));
    194     }
    195 
    196     // BEGIN android-changed
    197     public static byte[] decodeBase64(String s) {
    198         return Base64.decode(s, Base64.DEFAULT);
    199     }
    200 
    201     public static String base64Encode(byte[] b) {
    202         return Base64.encodeToString(b, Base64.DEFAULT);
    203     }
    204     // END android-changed
    205 
    206     public static OAuthSignatureMethod newSigner(OAuthMessage message,
    207             OAuthAccessor accessor) throws IOException, OAuthException {
    208         message.requireParameters(OAuth.OAUTH_SIGNATURE_METHOD);
    209         OAuthSignatureMethod signer = newMethod(message.getSignatureMethod(),
    210                 accessor);
    211         signer.setTokenSecret(accessor.tokenSecret);
    212         return signer;
    213     }
    214 
    215     /** The factory for signature methods. */
    216     public static OAuthSignatureMethod newMethod(String name,
    217             OAuthAccessor accessor) throws OAuthException {
    218         try {
    219             Class methodClass = NAME_TO_CLASS.get(name);
    220             if (methodClass != null) {
    221                 OAuthSignatureMethod method = (OAuthSignatureMethod) methodClass
    222                 .newInstance();
    223                 method.initialize(name, accessor);
    224                 return method;
    225             }
    226             OAuthProblemException problem = new OAuthProblemException(
    227             "signature_method_rejected");
    228             String acceptable = OAuth.percentEncode(NAME_TO_CLASS.keySet());
    229             if (acceptable.length() > 0) {
    230                 problem.setParameter("oauth_acceptable_signature_methods",
    231                         acceptable.toString());
    232             }
    233             throw problem;
    234         } catch (InstantiationException e) {
    235             throw new OAuthException(e);
    236         } catch (IllegalAccessException e) {
    237             throw new OAuthException(e);
    238         }
    239     }
    240 
    241     /**
    242      * Subsequently, newMethod(name) will attempt to instantiate the given
    243      * class, with no constructor parameters.
    244      */
    245     public static void registerMethodClass(String name, Class clazz) {
    246         NAME_TO_CLASS.put(name, clazz);
    247     }
    248 
    249     private static final Map<String, Class> NAME_TO_CLASS = new ConcurrentHashMap<String, Class>();
    250     static {
    251         registerMethodClass("HMAC-SHA1", HMAC_SHA1.class);
    252         registerMethodClass("PLAINTEXT", PLAINTEXT.class);
    253         registerMethodClass("RSA-SHA1", RSA_SHA1.class);
    254         registerMethodClass("HMAC-SHA1" + _ACCESSOR, HMAC_SHA1.class);
    255         registerMethodClass("PLAINTEXT" + _ACCESSOR, PLAINTEXT.class);
    256     }
    257 
    258     /** An efficiently sortable wrapper around a parameter. */
    259     private static class ComparableParameter implements
    260             Comparable<ComparableParameter> {
    261 
    262         ComparableParameter(Map.Entry value) {
    263             this.value = value;
    264             String n = toString(value.getKey());
    265             String v = toString(value.getValue());
    266             this.key = OAuth.percentEncode(n) + ' ' + OAuth.percentEncode(v);
    267             // ' ' is used because it comes before any character
    268             // that can appear in a percentEncoded string.
    269         }
    270 
    271         final Map.Entry value;
    272 
    273         private final String key;
    274 
    275         private static String toString(Object from) {
    276             return (from == null) ? null : from.toString();
    277         }
    278 
    279         public int compareTo(ComparableParameter that) {
    280             return this.key.compareTo(that.key);
    281         }
    282 
    283         @Override
    284         public String toString() {
    285             return key;
    286         }
    287 
    288     }
    289 
    290     /** Retrieve the original parameters from a sorted collection. */
    291     private static List<Map.Entry> getParameters(
    292             Collection<ComparableParameter> parameters) {
    293         if (parameters == null) {
    294             return null;
    295         }
    296         List<Map.Entry> list = new ArrayList<Map.Entry>(parameters.size());
    297         for (ComparableParameter parameter : parameters) {
    298             list.add(parameter.value);
    299         }
    300         return list;
    301     }
    302 
    303 }
    304