View Javadoc
1   package com.github.davidmoten.aws.lw.client.internal.auth;
2   
3   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.ALGORITHM;
4   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.SCHEME;
5   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.TERMINATOR;
6   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.dateStampFormat;
7   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.getCanonicalRequest;
8   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.getCanonicalizeHeaderNames;
9   import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.getCanonicalizedHeaderString;
10  import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.getCanonicalizedQueryString;
11  import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.getStringToSign;
12  import static com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4.sign;
13  
14  import java.net.URL;
15  import java.nio.charset.StandardCharsets;
16  import java.util.Date;
17  import java.util.Map;
18  
19  import com.github.davidmoten.aws.lw.client.internal.util.Util;
20  
21  /**
22   * Sample AWS4 signer demonstrating how to sign 'chunked' uploads
23   */
24  public final class Aws4SignerForChunkedUpload {
25  
26      /**
27       * SHA256 substitute marker used in place of x-amz-content-sha256 when employing
28       * chunked uploads
29       */
30      public static final String STREAMING_BODY_SHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
31  
32      private static final String CLRF = "\r\n";
33      private static final String CHUNK_STRING_TO_SIGN_PREFIX = "AWS4-HMAC-SHA256-PAYLOAD";
34      private static final String CHUNK_SIGNATURE_HEADER = ";chunk-signature=";
35      private static final int SIGNATURE_LENGTH = 64;
36      private static final byte[] FINAL_CHUNK = new byte[0];
37  
38      /**
39       * Tracks the previously computed signature value; for chunk 0 this will contain
40       * the signature included in the Authorization header. For subsequent chunks it
41       * contains the computed signature of the prior chunk.
42       */
43      private String lastComputedSignature;
44  
45      /**
46       * Date and time of the original signing computation, in ISO 8601 basic format,
47       * reused for each chunk
48       */
49      private String dateTimeStamp;
50  
51      /**
52       * The scope value of the original signing computation, reused for each chunk
53       */
54      private String scope;
55  
56      /**
57       * The derived signing key used in the original signature computation and
58       * re-used for each chunk
59       */
60      private byte[] signingKey;
61      private final URL endpointUrl;
62      private final String httpMethod;
63      private final String serviceName;
64      private final String regionName;
65  
66      public Aws4SignerForChunkedUpload(URL endpointUrl, String httpMethod, String serviceName,
67              String regionName) {
68          this.endpointUrl = endpointUrl;
69          this.httpMethod = httpMethod;
70          this.serviceName = serviceName;
71          this.regionName = regionName;
72      }
73  
74      /**
75       * Computes an AWS4 signature for a request, ready for inclusion as an
76       * 'Authorization' header.
77       * 
78       * @param headers         The request headers; 'Host' and 'X-Amz-Date' will be
79       *                        added to this set.
80       * @param queryParameters Any query parameters that will be added to the
81       *                        endpoint. The parameters should be specified in
82       *                        canonical format.
83       * @param bodyHash        Precomputed SHA256 hash of the request body content;
84       *                        this value should also be set as the header
85       *                        'X-Amz-Content-SHA256' for non-streaming uploads.
86       * @param awsAccessKey    The user's AWS Access Key.
87       * @param awsSecretKey    The user's AWS Secret Key.
88       * @return The computed authorization string for the request. This value needs
89       *         to be set as the header 'Authorization' on the subsequent HTTP
90       *         request.
91       */
92      public String computeSignature(Map<String, String> headers, Map<String, String> queryParameters,
93              String bodyHash, String awsAccessKey, String awsSecretKey) {
94          // first get the date and time for the subsequent request, and convert
95          // to ISO 8601 format for use in signature generation
96          Date now = new Date();
97          this.dateTimeStamp = AwsSignatureVersion4.dateTimeFormat().format(now);
98  
99          // update the headers with required 'x-amz-date' and 'host' values
100         headers.put("x-amz-date", dateTimeStamp);
101 
102         String hostHeader = endpointUrl.getHost();
103         int port = endpointUrl.getPort();
104         if (port > -1) {
105             hostHeader = hostHeader.concat(":" + port);
106         }
107         headers.put("Host", hostHeader);
108 
109         // canonicalize the headers; we need the set of header names as well as the
110         // names and values to go into the signature process
111         String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
112         String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
113 
114         // if any query string parameters have been supplied, canonicalize them
115         String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
116 
117         // canonicalize the various components of the request
118         String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod,
119                 canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders,
120                 bodyHash);
121         System.out.println("--------- Canonical request --------");
122         System.out.println(canonicalRequest);
123         System.out.println("------------------------------------");
124 
125         // construct the string to be signed
126         String dateStamp = dateStampFormat().format(now);
127         this.scope = dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR;
128         String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope,
129                 canonicalRequest);
130         System.out.println("--------- String to sign -----------");
131         System.out.println(stringToSign);
132         System.out.println("------------------------------------");
133 
134         // compute the signing key
135         byte[] kSecret = (SCHEME + awsSecretKey).getBytes(StandardCharsets.UTF_8);
136         byte[] kDate = sign(dateStamp, kSecret);
137         byte[] kRegion = sign(regionName, kDate);
138         byte[] kService = sign(serviceName, kRegion);
139         this.signingKey = sign(TERMINATOR, kService);
140         byte[] signature = sign(stringToSign, signingKey);
141 
142         // cache the computed signature ready for chunk 0 upload
143         lastComputedSignature = Util.toHex(signature);
144 
145         String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
146         String signedHeadersAuthorizationHeader = "SignedHeaders=" + canonicalizedHeaderNames;
147         String signatureAuthorizationHeader = "Signature=" + lastComputedSignature;
148 
149         String authorizationHeader = SCHEME + "-" + ALGORITHM + " " + credentialsAuthorizationHeader
150                 + ", " + signedHeadersAuthorizationHeader + ", " + signatureAuthorizationHeader;
151 
152         return authorizationHeader;
153     }
154     
155     /**
156      * Calculates the expanded payload size of our data when it is chunked
157      * 
158      * @param originalLength The true size of the data payload to be uploaded
159      * @param chunkSize      The size of each chunk we intend to send; each chunk
160      *                       will be prefixed with signed header data, expanding the
161      *                       overall size by a determinable amount
162      * @return The overall payload size to use as content-length on a chunked upload
163      */
164     public static long calculateChunkedContentLength(long originalLength, long chunkSize) {
165         if (originalLength <= 0) {
166             throw new IllegalArgumentException("Nonnegative content length expected.");
167         }
168 
169         long maxSizeChunks = originalLength / chunkSize;
170         long remainingBytes = originalLength % chunkSize;
171         return maxSizeChunks * calculateChunkHeaderLength(chunkSize)
172                 + (remainingBytes > 0 ? calculateChunkHeaderLength(remainingBytes) : 0)
173                 + calculateChunkHeaderLength(0);
174     }
175 
176     /**
177      * Returns the size of a chunk header, which only varies depending on the
178      * selected chunk size
179      * 
180      * @param chunkDataSize The intended size of each chunk; this is placed into the
181      *                      chunk header
182      * @return The overall size of the header that will prefix the user data in each
183      *         chunk
184      */
185     private static long calculateChunkHeaderLength(long chunkDataSize) {
186         return Long.toHexString(chunkDataSize).length() + CHUNK_SIGNATURE_HEADER.length()
187                 + SIGNATURE_LENGTH + CLRF.length() + chunkDataSize + CLRF.length();
188     }
189 
190     /**
191      * Returns a chunk for upload consisting of the signed 'header' or chunk prefix
192      * plus the user data. The signature of the chunk incorporates the signature of
193      * the previous chunk (or, if the first chunk, the signature of the headers
194      * portion of the request).
195      * 
196      * @param userDataLen The length of the user data contained in userData
197      * @param userData    Contains the user data to be sent in the upload chunk
198      * @return A new buffer of data for upload containing the chunk header plus user
199      *         data
200      */
201     public byte[] constructSignedChunk(int userDataLen, byte[] userData) {
202         // to keep our computation routine signatures simple, if the userData
203         // buffer contains less data than it could, shrink it. Note the special case
204         // to handle the requirement that we send an empty chunk to complete
205         // our chunked upload.
206         byte[] dataToChunk;
207         if (userDataLen == 0) {
208             dataToChunk = FINAL_CHUNK;
209         } else {
210             if (userDataLen < userData.length) {
211                 // shrink the chunkdata to fit
212                 dataToChunk = new byte[userDataLen];
213                 System.arraycopy(userData, 0, dataToChunk, 0, userDataLen);
214             } else {
215                 dataToChunk = userData;
216             }
217         }
218 
219         StringBuilder chunkHeader = new StringBuilder();
220 
221         // start with size of user data
222         chunkHeader.append(Integer.toHexString(dataToChunk.length));
223 
224         // nonsig-extension; we have none in these samples
225         String nonsigExtension = "";
226 
227         // if this is the first chunk, we package it with the signing result
228         // of the request headers, otherwise we use the cached signature
229         // of the previous chunk
230 
231         // sig-extension
232         String chunkStringToSign = CHUNK_STRING_TO_SIGN_PREFIX + "\n" + dateTimeStamp + "\n" + scope
233                 + "\n" + lastComputedSignature + "\n" + Util.toHex(Util.sha256(nonsigExtension))
234                 + "\n" + Util.toHex(Util.sha256(dataToChunk));
235 
236         // compute the V4 signature for the chunk
237         String chunkSignature = Util.toHex(AwsSignatureVersion4.sign(chunkStringToSign, signingKey));
238 
239         // cache the signature to include with the next chunk's signature computation
240         lastComputedSignature = chunkSignature;
241 
242         // construct the actual chunk, comprised of the non-signed extensions, the
243         // 'headers' we just signed and their signature, plus a newline then copy
244         // that plus the user's data to a payload to be written to the request stream
245         chunkHeader.append(nonsigExtension + CHUNK_SIGNATURE_HEADER + chunkSignature);
246         chunkHeader.append(CLRF);
247 
248         byte[] header = chunkHeader.toString().getBytes(StandardCharsets.UTF_8);
249         byte[] trailer = CLRF.getBytes(StandardCharsets.UTF_8);
250         byte[] signedChunk = new byte[header.length + dataToChunk.length + trailer.length];
251         System.arraycopy(header, 0, signedChunk, 0, header.length);
252         System.arraycopy(dataToChunk, 0, signedChunk, header.length, dataToChunk.length);
253         System.arraycopy(trailer, 0, signedChunk, header.length + dataToChunk.length,
254                 trailer.length);
255 
256         // this is the total data for the chunk that will be sent to the request stream
257         return signedChunk;
258     }
259 }