package com.xiaomi.infra.galaxy.fds.client.auth.signature; import java.net.URI; import java.net.URISyntaxException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import com.google.common.base.Preconditions; import com.google.common.collect.LinkedListMultimap; import com.sun.org.apache.xerces.internal.impl.dv.util.Base64; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.xiaomi.infra.galaxy.fds.client.model.SubResource; import com.xiaomi.infra.galaxy.fds.client.auth.Common; import com.xiaomi.infra.galaxy.fds.client.model.HttpMethod; import com.xiaomi.infra.galaxy.fds.client.auth.Utils; import com.xiaomi.infra.galaxy.fds.client.auth.XiaomiHeader; import com.xiaomi.infra.galaxy.fds.client.exception.GalaxyException; public class Signer { private static final Log LOG = LogFactory.getLog(Signer.class); private static final Set<String> SUB_RESOURCE_SET = new HashSet<String>(); private static final String XIAOMI_DATE = XiaomiHeader.DATE.getName(); static { for (SubResource r : SubResource.values()) { SUB_RESOURCE_SET.add(r.getName()); } } /** * Sign the specified http request. * * @param httpMethod The http request method({@link #HttpMethod}) * @param uri The uri string * @param httpHeaders The http request headers * @param secretAccessKeyId The user's secret access key * @param algorithm The sign algorithm({@link #SignAlgorithm}) * @return Byte buffer of the signed result * @throws NoSuchAlgorithmException * @throws InvalidKeyException * @throws URISyntaxException */ public static byte[] sign(HttpMethod httpMethod, URI uri, LinkedListMultimap<String, String> httpHeaders, String secretAccessKeyId, SignAlgorithm algorithm) throws NoSuchAlgorithmException, InvalidKeyException { Preconditions.checkNotNull(httpMethod); Preconditions.checkNotNull(uri); Preconditions.checkNotNull(secretAccessKeyId); Preconditions.checkNotNull(algorithm); String stringToSign = constructStringToSign(httpMethod, uri, httpHeaders); if (LOG.isDebugEnabled()) { LOG.debug("Sign for request: " + httpMethod + " " + uri + ", stringToSign=" + stringToSign); } Mac mac = Mac.getInstance(algorithm.name()); mac.init(new SecretKeySpec(secretAccessKeyId.getBytes(), algorithm.name())); return mac.doFinal(stringToSign.getBytes()); } /** * A handy version of {@link #sign(HttpMethod, String, LinkedListMultimap<String, String>, * String, SignAlgorithm)}, generates base64 encoded sign result. */ public static byte[] signToBase64(HttpMethod httpMethod, URI uri, LinkedListMultimap<String, String> httpHeaders, String secretAccessKeyId, SignAlgorithm algorithm) throws NoSuchAlgorithmException, InvalidKeyException { return Base64.encode(sign(httpMethod, uri, httpHeaders, secretAccessKeyId, algorithm)).getBytes(); } public static URI generatePresignedUri(String baseUri, String bucketName, String objectName, List<String> subResources, Date expiration, HttpMethod httpMethod, String accessId, String accessSecret, SignAlgorithm signAlgorithm) throws GalaxyException { try { URI uri = new URI(baseUri); URI encodedUri; if (subResources == null || subResources.isEmpty()) { encodedUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), "/" + bucketName + "/" + objectName, Common.GALAXY_ACCESS_KEY_ID + "=" + accessId + "&" + Common.EXPIRES + "=" + expiration.getTime(), null); } else { encodedUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), "/" + bucketName + "/" + objectName, StringUtils.join(subResources, "&") + "&" + Common.GALAXY_ACCESS_KEY_ID + "=" + accessId + "&" + Common.EXPIRES + "=" + expiration.getTime(), null); } byte[] signature = Signer.signToBase64(httpMethod, encodedUri, null, accessSecret, signAlgorithm); return new URI(encodedUri.toString() + "&" + Common.SIGNATURE + "=" + new String(signature)); } catch (URISyntaxException e) { LOG.error("Invalid URI syntax", e); throw new GalaxyException("Invalid URI syntax", e); } catch (InvalidKeyException e) { LOG.error("Invalid secret key spec", e); throw new GalaxyException("Invalid secret key spec", e); } catch (NoSuchAlgorithmException e) { LOG.error("Unsupported signature algorithm:" + signAlgorithm, e); throw new GalaxyException("Unsupported signature algorithm:" + signAlgorithm, e); } } static String constructStringToSign(HttpMethod httpMethod, URI uri, LinkedListMultimap<String, String> httpHeaders) { StringBuilder builder = new StringBuilder(); builder.append(httpMethod.name()).append("\n"); builder.append(checkAndGet(httpHeaders, Common.CONTENT_MD5).get(0)).append("\n"); builder.append(checkAndGet(httpHeaders, Common.CONTENT_TYPE).get(0)).append("\n"); long expires = 0; if ((expires = getExpires(uri)) > 0) { // For pre-signed URI builder.append(expires).append("\n"); } else { String xiaomiDate = checkAndGet(httpHeaders, XIAOMI_DATE).get(0); String date = ""; if ("".equals(xiaomiDate)) { date = checkAndGet(httpHeaders, Common.DATE).get(0); } builder.append(checkAndGet(date)).append("\n"); } builder.append(canonicalizeXiaomiHeaders(httpHeaders)); builder.append(canonicalizeResource(uri)); return builder.toString(); } static String canonicalizeXiaomiHeaders( LinkedListMultimap<String, String> headers) { if (headers == null) { return ""; } // 1. Sort the header and merge the values Map<String, String> sortedHeaders = new TreeMap<String, String>(); for (Entry<String, String> entry : headers.entries()) { String key = entry.getKey().toLowerCase(); if (!key.startsWith(Common.XIAOMI_HEADER_PREFIX)) { continue; } String value = entry.getValue(); String oldValue = sortedHeaders.get(key); if (oldValue== null) { sortedHeaders.put(key, value); } else { StringBuilder builder = new StringBuilder(); builder.append(oldValue).append(",").append(value); sortedHeaders.put(key, builder.toString()); } } // 2. TODO(wuzesheng) Unfold multiple lines long header // 3. Generate the canonicalized result StringBuilder result = new StringBuilder(); for (Entry<String, String> entry : sortedHeaders.entrySet()) { result.append(entry.getKey()).append(":") .append(entry.getValue()).append("\n"); } return result.toString(); } static String canonicalizeResource(URI uri) { StringBuilder result = new StringBuilder(); result.append(uri.getPath()); // 1. Parse and sort subresources TreeMap<String, String> sortedParams = new TreeMap<String, String>(); LinkedListMultimap<String, String> params = Utils.parseUriParameters(uri); for (Entry<String, String> entry : params.entries()) { String key = entry.getKey(); String value = entry.getValue(); if (SUB_RESOURCE_SET.contains(key)) { sortedParams.put(key, value); } } // 2. Generate the canonicalized result if (!sortedParams.isEmpty()) { result.append("?"); boolean isFirst = true; for (Entry<String, String> entry : sortedParams.entrySet()) { if (isFirst) { isFirst = false; result.append(entry.getKey()); } else { result.append("&").append(entry.getKey()); } if (!entry.getValue().isEmpty()) { result.append("=").append(entry.getValue()); } } } return result.toString(); } static String checkAndGet(String name) { return name == null ? "" : name; } static List<String> checkAndGet(LinkedListMultimap<String, String> headers, String header) { List<String> result = new LinkedList<String>(); if (headers == null) { result.add(""); return result; } List<String> values = headers.get(header); if (values == null || values.isEmpty()) { result.add(""); return result; } return values; } static long getExpires(URI uri) { LinkedListMultimap<String, String> params = Utils.parseUriParameters(uri); List<String> expires = params.get(Common.EXPIRES); if (!expires.isEmpty()) { return Long.parseLong(expires.get(0)); } return 0; } /** * The sign algorithm supported by Galaxy Rest Server * * Note: * The algorithm name must be one of the javax.crypto.Mac stand names. * Users can refer to the following page to see all the stand names: * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html#Mac"> * Java Cryptography Architecture Standard Algorithm Name Documentation</a> */ public enum SignAlgorithm { HmacMD5, HmacSHA1, HmacSHA256; } }