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/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
- /** The seed signature, timestamp and scope of a signed request, from the headers getAuthHeaders returned. */
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('authHeaders must be the headers getAuthHeaders returned (a SigV4a signature cannot start a chunk or event stream)');
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
- export const chunkStringToSign = (datetime, scope, previous, payloadHash) => `AWS4-HMAC-SHA256-PAYLOAD\n${datetime}\n${scope}\n${previous}\n${emptyHash}\n${payloadHash}`;
24
- export const trailerStringToSign = (datetime, scope, previous, trailerHash) => `AWS4-HMAC-SHA256-TRAILER\n${datetime}\n${scope}\n${previous}\n${trailerHash}`;
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((name) => {
30
- const value = String(trailers[name]);
31
- if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) throw new TypeError('a trailer name or value cannot hold a line break');
32
- return [name.toLowerCase(), value.trim()];
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
- /** The bytes of one chunk on the wire: size in hex, signature, the data. */
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
- /** The last chunk (no data), with the signed trailing headers when there are any. */
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
- const sign = (data) => (previous = hmacHex(key, chunkStringToSign(seed.datetime, seed.scope, previous, hexHash(data))));
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) return frameEnd(signature);
81
- const trailerSignature = hmacHex(key, trailerStringToSign(seed.datetime, seed.scope, signature, hexHash(text)));
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({ key: { kty: 'EC', crv: 'P-256', d: b64url(d), x: b64url(point.subarray(1, 33)), y: b64url(point.subarray(33)) }, format: 'jwk' });
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++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
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++) crc = crcTable[(crc ^ bytes[i]) & 255] ^ (crc >>> 8);
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
- /** The :date header of an event: name, type 8 (timestamp), milliseconds since the epoch as 8 bytes. */
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
- /** The signed event: prelude, the :date and :chunk-signature headers, the inner message as payload, checksums. */
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++) out[at + i] = parseInt(signatureHex.slice(2 * i, 2 * i + 2), 16);
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 { datetime, scope: `${datetime.slice(0, 8)}/${region}/${service}/aws4_request`, date: datetime.slice(0, 8) };
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] ^= key[i];
28
- outer[i] ^= key[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++) outer[64 + i] = (nibble(hex.charCodeAt(2 * i)) << 4) | nibble(hex.charCodeAt(2 * i + 1));
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
- const hmacHex = (key, text) => ((oneShotWorks ??= probe()) && text.length * 3 <= maxData && key.length <= 64 ? hmacHexOneShot(key, text) : hmacHexStreaming(key, text));
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 { symmetric, canonicalPath, canonicalQuery, insert, headerValue, signHeaderName, amzDatetime, payloadHashOf, stringToSign } from './base.js';
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
- export const encodeQueryPart = (text) => encodeURIComponent(text).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
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:') throw new TypeError('presign needs an http or https URL');
14
- if (url.search.length > 1 && (url.search.includes('X-Amz-Signature=') || url.search.includes('X-Amz-Algorithm='))) throw new TypeError('the URL is already presigned');
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) throw new RangeError('expires must be a whole number of seconds from 1 to 604800 (7 days)');
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) for (const name of Object.keys(signHeaders)) insert(names, values, signHeaderName(name, signHeaders[name]), headerValue(signHeaders[name]));
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++) canonicalHeaders += `${names[i]}:${values[i]}\n`;
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) auth += `&X-Amz-Security-Token=${encodeQueryPart(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 { canonicalRequest, datetime, date, region, service, credentialScope, signedHeaders, algorithm: scheme.algorithm, base: url.origin + url.pathname, query };
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
- /** The URL to hand out: the original URL with the authentication query parameters and the signature appended. */
38
- export const presignedUrl = (prepared, signature) => `${prepared.base}${prepared.query}&X-Amz-Signature=${signature}`;
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(getSigningKey(credentials.secretAccessKey, prepared.date, prepared.region, prepared.service), stringToSign(prepared, hexHash(prepared.canonicalRequest))),
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
- export const asymmetric = { algorithm: 'AWS4-ECDSA-P256-SHA256', scope: (date, _region, service) => `${date}/${service}/aws4_request` };
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 === '') throw new TypeError("SigV4a needs request.regionSet, for example 'us-east-1,us-west-2' or '*'");
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
- /** The request with the region set added as a signed header; `regionSet` is 'us-east-1,us-west-2' or '*'. */
15
- export const withRegionSet = (request) => ({ ...request, signHeaders: { ...request.signHeaders, 'x-amz-region-set': regionSetOf(request) } });
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
- export const regionSetQuery = (request) => `&X-Amz-Region-Set=${encodeQueryPart(regionSetOf(request))}`;
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
- export const kdfInput = (accessKeyId, counter) => `\0\0\0\x01AWS4-ECDSA-P256-SHA256\0${accessKeyId}${String.fromCharCode(counter)}\0\0\x01\0`;
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
- /** The private scalar (HMAC output + 1) when the HMAC output is at most the curve order minus 2, else undefined. */
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++) c = (c << 8n) | BigInt(k[i]);
27
- if (c > nMinusTwo) return undefined;
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) d[i] = Number(c & 0xffn);
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) return d;
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) signers.set(ecdsa, (sign = signerFor(hmac, ecdsa)));
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) keys.delete(keys.keys().next().value);
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(withRegionSet(request), credentials, payloadHashOf(request, hexHash), asymmetric);
82
- return authorize(prepared, credentials.accessKeyId, sign(credentials, stringToSign(prepared, hexHash(prepared.canonicalRequest))));
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(request, credentials, payloadHashOf(request, hexHash), asymmetric, regionSetQuery(request));
94
- return presignedUrl(prepared, sign(credentials, stringToSign(prepared, hexHash(prepared.canonicalRequest))));
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
  }