-
Notifications
You must be signed in to change notification settings - Fork 24
/
aws-sign-web.js
371 lines (350 loc) · 14.7 KB
/
aws-sign-web.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
/**
* AWS Signature v4 Implementation for Web Browsers
*
* Copyright (c) 2016-2018 Daniel Joos
*
* Distributed under MIT license. (See file LICENSE)
*/
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
/* global define */
define(['crypto-js/core', 'crypto-js/sha256', 'crypto-js/hmac-sha256'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('crypto-js/core'),
require('crypto-js/sha256'),
require('crypto-js/hmac-sha256'));
} else {
/* global CryptoJS */
root.awsSignWeb = factory(CryptoJS, CryptoJS.SHA256, CryptoJS.HmacSHA256);
}
}(this, function (CryptoJS) {
'use strict';
var defaultConfig = {
region: 'eu-west-1',
service: 'execute-api',
defaultContentType: 'application/json',
defaultAcceptType: 'application/json',
payloadSerializerFactory: JsonPayloadSerializer,
uriParserFactory: SimpleUriParser,
hasherFactory: CryptoJSHasher
};
/**
* Create a new signer object with the given configuration.
* Configuration must specify the AWS credentials used for the signing operation.
* It must contain the following properties:
* `accessKeyId`: The AWS IAM access key ID.
* `secretAccessKey`: The AWS IAM secret key.
* `sessionToken`: Optional session token, required for temporary credentials.
* @param {object} config The configuration object.
* @constructor
*/
var AwsSigner = function (config) {
this.config = extend({}, defaultConfig, config);
this.payloadSerializer = this.config.payloadSerializer ||
this.config.payloadSerializerFactory();
this.uriParser = this.config.uriParserFactory();
this.hasher = this.config.hasherFactory();
assertRequired(this.config.accessKeyId, 'AwsSigner requires AWS AccessKeyID');
assertRequired(this.config.secretAccessKey, 'AwsSigner requires AWS SecretAccessKey');
};
/**
* Create signature headers for the given request.
* Request must be in the format, known from the `$http` service of Angular:
* ```
* request = {
* headers: { ... },
* method: 'GET',
* url: 'http://...',
* params: { ... },
* data: ... // alternative: body
* };
* ```
* The resulting object contains the signature headers. For example, it can be merged into an
* existing `$http` config when dealing with Angular JS.
* @param {object} request The request to create the signature for. Will not be modified!
* @param {Date=} signDate Optional signature date to use. Current date-time is used if not specified.
* @returns Signed request headers.
*/
AwsSigner.prototype.sign = function (request, signDate) {
var workingSet = {
request: extend({}, request),
signDate: signDate || new Date(),
uri: this.uriParser(request.url)
};
prepare(this, workingSet);
buildCanonicalRequest(this, workingSet); // Step1: build the canonical request
buildStringToSign(this, workingSet); // Step2: build the string to sign
calculateSignature(this, workingSet); // Step3: calculate the signature hash
buildSignatureHeader(this, workingSet); // Step4: build the authorization header
return {
'Accept': workingSet.request.headers['accept'],
'Authorization': workingSet.authorization,
'Content-Type': workingSet.request.headers['content-type'],
'x-amz-date': workingSet.request.headers['x-amz-date'],
'x-amz-security-token': this.config.sessionToken || undefined
};
};
// Some preparations
function prepare(self, ws) {
var headers = {
'host': ws.uri.host,
'content-type': self.config.defaultContentType,
'accept': self.config.defaultAcceptType,
'x-amz-date': amzDate(ws.signDate)
};
// Remove accept/content-type headers if no default was configured.
if (!self.config.defaultAcceptType) {
delete headers['accept'];
}
if (!self.config.defaultContentType) {
delete headers['content-type'];
}
// Payload or not?
ws.request.method = ws.request.method.toUpperCase();
if (ws.request.body) {
ws.payload = ws.request.body;
} else if (ws.request.data && self.payloadSerializer) {
ws.payload = self.payloadSerializer(ws.request.data);
} else {
delete headers['content-type'];
}
// Headers
ws.request.headers = extend(
headers,
Object.keys(ws.request.headers || {}).reduce(function (normalized, key) {
normalized[key.toLowerCase()] = ws.request.headers[key];
return normalized;
}, {})
);
ws.sortedHeaderKeys = Object.keys(ws.request.headers).sort();
// Remove content-type parameters as some browser might change them on send
if (ws.request.headers['content-type']) {
ws.request.headers['content-type'] = ws.request.headers['content-type'].split(';')[0];
}
// Merge params to query params
if (typeof(ws.request.params) === 'object') {
extend(ws.uri.queryParams, ws.request.params);
}
}
// Convert the request to a canonical format.
function buildCanonicalRequest(self, ws) {
ws.signedHeaders = ws.sortedHeaderKeys.map(function (key) {
return key.toLowerCase();
}).join(';');
ws.canonicalRequest = String(ws.request.method).toUpperCase() + '\n' +
// Canonical URI:
ws.uri.path.split('/').map(function(seg) {
return uriEncode(seg);
}).join('/') + '\n' +
// Canonical Query String:
flatten(Object.keys(ws.uri.queryParams).sort().map(function (key) {
var queryParamsForKey = ws.uri.queryParams[key];
if (Array.isArray(queryParamsForKey)) {
// Sort is going to mutate this array, which we don't necessarily own, so we make a defensive copy
queryParamsForKey = [].slice.call(queryParamsForKey);
} else {
queryParamsForKey = [queryParamsForKey];
}
return queryParamsForKey.sort().map(function(val) {
return encodeURIComponent(key) + '=' + encodeURIComponent(val);
});
})).join('&') + '\n' +
// Canonical Headers:
ws.sortedHeaderKeys.map(function (key) {
return key.toLocaleLowerCase() + ':' + ws.request.headers[key];
}).join('\n') + '\n\n' +
// Signed Headers:
ws.signedHeaders + '\n' +
// Hashed Payload
self.hasher.hash((ws.payload) ? ws.payload : '');
}
// Construct the string that will be signed.
function buildStringToSign(self, ws) {
ws.credentialScope = [amzDate(ws.signDate, true), self.config.region, self.config.service,
'aws4_request'].join('/');
ws.stringToSign = 'AWS4-HMAC-SHA256' + '\n' +
amzDate(ws.signDate) + '\n' +
ws.credentialScope + '\n' +
self.hasher.hash(ws.canonicalRequest);
}
// Calculate the signature
function calculateSignature(self, ws) {
var hmac = self.hasher.hmac;
var signKey = hmac(
hmac(
hmac(
hmac(
'AWS4' + self.config.secretAccessKey,
amzDate(ws.signDate, true),
{hexOutput: false}
),
self.config.region,
{hexOutput: false, textInput: false}
),
self.config.service,
{hexOutput: false, textInput: false}
),
'aws4_request',
{hexOutput: false, textInput: false}
);
ws.signature = hmac(signKey, ws.stringToSign, {textInput: false});
}
// Build the signature HTTP header using the data in the working set.
function buildSignatureHeader(self, ws) {
ws.authorization = 'AWS4-HMAC-SHA256 ' +
'Credential=' + self.config.accessKeyId + '/' + ws.credentialScope + ', ' +
'SignedHeaders=' + ws.signedHeaders + ', ' +
'Signature=' + ws.signature;
}
// Format the given `Date` as AWS compliant date string.
// Time part gets omitted if second argument is set to `true`.
function amzDate(date, short) {
var result = date.toISOString().replace(/[:\-]|\.\d{3}/g, '').substr(0, 17);
if (short) {
return result.substr(0, 8);
}
return result;
}
/**
* Payload serializer factory implementation that converts the data to a JSON string.
*/
function JsonPayloadSerializer() {
return function(data) {
return JSON.stringify(data);
};
}
/**
* Simple URI parser factory.
* Uses an `a` document element for parsing given URIs.
* Therefore it most likely will only work in a web browser.
*/
function SimpleUriParser() {
var parser = document ? document.createElement('a') : {};
/**
* Parse the given URI.
* @param {string} uri The URI to parse.
* @returns JavaScript object with the parse results:
* `protocol`: The URI protocol part.
* `host`: Host part of the URI.
* `path`: Path part of the URI, always starting with a `/`
* `queryParams`: Query parameters as JavaScript object.
*/
return function (uri) {
parser.href = uri;
return {
protocol: parser.protocol,
host: parser.host.replace(/^(.*):((80)|(443))$/, '$1'),
path: ((parser.pathname.charAt(0) !== '/') ? '/' : '') +
decodeURI(parser.pathname),
queryParams: extractQueryParams(parser.search)
};
};
function extractQueryParams(search) {
return /^\??(.*)$/.exec(search)[1].split('&').reduce(function (result, arg) {
arg = /^(.+)=(.*)$/.exec(arg);
if (arg) {
var paramKey = decodeURI(arg[1]);
result[paramKey] = (
(typeof result[paramKey] != 'undefined' && result[paramKey] instanceof Array)
? result[paramKey]
: []
).concat(decodeURI(arg[2]));
}
return result;
}, {});
}
}
/**
* URI encode according to S3 requirements.
* See: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
*/
function uriEncode(input) {
return encodeURIComponent(input).replace(/[!'()*]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
});
}
/**
* Hash factory implementation using the SHA-256 hash algorithm of CryptoJS.
* Requires at least the CryptoJS rollups: `sha256.js` and `hmac-sha256.js`.
*/
function CryptoJSHasher() {
return {
/**
* Hash the given input using SHA-256 algorithm.
* The options can be used to control the in-/output of the hash operation.
* @param {*} input Input data.
* @param {object} options Options object:
* `hexOutput` -- Output the hash with hex encoding (default: `true`).
* `textInput` -- Interpret the input data as text (default: `true`).
* @returns The generated hash
*/
hash: function (input, options) {
options = extend({hexOutput: true, textInput: true}, options);
var hash = CryptoJS.SHA256(input);
if (options.hexOutput) {
return hash.toString(CryptoJS.enc.Hex);
}
return hash;
},
/**
* Create the HMAC of the given input data with the given key using the SHA-256
* hash algorithm.
* The options can be used to control the in-/output of the hash operation.
* @param {string} key Secret key.
* @param {*} input Input data.
* @param {object} options Options object:
* `hexOutput` -- Output the hash with hex encoding (default: `true`).
* `textInput` -- Interpret the input data as text (default: `true`).
* @returns The generated HMAC.
*/
hmac: function (key, input, options) {
options = extend({hexOutput: true, textInput: true}, options);
var hmac = CryptoJS.HmacSHA256(input, key, {asBytes: true});
if (options.hexOutput) {
return hmac.toString(CryptoJS.enc.Hex);
}
return hmac;
}
};
}
// Simple version of the `extend` function, known from Angular and Backbone.
// It merges the second (and all succeeding) argument(s) into the object, given as first
// argument. This is done recursively for all child objects, as well.
function extend(dest) {
var objs = [].slice.call(arguments, 1);
objs.forEach(function (obj) {
if (!obj || typeof(obj) !== 'object') {
return;
}
Object.keys(obj).forEach(function (key) {
var src = obj[key];
if (typeof(src) === 'undefined') {
return;
}
if (src !== null && typeof(src) === 'object') {
dest[key] = (Array.isArray(src) ? [] : {});
extend(dest[key], src);
} else {
dest[key] = src;
}
});
});
return dest;
}
// Short function that uses some JavaScript array methods to flatten an n-dimensional array.
function flatten(arr) {
return arr.reduce(function (flat, toFlatten) {
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
}, []);
}
// Throw an error if the given object is undefined.
function assertRequired(obj, msg) {
if (typeof(obj) === 'undefined' || !obj) {
throw new Error(msg);
}
}
return {
AwsSigner: AwsSigner
};
}));