sigflint 1.0.1 → 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 +8 -0
- package/README.md +10 -6
- package/dist/bun.js +259 -140
- package/dist/node.js +273 -149
- package/index.d.ts +8 -2
- package/package.json +47 -38
- package/src/base.js +261 -58
- package/src/bun.js +11 -0
- package/src/chunks.js +65 -20
- package/src/ecdsa-node.js +16 -1
- package/src/events.js +44 -6
- package/src/node.js +30 -4
- package/src/presign.js +78 -12
- package/src/v4a.js +89 -19
- package/src/web.js +194 -29
- package/types/base.d.ts +80 -33
- 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/index.d.ts
CHANGED
|
@@ -26,13 +26,19 @@ export interface EventSigner {
|
|
|
26
26
|
* Node.js, Bun and Deno return the headers directly. Cloudflare Workers and browsers have only promise-based
|
|
27
27
|
* WebCrypto and return a promise. Always `await` the result and the same code runs everywhere.
|
|
28
28
|
*/
|
|
29
|
-
export function getAuthHeaders(
|
|
29
|
+
export function getAuthHeaders(
|
|
30
|
+
request: SignRequest,
|
|
31
|
+
credentials: Credentials,
|
|
32
|
+
): MaybePromise<AuthHeaders>;
|
|
30
33
|
|
|
31
34
|
/** A presigned URL: the original URL with the authentication parameters and the signature added. `request.expires` is the lifetime in seconds (default 3600). */
|
|
32
35
|
export function presign(request: SignRequest, credentials: Credentials): MaybePromise<string>;
|
|
33
36
|
|
|
34
37
|
/** SigV4a (multi-region) headers; `request.regionSet` is required, for example 'us-east-1,us-west-2' or '*'. */
|
|
35
|
-
export function getAuthHeadersV4a(
|
|
38
|
+
export function getAuthHeadersV4a(
|
|
39
|
+
request: SignRequestV4a,
|
|
40
|
+
credentials: Credentials,
|
|
41
|
+
): MaybePromise<AuthHeaders>;
|
|
36
42
|
|
|
37
43
|
/** A SigV4a presigned URL; `request.regionSet` is required. */
|
|
38
44
|
export function presignV4a(request: SignRequestV4a, credentials: Credentials): MaybePromise<string>;
|
package/package.json
CHANGED
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigflint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Fast, dependency-free AWS Signature Version 4 signing for every AWS service: headers, presigned URLs, SigV4a, chunked uploads and event streams. Node.js, Bun, Deno, Cloudflare Workers and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"authorization",
|
|
6
7
|
"aws",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"bun",
|
|
9
|
+
"cloudflare-workers",
|
|
10
|
+
"deno",
|
|
10
11
|
"presigned-url",
|
|
12
|
+
"s3",
|
|
11
13
|
"signature",
|
|
12
14
|
"signing",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"deno",
|
|
16
|
-
"cloudflare-workers"
|
|
15
|
+
"sigv4",
|
|
16
|
+
"sigv4a"
|
|
17
17
|
],
|
|
18
|
+
"homepage": "https://github.com/bompus/sigflint#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/bompus/sigflint/issues"
|
|
21
|
+
},
|
|
18
22
|
"license": "MIT",
|
|
19
23
|
"author": "bompus",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/bompus/sigflint.git"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"dist",
|
|
31
|
+
"types",
|
|
32
|
+
"index.d.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
20
37
|
"type": "module",
|
|
21
38
|
"sideEffects": false,
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=22.12.0"
|
|
24
|
-
},
|
|
25
39
|
"main": "./dist/node.js",
|
|
26
40
|
"types": "./index.d.ts",
|
|
27
41
|
"exports": {
|
|
@@ -40,44 +54,39 @@
|
|
|
40
54
|
},
|
|
41
55
|
"./package.json": "./package.json"
|
|
42
56
|
},
|
|
43
|
-
"files": [
|
|
44
|
-
"src",
|
|
45
|
-
"dist",
|
|
46
|
-
"types",
|
|
47
|
-
"index.d.ts",
|
|
48
|
-
"README.md",
|
|
49
|
-
"LICENSE",
|
|
50
|
-
"CHANGELOG.md"
|
|
51
|
-
],
|
|
52
57
|
"scripts": {
|
|
53
|
-
"build": "
|
|
58
|
+
"build": "bun build src/node.js --target=node --outfile=dist/node.js && bun build src/bun.js --target=bun --outfile=dist/bun.js",
|
|
54
59
|
"pretest": "npm run build",
|
|
55
60
|
"test": "node --test \"test/*.test.js\"",
|
|
56
61
|
"pretest:bun": "npm run build",
|
|
57
62
|
"test:bun": "bun test test/",
|
|
58
|
-
"types": "tsc &&
|
|
63
|
+
"types": "tsc -p tsconfig.build.json && bun --check --tsconfig-override tsconfig.json && bun --check --tsconfig-override test/types/tsconfig.json",
|
|
64
|
+
"pretypes:streams": "npm ci --prefix conformance/streams",
|
|
65
|
+
"types:streams": "bun --check --tsconfig-override conformance/streams/tsconfig.json",
|
|
66
|
+
"pretypes:bench": "npm ci --prefix bench",
|
|
67
|
+
"types:bench": "bun --check --tsconfig-override bench/tsconfig.json",
|
|
59
68
|
"conformance": "node conformance/build.mjs && node conformance/build-more.mjs",
|
|
60
|
-
"prepack": "tsc && npm run build",
|
|
61
|
-
"lint": "
|
|
62
|
-
"format": "
|
|
63
|
-
"check:style": "npm run lint &&
|
|
64
|
-
"check": "npm
|
|
69
|
+
"prepack": "tsc -p tsconfig.build.json && npm run build",
|
|
70
|
+
"lint": "tsc -p tsconfig.build.json && oxlint --deny-warnings",
|
|
71
|
+
"format": "bun fmt --no-script && ruff format . && git ls-files -z '*.sh' | xargs -0 shfmt -w",
|
|
72
|
+
"check:style": "bun fmt --no-script --check && npm run lint && npm run check:py && npm run check:sh",
|
|
73
|
+
"check": "npm run check:style && npm run types && npm run types:streams && npm run types:bench && npm test && npm run check:package",
|
|
65
74
|
"check:benchmarked": "node bench/check-benchmarked.mjs",
|
|
66
|
-
"check:
|
|
75
|
+
"check:py": "ruff format --check . && ruff check .",
|
|
76
|
+
"check:sh": "git ls-files -z '*.sh' | xargs -0 shellcheck && git ls-files -z '*.sh' | xargs -0 shfmt -d -i 2 -bn -ci",
|
|
77
|
+
"check:package": "tsc -p tsconfig.build.json && npm run build && publint && attw --pack . --profile esm-only"
|
|
67
78
|
},
|
|
68
79
|
"devDependencies": {
|
|
69
80
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
81
|
+
"@types/bun": "1.4.3",
|
|
70
82
|
"@types/node": "^26.6.5",
|
|
71
|
-
"
|
|
83
|
+
"oxfmt": "0.73.0",
|
|
84
|
+
"oxlint": "1.88.0",
|
|
85
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
72
86
|
"publint": "^0.3.25",
|
|
73
|
-
"typescript": "
|
|
74
|
-
},
|
|
75
|
-
"repository": {
|
|
76
|
-
"type": "git",
|
|
77
|
-
"url": "git+https://github.com/bompus/sigflint.git"
|
|
87
|
+
"typescript": "7.1.0-beta"
|
|
78
88
|
},
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
}
|
|
82
|
-
"homepage": "https://github.com/bompus/sigflint#readme"
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=22.12.0"
|
|
91
|
+
}
|
|
83
92
|
}
|
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;
|
|
@@ -219,13 +335,29 @@ 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
362
|
const { url, signHeaders } = request;
|
|
231
363
|
const method = request.method.toUpperCase(); // 'get' is signed as GET, which is what fetch sends and what botocore signs
|
|
@@ -237,31 +369,42 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
|
|
|
237
369
|
const datetime = amzDatetime(request.now ?? Date.now());
|
|
238
370
|
const date = datetime.slice(0, 8);
|
|
239
371
|
|
|
240
|
-
|
|
372
|
+
/** @type {AuthHeaders} */
|
|
373
|
+
const headers = withHash
|
|
374
|
+
? { 'x-amz-date': datetime, 'x-amz-content-sha256': payloadHash }
|
|
375
|
+
: { 'x-amz-date': datetime }; // as returned
|
|
241
376
|
|
|
242
377
|
const extraNames = signHeaders === undefined ? [] : Object.keys(signHeaders);
|
|
243
378
|
let canonicalHeaders;
|
|
244
379
|
let signedHeaders;
|
|
245
380
|
if (extraNames.length === 0 && !sessionToken) {
|
|
246
381
|
// the usual request: only the headers that are always signed, already in sorted order
|
|
247
|
-
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`;
|
|
248
385
|
signedHeaders = withHash ? 'host;x-amz-content-sha256;x-amz-date' : 'host;x-amz-date';
|
|
249
386
|
} else {
|
|
250
387
|
// the extra headers (and the session token) in name order, then merged with the ones that are always signed;
|
|
251
388
|
// a header named like one of those, or like an earlier extra, replaces it
|
|
389
|
+
/** @type {string[]} */
|
|
252
390
|
const names = [];
|
|
391
|
+
/** @type {string[]} */
|
|
253
392
|
const values = [];
|
|
254
393
|
if (sessionToken) {
|
|
255
394
|
headers['x-amz-security-token'] = sessionToken;
|
|
256
395
|
insert(names, values, 'x-amz-security-token', sessionToken.trim());
|
|
257
396
|
}
|
|
258
397
|
for (const name of extraNames) {
|
|
259
|
-
const value = /** @type {
|
|
398
|
+
const value = /** @type {string} */ (
|
|
399
|
+
/** @type {Record<string, string>} */ (signHeaders)[name]
|
|
400
|
+
);
|
|
260
401
|
const lowerName = signHeaderName(name, value);
|
|
261
402
|
headers[lowerName] = value;
|
|
262
403
|
insert(names, values, lowerName, headerValue(value));
|
|
263
404
|
}
|
|
264
|
-
const baseNames = withHash
|
|
405
|
+
const baseNames = withHash
|
|
406
|
+
? ['host', 'x-amz-content-sha256', 'x-amz-date']
|
|
407
|
+
: ['host', 'x-amz-date'];
|
|
265
408
|
const baseValues = withHash ? [url.host, payloadHash, datetime] : [url.host, datetime];
|
|
266
409
|
const nBase = baseNames.length;
|
|
267
410
|
canonicalHeaders = '';
|
|
@@ -271,50 +414,102 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
|
|
|
271
414
|
while (i < names.length || j < nBase) {
|
|
272
415
|
let name;
|
|
273
416
|
let value;
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
427
|
} else {
|
|
279
|
-
name = baseNames[j];
|
|
280
|
-
value = baseValues[j++];
|
|
428
|
+
name = /** @type {string} */ (baseNames[j]);
|
|
429
|
+
value = /** @type {string} */ (baseValues[j++]);
|
|
281
430
|
}
|
|
282
|
-
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
|
|
283
434
|
canonicalHeaders += `${name}:${value}\n`;
|
|
284
435
|
signedHeaders += signedHeaders === '' ? name : ';' + name;
|
|
285
436
|
}
|
|
286
437
|
}
|
|
287
438
|
|
|
288
439
|
const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(url.search)}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
|
289
|
-
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
|
+
};
|
|
290
451
|
}
|
|
291
452
|
|
|
292
|
-
|
|
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}`;
|
|
293
461
|
|
|
294
|
-
/**
|
|
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
|
+
*/
|
|
295
468
|
export function authorize(prepared, accessKeyId, signature) {
|
|
296
469
|
prepared.headers.authorization = `${prepared.algorithm} Credential=${accessKeyId}/${prepared.credentialScope}, SignedHeaders=${prepared.signedHeaders}, Signature=${signature}`;
|
|
297
470
|
return prepared.headers;
|
|
298
471
|
}
|
|
299
472
|
|
|
473
|
+
/**
|
|
474
|
+
* @param {(key: string | Uint8Array, data: string) => Uint8Array} hmac
|
|
475
|
+
* @returns {(secretAccessKey: string, date: string, region: string, service: string) => Uint8Array}
|
|
476
|
+
*/
|
|
300
477
|
export function createKeys(hmac) {
|
|
478
|
+
/** @type {Map<string, Uint8Array>} */
|
|
301
479
|
const signingKeys = new Map();
|
|
302
480
|
const maxSigningKeys = 1000;
|
|
303
481
|
|
|
304
482
|
// the key used last is checked first, which skips building the lookup string on repeated calls
|
|
483
|
+
/** @type {string | undefined} */
|
|
305
484
|
let lastSecret;
|
|
485
|
+
/** @type {string | undefined} */
|
|
306
486
|
let lastDate;
|
|
487
|
+
/** @type {string | undefined} */
|
|
307
488
|
let lastRegion;
|
|
489
|
+
/** @type {string | undefined} */
|
|
308
490
|
let lastService;
|
|
491
|
+
/** @type {Uint8Array | undefined} */
|
|
309
492
|
let lastKey;
|
|
310
493
|
|
|
311
494
|
return function getSigningKey(secretAccessKey, date, region, service) {
|
|
312
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
secretAccessKey === lastSecret &&
|
|
497
|
+
date === lastDate &&
|
|
498
|
+
region === lastRegion &&
|
|
499
|
+
service === lastService
|
|
500
|
+
) {
|
|
501
|
+
return /** @type {Uint8Array} */ (lastKey);
|
|
502
|
+
}
|
|
313
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
|
|
314
504
|
let signingKey = signingKeys.get(cacheKey);
|
|
315
505
|
if (signingKey === undefined) {
|
|
316
|
-
signingKey = hmac(
|
|
317
|
-
|
|
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
|
+
}
|
|
318
513
|
signingKeys.set(cacheKey, signingKey);
|
|
319
514
|
}
|
|
320
515
|
lastSecret = secretAccessKey;
|
|
@@ -336,7 +531,15 @@ export function createSigner({ hexHash, hmacHex, getSigningKey }) {
|
|
|
336
531
|
return authorize(
|
|
337
532
|
prepared,
|
|
338
533
|
credentials.accessKeyId,
|
|
339
|
-
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
|
+
),
|
|
340
543
|
);
|
|
341
544
|
};
|
|
342
545
|
}
|