From a113003b5851ee8b99378cafe8627aba702bfbe9 Mon Sep 17 00:00:00 2001 From: Richard Lucas Date: Thu, 14 Jul 2016 13:08:12 -0700 Subject: [PATCH] Initial commit --- .gitignore | 5 + LICENSE | 202 +++++++++++++++ README.md | 51 ++++ pom.xml | 234 ++++++++++++++++++ .../aws/v4/signer/AwsCredentials.java | 35 +++ .../uk/co/lucasweb/aws/v4/signer/Base16.java | 48 ++++ .../aws/v4/signer/CanonicalHeaders.java | 106 ++++++++ .../aws/v4/signer/CanonicalRequest.java | 47 ++++ .../lucasweb/aws/v4/signer/HttpRequest.java | 47 ++++ .../uk/co/lucasweb/aws/v4/signer/Sha256.java | 48 ++++ .../uk/co/lucasweb/aws/v4/signer/Signer.java | 128 ++++++++++ .../aws/v4/signer/SigningException.java | 40 +++ .../co/lucasweb/aws/v4/signer/Throwables.java | 58 +++++ .../lucasweb/aws/v4/signer/package-info.java | 6 + .../co/lucasweb/aws/v4/signer/Base16Test.java | 37 +++ .../aws/v4/signer/CanonicalHeadersTest.java | 76 ++++++ .../aws/v4/signer/CanonicalRequestTest.java | 48 ++++ .../aws/v4/signer/HttpRequestTest.java | 43 ++++ .../co/lucasweb/aws/v4/signer/Sha256Test.java | 41 +++ .../co/lucasweb/aws/v4/signer/SignerTest.java | 105 ++++++++ 20 files changed, 1405 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/AwsCredentials.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/Base16.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeaders.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequest.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/HttpRequest.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/Sha256.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/Signer.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/SigningException.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/Throwables.java create mode 100644 src/main/java/uk/co/lucasweb/aws/v4/signer/package-info.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/Base16Test.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeadersTest.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequestTest.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/HttpRequestTest.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/Sha256Test.java create mode 100644 src/test/java/uk/co/lucasweb/aws/v4/signer/SignerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9959f3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +*.iml +*.iws +.DS_Store +target/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d716799 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Richard Lucas + + Licensed 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9fc81f --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +#aws-v4-signer-java + +aws-v4-signer-java is a lightweight, zero-dependency implementation of the AWS V4 signing algorithm required by many of the AWS services. + +Requires Java 8+. + +## Setup + +Add the latest aws-v4-signer-java Maven dependency to your project + +```xml + + uk.co.lucasweb + aws-v4-signer-java + 1.0 + +``` + +## Usage + +### S3 + +```java +String contentSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; +HttpRequest request = new HttpRequest("GET", new URI("https://examplebucket.s3.amazonaws.com?max-keys=2&prefix=J")); +CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Host", "examplebucket.s3.amazonaws.com") + .add("x-amz-date", "20130524T000000Z") + .add("x-amz-content-sha256", contentSha256) + .build(); +String signature = Signer.builder() + .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY)) + .buildS3(request, headers, contentSha256) + .getSignature(); +``` + +### Glacier + +```java +String contentSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; +HttpRequest request = new HttpRequest("PUT", new URI("https://glacier.us-east-1.amazonaws.com/-/vaults/examplevault")); +CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Host", "glacier.us-east-1.amazonaws.com") + .add("x-amz-date", "20120525T002453Z") + .add("x-amz-glacier-version", "2012-06-01") + .build(); +String signature = Signer.builder() + .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY)) + .buildGlacier(request, headers, contentSha256) + .getSignature(); +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..da3490f --- /dev/null +++ b/pom.xml @@ -0,0 +1,234 @@ + + + 4.0.0 + + uk.co.lucasweb + aws-v4-signer-java + 1.0 + + AWS V4 Signer for Java + A lightweight, zero-dependency implementation of the AWS V4 signing algorithm + https://github.com/lucasweb78/aws-v4-signer-java + 2016 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + Richard Lucas + + + + + scm:git:git@github.com:lucasweb78/aws-v4-signer-java.git + scm:git:git@github.com:lucasweb78/aws-v4-signer-java.git + git@github.com:lucasweb78/aws-v4-signer-java + HEAD + + + + github + https://github.com/lucasweb78/aws-v4-signer-java/issues + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + 3.2.0 + 4.12 + 1.10.19 + + + 3.0.1 + 3.0.2 + 2.10.4 + 2.5.2 + 2.8.2 + 2.19.1 + 1.5 + 3.0.2 + 0.7.7.201606060606 + 1.6.7 + + + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.mockito + mockito-all + ${mockito-all.version} + test + + + + + + + + maven-source-plugin + ${maven-source-plugin.version} + + + maven-jar-plugin + ${maven-jar-plugin.version} + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + maven-install-plugin + ${maven-install-plugin.version} + + + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.jacoco + jacoco-maven-plugin + + + agent + + prepare-agent + + + ${sonar.jacoco.reportPath} + + + + default-report + verify + + report + + + ${sonar.jacoco.reportPath} + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/AwsCredentials.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/AwsCredentials.java new file mode 100644 index 0000000..d9480bb --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/AwsCredentials.java @@ -0,0 +1,35 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +/** + * @author Richard Lucas + */ +public class AwsCredentials { + + private final String accessKey; + private final String secretKey; + + public AwsCredentials(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + public String getAccessKey() { + return accessKey; + } + + public String getSecretKey() { + return secretKey; + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/Base16.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/Base16.java new file mode 100644 index 0000000..7fa9384 --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/Base16.java @@ -0,0 +1,48 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import java.io.UnsupportedEncodingException; + +/** + * @author Richard Lucas + */ +public final class Base16 { + + private static final char[] ENC_TAB = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + private Base16() { + // hide default constructor + } + + public static String encode(String data) { + return Throwables.returnableInstance(() -> encode(getBytes(data)), SigningException::new); + } + + public static String encode(byte[] data) { + int length = data.length; + StringBuilder stringBuilder = new StringBuilder(length * 2); + int i = 0; + while (i < length) { + stringBuilder.append(ENC_TAB[(data[i] & 0xF0) >> 4]); + stringBuilder.append(ENC_TAB[data[i] & 0x0F]); + i++; + } + + return stringBuilder.toString(); + } + + private static byte[] getBytes(String data) throws UnsupportedEncodingException { + return data.getBytes("UTF-8"); + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeaders.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeaders.java new file mode 100644 index 0000000..b998716 --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeaders.java @@ -0,0 +1,106 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Canonical Headers. + *

+ * See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html for more information + *

+ * + * @author Richard Lucas + */ +public class CanonicalHeaders { + + private final String names; + private final String canonicalizedHeaders; + private final TreeMap> internalMap; + + private CanonicalHeaders(String names, String canonicalizedHeaders, TreeMap> internalMap) { + this.names = names; + this.canonicalizedHeaders = canonicalizedHeaders; + this.internalMap = internalMap; + } + + public String get() { + return canonicalizedHeaders; + } + + public String getNames() { + return names; + } + + public Optional getFirstValue(String name) { + return Optional.ofNullable(internalMap.get(name.toLowerCase())) + .map(values -> values.get(0)); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final TreeMap> internalMap = new TreeMap<>(); + + public Builder add(String name, String value) { + + if (name == null) { + throw new IllegalArgumentException("name is null"); + } + + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + String lowerCaseName = name.toLowerCase(); + internalMap.put(lowerCaseName, Optional.ofNullable(internalMap.get(lowerCaseName)) + .map(values -> { + values.add(value); + return values; + }) + .orElse(newValueListWithValue(value))); + return this; + } + + public CanonicalHeaders build() { + String names = internalMap.keySet() + .stream() + .map(String::toLowerCase) + .collect(Collectors.joining(";")); + + StringBuilder canonicalizedHeadersBuilder = new StringBuilder(); + internalMap.entrySet() + .forEach(header -> header.getValue().forEach(value -> canonicalizedHeadersBuilder + .append(header.getKey().toLowerCase()) + .append(':') + .append(value) + .append('\n')) + ); + + return new CanonicalHeaders(names, canonicalizedHeadersBuilder.toString(), internalMap); + } + + private List newValueListWithValue(String value) { + List values = new ArrayList<>(); + values.add(value); + return values; + } + + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequest.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequest.java new file mode 100644 index 0000000..9dff7cb --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequest.java @@ -0,0 +1,47 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +/** + * @author Richard Lucas + */ +public class CanonicalRequest { + + private final HttpRequest httpRequest; + private final CanonicalHeaders headers; + private final String contentSha256; + + public CanonicalRequest(HttpRequest httpRequest, CanonicalHeaders headers, String contentSha256) { + this.httpRequest = httpRequest; + this.headers = headers; + this.contentSha256 = contentSha256; + } + + public String get() { + return httpRequest.getMethod() + + "\n" + httpRequest.getPath() + + "\n" + httpRequest.getQuery() + + "\n" + headers.get() + + "\n" + headers.getNames() + + "\n" + contentSha256; + } + + public CanonicalHeaders getHeaders() { + return headers; + } + + @Override + public String toString() { + return get(); + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/HttpRequest.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/HttpRequest.java new file mode 100644 index 0000000..2ec1971 --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/HttpRequest.java @@ -0,0 +1,47 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import java.net.URI; +import java.util.Optional; + +/** + * @author Richard Lucas + */ +public class HttpRequest { + + private final String method; + private final URI uri; + + public HttpRequest(String method, URI uri) { + this.method = method; + this.uri = uri; + } + + public String getMethod() { + return method; + } + + public String getPath() { + if ("".equals(uri.getPath())) { + return "/"; + }else { + return uri.getPath(); + } + } + + public String getQuery() { + return Optional.ofNullable(uri.getQuery()) + .orElse(""); + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/Sha256.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/Sha256.java new file mode 100644 index 0000000..f9f648f --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/Sha256.java @@ -0,0 +1,48 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import java.nio.charset.Charset; +import java.security.MessageDigest; + +/** + * @author Richard Lucas + */ +public final class Sha256 { + + private static final String SHA_256 = "SHA-256"; + private static final String ZERO = "0"; + private static final char[] hexDigits = "0123456789abcdef".toCharArray(); + + private Sha256() { + // hide default constructor + } + + public static String get(String value, Charset charset) { + return Throwables.returnableInstance(() -> { + MessageDigest md = MessageDigest.getInstance(SHA_256); + byte[] bytes = value.getBytes(charset); + md.update(bytes); + int b = md.getDigestLength(); + return bytesToHex(md.digest()); + }, SigningException::new); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(2 * bytes.length); + for (byte b : bytes) { + sb.append(hexDigits[(b >> 4) & 0xf]).append(hexDigits[b & 0xf]); + } + return sb.toString(); + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/Signer.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/Signer.java new file mode 100644 index 0000000..01063ba --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/Signer.java @@ -0,0 +1,128 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; + +/** + * @author Richard Lucas + */ +public class Signer { + + private static final String AUTH_TAG = "AWS4"; + private static final String ALGORITHM = AUTH_TAG + "-HMAC-SHA256"; + private static final String TERMINATION_STRING = "aws4_request"; + private static final Charset UTF_8 = Throwables.returnableInstance(() -> Charset.forName("UTF-8"), SigningException::new); + private static final String X_AMZ_DATE = "X-Amz-Date"; + private static final String HMAC_SHA256 = "HmacSHA256"; + + private final CanonicalRequest request; + private final AwsCredentials awsCredentials; + private final String service; + private final String region; + + private Signer(CanonicalRequest request, AwsCredentials awsCredentials, String service, String region) { + this.request = request; + this.awsCredentials = awsCredentials; + this.service = service; + this.region = region; + } + + public String getSignature() { + String date = request.getHeaders().getFirstValue(X_AMZ_DATE) + .orElseThrow(() -> new SigningException("headers missing '" + X_AMZ_DATE + "' header")); + String dateWithoutTimestamp = formatDateWithoutTimestamp(date); + String credentialScope = buildCredentialScope(dateWithoutTimestamp, service, region); + String hashedCanonicalRequest = Sha256.get(request.get(), UTF_8); + String stringToSign = buildStringToSign(date, credentialScope, hashedCanonicalRequest); + String signature = buildSignature(awsCredentials.getSecretKey(), dateWithoutTimestamp, stringToSign, service, region); + return buildAuthHeader(awsCredentials.getAccessKey(), credentialScope, request.getHeaders().getNames(), signature); + } + + public static Builder builder() { + return new Builder(); + } + + private static String formatDateWithoutTimestamp(String date) { + return date.substring(0, 8); + } + + private static String buildStringToSign(String date, String credentialScope, String hashedCanonicalRequest) { + return ALGORITHM + "\n" + date + "\n" + credentialScope + "\n" + hashedCanonicalRequest; + } + + private static String buildCredentialScope(String dateWithoutTimeStamp, String service, String region) { + return dateWithoutTimeStamp + "/" + region + "/" + service + "/" + TERMINATION_STRING; + } + + private static String buildAuthHeader(String accessKey, String credentialScope, String signedHeaders, String signature) { + return ALGORITHM + " " + "Credential=" + accessKey + "/" + credentialScope + ", " + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; + } + + private static byte[] hmacSha256(byte[] key, String value) { + try { + String algorithm = HMAC_SHA256; + Mac mac = Mac.getInstance(algorithm); + SecretKeySpec signingKey = new SecretKeySpec(key, algorithm); + mac.init(signingKey); + return mac.doFinal(value.getBytes(UTF_8)); + } catch (Exception e) { + throw new SigningException("Error signing request", e); + } + } + + private static String buildSignature(String secretKey, String dateWithoutTimestamp, String stringToSign, String service, String region) { + byte[] kSecret = (AUTH_TAG + secretKey).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 Base16.encode(hmacSha256(kSigning, stringToSign)).toLowerCase(); + } + + public static class Builder { + + private static final String DEFAULT_REGION = "us-east-1"; + private static final String S3 = "s3"; + private static final String GLACIER = "glacier"; + + private AwsCredentials awsCredentials; + private String region = DEFAULT_REGION; + + public Builder awsCredentials(AwsCredentials awsCredentials) { + this.awsCredentials = awsCredentials; + return this; + } + + public Builder region(String region) { + this.region = region; + return this; + } + + public Signer build(HttpRequest request, CanonicalHeaders headers, String service, String contentSha256) { + // TODO get awsCredentials from system and environment properties if not set + return new Signer(new CanonicalRequest(request, headers, contentSha256), awsCredentials, service, region); + } + + public Signer buildS3(HttpRequest request, CanonicalHeaders headers, String contentSha256) { + return build(request, headers, S3, contentSha256); + } + + public Signer buildGlacier(HttpRequest request, CanonicalHeaders headers, String contentSha256) { + return build(request, headers, GLACIER, contentSha256); + } + + } +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/SigningException.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/SigningException.java new file mode 100644 index 0000000..5043e7a --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/SigningException.java @@ -0,0 +1,40 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +/** + * @author Richard Lucas + */ +public class SigningException extends RuntimeException { + + public SigningException() { + super(); + } + + public SigningException(String message) { + super(message); + } + + public SigningException(String message, Throwable cause) { + super(message, cause); + } + + public SigningException(Throwable cause) { + super(cause); + } + + public SigningException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/Throwables.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/Throwables.java new file mode 100644 index 0000000..db8a84a --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/Throwables.java @@ -0,0 +1,58 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +/** + * Invokes a function wrapping any checked exceptions thrown by the function in an unchecked exception. + * + * @author Richard Lucas + */ +public final class Throwables { + + private Throwables() { + // hide default constructor + } + + @FunctionalInterface + public interface ExceptionWrapper { + E wrap(Exception e); + } + + @FunctionalInterface + public interface ReturnableInstance { + R apply() throws Exception; + } + + @FunctionalInterface + public interface VoidInstance { + void apply() throws Exception; + } + + public static void voidInstance(VoidInstance voidInstance, ExceptionWrapper wrapper) throws E { + try { + voidInstance.apply(); + } catch (Exception e) { + throw wrapper.wrap(e); + } + } + + public static R returnableInstance(ReturnableInstance returnableInstance, ExceptionWrapper wrapper) throws E { + try { + return returnableInstance.apply(); + } catch (Exception e) { + throw wrapper.wrap(e); + } + } + + +} diff --git a/src/main/java/uk/co/lucasweb/aws/v4/signer/package-info.java b/src/main/java/uk/co/lucasweb/aws/v4/signer/package-info.java new file mode 100644 index 0000000..aaf3f36 --- /dev/null +++ b/src/main/java/uk/co/lucasweb/aws/v4/signer/package-info.java @@ -0,0 +1,6 @@ +/** + * AWS V4 Signer + * + * @author Richard Lucas + */ +package uk.co.lucasweb.aws.v4.signer; \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/Base16Test.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/Base16Test.java new file mode 100644 index 0000000..d6e0447 --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/Base16Test.java @@ -0,0 +1,37 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Richard Lucas + */ +public class Base16Test { + + @Test + public void shouldEncodeUTF8String() throws Exception { + String data = "0123456abcdef23abc"; + assertThat(Base16.encode(data)) + .isEqualTo("303132333435366162636465663233616263"); + } + + @Test + public void shouldEncodeBytes() throws Exception { + byte[] data = "0123456abcdef23abc".getBytes("UTF-8"); + assertThat(Base16.encode(data)) + .isEqualTo("303132333435366162636465663233616263"); + } +} \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeadersTest.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeadersTest.java new file mode 100644 index 0000000..0c2b406 --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalHeadersTest.java @@ -0,0 +1,76 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Richard Lucas + */ +public class CanonicalHeadersTest { + + @Test + public void shouldBuildCanonicalizedHeaders() throws Exception { + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("test", "one") + .add("test", "two") + .add("hello", "world") + .build(); + + assertThat(headers.getNames()).isEqualTo("hello;test"); + assertThat(headers.get()).isEqualTo("hello:world\ntest:one\ntest:two\n"); + } + + @Test + public void shouldThrowIllegalArgumentExceptionIfNameIsNull() throws Exception { + assertThatThrownBy(() -> CanonicalHeaders.builder() + .add(null, "one")) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("name is null"); + } + + @Test + public void shouldThrowIllegalArgumentExceptionIfValueIsNull() throws Exception { + assertThatThrownBy(() -> CanonicalHeaders.builder() + .add("test", null)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("value is null"); + } + + @Test + public void shouldGetFirstValue() throws Exception { + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("test", "one") + .add("test", "two") + .add("hello", "world") + .build(); + + assertThat(headers.getFirstValue("test")) + .hasValue("one"); + } + + @Test + public void shouldReturnEmptyOptionOnGetFirstValue() throws Exception { + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("test", "one") + .add("test", "two") + .add("hello", "world") + .build(); + + assertThat(headers.getFirstValue("bad")) + .isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequestTest.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequestTest.java new file mode 100644 index 0000000..5b77b6b --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/CanonicalRequestTest.java @@ -0,0 +1,48 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Richard Lucas + */ +public class CanonicalRequestTest { + + private static final String EXPECTED = "PUT\n" + + "/-/vaults/examplevault\n" + + "\n" + + "host:glacier.us-east-1.amazonaws.com\n" + + "x-amz-date:20120525T002453Z\n" + + "x-amz-glacier-version:2012-06-01\n" + + "\n" + + "host;x-amz-date;x-amz-glacier-version\n" + + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + @Test + public void shouldGetCanonicalRequest() throws Exception { + HttpRequest request = new HttpRequest("PUT", new URI("https://glacier.us-east-1.amazonaws.com/-/vaults/examplevault")); + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Host", "glacier.us-east-1.amazonaws.com") + .add("x-amz-date", "20120525T002453Z") + .add("x-amz-glacier-version", "2012-06-01") + .build(); + String hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + assertThat(new CanonicalRequest(request, headers, hash).get()) + .isEqualTo(EXPECTED); + } +} \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/HttpRequestTest.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/HttpRequestTest.java new file mode 100644 index 0000000..c2fbe4b --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/HttpRequestTest.java @@ -0,0 +1,43 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Richard Lucas + */ +public class HttpRequestTest { + + @Test + public void shouldGetHttpMethod() throws Exception { + assertThat(new HttpRequest("GET", new URI("http://localhost/test")).getMethod()) + .isEqualTo("GET"); + } + + @Test + public void shouldGetQuery() throws Exception { + assertThat(new HttpRequest("GET", new URI("http://localhost/test?test=one&hello=world")).getQuery()) + .isEqualTo("test=one&hello=world"); + } + + @Test + public void shouldReturnEmptyStringIfNoQuery() throws Exception { + assertThat(new HttpRequest("GET", new URI("http://localhost/test")).getQuery()) + .isEqualTo(""); + } +} \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/Sha256Test.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/Sha256Test.java new file mode 100644 index 0000000..2f0724e --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/Sha256Test.java @@ -0,0 +1,41 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import java.nio.charset.Charset; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Richard Lucas + */ +public class Sha256Test { + + private static final String TEST = "PUT\n" + + "/-/vaults/examplevault\n" + + "\n" + + "host:glacier.us-east-1.amazonaws.com\n" + + "x-amz-date:20120525T002453Z\n" + + "x-amz-glacier-version:2012-06-01\n" + + "\n" + + "host;x-amz-date;x-amz-glacier-version\n" + + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + @Test + public void shouldGetSha256() throws Exception { + assertThat(Sha256.get(TEST, Charset.forName("UTF-8"))) + .isEqualTo("5f1da1a2d0feb614dd03d71e87928b8e449ac87614479332aced3a701f916743"); + } +} \ No newline at end of file diff --git a/src/test/java/uk/co/lucasweb/aws/v4/signer/SignerTest.java b/src/test/java/uk/co/lucasweb/aws/v4/signer/SignerTest.java new file mode 100644 index 0000000..1854b6a --- /dev/null +++ b/src/test/java/uk/co/lucasweb/aws/v4/signer/SignerTest.java @@ -0,0 +1,105 @@ +/** + * Licensed 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. + * + * Copyright 2016 the original author or authors. + */ +package uk.co.lucasweb.aws.v4.signer; + +import org.junit.Test; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Richard Lucas + */ +public class SignerTest { + + private static final String ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"; + private static final String SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"; + + @Test + public void shouldSignRequest() throws Exception { + // the values used in this test are from the example http://docs.aws.amazon.com/amazonglacier/latest/dev/amazon-glacier-signing-requests.html + String hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + HttpRequest request = new HttpRequest("PUT", new URI("https://glacier.us-east-1.amazonaws.com/-/vaults/examplevault")); + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Host", "glacier.us-east-1.amazonaws.com") + .add("x-amz-date", "20120525T002453Z") + .add("x-amz-glacier-version", "2012-06-01") + .build(); + + String signature = Signer.builder() + .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY)) + .buildGlacier(request, headers, hash) + .getSignature(); + + String expectedSignature = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20120525/us-east-1/glacier/aws4_request, " + + "SignedHeaders=host;x-amz-date;x-amz-glacier-version, Signature=3ce5b2f2fffac9262b4da9256f8d086b4aaf42eba5f111c21681a65a127b7c2a"; + + assertThat(signature).isEqualTo(expectedSignature); + } + + @Test + public void shouldSignRequestWithQueryParam() throws Exception { + String hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + HttpRequest request = new HttpRequest("GET", new URI("https://examplebucket.s3.amazonaws.com?max-keys=2&prefix=J")); + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Host", "examplebucket.s3.amazonaws.com") + .add("x-amz-date", "20130524T000000Z") + .add("x-amz-content-sha256", hash) + .build(); + + String signature = Signer.builder() + .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY)) + .buildS3(request, headers, hash) + .getSignature(); + + String expectedSignature = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, " + + "SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7"; + + assertThat(signature).isEqualTo(expectedSignature); + } + + @Test + public void shouldSignStreamingRequest() throws Exception { + // see http://docs.aws.amazon.com/amazonglacier/latest/dev/amazon-glacier-signing-requests.html + String contentHash = "79da47e784b181ae04e5b5119fcc953d944acad5e0583fa0899d554a79eb77eb"; + String treeHash = "05c734c3f16b23358bb49c959d1420edac9f28ee844bf9b0580754c0f540acd8"; + URI uri = new URI("https://glacier.us-east-1.amazonaws.com/-/vaults/dev2/multipart-uploads/j3eqysOZoNF3UiEoN3k_b6bdRGGdzgEfsLoUyZhMIwKRMuDLEYRw2nlCh8QXQ_dzqQMxrgFtmZjatxbFIZ9HpnIUi93B"); + + HttpRequest request = new HttpRequest("PUT", uri); + CanonicalHeaders headers = CanonicalHeaders.builder() + .add("Accept", "application/json") + .add("Content-Length", "1049350") + .add("Content-Range", "bytes 0-1049349/*") + .add("Content-Type", "binary/octet-stream") + .add("Host", "glacier.us-east-1.amazonaws.com") + .add("user-agent", "aws-sdk-java/1.9.26 Mac_OS_X/10.10.3 Java_HotSpot(TM)_64-Bit_Server_VM/25.0-b70/1.8.0") + .add("x-amz-content-sha256", contentHash) + .add("X-Amz-Date", "20150424T222200Z") + .add("x-amz-glacier-version", "2012-06-01") + .add("x-amz-sha256-tree-hash", treeHash) + .add("X-Amz-Target", "Glacier.UploadMultipartPart") + .build(); + + String signature = Signer.builder() + .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY)) + .buildGlacier(request, headers, contentHash) + .getSignature(); + + String expectedSignature = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20150424/us-east-1/glacier/aws4_request, " + + "SignedHeaders=accept;content-length;content-range;content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-glacier-version;x-amz-sha256-tree-hash;x-amz-target, " + + "Signature=3ee337a197d3b15e719fd21acf378ef2d62f73159ff3c47fc0204e27e5ee9fb1"; + + assertThat(signature).isEqualTo(expectedSignature); + } +} \ No newline at end of file