package com.xiaomi.infra.galaxy.client.authentication.signature;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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 com.xiaomi.infra.galaxy.client.authentication.HttpKeys;
import com.xiaomi.infra.galaxy.client.authentication.HttpMethod;
public class Signer {
private static final Set<String> SUB_RESOURCE_SET = new HashSet<String>();
private static final String XIAOMI_DATE = HttpKeys.MI_DATE;
static {
for (SubResource r : SubResource.values()) {
SUB_RESOURCE_SET.add(r.getName());
}
}
/**
* Sign the specified http request.
*
* @param httpMethod The http request method
* @param uri The uri string
* @param httpHeaders The http request headers
* @param secretAccessKeyId The user's secret access key
* @param algorithm The sign algorithm
* @return Byte buffer of the signed result
* @throws java.security.NoSuchAlgorithmException
* @throws java.security.InvalidKeyException
*/
public static byte[] sign(HttpMethod httpMethod, URI uri,
LinkedListMultimap<String, String> httpHeaders, String secretAccessKeyId,
SignAlgorithm algorithm) throws NoSuchAlgorithmException,
InvalidKeyException {
Preconditions.checkNotNull(httpMethod, "Http method");
Preconditions.checkNotNull(uri, "uri");
Preconditions.checkNotNull(secretAccessKeyId, "secret access key");
Preconditions.checkNotNull(algorithm, "algorithm");
String stringToSign = constructStringToSign(httpMethod,
uri, httpHeaders);
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, java.net.URI, LinkedListMultimap,
* String, SignAlgorithm)}, generates base64 encoded sign result.
*/
public static String 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));
}
/**
* @see #sign(HttpMethod, java.net.URI, LinkedListMultimap, String, SignAlgorithm)
* @see #signToBase64(HttpMethod, java.net.URI, LinkedListMultimap, String, SignAlgorithm)
*
* @param httpMethod The http request method
* @param uri The uri string
* @param httpHeaders The http request headers
* @param accessKeyId The user's access key
* @param secretAccessKeyId The user's secret access key
* @param algorithm The sign algorithm
* @return return the value of Authorization Http header
*/
public static String getAuthorizationHeader(HttpMethod httpMethod, URI uri,
LinkedListMultimap<String, String> httpHeaders, String accessKeyId,
String secretAccessKeyId, SignAlgorithm algorithm)
throws NoSuchAlgorithmException, InvalidKeyException {
String signature = signToBase64(httpMethod, uri, httpHeaders,
secretAccessKeyId, algorithm);
return "Galaxy-V2 " + accessKeyId + ":" + signature;
}
private static LinkedListMultimap<String, String> parseUriParameters(URI uri) {
LinkedListMultimap<String, String> params = LinkedListMultimap.create();
String query = uri.getQuery();
if (query != null) {
for (String param : query.split("&")) {
String[] kv = param.split("=");
if (kv.length >= 2) {
params.put(kv[0], param.substring(kv[0].length() + 1));
} else {
params.put(kv[0], "");
}
}
}
return params;
}
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,
HttpKeys.CONTENT_MD5).get(0)).append("\n");
builder.append(checkAndGet(httpHeaders,
HttpKeys.CONTENT_TYPE).get(0)).append("\n");
long expires;
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, HttpKeys.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 (String key : headers.keySet()) {
if (!key.toLowerCase().startsWith(HttpKeys.XIAOMI_HEADER_PREFIX)) {
continue;
}
StringBuilder builder = new StringBuilder();
int index = 0;
for (String value : headers.get(key)) {
if (index != 0) {
builder.append(",");
}
builder.append(value);
index++;
}
sortedHeaders.put(key, builder.toString());
}
// 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 = parseUriParameters(uri);
for (String key : params.keySet()) {
for (String value : params.get(key)) {
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 = parseUriParameters(uri);
List<String> expires = params.get(HttpKeys.EXPIRES);
if (expires != null && !expires.isEmpty()) {
return Long.parseLong(expires.get(0));
}
return 0;
}
}