· 4 years ago · Jan 22, 2021, 06:02 PM
1import SHA256 from 'crypto-js/sha256';
2import encHex from 'crypto-js/enc-hex';
3import HmacSHA256 from 'crypto-js/hmac-sha256';
4
5const sigV4Client = {};
6sigV4Client.newClient = (config) => {
7 const AWS_SHA_256 = 'AWS4-HMAC-SHA256';
8 const AWS4_REQUEST = 'aws4_request';
9 const AWS4 = 'AWS4';
10 const X_AMZ_DATE = 'x-amz-date';
11 const X_AMZ_SECURITY_TOKEN = 'x-amz-security-token';
12 const HOST = 'host';
13 const AUTHORIZATION = 'Authorization';
14
15 function hash(value) {
16 return SHA256(value); // eslint-disable-line
17 }
18
19 function hexEncode(value) {
20 return value.toString(encHex);
21 }
22
23 function hmac(secret, value) {
24 return HmacSHA256(value, secret, { asBytes: true }); // eslint-disable-line
25 }
26
27 function buildCanonicalRequest(method, path, queryParams, headers, payload) {
28 const bPath = buildCanonicalUri(path);
29 const bParams = buildCanonicalQueryString(queryParams);
30 const bHeaders = buildCanonicalHeaders(headers);
31 const bSHeaders = buildCanonicalSignedHeaders(headers);
32 const bPayload = hexEncode(hash(payload));
33 return (
34 `${method}\n${bPath}\n${bParams}\n${bHeaders}\n${bSHeaders}\n${bPayload}`
35 );
36 }
37
38 function hashCanonicalRequest(request) {
39 return hexEncode(hash(request));
40 }
41
42 function buildCanonicalUri(uri) {
43 return encodeURI(uri);
44 }
45
46 function buildCanonicalQueryString(queryParams) {
47 if (Object.keys(queryParams).length < 1) {
48 return '';
49 }
50
51 const sortedQueryParams = [];
52
53 Object.keys(queryParams).forEach((property) => {
54 if (Object.prototype.hasOwnProperty.call(queryParams, property)) {
55 sortedQueryParams.push(property);
56 }
57 });
58 sortedQueryParams.sort();
59
60 let canonicalQueryString = '';
61 for (let i = 0; i < sortedQueryParams.length; i += 1) {
62 canonicalQueryString +=
63 `${sortedQueryParams[i]}=${encodeURIComponent(queryParams[sortedQueryParams[i]])}&`;
64 }
65 return canonicalQueryString.substr(0, canonicalQueryString.length - 1);
66 }
67
68 function buildCanonicalHeaders(headers) {
69 let canonicalHeaders = '';
70 const sortedKeys = [];
71 Object.keys(headers).forEach((property) => {
72 if (Object.prototype.hasOwnProperty.call(headers, property)) {
73 sortedKeys.push(property);
74 }
75 });
76 sortedKeys.sort();
77
78 for (let i = 0; i < sortedKeys.length; i += 1) {
79 canonicalHeaders +=
80 `${sortedKeys[i].toLowerCase()}:${headers[sortedKeys[i]]}\n`;
81 }
82 return canonicalHeaders;
83 }
84
85 function buildCanonicalSignedHeaders(headers) {
86 const sortedKeys = [];
87 Object.keys(headers).forEach((property) => {
88 if (Object.prototype.hasOwnProperty.call(headers, property)) {
89 sortedKeys.push(property.toLowerCase());
90 }
91 });
92 sortedKeys.sort();
93
94 return sortedKeys.join(';');
95 }
96
97 function buildStringToSign(
98 datetime,
99 credentialScope,
100 hashedCanonicalRequest
101 ) {
102 return (
103 `${AWS_SHA_256}\n${datetime}\n${credentialScope}\n${hashedCanonicalRequest}`
104 );
105 }
106
107 function buildCredentialScope(datetime, region, service) {
108 return (
109 `${datetime.substr(0, 8)}/${region}/${service}/${AWS4_REQUEST}`
110 );
111 }
112
113 function calculateSigningKey(secretKey, datetime, region, service) {
114 return hmac(
115 hmac(
116 hmac(hmac(AWS4 + secretKey, datetime.substr(0, 8)), region),
117 service
118 ),
119 AWS4_REQUEST
120 );
121 }
122
123 function calculateSignature(key, stringToSign) {
124 return hexEncode(hmac(key, stringToSign));
125 }
126
127 function extractHostname(url) {
128 let hostname;
129
130 if (url.indexOf('://') > -1) {
131 hostname = url.split('/')[2];
132 } else {
133 hostname = url.split('/')[0];
134 }
135
136 hostname = hostname.split(':')[0];
137 hostname = hostname.split('?')[0];
138
139 return hostname;
140 }
141
142 function buildAuthorizationHeader(
143 accessKey,
144 credentialScope,
145 headers,
146 signature
147 ) {
148 const signedHeaders = buildCanonicalSignedHeaders(headers);
149 return (
150 `${AWS_SHA_256} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
151 );
152 }
153
154 const awsSigV4Client = {};
155 if (config.accessKey === undefined || config.secretKey === undefined) {
156 return awsSigV4Client;
157 }
158 awsSigV4Client.accessKey = config.accessKey;
159 awsSigV4Client.secretKey = config.secretKey;
160 awsSigV4Client.sessionToken = config.sessionToken;
161 awsSigV4Client.serviceName = config.serviceName || 'execute-api';
162 awsSigV4Client.region = config.region || 'us-east-1';
163 awsSigV4Client.defaultAcceptType =
164 config.defaultAcceptType || 'application/json';
165 awsSigV4Client.defaultContentType =
166 config.defaultContentType || 'application/json';
167
168 const invokeUrl = config.endpoint;
169 const endpoint = /(^https?:\/\/[^/]+)/g.exec(invokeUrl)[1];
170 const pathComponent = invokeUrl.substring(endpoint.length);
171
172 awsSigV4Client.endpoint = endpoint;
173 awsSigV4Client.pathComponent = pathComponent;
174
175 awsSigV4Client.signRequest = (request) => {
176 const verb = request.method.toUpperCase();
177 const path = awsSigV4Client.pathComponent + request.path;
178 const queryParams = { ...request.queryParams };
179 const headers = { ...request.headers };
180
181 // If the user has not specified an override for Content type the use default
182 if (headers['Content-Type'] === undefined) {
183 headers['Content-Type'] = awsSigV4Client.defaultContentType;
184 }
185
186 // If the user has not specified an override for Accept type the use default
187 if (headers.Accept === undefined) {
188 headers.Accept = awsSigV4Client.defaultAcceptType;
189 }
190
191 let body = { ...request.body };
192 // override request body and set to empty when signing GET requests
193 if (request.body === undefined || verb === 'GET') {
194 body = '';
195 } else {
196 body = JSON.stringify(body);
197 }
198
199 // If there is no body remove the content-type header so it is not
200 // included in SigV4 calculation
201 if (body === '' || body === undefined || body === null) {
202 delete headers['Content-Type'];
203 }
204
205 const datetime = new Date()
206 .toISOString()
207 .replace(/\.\d{3}Z$/, 'Z')
208 .replace(/[:-]|\.\d{3}/g, '');
209 headers[X_AMZ_DATE] = datetime;
210 headers[HOST] = extractHostname(awsSigV4Client.endpoint);
211
212 const canonicalRequest = buildCanonicalRequest(
213 verb,
214 path,
215 queryParams,
216 headers,
217 body
218 );
219 const hashedCanonicalRequest = hashCanonicalRequest(canonicalRequest);
220 const credentialScope = buildCredentialScope(
221 datetime,
222 awsSigV4Client.region,
223 awsSigV4Client.serviceName
224 );
225 const stringToSign = buildStringToSign(
226 datetime,
227 credentialScope,
228 hashedCanonicalRequest
229 );
230 const signingKey = calculateSigningKey(
231 awsSigV4Client.secretKey,
232 datetime,
233 awsSigV4Client.region,
234 awsSigV4Client.serviceName
235 );
236 const signature = calculateSignature(signingKey, stringToSign);
237 headers[AUTHORIZATION] = buildAuthorizationHeader(
238 awsSigV4Client.accessKey,
239 credentialScope,
240 headers,
241 signature
242 );
243 if (
244 awsSigV4Client.sessionToken !== undefined &&
245 awsSigV4Client.sessionToken !== ''
246 ) {
247 headers[X_AMZ_SECURITY_TOKEN] = awsSigV4Client.sessionToken;
248 }
249 delete headers[HOST];
250
251 let url = awsSigV4Client.endpoint + path;
252 const queryString = buildCanonicalQueryString(queryParams);
253 if (queryString !== '') {
254 url = `${url}?${queryString}`;
255 }
256
257 // Need to re-attach Content-Type if it is not specified at this point
258 if (headers['Content-Type'] === undefined) {
259 headers['Content-Type'] = awsSigV4Client.defaultContentType;
260 }
261
262 return {
263 headers,
264 url,
265 };
266 };
267
268 return awsSigV4Client;
269};
270
271export default sigV4Client;
272