blob: 80cc84fb929b22b974bf5444bcbd0c4728ce47cb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.glacier.util;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.util.Closeables2.closeQuietly;
import java.io.IOException;
import java.util.Locale;
import java.util.Map.Entry;
import javax.crypto.Mac;
import org.jclouds.crypto.Crypto;
import org.jclouds.glacier.reference.GlacierHeaders;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.hash.HashingInputStream;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
// TODO: Query parameters, not necessary for Glacier
// TODO: Endpoint on buildCredentialScope is being read from the static string. Uncool.
/**
* Signs requests using the AWSv4 signing algorithm
*
* @see <a href="http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html" />
*/
public final class AWSRequestSignerV4 {
public static final String AUTH_TAG = "AWS4";
public static final String HEADER_TAG = "x-amz-";
public static final String ALGORITHM = AUTH_TAG + "-HMAC-SHA256";
public static final String TERMINATION_STRING = "aws4_request";
public static final String REGION = "us-east-1";
public static final String SERVICE = "glacier";
private final Crypto crypto;
private final String identity;
private final String credential;
public AWSRequestSignerV4(String identity, String credential, Crypto crypto) {
this.crypto = checkNotNull(crypto, "crypto");
this.identity = checkNotNull(identity, "identity");
this.credential = checkNotNull(credential, "credential");
}
private static HashCode buildHashedCanonicalRequest(String method, String endpoint, HashCode hashedPayload,
String canonicalizedHeadersString, String signedHeaders) {
return Hashing.sha256().newHasher()
.putString(method, UTF_8)
.putString("\n", UTF_8)
.putString(endpoint, UTF_8)
.putString("\n", UTF_8)
.putString("\n", UTF_8)
.putString(canonicalizedHeadersString, UTF_8)
.putString("\n", UTF_8)
.putString(signedHeaders, UTF_8)
.putString("\n", UTF_8)
.putString(hashedPayload.toString(), UTF_8)
.hash();
}
private static String createStringToSign(String date, String credentialScope, HashCode hashedCanonicalRequest) {
return ALGORITHM + "\n" + date + "\n" + credentialScope + "\n" + hashedCanonicalRequest.toString();
}
private static String formatDateWithoutTimestamp(String date) {
return date.substring(0, 8);
}
private static String buildCredentialScope(String dateWithoutTimeStamp) {
return dateWithoutTimeStamp + "/" + REGION + "/" + SERVICE + "/" + TERMINATION_STRING;
}
private static Multimap<String, String> buildCanonicalizedHeadersMap(HttpRequest request) {
Multimap<String, String> headers = request.getHeaders();
SortedSetMultimap<String, String> canonicalizedHeaders = TreeMultimap.create();
for (Entry<String, String> header : headers.entries()) {
if (header.getKey() == null)
continue;
String key = header.getKey().toString().toLowerCase(Locale.getDefault());
// Ignore any headers that are not particularly interesting.
if (key.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE) || key.equalsIgnoreCase(HttpHeaders.CONTENT_MD5)
|| key.equalsIgnoreCase(HttpHeaders.HOST) || key.startsWith(HEADER_TAG)) {
canonicalizedHeaders.put(key, header.getValue());
}
}
return canonicalizedHeaders;
}
private static String buildCanonicalizedHeadersString(Multimap<String, String> canonicalizedHeadersMap) {
StringBuilder canonicalizedHeadersBuffer = new StringBuilder();
for (Entry<String, String> header : canonicalizedHeadersMap.entries()) {
String key = header.getKey();
canonicalizedHeadersBuffer.append(key.toLowerCase()).append(':').append(header.getValue()).append('\n');
}
return canonicalizedHeadersBuffer.toString();
}
private static String buildSignedHeaders(Multimap<String, String> canonicalizedHeadersMap) {
return Joiner.on(';').join(Iterables.transform(canonicalizedHeadersMap.keySet(), new Function<String, String>() {
@Override
public String apply(String input) {
return input.toLowerCase();
}
}));
}
private static HashCode buildHashedPayload(HttpRequest request) {
HashingInputStream his = null;
try {
his = new HashingInputStream(Hashing.sha256(),
request.getPayload() == null ? ByteSource.empty().openStream() : request.getPayload().openStream());
ByteStreams.copy(his, ByteStreams.nullOutputStream());
return his.hash();
} catch (IOException e) {
throw new HttpException("Error signing request", e);
} finally {
closeQuietly(his);
}
}
private static String buildAuthHeader(String accessKey, String credentialScope, String signedHeaders,
String signature) {
return ALGORITHM + " " + "Credential=" + accessKey + "/" + credentialScope + "," + "SignedHeaders="
+ signedHeaders + "," + "Signature=" + signature;
}
private byte[] hmacSha256(byte[] key, String s) {
try {
Mac hmacSHA256 = crypto.hmacSHA256(key);
return hmacSHA256.doFinal(s.getBytes());
} catch (Exception e) {
throw new HttpException("Error signing request", e);
}
}
private String buildSignature(String dateWithoutTimestamp, String stringToSign) {
byte[] kSecret = (AUTH_TAG + credential).getBytes(UTF_8);
byte[] kDate = hmacSha256(kSecret, dateWithoutTimestamp);
byte[] kRegion = hmacSha256(kDate, REGION);
byte[] kService = hmacSha256(kRegion, SERVICE);
byte[] kSigning = hmacSha256(kService, TERMINATION_STRING);
return BaseEncoding.base16().encode(hmacSha256(kSigning, stringToSign)).toLowerCase();
}
public HttpRequest sign(HttpRequest request) {
// Grab the needed data to build the signature
Multimap<String, String> canonicalizedHeadersMap = buildCanonicalizedHeadersMap(request);
String canonicalizedHeadersString = buildCanonicalizedHeadersString(canonicalizedHeadersMap);
String signedHeaders = buildSignedHeaders(canonicalizedHeadersMap);
String date = request.getFirstHeaderOrNull(GlacierHeaders.ALTERNATE_DATE);
String dateWithoutTimestamp = formatDateWithoutTimestamp(date);
String method = request.getMethod();
String endpoint = request.getEndpoint().getRawPath();
String credentialScope = buildCredentialScope(dateWithoutTimestamp);
HashCode hashedPayload = buildHashedPayload(request);
// Task 1: Create a Canonical Request For Signature Version 4.
HashCode hashedCanonicalRequest = buildHashedCanonicalRequest(method, endpoint, hashedPayload,
canonicalizedHeadersString, signedHeaders);
// Task 2: Create a String to Sign for Signature Version 4.
String stringToSign = createStringToSign(date, credentialScope, hashedCanonicalRequest);
// Task 3: Calculate the AWS Signature Version 4.
String signature = buildSignature(dateWithoutTimestamp, stringToSign);
// Sign the request
String authHeader = buildAuthHeader(identity, credentialScope, signedHeaders, signature);
request = request.toBuilder().replaceHeader(HttpHeaders.AUTHORIZATION, authHeader).build();
return request;
}
}