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