sigflint 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +26 -8
- package/dist/bun.js +267 -142
- package/dist/node.js +281 -151
- package/index.d.ts +8 -2
- package/package.json +47 -38
- package/src/base.js +264 -60
- package/src/bun.js +11 -0
- package/src/chunks.js +66 -17
- package/src/ecdsa-node.js +16 -1
- package/src/events.js +44 -6
- package/src/node.js +30 -4
- package/src/presign.js +80 -13
- package/src/v4a.js +89 -19
- package/src/web.js +195 -30
- package/types/base.d.ts +82 -35
- package/types/chunks.d.ts +37 -11
- package/types/ecdsa-node.d.ts +7 -2
- package/types/events.d.ts +26 -6
- package/types/presign.d.ts +20 -10
- package/types/v4a.d.ts +42 -13
package/src/base.js
CHANGED
|
@@ -9,20 +9,36 @@
|
|
|
9
9
|
|
|
10
10
|
export const emptyBodyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} Scheme
|
|
14
|
+
* @property {string} algorithm
|
|
15
|
+
* @property {(date: string, region: string, service: string) => string} scope
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The scheme of a signature: its algorithm name and how the credential scope is written. SigV4a (v4a.js) has its own.
|
|
20
|
+
* @type {Scheme}
|
|
21
|
+
*/
|
|
22
|
+
export const symmetric = {
|
|
23
|
+
algorithm: 'AWS4-HMAC-SHA256',
|
|
24
|
+
scope: (date, region, service) => `${date}/${region}/${service}/aws4_request`,
|
|
25
|
+
};
|
|
14
26
|
|
|
15
27
|
const reSafePath = /^[A-Za-z0-9\-._~/]*$/;
|
|
16
28
|
const reSafeQuery = /^[A-Za-z0-9\-._~=&]*$/;
|
|
17
29
|
const reSpaces = /[ \t]+/g;
|
|
18
|
-
const reDirty = /\t|
|
|
30
|
+
const reDirty = /\t| {2}/;
|
|
19
31
|
|
|
20
32
|
// ASCII code -> what it is written as in a canonical path / query. url.pathname and url.search are always ASCII (the URL parser percent-encodes everything else).
|
|
21
33
|
// Built on first use, so importing this module costs nothing.
|
|
34
|
+
/** @type {string[] | undefined} */
|
|
22
35
|
let pathTable;
|
|
36
|
+
/** @type {string[] | undefined} */
|
|
23
37
|
let queryTable;
|
|
24
38
|
function tables() {
|
|
25
|
-
if (pathTable !== undefined)
|
|
39
|
+
if (pathTable !== undefined) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
26
42
|
pathTable = new Array(256);
|
|
27
43
|
queryTable = new Array(256);
|
|
28
44
|
for (let b = 0; b < 256; b++) {
|
|
@@ -34,22 +50,35 @@ function tables() {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
|
|
53
|
+
/** @param {number} code */
|
|
37
54
|
function hexValue(code) {
|
|
38
|
-
if (code >= 48 && code <= 57)
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
if (code >= 48 && code <= 57) {
|
|
56
|
+
return code - 48;
|
|
57
|
+
} // 0-9
|
|
58
|
+
if (code >= 65 && code <= 70) {
|
|
59
|
+
return code - 55;
|
|
60
|
+
} // A-F
|
|
61
|
+
if (code >= 97 && code <= 102) {
|
|
62
|
+
return code - 87;
|
|
63
|
+
} // a-f
|
|
41
64
|
return -1;
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
// Decodes %XX escapes to bytes (a '%' that does not start an escape is the byte 0x25) and encodes each byte with the table; '+' is a space.
|
|
45
68
|
// Runs of characters that are already canonical are copied as one piece.
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} string
|
|
71
|
+
* @param {string[]} table
|
|
72
|
+
*/
|
|
46
73
|
function canonicalEncode(string, table) {
|
|
47
74
|
let out = '';
|
|
48
75
|
let start = 0;
|
|
49
76
|
for (let i = 0; i < string.length; i++) {
|
|
50
77
|
const code = string.charCodeAt(i);
|
|
51
|
-
const same = table[code];
|
|
52
|
-
if (same.length === 1 && code !== 43)
|
|
78
|
+
const same = /** @type {string} */ (table[code]);
|
|
79
|
+
if (same.length === 1 && code !== 43) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
53
82
|
out += string.slice(start, i);
|
|
54
83
|
start = i + 1;
|
|
55
84
|
if (code === 37) {
|
|
@@ -69,39 +98,69 @@ function canonicalEncode(string, table) {
|
|
|
69
98
|
return start === 0 ? string : out + string.slice(start);
|
|
70
99
|
}
|
|
71
100
|
|
|
101
|
+
/** @param {string} pathname */
|
|
72
102
|
function canonicalPathGeneral(pathname) {
|
|
73
103
|
const parts = pathname.split('/');
|
|
74
104
|
const kept = [];
|
|
75
105
|
for (let i = 0; i < parts.length; i++) {
|
|
76
106
|
const part = parts[i];
|
|
77
|
-
if (part === '' || part === '.')
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
if (part === '' || part === '.') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (part === '..') {
|
|
111
|
+
kept.pop();
|
|
112
|
+
} else {
|
|
113
|
+
kept.push(part);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const path =
|
|
117
|
+
(pathname.startsWith('/') ? '/' : '') +
|
|
118
|
+
kept.join('/') +
|
|
119
|
+
(pathname.endsWith('/') && kept.length > 0 ? '/' : '');
|
|
120
|
+
if (reSafePath.test(path)) {
|
|
121
|
+
return path === '' ? '/' : path;
|
|
80
122
|
}
|
|
81
|
-
const path = (pathname.startsWith('/') ? '/' : '') + kept.join('/') + (pathname.endsWith('/') && kept.length > 0 ? '/' : '');
|
|
82
|
-
if (reSafePath.test(path)) return path === '' ? '/' : path;
|
|
83
123
|
tables();
|
|
84
124
|
let out = '';
|
|
85
|
-
for (let i = 0; i < path.length; i++)
|
|
125
|
+
for (let i = 0; i < path.length; i++) {
|
|
126
|
+
out +=
|
|
127
|
+
/** @type {string[]} */ (queryTable)[path.charCodeAt(i)] === path[i] || path[i] === '/'
|
|
128
|
+
? path[i]
|
|
129
|
+
: /** @type {string[]} */ (queryTable)[path.charCodeAt(i)];
|
|
130
|
+
}
|
|
86
131
|
return out;
|
|
87
132
|
}
|
|
88
133
|
|
|
134
|
+
/** @param {string} pathname */
|
|
89
135
|
function canonicalPathS3(pathname) {
|
|
90
|
-
if (reSafePath.test(pathname))
|
|
136
|
+
if (reSafePath.test(pathname)) {
|
|
137
|
+
return pathname === '' ? '/' : pathname;
|
|
138
|
+
}
|
|
91
139
|
tables();
|
|
92
|
-
return canonicalEncode(pathname, pathTable);
|
|
140
|
+
return canonicalEncode(pathname, /** @type {string[]} */ (pathTable));
|
|
93
141
|
}
|
|
94
142
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The canonical path for a service.
|
|
145
|
+
* @param {string} service
|
|
146
|
+
* @param {string} pathname
|
|
147
|
+
*/
|
|
148
|
+
export const canonicalPath = (service, pathname) =>
|
|
149
|
+
service === 's3' ? canonicalPathS3(pathname) : canonicalPathGeneral(pathname);
|
|
97
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {[string, string]} a
|
|
153
|
+
* @param {[string, string]} b
|
|
154
|
+
*/
|
|
98
155
|
function comparePairs(a, b) {
|
|
99
156
|
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
/** search
|
|
159
|
+
/** @param {string} search - url.search, which is '' or starts with '?' */
|
|
103
160
|
export function canonicalQuery(search) {
|
|
104
|
-
if (search.length < 2)
|
|
161
|
+
if (search.length < 2) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
105
164
|
const query = search.slice(1);
|
|
106
165
|
const simple = reSafeQuery.test(query);
|
|
107
166
|
if (simple) {
|
|
@@ -113,46 +172,85 @@ export function canonicalQuery(search) {
|
|
|
113
172
|
let start = 0;
|
|
114
173
|
for (;;) {
|
|
115
174
|
let end = query.indexOf('&', start);
|
|
116
|
-
if (end === -1)
|
|
175
|
+
if (end === -1) {
|
|
176
|
+
end = query.length;
|
|
177
|
+
}
|
|
117
178
|
let eq = query.indexOf('=', start);
|
|
118
|
-
if (eq === -1 || eq > end)
|
|
179
|
+
if (eq === -1 || eq > end) {
|
|
180
|
+
eq = end;
|
|
181
|
+
}
|
|
119
182
|
const name = query.slice(start, eq);
|
|
120
183
|
const value = query.slice(eq + 1, end);
|
|
121
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
end === start ||
|
|
186
|
+
eq === start ||
|
|
187
|
+
value.includes('=') ||
|
|
188
|
+
(start > 0 && (name < previousName || (name === previousName && value <= previousValue)))
|
|
189
|
+
) {
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
122
192
|
out += (start === 0 ? '' : '&') + name + '=' + value;
|
|
123
|
-
if (end === query.length)
|
|
193
|
+
if (end === query.length) {
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
124
196
|
previousName = name;
|
|
125
197
|
previousValue = value;
|
|
126
198
|
start = end + 1;
|
|
127
199
|
}
|
|
128
200
|
}
|
|
129
201
|
tables();
|
|
202
|
+
/** @type {[string, string][]} */
|
|
130
203
|
const pairs = [];
|
|
131
204
|
for (const part of query.split('&')) {
|
|
132
|
-
if (part === '')
|
|
205
|
+
if (part === '') {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
133
208
|
const eq = part.indexOf('=');
|
|
134
209
|
const rawName = eq === -1 ? part : part.slice(0, eq);
|
|
135
210
|
const rawValue = eq === -1 ? '' : part.slice(eq + 1);
|
|
136
211
|
// a '=' inside a value must be encoded, so the plain-characters shortcut applies only to values without one
|
|
137
|
-
pairs.push(
|
|
212
|
+
pairs.push(
|
|
213
|
+
simple && !rawValue.includes('=')
|
|
214
|
+
? [rawName, rawValue]
|
|
215
|
+
: [
|
|
216
|
+
canonicalEncode(rawName, /** @type {string[]} */ (queryTable)),
|
|
217
|
+
canonicalEncode(rawValue, /** @type {string[]} */ (queryTable)),
|
|
218
|
+
],
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (pairs.length > 1) {
|
|
222
|
+
pairs.sort(comparePairs);
|
|
138
223
|
}
|
|
139
|
-
if (pairs.length > 1) pairs.sort(comparePairs);
|
|
140
224
|
let out = '';
|
|
141
|
-
for (let i = 0; i < pairs.length; i++)
|
|
225
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
226
|
+
out +=
|
|
227
|
+
(i === 0 ? '' : '&') +
|
|
228
|
+
/** @type {[string, string]} */ (pairs[i])[0] +
|
|
229
|
+
'=' +
|
|
230
|
+
/** @type {[string, string]} */ (pairs[i])[1];
|
|
231
|
+
}
|
|
142
232
|
return out;
|
|
143
233
|
}
|
|
144
234
|
|
|
145
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Inserts into the name-ordered lists, replacing an entry with the same name.
|
|
237
|
+
* @param {string[]} names
|
|
238
|
+
* @param {string[]} values
|
|
239
|
+
* @param {string} name
|
|
240
|
+
* @param {string} value
|
|
241
|
+
*/
|
|
146
242
|
export function insert(names, values, name, value) {
|
|
147
243
|
let i = names.length;
|
|
148
|
-
while (i > 0 && names[i - 1] > name)
|
|
244
|
+
while (i > 0 && /** @type {string} */ (names[i - 1]) > name) {
|
|
245
|
+
i--;
|
|
246
|
+
}
|
|
149
247
|
if (i > 0 && names[i - 1] === name) {
|
|
150
248
|
values[i - 1] = value;
|
|
151
249
|
return;
|
|
152
250
|
}
|
|
153
251
|
for (let j = names.length; j > i; j--) {
|
|
154
|
-
names[j] = names[j - 1];
|
|
155
|
-
values[j] = values[j - 1];
|
|
252
|
+
names[j] = /** @type {string} */ (names[j - 1]);
|
|
253
|
+
values[j] = /** @type {string} */ (values[j - 1]);
|
|
156
254
|
}
|
|
157
255
|
names[i] = name;
|
|
158
256
|
values[i] = value;
|
|
@@ -161,15 +259,24 @@ export function insert(names, values, name, value) {
|
|
|
161
259
|
/**
|
|
162
260
|
* Refuses a signHeaders entry that cannot be signed as given: x-amz-date and authorization are set by the signer, `__proto__` cannot be a property of the
|
|
163
261
|
* returned headers, and a line break in a value would start another canonical header line. Returns the lowercase name.
|
|
262
|
+
* @param {string} name
|
|
263
|
+
* @param {string} value
|
|
164
264
|
*/
|
|
165
265
|
export function signHeaderName(name, value) {
|
|
166
266
|
const lowerName = name.toLowerCase();
|
|
167
|
-
if (lowerName === 'x-amz-date' || lowerName === 'authorization' || lowerName === '__proto__')
|
|
168
|
-
|
|
267
|
+
if (lowerName === 'x-amz-date' || lowerName === 'authorization' || lowerName === '__proto__') {
|
|
268
|
+
throw new TypeError(`signHeaders cannot contain ${lowerName}`);
|
|
269
|
+
}
|
|
270
|
+
if (/[\r\n]/.test(value)) {
|
|
271
|
+
throw new TypeError(`signHeaders ${lowerName} has a line break in its value`);
|
|
272
|
+
}
|
|
169
273
|
return lowerName;
|
|
170
274
|
}
|
|
171
275
|
|
|
172
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* A header value as it is signed: trimmed, runs of spaces and tabs collapsed.
|
|
278
|
+
* @param {unknown} value
|
|
279
|
+
*/
|
|
173
280
|
export const headerValue = (value) => {
|
|
174
281
|
const trimmed = String(value).trim();
|
|
175
282
|
return reDirty.test(trimmed) ? trimmed.replace(reSpaces, ' ') : trimmed;
|
|
@@ -178,11 +285,20 @@ export const headerValue = (value) => {
|
|
|
178
285
|
// 20130524T000000Z, recomputed only when the second changes
|
|
179
286
|
let lastSecond = NaN;
|
|
180
287
|
let lastDatetime = '';
|
|
288
|
+
/** @param {number} now - milliseconds since the epoch */
|
|
181
289
|
export function amzDatetime(now) {
|
|
182
290
|
const second = Math.floor(now / 1000);
|
|
183
291
|
if (second !== lastSecond) {
|
|
184
292
|
const iso = new Date(second * 1000).toISOString(); // 2013-05-24T00:00:00.000Z
|
|
185
|
-
lastDatetime =
|
|
293
|
+
lastDatetime =
|
|
294
|
+
iso.slice(0, 4) +
|
|
295
|
+
iso.slice(5, 7) +
|
|
296
|
+
iso.slice(8, 10) +
|
|
297
|
+
'T' +
|
|
298
|
+
iso.slice(11, 13) +
|
|
299
|
+
iso.slice(14, 16) +
|
|
300
|
+
iso.slice(17, 19) +
|
|
301
|
+
'Z';
|
|
186
302
|
lastSecond = second;
|
|
187
303
|
}
|
|
188
304
|
return lastDatetime;
|
|
@@ -190,7 +306,7 @@ export function amzDatetime(now) {
|
|
|
190
306
|
|
|
191
307
|
/**
|
|
192
308
|
* @typedef {Object} SignRequest
|
|
193
|
-
* @property {string} method - GET, HEAD, PUT, POST, DELETE
|
|
309
|
+
* @property {string} method - GET, HEAD, PUT, POST, DELETE; any case, signed in upper case (as botocore does)
|
|
194
310
|
* @property {URL} url
|
|
195
311
|
* @property {string} [service] - default 's3'
|
|
196
312
|
* @property {string} [region] - for example us-west-2
|
|
@@ -219,15 +335,32 @@ export function amzDatetime(now) {
|
|
|
219
335
|
* @property {ReturnType<typeof createKeys>} getSigningKey - the entry's one signing-key cache, shared by every symmetric signer
|
|
220
336
|
*/
|
|
221
337
|
|
|
222
|
-
/**
|
|
338
|
+
/**
|
|
339
|
+
* The payload hash for x-amz-content-sha256. The hash function may return a promise, which is then the result.
|
|
340
|
+
* @template {string | Promise<string>} H
|
|
341
|
+
* @param {SignRequest} request
|
|
342
|
+
* @param {(data: string) => H} hexHash
|
|
343
|
+
* @returns {string | H}
|
|
344
|
+
*/
|
|
223
345
|
export function payloadHashOf(request, hexHash) {
|
|
224
346
|
const body = request.body ?? '';
|
|
225
|
-
return request.unsignedPayload
|
|
347
|
+
return request.unsignedPayload
|
|
348
|
+
? 'UNSIGNED-PAYLOAD'
|
|
349
|
+
: body.length === 0
|
|
350
|
+
? emptyBodyHash
|
|
351
|
+
: hexHash(body);
|
|
226
352
|
}
|
|
227
353
|
|
|
228
|
-
/**
|
|
354
|
+
/**
|
|
355
|
+
* Everything up to the hashing: the headers to return, the canonical request and the credential scope.
|
|
356
|
+
* @param {SignRequest} request
|
|
357
|
+
* @param {Credentials} credentials
|
|
358
|
+
* @param {string} payloadHash
|
|
359
|
+
* @param {Scheme} [scheme]
|
|
360
|
+
*/
|
|
229
361
|
export function prepare(request, credentials, payloadHash, scheme = symmetric) {
|
|
230
|
-
const {
|
|
362
|
+
const { url, signHeaders } = request;
|
|
363
|
+
const method = request.method.toUpperCase(); // 'get' is signed as GET, which is what fetch sends and what botocore signs
|
|
231
364
|
const region = request.region ?? '';
|
|
232
365
|
const service = request.service ?? 's3';
|
|
233
366
|
// S3 always sends x-amz-content-sha256; other services only when the payload is unsigned
|
|
@@ -236,31 +369,42 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
|
|
|
236
369
|
const datetime = amzDatetime(request.now ?? Date.now());
|
|
237
370
|
const date = datetime.slice(0, 8);
|
|
238
371
|
|
|
239
|
-
|
|
372
|
+
/** @type {AuthHeaders} */
|
|
373
|
+
const headers = withHash
|
|
374
|
+
? { 'x-amz-date': datetime, 'x-amz-content-sha256': payloadHash }
|
|
375
|
+
: { 'x-amz-date': datetime }; // as returned
|
|
240
376
|
|
|
241
377
|
const extraNames = signHeaders === undefined ? [] : Object.keys(signHeaders);
|
|
242
378
|
let canonicalHeaders;
|
|
243
379
|
let signedHeaders;
|
|
244
380
|
if (extraNames.length === 0 && !sessionToken) {
|
|
245
381
|
// the usual request: only the headers that are always signed, already in sorted order
|
|
246
|
-
canonicalHeaders = withHash
|
|
382
|
+
canonicalHeaders = withHash
|
|
383
|
+
? `host:${url.host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${datetime}\n`
|
|
384
|
+
: `host:${url.host}\nx-amz-date:${datetime}\n`;
|
|
247
385
|
signedHeaders = withHash ? 'host;x-amz-content-sha256;x-amz-date' : 'host;x-amz-date';
|
|
248
386
|
} else {
|
|
249
387
|
// the extra headers (and the session token) in name order, then merged with the ones that are always signed;
|
|
250
388
|
// a header named like one of those, or like an earlier extra, replaces it
|
|
389
|
+
/** @type {string[]} */
|
|
251
390
|
const names = [];
|
|
391
|
+
/** @type {string[]} */
|
|
252
392
|
const values = [];
|
|
253
393
|
if (sessionToken) {
|
|
254
394
|
headers['x-amz-security-token'] = sessionToken;
|
|
255
395
|
insert(names, values, 'x-amz-security-token', sessionToken.trim());
|
|
256
396
|
}
|
|
257
397
|
for (const name of extraNames) {
|
|
258
|
-
const value = /** @type {
|
|
398
|
+
const value = /** @type {string} */ (
|
|
399
|
+
/** @type {Record<string, string>} */ (signHeaders)[name]
|
|
400
|
+
);
|
|
259
401
|
const lowerName = signHeaderName(name, value);
|
|
260
402
|
headers[lowerName] = value;
|
|
261
403
|
insert(names, values, lowerName, headerValue(value));
|
|
262
404
|
}
|
|
263
|
-
const baseNames = withHash
|
|
405
|
+
const baseNames = withHash
|
|
406
|
+
? ['host', 'x-amz-content-sha256', 'x-amz-date']
|
|
407
|
+
: ['host', 'x-amz-date'];
|
|
264
408
|
const baseValues = withHash ? [url.host, payloadHash, datetime] : [url.host, datetime];
|
|
265
409
|
const nBase = baseNames.length;
|
|
266
410
|
canonicalHeaders = '';
|
|
@@ -270,50 +414,102 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
|
|
|
270
414
|
while (i < names.length || j < nBase) {
|
|
271
415
|
let name;
|
|
272
416
|
let value;
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
417
|
+
if (
|
|
418
|
+
j === nBase ||
|
|
419
|
+
(i < names.length &&
|
|
420
|
+
/** @type {string} */ (names[i]) <= /** @type {string} */ (baseNames[j]))
|
|
421
|
+
) {
|
|
422
|
+
name = /** @type {string} */ (names[i]);
|
|
423
|
+
value = /** @type {string} */ (values[i++]);
|
|
424
|
+
if (j < nBase && name === baseNames[j]) {
|
|
425
|
+
j++;
|
|
426
|
+
}
|
|
277
427
|
} else {
|
|
278
|
-
name = baseNames[j];
|
|
279
|
-
value = baseValues[j++];
|
|
428
|
+
name = /** @type {string} */ (baseNames[j]);
|
|
429
|
+
value = /** @type {string} */ (baseValues[j++]);
|
|
280
430
|
}
|
|
281
|
-
if (name === 'x-amz-content-sha256')
|
|
431
|
+
if (name === 'x-amz-content-sha256') {
|
|
432
|
+
payloadHash = value;
|
|
433
|
+
} // a caller-supplied one replaces the computed one
|
|
282
434
|
canonicalHeaders += `${name}:${value}\n`;
|
|
283
435
|
signedHeaders += signedHeaders === '' ? name : ';' + name;
|
|
284
436
|
}
|
|
285
437
|
}
|
|
286
438
|
|
|
287
439
|
const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(url.search)}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
|
288
|
-
return {
|
|
440
|
+
return {
|
|
441
|
+
headers,
|
|
442
|
+
canonicalRequest,
|
|
443
|
+
datetime,
|
|
444
|
+
date,
|
|
445
|
+
region,
|
|
446
|
+
service,
|
|
447
|
+
credentialScope: scheme.scope(date, region, service),
|
|
448
|
+
signedHeaders,
|
|
449
|
+
algorithm: scheme.algorithm,
|
|
450
|
+
};
|
|
289
451
|
}
|
|
290
452
|
|
|
291
|
-
|
|
453
|
+
/** @typedef {{ algorithm: string, datetime: string, credentialScope: string }} StringToSignParts */
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {StringToSignParts} prepared
|
|
457
|
+
* @param {string} canonicalRequestHash
|
|
458
|
+
*/
|
|
459
|
+
export const stringToSign = (prepared, canonicalRequestHash) =>
|
|
460
|
+
`${prepared.algorithm}\n${prepared.datetime}\n${prepared.credentialScope}\n${canonicalRequestHash}`;
|
|
292
461
|
|
|
293
|
-
/**
|
|
462
|
+
/**
|
|
463
|
+
* Adds the authorization header to the headers to return.
|
|
464
|
+
* @param {ReturnType<typeof prepare>} prepared
|
|
465
|
+
* @param {string} accessKeyId
|
|
466
|
+
* @param {string} signature
|
|
467
|
+
*/
|
|
294
468
|
export function authorize(prepared, accessKeyId, signature) {
|
|
295
469
|
prepared.headers.authorization = `${prepared.algorithm} Credential=${accessKeyId}/${prepared.credentialScope}, SignedHeaders=${prepared.signedHeaders}, Signature=${signature}`;
|
|
296
470
|
return prepared.headers;
|
|
297
471
|
}
|
|
298
472
|
|
|
473
|
+
/**
|
|
474
|
+
* @param {(key: string | Uint8Array, data: string) => Uint8Array} hmac
|
|
475
|
+
* @returns {(secretAccessKey: string, date: string, region: string, service: string) => Uint8Array}
|
|
476
|
+
*/
|
|
299
477
|
export function createKeys(hmac) {
|
|
478
|
+
/** @type {Map<string, Uint8Array>} */
|
|
300
479
|
const signingKeys = new Map();
|
|
301
480
|
const maxSigningKeys = 1000;
|
|
302
481
|
|
|
303
482
|
// the key used last is checked first, which skips building the lookup string on repeated calls
|
|
483
|
+
/** @type {string | undefined} */
|
|
304
484
|
let lastSecret;
|
|
485
|
+
/** @type {string | undefined} */
|
|
305
486
|
let lastDate;
|
|
487
|
+
/** @type {string | undefined} */
|
|
306
488
|
let lastRegion;
|
|
489
|
+
/** @type {string | undefined} */
|
|
307
490
|
let lastService;
|
|
491
|
+
/** @type {Uint8Array | undefined} */
|
|
308
492
|
let lastKey;
|
|
309
493
|
|
|
310
494
|
return function getSigningKey(secretAccessKey, date, region, service) {
|
|
311
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
secretAccessKey === lastSecret &&
|
|
497
|
+
date === lastDate &&
|
|
498
|
+
region === lastRegion &&
|
|
499
|
+
service === lastService
|
|
500
|
+
) {
|
|
501
|
+
return /** @type {Uint8Array} */ (lastKey);
|
|
502
|
+
}
|
|
312
503
|
const cacheKey = `${date}\n${region}\n${service}\n${secretAccessKey}`; // a region or service cannot contain a line break, so two scopes never share a key
|
|
313
504
|
let signingKey = signingKeys.get(cacheKey);
|
|
314
505
|
if (signingKey === undefined) {
|
|
315
|
-
signingKey = hmac(
|
|
316
|
-
|
|
506
|
+
signingKey = hmac(
|
|
507
|
+
hmac(hmac(hmac('AWS4' + secretAccessKey, date), region), service),
|
|
508
|
+
'aws4_request',
|
|
509
|
+
);
|
|
510
|
+
if (signingKeys.size >= maxSigningKeys) {
|
|
511
|
+
signingKeys.delete(/** @type {string} */ (signingKeys.keys().next().value));
|
|
512
|
+
}
|
|
317
513
|
signingKeys.set(cacheKey, signingKey);
|
|
318
514
|
}
|
|
319
515
|
lastSecret = secretAccessKey;
|
|
@@ -335,7 +531,15 @@ export function createSigner({ hexHash, hmacHex, getSigningKey }) {
|
|
|
335
531
|
return authorize(
|
|
336
532
|
prepared,
|
|
337
533
|
credentials.accessKeyId,
|
|
338
|
-
hmacHex(
|
|
534
|
+
hmacHex(
|
|
535
|
+
getSigningKey(
|
|
536
|
+
credentials.secretAccessKey,
|
|
537
|
+
prepared.date,
|
|
538
|
+
prepared.region,
|
|
539
|
+
prepared.service,
|
|
540
|
+
),
|
|
541
|
+
stringToSign(prepared, hexHash(prepared.canonicalRequest)),
|
|
542
|
+
),
|
|
339
543
|
);
|
|
340
544
|
};
|
|
341
545
|
}
|
package/src/bun.js
CHANGED
|
@@ -10,15 +10,26 @@ import { createEventSigner } from './events.js';
|
|
|
10
10
|
const bun = /** @type {any} */ (globalThis).Bun;
|
|
11
11
|
|
|
12
12
|
// the HMAC state for the signing key in use is built once and cloned for each signature
|
|
13
|
+
/** @type {Uint8Array | undefined} */
|
|
13
14
|
let baseKey;
|
|
15
|
+
/** @type {any} */
|
|
14
16
|
let base;
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {string | Uint8Array} key
|
|
20
|
+
* @param {string} data
|
|
21
|
+
*/
|
|
16
22
|
const hmac = (key, data) => crypto.createHmac('sha256', key).update(data).digest();
|
|
17
23
|
const getSigningKey = /* @__PURE__ */ createKeys(hmac);
|
|
24
|
+
/** @param {string | Uint8Array} data */
|
|
18
25
|
const hexHash = (data) => bun.SHA256.hash(data, 'hex');
|
|
19
26
|
const primitives = {
|
|
20
27
|
hexHash,
|
|
21
28
|
hmac,
|
|
29
|
+
/**
|
|
30
|
+
* @param {Uint8Array} key
|
|
31
|
+
* @param {string} data
|
|
32
|
+
*/
|
|
22
33
|
hmacHex: (key, data) => {
|
|
23
34
|
if (key !== baseKey) {
|
|
24
35
|
baseKey = key;
|