001: /*
002: * Copyright 2005-2007 Noelios Consulting.
003: *
004: * The contents of this file are subject to the terms of the Common Development
005: * and Distribution License (the "License"). You may not use this file except in
006: * compliance with the License.
007: *
008: * You can obtain a copy of the license at
009: * http://www.opensource.org/licenses/cddl1.txt See the License for the specific
010: * language governing permissions and limitations under the License.
011: *
012: * When distributing Covered Code, include this CDDL HEADER in each file and
013: * include the License file at http://www.opensource.org/licenses/cddl1.txt If
014: * applicable, add the following below this CDDL HEADER, with the fields
015: * enclosed by brackets "[]" replaced with your own identifying information:
016: * Portions Copyright [yyyy] [name of copyright owner]
017: */
018:
019: package com.noelios.restlet.util;
020:
021: import java.io.UnsupportedEncodingException;
022: import java.security.InvalidKeyException;
023: import java.security.NoSuchAlgorithmException;
024: import java.util.Date;
025: import java.util.SortedMap;
026: import java.util.TreeMap;
027: import java.util.Map.Entry;
028: import java.util.logging.Level;
029: import java.util.logging.Logger;
030:
031: import javax.crypto.Mac;
032: import javax.crypto.spec.SecretKeySpec;
033:
034: import org.restlet.data.ChallengeRequest;
035: import org.restlet.data.ChallengeResponse;
036: import org.restlet.data.ChallengeScheme;
037: import org.restlet.data.Form;
038: import org.restlet.data.Method;
039: import org.restlet.data.Parameter;
040: import org.restlet.data.Reference;
041: import org.restlet.data.Request;
042: import org.restlet.util.DateUtils;
043: import org.restlet.util.Series;
044:
045: import com.noelios.restlet.Engine;
046: import com.noelios.restlet.http.HttpConstants;
047:
048: /**
049: * Security data manipulation utilities.
050: *
051: * @author Jerome Louvel (contact@noelios.com)
052: */
053: public class SecurityUtils {
054: /**
055: * Formats a challenge request as a HTTP header value.
056: *
057: * @param request
058: * The challenge request to format.
059: * @return The authenticate header value.
060: */
061: public static String format(ChallengeRequest request) {
062: StringBuilder sb = new StringBuilder();
063: sb.append(request.getScheme().getTechnicalName());
064:
065: if (request.getRealm() != null) {
066: sb.append(" realm=\"").append(request.getRealm()).append(
067: '"');
068: }
069:
070: return sb.toString();
071: }
072:
073: /**
074: * Formats a challenge response as raw credentials.
075: *
076: * @param challenge
077: * The challenge response to format.
078: * @param request
079: * The parent request.
080: * @param httpHeaders
081: * The current request HTTP headers.
082: * @return The authorization header value.
083: */
084: public static String format(ChallengeResponse challenge,
085: Request request, Series<Parameter> httpHeaders) {
086: StringBuilder sb = new StringBuilder();
087: sb.append(challenge.getScheme().getTechnicalName()).append(' ');
088:
089: String secret = (challenge.getSecret() == null) ? null
090: : new String(challenge.getSecret());
091:
092: if (challenge.getCredentials() != null) {
093: sb.append(challenge.getCredentials());
094: } else if (challenge.getScheme().equals(
095: ChallengeScheme.HTTP_AWS)) {
096: // Setup the method name
097: String methodName = request.getMethod().getName();
098:
099: // Setup the Date header
100: String date = "";
101:
102: if (httpHeaders.getFirstValue("X-Amz-Date", true) == null) {
103: // X-Amz-Date header didn't override the standard Date header
104: date = httpHeaders.getFirstValue(
105: HttpConstants.HEADER_DATE, true);
106: if (date == null) {
107: // Add a fresh Date header
108: date = DateUtils.format(new Date(),
109: DateUtils.FORMAT_RFC_1123.get(0));
110: httpHeaders.add(HttpConstants.HEADER_DATE, date);
111: }
112: }
113:
114: // Setup the ContentType header
115: String contentMd5 = httpHeaders.getFirstValue(
116: HttpConstants.HEADER_CONTENT_MD5, true);
117: if (contentMd5 == null)
118: contentMd5 = "";
119:
120: // Setup the ContentType header
121: String contentType = httpHeaders.getFirstValue(
122: HttpConstants.HEADER_CONTENT_TYPE, true);
123: if (contentType == null) {
124: boolean applyPatch = false;
125:
126: // This patch seems to apply to Sun JVM only.
127: String jvmVendor = System.getProperty("java.vm.vendor");
128: if (jvmVendor != null
129: && (jvmVendor.toLowerCase()).startsWith("sun")) {
130: int majorVersionNumber = Engine
131: .getJavaMajorVersion();
132: int minorVersionNumber = Engine
133: .getJavaMinorVersion();
134:
135: if (majorVersionNumber == 1) {
136: if (minorVersionNumber < 5) {
137: applyPatch = true;
138: } else if (minorVersionNumber == 5) {
139: // Sun fixed the bug in update 10
140: applyPatch = (Engine.getJavaUpdateVersion() < 10);
141: }
142: }
143: }
144:
145: if (applyPatch
146: && !request.getMethod().equals(Method.PUT)) {
147: contentType = "application/x-www-form-urlencoded";
148: } else {
149: contentType = "";
150: }
151: }
152:
153: // Setup the canonicalized AmzHeaders
154: String canonicalizedAmzHeaders = getCanonicalizedAmzHeaders(httpHeaders);
155:
156: // Setup the canonicalized resource name
157: String canonicalizedResource = getCanonicalizedResourceName(request
158: .getResourceRef());
159:
160: // Setup the message part
161: StringBuilder rest = new StringBuilder();
162: rest.append(methodName).append('\n').append(contentMd5)
163: .append('\n').append(contentType).append('\n')
164: .append(date).append('\n').append(
165: canonicalizedAmzHeaders).append(
166: canonicalizedResource);
167:
168: // Append the AWS credentials
169: sb.append(challenge.getIdentifier()).append(':').append(
170: Base64.encodeBytes(toHMac(rest.toString(), secret),
171: Base64.DONT_BREAK_LINES));
172: } else if (challenge.getScheme().equals(
173: ChallengeScheme.HTTP_BASIC)) {
174: try {
175: String credentials = challenge.getIdentifier() + ':'
176: + secret;
177: sb
178: .append(Base64.encodeBytes(credentials
179: .getBytes("US-ASCII"),
180: Base64.DONT_BREAK_LINES));
181: } catch (UnsupportedEncodingException e) {
182: throw new RuntimeException(
183: "Unsupported encoding, unable to encode credentials");
184: }
185: } else if (challenge.getScheme().equals(
186: ChallengeScheme.SMTP_PLAIN)) {
187: try {
188: String credentials = "^@" + challenge.getIdentifier()
189: + "^@" + secret;
190: sb
191: .append(Base64.encodeBytes(credentials
192: .getBytes("US-ASCII"),
193: Base64.DONT_BREAK_LINES));
194: } catch (UnsupportedEncodingException e) {
195: throw new RuntimeException(
196: "Unsupported encoding, unable to encode credentials");
197: }
198: } else {
199: throw new IllegalArgumentException(
200: "Challenge scheme not supported by this implementation, or credentials not set for custom schemes.");
201: }
202:
203: return sb.toString();
204: }
205:
206: /**
207: * Returns the canonicalized AMZ headers.
208: *
209: * @param requestHeaders
210: * The list of request headers.
211: * @return The canonicalized AMZ headers.
212: */
213: private static String getCanonicalizedAmzHeaders(
214: Series<Parameter> requestHeaders) {
215: // Filter out all the AMZ headers required for AWS authentication
216: SortedMap<String, String> amzHeaders = new TreeMap<String, String>();
217: String headerName;
218: for (Parameter param : requestHeaders) {
219: headerName = param.getName().toLowerCase();
220: if (headerName.startsWith("x-amz-")) {
221: if (!amzHeaders.containsKey(headerName)) {
222: amzHeaders.put(headerName, requestHeaders
223: .getValues(headerName));
224: }
225: }
226: }
227:
228: // Concatenate all AMZ headers
229: StringBuilder sb = new StringBuilder();
230: for (Entry<String, String> entry : amzHeaders.entrySet()) {
231: sb.append(entry.getKey()).append(':').append(
232: entry.getValue()).append("\n");
233: }
234:
235: return sb.toString();
236: }
237:
238: /**
239: * Returns the canonicalized resource name.
240: *
241: * @param resourceRef
242: * The resource reference.
243: * @return The canonicalized resource name.
244: */
245: private static String getCanonicalizedResourceName(
246: Reference resourceRef) {
247: StringBuilder sb = new StringBuilder();
248: sb.append(resourceRef.getPath());
249:
250: Form query = resourceRef.getQueryAsForm();
251: if (query.getFirst("acl", true) != null) {
252: sb.append("?acl");
253: } else if (query.getFirst("torrent", true) != null) {
254: sb.append("?torrent");
255: }
256:
257: return sb.toString();
258: }
259:
260: /**
261: * Parses an authenticate header into a challenge request.
262: *
263: * @param header
264: * The HTTP header value to parse.
265: * @return The parsed challenge request.
266: */
267: public static ChallengeRequest parseRequest(String header) {
268: ChallengeRequest result = null;
269:
270: if (header != null) {
271: int space = header.indexOf(' ');
272:
273: if (space != -1) {
274: String scheme = header.substring(0, space);
275: String realm = header.substring(space + 1);
276: int equals = realm.indexOf('=');
277: String realmValue = realm.substring(equals + 2, realm
278: .length() - 1);
279: result = new ChallengeRequest(new ChallengeScheme(
280: "HTTP_" + scheme, scheme), realmValue);
281: }
282: }
283:
284: return result;
285: }
286:
287: /**
288: * Parses an authorization header into a challenge response.
289: *
290: * @param request
291: * The request.
292: * @param logger
293: * The logger to use.
294: * @param header
295: * The header value to parse.
296: * @return The parsed challenge response.
297: */
298: public static ChallengeResponse parseResponse(Request request,
299: Logger logger, String header) {
300: ChallengeResponse result = null;
301:
302: if (header != null) {
303: int space = header.indexOf(' ');
304:
305: if (space != -1) {
306: String scheme = header.substring(0, space);
307: String credentials = header.substring(space + 1);
308: result = new ChallengeResponse(new ChallengeScheme(
309: "HTTP_" + scheme, scheme), credentials);
310:
311: if (result.getScheme().equals(
312: ChallengeScheme.HTTP_BASIC)) {
313: try {
314: byte[] credentialsEncoded = Base64
315: .decode(result.getCredentials());
316: if (credentialsEncoded == null) {
317: logger
318: .warning("Cannot decode credentials: "
319: + result.getCredentials());
320: return null;
321: }
322:
323: credentials = new String(credentialsEncoded,
324: "US-ASCII");
325: int separator = credentials.indexOf(':');
326:
327: if (separator == -1) {
328: // Log the blocking
329: logger
330: .warning("Invalid credentials given by client with IP: "
331: + ((request != null) ? request
332: .getClientInfo()
333: .getAddress()
334: : "?"));
335: } else {
336: result.setIdentifier(credentials.substring(
337: 0, separator));
338: result.setSecret(credentials
339: .substring(separator + 1));
340:
341: // Log the authentication result
342: if (logger != null) {
343: logger
344: .info("Basic HTTP authentication succeeded: identifier="
345: + result
346: .getIdentifier()
347: + ".");
348: }
349: }
350: } catch (UnsupportedEncodingException e) {
351: logger.log(Level.WARNING,
352: "Unsupported encoding error", e);
353: }
354: } else {
355: // Authentication impossible, scheme not supported
356: logger
357: .log(
358: Level.FINE,
359: "Authentication impossible: scheme not supported: "
360: + result.getScheme()
361: .getName()
362: + ". Please override the Guard.authenticate method.");
363: }
364: }
365: }
366:
367: return result;
368: }
369:
370: /**
371: * Converts a source string to its HMAC/SHA-1 value.
372: *
373: * @param source
374: * The source string to convert.
375: * @param secretKey
376: * The secret key to use for conversion.
377: * @return The HMac value of the source string.
378: */
379: public static byte[] toHMac(String source, String secretKey) {
380: byte[] result = null;
381:
382: try {
383: // Create the HMAC/SHA1 key
384: SecretKeySpec signingKey = new SecretKeySpec(secretKey
385: .getBytes(), "HmacSHA1");
386:
387: // Create the message authentication code (MAC)
388: Mac mac = Mac.getInstance("HmacSHA1");
389: mac.init(signingKey);
390:
391: // Compute the HMAC value
392: result = mac.doFinal(source.getBytes());
393: } catch (NoSuchAlgorithmException nsae) {
394: throw new RuntimeException(
395: "Could not find the SHA-1 algorithm. HMac conversion failed.",
396: nsae);
397: } catch (InvalidKeyException ike) {
398: throw new RuntimeException(
399: "Invalid key exception detected. HMac conversion failed.",
400: ike);
401: }
402:
403: return result;
404: }
405: }
|