sigflint 1.0.1 → 1.0.3
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 +12 -0
- package/README.md +21 -7
- 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/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;
|
package/src/chunks.js
CHANGED
|
@@ -4,39 +4,69 @@ import { emptyBodyHash as emptyHash } from './base.js';
|
|
|
4
4
|
|
|
5
5
|
export const utf8 = /* @__PURE__ */ new TextEncoder();
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The seed signature, timestamp and scope of a signed request, from the headers getAuthHeaders returned.
|
|
9
|
+
* @param {import('./base.js').AuthHeaders} authHeaders
|
|
10
|
+
*/
|
|
8
11
|
export function seedOf(authHeaders) {
|
|
9
12
|
const auth = authHeaders.authorization;
|
|
10
|
-
if (typeof auth !== 'string' || !auth.startsWith('AWS4-HMAC-SHA256 '))
|
|
11
|
-
throw new TypeError(
|
|
13
|
+
if (typeof auth !== 'string' || !auth.startsWith('AWS4-HMAC-SHA256 ')) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
'authHeaders must be the headers getAuthHeaders returned (a SigV4a signature cannot start a chunk or event stream)',
|
|
16
|
+
);
|
|
17
|
+
}
|
|
12
18
|
const scope = auth.slice(auth.indexOf('Credential=') + 11, auth.indexOf(',')).split('/'); // access key, date, region, service, aws4_request
|
|
13
19
|
return {
|
|
14
20
|
signature: auth.slice(auth.lastIndexOf('Signature=') + 10),
|
|
15
|
-
datetime: authHeaders['x-amz-date'],
|
|
16
|
-
date: scope[1],
|
|
17
|
-
region: scope[2],
|
|
18
|
-
service: scope[3],
|
|
21
|
+
datetime: /** @type {string} */ (authHeaders['x-amz-date']),
|
|
22
|
+
date: /** @type {string} */ (scope[1]),
|
|
23
|
+
region: /** @type {string} */ (scope[2]),
|
|
24
|
+
service: /** @type {string} */ (scope[3]),
|
|
19
25
|
scope: scope.slice(1).join('/'),
|
|
20
26
|
};
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} datetime
|
|
31
|
+
* @param {string} scope
|
|
32
|
+
* @param {string} previous
|
|
33
|
+
* @param {string} payloadHash
|
|
34
|
+
*/
|
|
35
|
+
export const chunkStringToSign = (datetime, scope, previous, payloadHash) =>
|
|
36
|
+
`AWS4-HMAC-SHA256-PAYLOAD\n${datetime}\n${scope}\n${previous}\n${emptyHash}\n${payloadHash}`;
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} datetime
|
|
39
|
+
* @param {string} scope
|
|
40
|
+
* @param {string} previous
|
|
41
|
+
* @param {string} trailerHash
|
|
42
|
+
*/
|
|
43
|
+
export const trailerStringToSign = (datetime, scope, previous, trailerHash) =>
|
|
44
|
+
`AWS4-HMAC-SHA256-TRAILER\n${datetime}\n${scope}\n${previous}\n${trailerHash}`;
|
|
25
45
|
|
|
26
|
-
/** Trailing headers in canonical form: lowercase names in name order, each line ending in a newline. A line break in a name or value would start another trailer line, so it throws.
|
|
46
|
+
/** Trailing headers in canonical form: lowercase names in name order, each line ending in a newline. A line break in a name or value would start another trailer line, so it throws.
|
|
47
|
+
* @param {Record<string, string>} trailers
|
|
48
|
+
*/
|
|
27
49
|
export function canonicalTrailers(trailers) {
|
|
28
50
|
return Object.keys(trailers)
|
|
29
|
-
.map(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
.map(
|
|
52
|
+
/** @returns {[string, string]} */ (name) => {
|
|
53
|
+
const value = String(trailers[name]);
|
|
54
|
+
if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) {
|
|
55
|
+
throw new TypeError('a trailer name or value cannot hold a line break');
|
|
56
|
+
}
|
|
57
|
+
return [name.toLowerCase(), value.trim()];
|
|
58
|
+
},
|
|
59
|
+
)
|
|
34
60
|
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
35
61
|
.map(([name, value]) => `${name}:${value}\n`)
|
|
36
62
|
.join('');
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The bytes of one chunk on the wire: size in hex, signature, the data.
|
|
67
|
+
* @param {string} signature
|
|
68
|
+
* @param {Uint8Array} data
|
|
69
|
+
*/
|
|
40
70
|
export function frameChunk(signature, data) {
|
|
41
71
|
const head = utf8.encode(`${data.length.toString(16)};chunk-signature=${signature}\r\n`);
|
|
42
72
|
const out = new Uint8Array(head.length + data.length + 2);
|
|
@@ -47,7 +77,12 @@ export function frameChunk(signature, data) {
|
|
|
47
77
|
return out;
|
|
48
78
|
}
|
|
49
79
|
|
|
50
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* The last chunk (no data), with the signed trailing headers when there are any.
|
|
82
|
+
* @param {string} signature
|
|
83
|
+
* @param {string} [trailers] - canonical trailing headers
|
|
84
|
+
* @param {string} [trailerSignature]
|
|
85
|
+
*/
|
|
51
86
|
export function frameEnd(signature, trailers, trailerSignature) {
|
|
52
87
|
return utf8.encode(
|
|
53
88
|
trailers === undefined
|
|
@@ -71,14 +106,24 @@ export function createChunkSigner({ hexHash, hmacHex, getSigningKey }) {
|
|
|
71
106
|
const seed = seedOf(authHeaders);
|
|
72
107
|
const key = getSigningKey(credentials.secretAccessKey, seed.date, seed.region, seed.service);
|
|
73
108
|
let previous = seed.signature;
|
|
74
|
-
|
|
109
|
+
/** @param {Uint8Array} data */
|
|
110
|
+
const sign = (data) =>
|
|
111
|
+
(previous = hmacHex(
|
|
112
|
+
key,
|
|
113
|
+
chunkStringToSign(seed.datetime, seed.scope, previous, hexHash(data)),
|
|
114
|
+
));
|
|
75
115
|
return {
|
|
76
116
|
chunk: (data) => (data.length === 0 ? data : frameChunk(sign(data), data)), // a zero-length chunk would end the upload; it is not sent
|
|
77
117
|
end(trailers) {
|
|
78
118
|
const text = trailers === undefined ? undefined : canonicalTrailers(trailers); // before sign(), so a refused trailer leaves the chain where it was
|
|
79
119
|
const signature = sign(new Uint8Array(0));
|
|
80
|
-
if (text === undefined)
|
|
81
|
-
|
|
120
|
+
if (text === undefined) {
|
|
121
|
+
return frameEnd(signature);
|
|
122
|
+
}
|
|
123
|
+
const trailerSignature = hmacHex(
|
|
124
|
+
key,
|
|
125
|
+
trailerStringToSign(seed.datetime, seed.scope, signature, hexHash(text)),
|
|
126
|
+
);
|
|
82
127
|
return frameEnd(signature, text, trailerSignature);
|
|
83
128
|
},
|
|
84
129
|
};
|
package/src/ecdsa-node.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
// SigV4a's ECDSA P-256 for the entries that have node:crypto (Node, Deno, Bun). The key is rebuilt from the derived scalar; ECDH supplies the public point that a JWK needs.
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
|
|
4
|
+
/** @param {Uint8Array} bytes */
|
|
4
5
|
const b64url = (bytes) => Buffer.from(bytes).toString('base64url');
|
|
5
6
|
|
|
6
7
|
export const ecdsa = {
|
|
8
|
+
/** @param {Uint8Array} d */
|
|
7
9
|
key(d) {
|
|
8
10
|
const ecdh = crypto.createECDH('prime256v1');
|
|
9
11
|
ecdh.setPrivateKey(d);
|
|
10
12
|
const point = ecdh.getPublicKey();
|
|
11
|
-
return crypto.createPrivateKey({
|
|
13
|
+
return crypto.createPrivateKey({
|
|
14
|
+
key: {
|
|
15
|
+
kty: 'EC',
|
|
16
|
+
crv: 'P-256',
|
|
17
|
+
d: b64url(d),
|
|
18
|
+
x: b64url(point.subarray(1, 33)),
|
|
19
|
+
y: b64url(point.subarray(33)),
|
|
20
|
+
},
|
|
21
|
+
format: 'jwk',
|
|
22
|
+
});
|
|
12
23
|
},
|
|
24
|
+
/**
|
|
25
|
+
* @param {crypto.KeyObject} key
|
|
26
|
+
* @param {string} text
|
|
27
|
+
*/
|
|
13
28
|
sign: (key, text) => crypto.sign('sha256', text, { key, dsaEncoding: 'der' }).toString('hex'),
|
|
14
29
|
};
|
package/src/events.js
CHANGED
|
@@ -2,22 +2,37 @@
|
|
|
2
2
|
import { amzDatetime } from './base.js';
|
|
3
3
|
import { seedOf, utf8 } from './chunks.js';
|
|
4
4
|
|
|
5
|
+
/** @type {Int32Array | undefined} */
|
|
5
6
|
let crcTable;
|
|
7
|
+
/**
|
|
8
|
+
* @param {Uint8Array} bytes
|
|
9
|
+
* @param {number} start
|
|
10
|
+
* @param {number} end
|
|
11
|
+
*/
|
|
6
12
|
function crc32(bytes, start, end) {
|
|
7
13
|
if (crcTable === undefined) {
|
|
8
14
|
crcTable = new Int32Array(256);
|
|
9
15
|
for (let n = 0; n < 256; n++) {
|
|
10
16
|
let c = n;
|
|
11
|
-
for (let k = 0; k < 8; k++)
|
|
17
|
+
for (let k = 0; k < 8; k++) {
|
|
18
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
19
|
+
}
|
|
12
20
|
crcTable[n] = c;
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
let crc = -1;
|
|
16
|
-
for (let i = start; i < end; i++)
|
|
24
|
+
for (let i = start; i < end; i++) {
|
|
25
|
+
crc =
|
|
26
|
+
/** @type {number} */ (crcTable[(crc ^ /** @type {number} */ (bytes[i])) & 255]) ^
|
|
27
|
+
(crc >>> 8);
|
|
28
|
+
}
|
|
17
29
|
return ~crc >>> 0;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The :date header of an event: name, type 8 (timestamp), milliseconds since the epoch as 8 bytes.
|
|
34
|
+
* @param {number} ms
|
|
35
|
+
*/
|
|
21
36
|
export function dateHeader(ms) {
|
|
22
37
|
const out = new Uint8Array(15);
|
|
23
38
|
out[0] = 5;
|
|
@@ -27,7 +42,12 @@ export function dateHeader(ms) {
|
|
|
27
42
|
return out;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The signed event: prelude, the :date and :chunk-signature headers, the inner message as payload, checksums.
|
|
47
|
+
* @param {Uint8Array} dateBytes
|
|
48
|
+
* @param {string} signatureHex
|
|
49
|
+
* @param {Uint8Array} inner
|
|
50
|
+
*/
|
|
31
51
|
export function frameEvent(dateBytes, signatureHex, inner) {
|
|
32
52
|
const headersLength = dateBytes.length + 52;
|
|
33
53
|
const out = new Uint8Array(16 + headersLength + inner.length);
|
|
@@ -43,18 +63,36 @@ export function frameEvent(dateBytes, signatureHex, inner) {
|
|
|
43
63
|
out[at++] = 6;
|
|
44
64
|
view.setUint16(at, 32);
|
|
45
65
|
at += 2;
|
|
46
|
-
for (let i = 0; i < 32; i++)
|
|
66
|
+
for (let i = 0; i < 32; i++) {
|
|
67
|
+
out[at + i] = parseInt(signatureHex.slice(2 * i, 2 * i + 2), 16);
|
|
68
|
+
}
|
|
47
69
|
at += 32;
|
|
48
70
|
out.set(inner, at);
|
|
49
71
|
view.setUint32(out.length - 4, crc32(out, 0, out.length - 4));
|
|
50
72
|
return out;
|
|
51
73
|
}
|
|
52
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} datetime
|
|
77
|
+
* @param {string} scope
|
|
78
|
+
* @param {string} previous
|
|
79
|
+
* @param {string} headersHash
|
|
80
|
+
* @param {string} payloadHash
|
|
81
|
+
*/
|
|
53
82
|
export const eventStringToSign = (datetime, scope, previous, headersHash, payloadHash) =>
|
|
54
83
|
`AWS4-HMAC-SHA256-PAYLOAD\n${datetime}\n${scope}\n${previous}\n${headersHash}\n${payloadHash}`;
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} ms
|
|
86
|
+
* @param {string} region
|
|
87
|
+
* @param {string} service
|
|
88
|
+
*/
|
|
55
89
|
export const eventScope = (ms, region, service) => {
|
|
56
90
|
const datetime = amzDatetime(ms);
|
|
57
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
datetime,
|
|
93
|
+
scope: `${datetime.slice(0, 8)}/${region}/${service}/aws4_request`,
|
|
94
|
+
date: datetime.slice(0, 8),
|
|
95
|
+
};
|
|
58
96
|
};
|
|
59
97
|
|
|
60
98
|
/**
|
package/src/node.js
CHANGED
|
@@ -12,33 +12,51 @@ const maxData = 1024;
|
|
|
12
12
|
const inner = /* @__PURE__ */ new Uint8Array(64 + maxData); // the ipad block, then the data
|
|
13
13
|
const outer = /* @__PURE__ */ new Uint8Array(64 + 32); // the opad block, then the inner digest
|
|
14
14
|
const data = /* @__PURE__ */ inner.subarray(64);
|
|
15
|
+
/** @type {Uint8Array[]} */
|
|
15
16
|
const innerViews = [];
|
|
17
|
+
/** @type {Uint8Array | undefined} */
|
|
16
18
|
let blockKey;
|
|
19
|
+
/** @param {number} c */
|
|
17
20
|
const nibble = (c) => (c > 96 ? c - 87 : c - 48);
|
|
18
21
|
|
|
19
22
|
// HMAC-SHA-256 as two one-shot hashes over preallocated buffers: it avoids creating a Hmac object per signature.
|
|
20
23
|
// Only a key of at most 64 bytes is handled; the signing key is always 32 bytes.
|
|
24
|
+
/**
|
|
25
|
+
* @param {Uint8Array} key
|
|
26
|
+
* @param {string} text
|
|
27
|
+
*/
|
|
21
28
|
function hmacHexOneShot(key, text) {
|
|
22
29
|
if (key !== blockKey) {
|
|
23
30
|
blockKey = key;
|
|
24
31
|
inner.fill(0x36, 0, 64);
|
|
25
32
|
outer.fill(0x5c, 0, 64);
|
|
26
33
|
for (let i = 0; i < key.length; i++) {
|
|
27
|
-
inner[i]
|
|
28
|
-
outer[i]
|
|
34
|
+
inner[i] = /** @type {number} */ (inner[i]) ^ /** @type {number} */ (key[i]);
|
|
35
|
+
outer[i] = /** @type {number} */ (outer[i]) ^ /** @type {number} */ (key[i]);
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
const end = 64 + utf8.encodeInto(text, data).written;
|
|
32
39
|
const hex = crypto.hash('sha256', (innerViews[end] ??= inner.subarray(0, end)), 'hex');
|
|
33
|
-
for (let i = 0; i < 32; i++)
|
|
40
|
+
for (let i = 0; i < 32; i++) {
|
|
41
|
+
outer[64 + i] = (nibble(hex.charCodeAt(2 * i)) << 4) | nibble(hex.charCodeAt(2 * i + 1));
|
|
42
|
+
}
|
|
34
43
|
return crypto.hash('sha256', outer, 'hex');
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @param {Uint8Array} key
|
|
48
|
+
* @param {string} text
|
|
49
|
+
*/
|
|
37
50
|
const hmacHexStreaming = (key, text) => crypto.createHmac('sha256', key).update(text).digest('hex');
|
|
38
51
|
|
|
52
|
+
/**
|
|
53
|
+
* @param {string | Uint8Array} key
|
|
54
|
+
* @param {string} text
|
|
55
|
+
*/
|
|
39
56
|
const hmac = (key, text) => crypto.createHmac('sha256', key).update(text).digest();
|
|
40
57
|
|
|
41
58
|
// The one-shot form is used only when it agrees with the streaming one on a sample, and only for short data. The sample is checked on first use.
|
|
59
|
+
/** @type {boolean | undefined} */
|
|
42
60
|
let oneShotWorks;
|
|
43
61
|
function probe() {
|
|
44
62
|
const key = hmac('probe', 'key');
|
|
@@ -47,9 +65,17 @@ function probe() {
|
|
|
47
65
|
blockKey = undefined;
|
|
48
66
|
return works;
|
|
49
67
|
}
|
|
50
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @param {Uint8Array} key
|
|
70
|
+
* @param {string} text
|
|
71
|
+
*/
|
|
72
|
+
const hmacHex = (key, text) =>
|
|
73
|
+
(oneShotWorks ??= probe()) && text.length * 3 <= maxData && key.length <= 64
|
|
74
|
+
? hmacHexOneShot(key, text)
|
|
75
|
+
: hmacHexStreaming(key, text);
|
|
51
76
|
|
|
52
77
|
const getSigningKey = /* @__PURE__ */ createKeys(hmac);
|
|
78
|
+
/** @param {string | Uint8Array} text */
|
|
53
79
|
const hexHash = (text) => crypto.hash('sha256', text, 'hex');
|
|
54
80
|
const primitives = { hexHash, hmac, hmacHex, getSigningKey };
|
|
55
81
|
|
package/src/presign.js
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
// Presigned URLs: the signature goes in the query string instead of the authorization header.
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
symmetric,
|
|
4
|
+
canonicalPath,
|
|
5
|
+
canonicalQuery,
|
|
6
|
+
insert,
|
|
7
|
+
headerValue,
|
|
8
|
+
signHeaderName,
|
|
9
|
+
amzDatetime,
|
|
10
|
+
payloadHashOf,
|
|
11
|
+
stringToSign,
|
|
12
|
+
} from './base.js';
|
|
3
13
|
|
|
4
|
-
|
|
14
|
+
/** @param {string} text */
|
|
15
|
+
export const encodeQueryPart = (text) =>
|
|
16
|
+
encodeURIComponent(text).replace(
|
|
17
|
+
/[!'()*]/g,
|
|
18
|
+
(c) => '%' + c.charCodeAt(0).toString(16).toUpperCase(),
|
|
19
|
+
);
|
|
5
20
|
|
|
6
21
|
/**
|
|
7
22
|
* Everything up to the hashing for a presigned URL. `request.expires` is the lifetime in seconds (default 3600). Only the host and request.signHeaders are
|
|
8
23
|
* signed; S3 URLs are always UNSIGNED-PAYLOAD, other services sign the body hash. `extra` is appended to the authentication parameters.
|
|
24
|
+
* @param {import('./base.js').SignRequest} request
|
|
25
|
+
* @param {import('./base.js').Credentials} credentials
|
|
26
|
+
* @param {string} payloadHash
|
|
27
|
+
* @param {import('./base.js').Scheme} [scheme]
|
|
28
|
+
* @param {string} [extra]
|
|
9
29
|
*/
|
|
10
30
|
export function preparePresign(request, credentials, payloadHash, scheme = symmetric, extra = '') {
|
|
11
31
|
const { url, signHeaders } = request;
|
|
12
32
|
const method = request.method.toUpperCase();
|
|
13
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
14
|
-
|
|
33
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
34
|
+
throw new TypeError('presign needs an http or https URL');
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
url.search.length > 1 &&
|
|
38
|
+
(url.search.includes('X-Amz-Signature=') || url.search.includes('X-Amz-Algorithm='))
|
|
39
|
+
) {
|
|
40
|
+
throw new TypeError('the URL is already presigned');
|
|
41
|
+
}
|
|
15
42
|
const expires = request.expires ?? 3600;
|
|
16
|
-
if (!Number.isInteger(expires) || expires < 1 || expires > 604800)
|
|
43
|
+
if (!Number.isInteger(expires) || expires < 1 || expires > 604800) {
|
|
44
|
+
throw new RangeError('expires must be a whole number of seconds from 1 to 604800 (7 days)');
|
|
45
|
+
}
|
|
17
46
|
const region = request.region ?? '';
|
|
18
47
|
const service = request.service ?? 's3';
|
|
19
48
|
const datetime = amzDatetime(request.now ?? Date.now());
|
|
@@ -21,21 +50,50 @@ export function preparePresign(request, credentials, payloadHash, scheme = symme
|
|
|
21
50
|
const credentialScope = scheme.scope(date, region, service);
|
|
22
51
|
const names = ['host'];
|
|
23
52
|
const values = [url.host];
|
|
24
|
-
if (signHeaders !== undefined)
|
|
53
|
+
if (signHeaders !== undefined) {
|
|
54
|
+
for (const name of Object.keys(signHeaders)) {
|
|
55
|
+
insert(
|
|
56
|
+
names,
|
|
57
|
+
values,
|
|
58
|
+
signHeaderName(name, /** @type {string} */ (signHeaders[name])),
|
|
59
|
+
headerValue(signHeaders[name]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
25
63
|
let canonicalHeaders = '';
|
|
26
|
-
for (let i = 0; i < names.length; i++)
|
|
64
|
+
for (let i = 0; i < names.length; i++) {
|
|
65
|
+
canonicalHeaders += `${names[i]}:${values[i]}\n`;
|
|
66
|
+
}
|
|
27
67
|
const signedHeaders = names.join(';');
|
|
28
68
|
let auth = `X-Amz-Algorithm=${scheme.algorithm}&X-Amz-Credential=${encodeQueryPart(`${credentials.accessKeyId}/${credentialScope}`)}&X-Amz-Date=${datetime}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=${encodeQueryPart(signedHeaders)}`;
|
|
29
|
-
if (credentials.sessionToken)
|
|
69
|
+
if (credentials.sessionToken) {
|
|
70
|
+
auth += `&X-Amz-Security-Token=${encodeQueryPart(credentials.sessionToken)}`;
|
|
71
|
+
}
|
|
30
72
|
auth += extra;
|
|
31
73
|
const query = url.search.length > 1 ? url.search + '&' + auth : '?' + auth;
|
|
32
74
|
const hash = service === 's3' || request.unsignedPayload ? 'UNSIGNED-PAYLOAD' : payloadHash;
|
|
33
75
|
const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(query)}\n${canonicalHeaders}\n${signedHeaders}\n${hash}`;
|
|
34
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
canonicalRequest,
|
|
78
|
+
datetime,
|
|
79
|
+
date,
|
|
80
|
+
region,
|
|
81
|
+
service,
|
|
82
|
+
credentialScope,
|
|
83
|
+
signedHeaders,
|
|
84
|
+
algorithm: scheme.algorithm,
|
|
85
|
+
base: url.origin + url.pathname,
|
|
86
|
+
query,
|
|
87
|
+
};
|
|
35
88
|
}
|
|
36
89
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The URL to hand out: the original URL with the authentication query parameters and the signature appended.
|
|
92
|
+
* @param {{ base: string, query: string }} prepared
|
|
93
|
+
* @param {string} signature
|
|
94
|
+
*/
|
|
95
|
+
export const presignedUrl = (prepared, signature) =>
|
|
96
|
+
`${prepared.base}${prepared.query}&X-Amz-Signature=${signature}`;
|
|
39
97
|
|
|
40
98
|
/**
|
|
41
99
|
* @param {import('./base.js').Primitives} primitives
|
|
@@ -46,7 +104,15 @@ export function createPresigner({ hexHash, hmacHex, getSigningKey }) {
|
|
|
46
104
|
const prepared = preparePresign(request, credentials, payloadHashOf(request, hexHash));
|
|
47
105
|
return presignedUrl(
|
|
48
106
|
prepared,
|
|
49
|
-
hmacHex(
|
|
107
|
+
hmacHex(
|
|
108
|
+
getSigningKey(
|
|
109
|
+
credentials.secretAccessKey,
|
|
110
|
+
prepared.date,
|
|
111
|
+
prepared.region,
|
|
112
|
+
prepared.service,
|
|
113
|
+
),
|
|
114
|
+
stringToSign(prepared, hexHash(prepared.canonicalRequest)),
|
|
115
|
+
),
|
|
50
116
|
);
|
|
51
117
|
};
|
|
52
118
|
}
|
package/src/v4a.js
CHANGED
|
@@ -3,60 +3,112 @@
|
|
|
3
3
|
import { prepare, payloadHashOf, stringToSign, authorize } from './base.js';
|
|
4
4
|
import { preparePresign, presignedUrl, encodeQueryPart } from './presign.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** @type {import('./base.js').Scheme} */
|
|
7
|
+
export const asymmetric = {
|
|
8
|
+
algorithm: 'AWS4-ECDSA-P256-SHA256',
|
|
9
|
+
scope: (date, _region, service) => `${date}/${service}/aws4_request`,
|
|
10
|
+
};
|
|
7
11
|
|
|
12
|
+
/** @param {import('./base.js').SignRequest} request */
|
|
8
13
|
function regionSetOf(request) {
|
|
9
14
|
const regionSet = request.regionSet;
|
|
10
|
-
if (typeof regionSet !== 'string' || regionSet === '')
|
|
15
|
+
if (typeof regionSet !== 'string' || regionSet === '') {
|
|
16
|
+
throw new TypeError("SigV4a needs request.regionSet, for example 'us-east-1,us-west-2' or '*'");
|
|
17
|
+
}
|
|
11
18
|
return regionSet;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The request with the region set added as a signed header; `regionSet` is 'us-east-1,us-west-2' or '*'.
|
|
23
|
+
* @param {import('./base.js').SignRequest} request
|
|
24
|
+
*/
|
|
25
|
+
export const withRegionSet = (request) => ({
|
|
26
|
+
...request,
|
|
27
|
+
signHeaders: { ...request.signHeaders, 'x-amz-region-set': regionSetOf(request) },
|
|
28
|
+
});
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
/** @param {import('./base.js').SignRequest} request */
|
|
31
|
+
export const regionSetQuery = (request) =>
|
|
32
|
+
`&X-Amz-Region-Set=${encodeQueryPart(regionSetOf(request))}`;
|
|
18
33
|
|
|
19
34
|
// 4-byte counter 1, label, 0x00, the access key id, the external counter, 256 as 4 bytes
|
|
20
|
-
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} accessKeyId
|
|
37
|
+
* @param {number} counter
|
|
38
|
+
*/
|
|
39
|
+
export const kdfInput = (accessKeyId, counter) =>
|
|
40
|
+
`\0\0\0\x01AWS4-ECDSA-P256-SHA256\0${accessKeyId}${String.fromCharCode(counter)}\0\0\x01\0`;
|
|
21
41
|
|
|
22
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The private scalar (HMAC output + 1) when the HMAC output is at most the curve order minus 2, else undefined.
|
|
44
|
+
* @param {Uint8Array} k
|
|
45
|
+
*/
|
|
23
46
|
export function scalarFrom(k) {
|
|
24
47
|
const nMinusTwo = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc63254fn;
|
|
25
48
|
let c = 0n;
|
|
26
|
-
for (let i = 0; i < 32; i++)
|
|
27
|
-
|
|
49
|
+
for (let i = 0; i < 32; i++) {
|
|
50
|
+
c = (c << 8n) | BigInt(/** @type {number} */ (k[i]));
|
|
51
|
+
}
|
|
52
|
+
if (c > nMinusTwo) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
28
55
|
c += 1n;
|
|
29
56
|
const d = new Uint8Array(32);
|
|
30
|
-
for (let i = 31; i >= 0; i--, c >>= 8n)
|
|
57
|
+
for (let i = 31; i >= 0; i--, c >>= 8n) {
|
|
58
|
+
d[i] = Number(c & 0xffn);
|
|
59
|
+
}
|
|
31
60
|
return d;
|
|
32
61
|
}
|
|
33
62
|
|
|
34
|
-
/** The SigV4a private key: the P-256 scalar derived from the secret key by the counter-mode HMAC construction in the AWS documentation (32 bytes, big-endian).
|
|
63
|
+
/** The SigV4a private key: the P-256 scalar derived from the secret key by the counter-mode HMAC construction in the AWS documentation (32 bytes, big-endian).
|
|
64
|
+
* @param {AsymmetricPrimitives['hmac']} hmac
|
|
65
|
+
* @param {string} accessKeyId
|
|
66
|
+
* @param {string} secretAccessKey
|
|
67
|
+
*/
|
|
35
68
|
export function deriveP256(hmac, accessKeyId, secretAccessKey) {
|
|
36
69
|
for (let counter = 1; counter < 128; counter++) {
|
|
37
70
|
const d = scalarFrom(hmac('AWS4A' + secretAccessKey, kdfInput(accessKeyId, counter)));
|
|
38
|
-
if (d !== undefined)
|
|
71
|
+
if (d !== undefined) {
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
39
74
|
}
|
|
40
75
|
throw new Error('no SigV4a key for these credentials');
|
|
41
76
|
}
|
|
42
77
|
|
|
78
|
+
/** @type {WeakMap<AsymmetricPrimitives['ecdsa'], (credentials: import('./base.js').Credentials, text: string) => string>} */
|
|
43
79
|
const signers = /* @__PURE__ */ new WeakMap(); // ecdsa -> its signer, so the header and presign functions of one entry share one key cache
|
|
44
80
|
|
|
45
|
-
/** A signer from the string to sign to a hex DER ECDSA signature, with a cache of the keys built from credentials. Asking again for the same `ecdsa` returns the same signer.
|
|
81
|
+
/** A signer from the string to sign to a hex DER ECDSA signature, with a cache of the keys built from credentials. Asking again for the same `ecdsa` returns the same signer.
|
|
82
|
+
* @param {AsymmetricPrimitives['hmac']} hmac
|
|
83
|
+
* @param {AsymmetricPrimitives['ecdsa']} ecdsa
|
|
84
|
+
*/
|
|
46
85
|
export function createAsymmetric(hmac, ecdsa) {
|
|
47
86
|
let sign = signers.get(ecdsa);
|
|
48
|
-
if (sign === undefined)
|
|
87
|
+
if (sign === undefined) {
|
|
88
|
+
signers.set(ecdsa, (sign = signerFor(hmac, ecdsa)));
|
|
89
|
+
}
|
|
49
90
|
return sign;
|
|
50
91
|
}
|
|
51
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {AsymmetricPrimitives['hmac']} hmac
|
|
95
|
+
* @param {AsymmetricPrimitives['ecdsa']} ecdsa
|
|
96
|
+
*/
|
|
52
97
|
function signerFor(hmac, ecdsa) {
|
|
98
|
+
/** @type {Map<string, unknown>} */
|
|
53
99
|
const keys = new Map();
|
|
100
|
+
/**
|
|
101
|
+
* @param {import('./base.js').Credentials} credentials
|
|
102
|
+
* @param {string} text
|
|
103
|
+
*/
|
|
54
104
|
return function sign(credentials, text) {
|
|
55
105
|
const id = `${credentials.accessKeyId}/${credentials.secretAccessKey}`;
|
|
56
106
|
let key = keys.get(id);
|
|
57
107
|
if (key === undefined) {
|
|
58
108
|
key = ecdsa.key(deriveP256(hmac, credentials.accessKeyId, credentials.secretAccessKey));
|
|
59
|
-
if (keys.size >= 1000)
|
|
109
|
+
if (keys.size >= 1000) {
|
|
110
|
+
keys.delete(/** @type {string} */ (keys.keys().next().value));
|
|
111
|
+
}
|
|
60
112
|
keys.set(id, key);
|
|
61
113
|
}
|
|
62
114
|
return ecdsa.sign(key, text);
|
|
@@ -78,8 +130,17 @@ function signerFor(hmac, ecdsa) {
|
|
|
78
130
|
export function createSignerV4a({ hexHash, hmac, ecdsa }) {
|
|
79
131
|
const sign = createAsymmetric(hmac, ecdsa);
|
|
80
132
|
return function getAuthHeadersV4a(request, credentials) {
|
|
81
|
-
const prepared = prepare(
|
|
82
|
-
|
|
133
|
+
const prepared = prepare(
|
|
134
|
+
withRegionSet(request),
|
|
135
|
+
credentials,
|
|
136
|
+
payloadHashOf(request, hexHash),
|
|
137
|
+
asymmetric,
|
|
138
|
+
);
|
|
139
|
+
return authorize(
|
|
140
|
+
prepared,
|
|
141
|
+
credentials.accessKeyId,
|
|
142
|
+
sign(credentials, stringToSign(prepared, hexHash(prepared.canonicalRequest))),
|
|
143
|
+
);
|
|
83
144
|
};
|
|
84
145
|
}
|
|
85
146
|
|
|
@@ -90,7 +151,16 @@ export function createSignerV4a({ hexHash, hmac, ecdsa }) {
|
|
|
90
151
|
export function createPresignerV4a({ hexHash, hmac, ecdsa }) {
|
|
91
152
|
const sign = createAsymmetric(hmac, ecdsa);
|
|
92
153
|
return function presignV4a(request, credentials) {
|
|
93
|
-
const prepared = preparePresign(
|
|
94
|
-
|
|
154
|
+
const prepared = preparePresign(
|
|
155
|
+
request,
|
|
156
|
+
credentials,
|
|
157
|
+
payloadHashOf(request, hexHash),
|
|
158
|
+
asymmetric,
|
|
159
|
+
regionSetQuery(request),
|
|
160
|
+
);
|
|
161
|
+
return presignedUrl(
|
|
162
|
+
prepared,
|
|
163
|
+
sign(credentials, stringToSign(prepared, hexHash(prepared.canonicalRequest))),
|
|
164
|
+
);
|
|
95
165
|
};
|
|
96
166
|
}
|