forked from mongodb/node-mongodb-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaws4.ts
More file actions
202 lines (184 loc) · 8.93 KB
/
aws4.ts
File metadata and controls
202 lines (184 loc) · 8.93 KB
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
import { BSON } from '../../bson';
import { type AWSCredentials } from '../../deps';
export type Options = {
path: '/';
body: string;
host: string;
method: 'POST';
headers: {
'Content-Type': 'application/x-www-form-urlencoded';
'Content-Length': number;
'X-MongoDB-Server-Nonce': string;
'X-MongoDB-GS2-CB-Flag': 'n';
};
service: string;
region: string;
date: Date;
};
export type SignedHeaders = {
headers: {
Authorization: string;
'X-Amz-Date': string;
};
};
/**
* Calculates the SHA-256 hash of a string.
*
* @param str - String to hash.
* @returns Hexadecimal representation of the hash.
*/
const getHash = async (str: string): Promise<string> => {
const data = new Uint8Array(BSON.onDemand.ByteUtils.utf8ByteLength(str));
BSON.onDemand.ByteUtils.encodeUTF8Into(data, str, 0);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashHex = BSON.onDemand.ByteUtils.toHex(new Uint8Array(hashBuffer));
return hashHex;
};
/**
* Calculates the HMAC-SHA256 of a string using the provided key.
* @param key - Key to use for HMAC calculation. Can be a string or Uint8Array.
* @param str - String to calculate HMAC for.
* @returns Uint8Array containing the HMAC-SHA256 digest.
*/
const getHmacBuffer = async (key: string | Uint8Array, str: string): Promise<Uint8Array> => {
let keyData: Uint8Array;
if (typeof key === 'string') {
keyData = new Uint8Array(BSON.onDemand.ByteUtils.utf8ByteLength(key));
BSON.onDemand.ByteUtils.encodeUTF8Into(keyData, key, 0);
} else {
keyData = key;
}
const importedKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
);
const strData = new Uint8Array(BSON.onDemand.ByteUtils.utf8ByteLength(str));
BSON.onDemand.ByteUtils.encodeUTF8Into(strData, str, 0);
const signature = await crypto.subtle.sign('HMAC', importedKey, strData);
const digest = new Uint8Array(signature);
return digest;
};
/**
* Converts header values according to AWS requirements,
* From https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-canonical-request
* For values, you must:
- trim any leading or trailing spaces.
- convert sequential spaces to a single space.
* @param value - Header value to convert.
* @returns - Converted header value.
*/
const convertHeaderValue = (value: string | number) => {
return value.toString().trim().replace(/\s+/g, ' ');
};
/**
* This method implements AWS Signature 4 logic for a very specific request format.
* The signing logic is described here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
*/
export async function aws4Sign(
options: Options,
credentials: AWSCredentials
): Promise<SignedHeaders> {
/**
* From the spec: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
*
* Summary of signing steps
* 1. Create a canonical request
* Arrange the contents of your request (host, action, headers, etc.) into a standard canonical format. The canonical request is one of the inputs used to create the string to sign.
* 2. Create a hash of the canonical request
* Hash the canonical request using the same algorithm that you used to create the hash of the payload. The hash of the canonical request is a string of lowercase hexadecimal characters.
* 3. Create a string to sign
* Create a string to sign with the canonical request and extra information such as the algorithm, request date, credential scope, and the hash of the canonical request.
* 4. Derive a signing key
* Use the secret access key to derive the key used to sign the request.
* 5. Calculate the signature
* Perform a keyed hash operation on the string to sign using the derived signing key as the hash key.
* 6. Add the signature to the request
* Add the calculated signature to an HTTP header or to the query string of the request.
*/
// 1: Create a canonical request
// Date – The date and time used to sign the request.
const date = options.date;
// RequestDateTime – The date and time used in the credential scope. This value is the current UTC time in ISO 8601 format (for example, 20130524T000000Z).
const requestDateTime = date.toISOString().replace(/[:-]|\.\d{3}/g, '');
// RequestDate – The date used in the credential scope. This value is the current UTC date in YYYYMMDD format (for example, 20130524).
const requestDate = requestDateTime.substring(0, 8);
// Method – The HTTP request method. For us, this is always 'POST'.
const method = options.method;
// CanonicalUri – The URI-encoded version of the absolute path component URI, starting with the / that follows the domain name and up to the end of the string
// For our requests, this is always '/'
const canonicalUri = options.path;
// CanonicalQueryString – The URI-encoded query string parameters. For our requests, there are no query string parameters, so this is always an empty string.
const canonicalQuerystring = '';
// CanonicalHeaders – A list of request headers with their values. Individual header name and value pairs are separated by the newline character ("\n").
// All of our known/expected headers are included here, there are no extra headers.
const headers = new Headers({
'content-length': convertHeaderValue(options.headers['Content-Length']),
'content-type': convertHeaderValue(options.headers['Content-Type']),
host: convertHeaderValue(options.host),
'x-amz-date': convertHeaderValue(requestDateTime),
'x-mongodb-gs2-cb-flag': convertHeaderValue(options.headers['X-MongoDB-GS2-CB-Flag']),
'x-mongodb-server-nonce': convertHeaderValue(options.headers['X-MongoDB-Server-Nonce'])
});
// If session token is provided, include it in the headers
if ('sessionToken' in credentials && credentials.sessionToken) {
headers.append('x-amz-security-token', convertHeaderValue(credentials.sessionToken));
}
// Canonical headers are lowercased and sorted.
const canonicalHeaders = Array.from(headers.entries())
.map(([key, value]) => `${key.toLowerCase()}:${value}`)
.sort()
.join('\n');
const canonicalHeaderNames = Array.from(headers.keys()).map(header => header.toLowerCase());
// SignedHeaders – An alphabetically sorted, semicolon-separated list of lowercase request header names.
const signedHeaders = canonicalHeaderNames.sort().join(';');
// HashedPayload – A string created using the payload in the body of the HTTP request as input to a hash function. This string uses lowercase hexadecimal characters.
const hashedPayload = await getHash(options.body);
// CanonicalRequest – A string that includes the above elements, separated by newline characters.
const canonicalRequest = [
method,
canonicalUri,
canonicalQuerystring,
canonicalHeaders + '\n',
signedHeaders,
hashedPayload
].join('\n');
// 2. Create a hash of the canonical request
// HashedCanonicalRequest – A string created by using the canonical request as input to a hash function.
const hashedCanonicalRequest = await getHash(canonicalRequest);
// 3. Create a string to sign
// Algorithm – The algorithm used to create the hash of the canonical request. For SigV4, use AWS4-HMAC-SHA256.
const algorithm = 'AWS4-HMAC-SHA256';
// CredentialScope – The credential scope, which restricts the resulting signature to the specified Region and service.
// Has the following format: YYYYMMDD/region/service/aws4_request.
const credentialScope = `${requestDate}/${options.region}/${options.service}/aws4_request`;
// StringToSign – A string that includes the above elements, separated by newline characters.
const stringToSign = [algorithm, requestDateTime, credentialScope, hashedCanonicalRequest].join(
'\n'
);
// 4. Derive a signing key
// To derive a signing key for SigV4, perform a succession of keyed hash operations (HMAC) on the request date, Region, and service, with your AWS secret access key as the key for the initial hashing operation.
const dateKey = await getHmacBuffer('AWS4' + credentials.secretAccessKey, requestDate);
const dateRegionKey = await getHmacBuffer(dateKey, options.region);
const dateRegionServiceKey = await getHmacBuffer(dateRegionKey, options.service);
const signingKey = await getHmacBuffer(dateRegionServiceKey, 'aws4_request');
// 5. Calculate the signature
const signatureBuffer = await getHmacBuffer(signingKey, stringToSign);
const signature = BSON.onDemand.ByteUtils.toHex(signatureBuffer);
// 6. Add the signature to the request
// Calculate the Authorization header
const authorizationHeader = [
'AWS4-HMAC-SHA256 Credential=' + credentials.accessKeyId + '/' + credentialScope,
'SignedHeaders=' + signedHeaders,
'Signature=' + signature
].join(', ');
// Return the calculated headers
return {
headers: {
Authorization: authorizationHeader,
'X-Amz-Date': requestDateTime
}
};
}