sigflint 0.0.0 → 1.0.0

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/node.js ADDED
@@ -0,0 +1,61 @@
1
+ // Node.js, Deno and Cloudflare Workers with the nodejs_compat flag: node:crypto.
2
+ import crypto from 'node:crypto';
3
+ import { createSigner, createKeys } from './base.js';
4
+ import { createPresigner } from './presign.js';
5
+ import { createSignerV4a, createPresignerV4a } from './v4a.js';
6
+ import { ecdsa } from './ecdsa-node.js';
7
+ import { createChunkSigner } from './chunks.js';
8
+ import { createEventSigner } from './events.js';
9
+
10
+ const utf8 = /* @__PURE__ */ new TextEncoder();
11
+ const maxData = 1024;
12
+ const inner = /* @__PURE__ */ new Uint8Array(64 + maxData); // the ipad block, then the data
13
+ const outer = /* @__PURE__ */ new Uint8Array(64 + 32); // the opad block, then the inner digest
14
+ const data = /* @__PURE__ */ inner.subarray(64);
15
+ const innerViews = [];
16
+ let blockKey;
17
+ const nibble = (c) => (c > 96 ? c - 87 : c - 48);
18
+
19
+ // HMAC-SHA-256 as two one-shot hashes over preallocated buffers: it avoids creating a Hmac object per signature.
20
+ // Only a key of at most 64 bytes is handled; the signing key is always 32 bytes.
21
+ function hmacHexOneShot(key, text) {
22
+ if (key !== blockKey) {
23
+ blockKey = key;
24
+ inner.fill(0x36, 0, 64);
25
+ outer.fill(0x5c, 0, 64);
26
+ for (let i = 0; i < key.length; i++) {
27
+ inner[i] ^= key[i];
28
+ outer[i] ^= key[i];
29
+ }
30
+ }
31
+ const end = 64 + utf8.encodeInto(text, data).written;
32
+ 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));
34
+ return crypto.hash('sha256', outer, 'hex');
35
+ }
36
+
37
+ const hmacHexStreaming = (key, text) => crypto.createHmac('sha256', key).update(text).digest('hex');
38
+
39
+ const hmac = (key, text) => crypto.createHmac('sha256', key).update(text).digest();
40
+
41
+ // 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.
42
+ let oneShotWorks;
43
+ function probe() {
44
+ const key = hmac('probe', 'key');
45
+ const text = 'AWS4-HMAC-SHA256\n20130524T000000Z';
46
+ const works = hmacHexOneShot(key, text) === hmacHexStreaming(key, text);
47
+ blockKey = undefined;
48
+ return works;
49
+ }
50
+ const hmacHex = (key, text) => ((oneShotWorks ??= probe()) && text.length * 3 <= maxData && key.length <= 64 ? hmacHexOneShot(key, text) : hmacHexStreaming(key, text));
51
+
52
+ const getSigningKey = /* @__PURE__ */ createKeys(hmac);
53
+ const hexHash = (text) => crypto.hash('sha256', text, 'hex');
54
+ const primitives = { hexHash, hmac, hmacHex, getSigningKey };
55
+
56
+ export const getAuthHeaders = /* @__PURE__ */ createSigner(primitives);
57
+ export const presign = /* @__PURE__ */ createPresigner(primitives);
58
+ export const getAuthHeadersV4a = /* @__PURE__ */ createSignerV4a({ hexHash, hmac, ecdsa });
59
+ export const presignV4a = /* @__PURE__ */ createPresignerV4a({ hexHash, hmac, ecdsa });
60
+ export const chunkSigner = /* @__PURE__ */ createChunkSigner(primitives);
61
+ export const eventSigner = /* @__PURE__ */ createEventSigner(primitives);
package/src/presign.js ADDED
@@ -0,0 +1,51 @@
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';
3
+
4
+ export const encodeQueryPart = (text) => encodeURIComponent(text).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
5
+
6
+ /**
7
+ * 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
+ * signed; S3 URLs are always UNSIGNED-PAYLOAD, other services sign the body hash. `extra` is appended to the authentication parameters.
9
+ */
10
+ export function preparePresign(request, credentials, payloadHash, scheme = symmetric, extra = '') {
11
+ const { method, url, signHeaders } = request;
12
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new TypeError('presign needs an http or https URL');
13
+ 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');
14
+ const expires = request.expires ?? 3600;
15
+ 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)');
16
+ const region = request.region ?? '';
17
+ const service = request.service ?? 's3';
18
+ const datetime = amzDatetime(request.now ?? Date.now());
19
+ const date = datetime.slice(0, 8);
20
+ const credentialScope = scheme.scope(date, region, service);
21
+ const names = ['host'];
22
+ const values = [url.host];
23
+ if (signHeaders !== undefined) for (const name of Object.keys(signHeaders)) insert(names, values, signHeaderName(name, signHeaders[name]), headerValue(signHeaders[name]));
24
+ let canonicalHeaders = '';
25
+ for (let i = 0; i < names.length; i++) canonicalHeaders += `${names[i]}:${values[i]}\n`;
26
+ const signedHeaders = names.join(';');
27
+ 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)}`;
28
+ if (credentials.sessionToken) auth += `&X-Amz-Security-Token=${encodeQueryPart(credentials.sessionToken)}`;
29
+ auth += extra;
30
+ const query = url.search.length > 1 ? url.search + '&' + auth : '?' + auth;
31
+ const hash = service === 's3' || request.unsignedPayload ? 'UNSIGNED-PAYLOAD' : payloadHash;
32
+ const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(query)}\n${canonicalHeaders}\n${signedHeaders}\n${hash}`;
33
+ return { canonicalRequest, datetime, date, region, service, credentialScope, signedHeaders, algorithm: scheme.algorithm, base: url.origin + url.pathname, query };
34
+ }
35
+
36
+ /** The URL to hand out: the original URL with the authentication query parameters and the signature appended. */
37
+ export const presignedUrl = (prepared, signature) => `${prepared.base}${prepared.query}&X-Amz-Signature=${signature}`;
38
+
39
+ /**
40
+ * @param {import('./base.js').Primitives} primitives
41
+ * @returns {(request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string}
42
+ */
43
+ export function createPresigner({ hexHash, hmacHex, getSigningKey }) {
44
+ return function presign(request, credentials) {
45
+ const prepared = preparePresign(request, credentials, payloadHashOf(request, hexHash));
46
+ return presignedUrl(
47
+ prepared,
48
+ hmacHex(getSigningKey(credentials.secretAccessKey, prepared.date, prepared.region, prepared.service), stringToSign(prepared, hexHash(prepared.canonicalRequest))),
49
+ );
50
+ };
51
+ }
package/src/v4a.js ADDED
@@ -0,0 +1,96 @@
1
+ // SigV4a (multi-region): ECDSA P-256 instead of HMAC, a credential scope without the region, and the region set as a signed header (or a query parameter when
2
+ // presigning). The runtime entries supply the ECDSA primitive. Nothing in the symmetric path imports this file.
3
+ import { prepare, payloadHashOf, stringToSign, authorize } from './base.js';
4
+ import { preparePresign, presignedUrl, encodeQueryPart } from './presign.js';
5
+
6
+ export const asymmetric = { algorithm: 'AWS4-ECDSA-P256-SHA256', scope: (date, _region, service) => `${date}/${service}/aws4_request` };
7
+
8
+ function regionSetOf(request) {
9
+ 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 '*'");
11
+ return regionSet;
12
+ }
13
+
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) } });
16
+
17
+ export const regionSetQuery = (request) => `&X-Amz-Region-Set=${encodeQueryPart(regionSetOf(request))}`;
18
+
19
+ // 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`;
21
+
22
+ /** The private scalar (HMAC output + 1) when the HMAC output is at most the curve order minus 2, else undefined. */
23
+ export function scalarFrom(k) {
24
+ const nMinusTwo = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc63254fn;
25
+ let c = 0n;
26
+ for (let i = 0; i < 32; i++) c = (c << 8n) | BigInt(k[i]);
27
+ if (c > nMinusTwo) return undefined;
28
+ c += 1n;
29
+ const d = new Uint8Array(32);
30
+ for (let i = 31; i >= 0; i--, c >>= 8n) d[i] = Number(c & 0xffn);
31
+ return d;
32
+ }
33
+
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). */
35
+ export function deriveP256(hmac, accessKeyId, secretAccessKey) {
36
+ for (let counter = 1; counter < 128; counter++) {
37
+ const d = scalarFrom(hmac('AWS4A' + secretAccessKey, kdfInput(accessKeyId, counter)));
38
+ if (d !== undefined) return d;
39
+ }
40
+ throw new Error('no SigV4a key for these credentials');
41
+ }
42
+
43
+ const signers = /* @__PURE__ */ new WeakMap(); // ecdsa -> its signer, so the header and presign functions of one entry share one key cache
44
+
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. */
46
+ export function createAsymmetric(hmac, ecdsa) {
47
+ let sign = signers.get(ecdsa);
48
+ if (sign === undefined) signers.set(ecdsa, (sign = signerFor(hmac, ecdsa)));
49
+ return sign;
50
+ }
51
+
52
+ function signerFor(hmac, ecdsa) {
53
+ const keys = new Map();
54
+ return function sign(credentials, text) {
55
+ const id = `${credentials.accessKeyId}/${credentials.secretAccessKey}`;
56
+ let key = keys.get(id);
57
+ if (key === undefined) {
58
+ key = ecdsa.key(deriveP256(hmac, credentials.accessKeyId, credentials.secretAccessKey));
59
+ if (keys.size >= 1000) keys.delete(keys.keys().next().value);
60
+ keys.set(id, key);
61
+ }
62
+ return ecdsa.sign(key, text);
63
+ };
64
+ }
65
+
66
+ /**
67
+ * @typedef {Object} AsymmetricPrimitives
68
+ * @property {(data: string | Uint8Array) => string} hexHash - SHA-256 as lowercase hex
69
+ * @property {(key: string | Uint8Array, data: string) => Uint8Array} hmac - HMAC-SHA-256 as bytes
70
+ * @property {{ key: (scalar: Uint8Array) => unknown, sign: (key: any, text: string) => string }} ecdsa - the entry's ECDSA P-256 primitive; built here rather than in the entry so a bundle without SigV4a does not carry it
71
+ */
72
+
73
+ /**
74
+ * The synchronous entries' SigV4a header signer.
75
+ * @param {AsymmetricPrimitives} primitives
76
+ * @returns {(request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => import('./base.js').AuthHeaders}
77
+ */
78
+ export function createSignerV4a({ hexHash, hmac, ecdsa }) {
79
+ const sign = createAsymmetric(hmac, ecdsa);
80
+ 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))));
83
+ };
84
+ }
85
+
86
+ /**
87
+ * @param {AsymmetricPrimitives} primitives
88
+ * @returns {(request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string}
89
+ */
90
+ export function createPresignerV4a({ hexHash, hmac, ecdsa }) {
91
+ const sign = createAsymmetric(hmac, ecdsa);
92
+ 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))));
95
+ };
96
+ }
package/src/web.js ADDED
@@ -0,0 +1,187 @@
1
+ // Cloudflare Workers without nodejs_compat, browsers, and anywhere else that has WebCrypto but not node:crypto.
2
+ // WebCrypto only offers promises, so this entry returns a promise; everything else matches the other entries.
3
+ import { payloadHashOf, prepare, stringToSign, authorize } from './base.js';
4
+ import { preparePresign, presignedUrl } from './presign.js';
5
+ import { asymmetric, withRegionSet, regionSetQuery, kdfInput, scalarFrom } from './v4a.js';
6
+ import { seedOf, chunkStringToSign, trailerStringToSign, canonicalTrailers, frameChunk, frameEnd } from './chunks.js';
7
+ import { dateHeader, frameEvent, eventStringToSign, eventScope } from './events.js';
8
+
9
+ const subtle = /* @__PURE__ */ (() => globalThis.crypto.subtle)();
10
+ const utf8 = /* @__PURE__ */ new TextEncoder();
11
+
12
+ const hexOfByte = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
13
+ function toHex(buffer) {
14
+ const bytes = new Uint8Array(buffer);
15
+ let out = '';
16
+ for (let i = 0; i < bytes.length; i++) out += hexOfByte[bytes[i]];
17
+ return out;
18
+ }
19
+
20
+ const hexHash = async (data) => toHex(await subtle.digest('SHA-256', typeof data === 'string' ? utf8.encode(data) : data));
21
+ const importKey = (raw) => subtle.importKey('raw', raw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
22
+ // the next key in the derivation chain: the HMAC of data, as a key
23
+ const deriveKey = async (key, data) => importKey(await subtle.sign('HMAC', key, utf8.encode(data)));
24
+
25
+ const signingKeys = new Map();
26
+ const maxSigningKeys = 1000;
27
+ let lastSecret;
28
+ let lastDate;
29
+ let lastRegion;
30
+ let lastService;
31
+ let lastKey;
32
+
33
+ async function getSigningKey(secretAccessKey, date, region, service) {
34
+ if (secretAccessKey === lastSecret && date === lastDate && region === lastRegion && service === lastService) return lastKey;
35
+ const cacheKey = `${date}\n${region}\n${service}\n${secretAccessKey}`;
36
+ let signingKey = signingKeys.get(cacheKey);
37
+ if (signingKey === undefined) {
38
+ const dateKey = await deriveKey(await importKey(utf8.encode('AWS4' + secretAccessKey)), date);
39
+ signingKey = await deriveKey(await deriveKey(await deriveKey(dateKey, region), service), 'aws4_request');
40
+ if (signingKeys.size >= maxSigningKeys) signingKeys.delete(signingKeys.keys().next().value);
41
+ signingKeys.set(cacheKey, signingKey);
42
+ }
43
+ lastSecret = secretAccessKey;
44
+ lastDate = date;
45
+ lastRegion = region;
46
+ lastService = service;
47
+ lastKey = signingKey;
48
+ return signingKey;
49
+ }
50
+
51
+ const signSymmetric = async (credentials, prepared, text) =>
52
+ toHex(await subtle.sign('HMAC', await getSigningKey(credentials.secretAccessKey, prepared.date, prepared.region, prepared.service), utf8.encode(text)));
53
+
54
+ // SigV4a: the derived scalar goes in as a PKCS#8 private key without the public point, which WebCrypto computes itself
55
+ const pkcs8 = /* @__PURE__ */ Uint8Array.from([
56
+ 0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x27, 0x30,
57
+ 0x25, 0x02, 0x01, 0x01, 0x04, 0x20,
58
+ ]);
59
+ const asymmetricKeys = new Map();
60
+ async function getAsymmetricKey({ accessKeyId, secretAccessKey }) {
61
+ const id = `${accessKeyId}/${secretAccessKey}`;
62
+ let key = asymmetricKeys.get(id);
63
+ if (key === undefined) {
64
+ const root = await importKey(utf8.encode('AWS4A' + secretAccessKey));
65
+ for (let counter = 1; key === undefined; counter++) {
66
+ if (counter > 127) throw new Error('no SigV4a key for these credentials');
67
+ const d = scalarFrom(new Uint8Array(await subtle.sign('HMAC', root, utf8.encode(kdfInput(accessKeyId, counter)))));
68
+ if (d !== undefined) {
69
+ const der = new Uint8Array(pkcs8.length + 32);
70
+ der.set(pkcs8);
71
+ der.set(d, pkcs8.length);
72
+ key = await subtle.importKey('pkcs8', der, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
73
+ }
74
+ }
75
+ if (asymmetricKeys.size >= 1000) asymmetricKeys.delete(asymmetricKeys.keys().next().value);
76
+ asymmetricKeys.set(id, key);
77
+ }
78
+ return key;
79
+ }
80
+ // r and s as DER integers: leading zeros dropped, a zero byte in front when the top bit is set
81
+ function derInteger(bytes) {
82
+ let start = 0;
83
+ while (start < bytes.length - 1 && bytes[start] === 0) start++;
84
+ const body = bytes.subarray(start);
85
+ return body[0] & 0x80 ? [0x02, body.length + 1, 0, ...body] : [0x02, body.length, ...body];
86
+ }
87
+ async function signAsymmetric(credentials, text) {
88
+ const raw = new Uint8Array(await subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, await getAsymmetricKey(credentials), utf8.encode(text)));
89
+ const body = [...derInteger(raw.subarray(0, 32)), ...derInteger(raw.subarray(32))];
90
+ return toHex(Uint8Array.from([0x30, body.length, ...body]));
91
+ }
92
+
93
+ /**
94
+ * Same request and credentials as the other entries; resolves to the headers to send.
95
+ * @param {import('./base.js').SignRequest} request
96
+ * @param {import('./base.js').Credentials} credentials
97
+ * @returns {Promise<import('./base.js').AuthHeaders>}
98
+ */
99
+ export async function getAuthHeaders(request, credentials) {
100
+ const prepared = prepare(request, credentials, await payloadHashOf(request, hexHash));
101
+ return authorize(prepared, credentials.accessKeyId, await signSymmetric(credentials, prepared, stringToSign(prepared, await hexHash(prepared.canonicalRequest))));
102
+ }
103
+
104
+ /**
105
+ * SigV4a: the same, signed with ECDSA for the regions in `request.regionSet` ('us-east-1,us-west-2' or '*').
106
+ * @param {import('./base.js').SignRequest} request
107
+ * @param {import('./base.js').Credentials} credentials
108
+ * @returns {Promise<import('./base.js').AuthHeaders>}
109
+ */
110
+ export async function getAuthHeadersV4a(request, credentials) {
111
+ const prepared = prepare(withRegionSet(request), credentials, await payloadHashOf(request, hexHash), asymmetric);
112
+ return authorize(prepared, credentials.accessKeyId, await signAsymmetric(credentials, stringToSign(prepared, await hexHash(prepared.canonicalRequest))));
113
+ }
114
+
115
+ /**
116
+ * A presigned URL for the same request and credentials; `request.expires` is the lifetime in seconds (default 3600).
117
+ * @param {import('./base.js').SignRequest} request
118
+ * @param {import('./base.js').Credentials} credentials
119
+ * @returns {Promise<string>}
120
+ */
121
+ export async function presign(request, credentials) {
122
+ const prepared = preparePresign(request, credentials, await payloadHashOf(request, hexHash));
123
+ return presignedUrl(prepared, await signSymmetric(credentials, prepared, stringToSign(prepared, await hexHash(prepared.canonicalRequest))));
124
+ }
125
+
126
+ /**
127
+ * SigV4a presigned URL for the regions in `request.regionSet`.
128
+ * @param {import('./base.js').SignRequest} request
129
+ * @param {import('./base.js').Credentials} credentials
130
+ * @returns {Promise<string>}
131
+ */
132
+ export async function presignV4a(request, credentials) {
133
+ const prepared = preparePresign(request, credentials, await payloadHashOf(request, hexHash), asymmetric, regionSetQuery(request));
134
+ return presignedUrl(prepared, await signAsymmetric(credentials, stringToSign(prepared, await hexHash(prepared.canonicalRequest))));
135
+ }
136
+
137
+ const signWith = async (key, text) => toHex(await subtle.sign('HMAC', key, utf8.encode(text)));
138
+
139
+ /**
140
+ * Signs an aws-chunked upload chunk by chunk; the same shape as the other entries, with promises.
141
+ * @param {import('./base.js').AuthHeaders} authHeaders - the headers getAuthHeaders returned for the request
142
+ * @param {import('./base.js').Credentials} credentials
143
+ * @returns {{ chunk: (data: Uint8Array) => Promise<Uint8Array>, end: (trailers?: Record<string, string>) => Promise<Uint8Array> }}
144
+ */
145
+ export function chunkSigner(authHeaders, credentials) {
146
+ const seed = seedOf(authHeaders);
147
+ let previous = seed.signature;
148
+ const keyPromise = getSigningKey(credentials.secretAccessKey, seed.date, seed.region, seed.service);
149
+ // chunks are signed one after the other: a call waits for the one before it
150
+ /** @type {Promise<any>} */
151
+ let turn = Promise.resolve();
152
+ const sign = (data) => (turn = turn.then(async () => (previous = await signWith(await keyPromise, chunkStringToSign(seed.datetime, seed.scope, previous, await hexHash(data))))));
153
+ return {
154
+ chunk: async (data) => (data.length === 0 ? data : frameChunk(await sign(data), data)),
155
+ async end(trailers) {
156
+ const signature = await sign(new Uint8Array(0));
157
+ if (trailers === undefined) return frameEnd(signature);
158
+ const text = canonicalTrailers(trailers);
159
+ return frameEnd(signature, text, await signWith(await keyPromise, trailerStringToSign(seed.datetime, seed.scope, signature, await hexHash(text))));
160
+ },
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Signs event-stream messages in order.
166
+ * @param {import('./base.js').AuthHeaders} authHeaders - the headers getAuthHeaders returned for the request
167
+ * @param {import('./base.js').Credentials} credentials
168
+ * @returns {{ sign: (message: Uint8Array, now?: number) => Promise<Uint8Array> }}
169
+ */
170
+ export function eventSigner(authHeaders, credentials) {
171
+ const seed = seedOf(authHeaders);
172
+ let previous = seed.signature;
173
+ /** @type {Promise<any>} */
174
+ let turn = Promise.resolve();
175
+ return {
176
+ sign: (message, now = Date.now()) =>
177
+ (turn = turn.then(async () => {
178
+ const { datetime, scope, date } = eventScope(now, seed.region, seed.service);
179
+ const header = dateHeader(now);
180
+ previous = await signWith(
181
+ await getSigningKey(credentials.secretAccessKey, date, seed.region, seed.service),
182
+ eventStringToSign(datetime, scope, previous, await hexHash(header), await hexHash(message)),
183
+ );
184
+ return frameEvent(header, previous, message);
185
+ })),
186
+ };
187
+ }
@@ -0,0 +1,145 @@
1
+ export declare const emptyBodyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
2
+ /** The scheme of a signature: its algorithm name and how the credential scope is written. SigV4a (v4a.js) has its own. */
3
+ export declare const symmetric: {
4
+ algorithm: string;
5
+ scope: (date: any, region: any, service: any) => string;
6
+ };
7
+ /** The canonical path for a service. */
8
+ export declare const canonicalPath: (service: any, pathname: any) => any;
9
+ /** search is url.search, which is '' or starts with '?' */
10
+ export declare function canonicalQuery(search: any): string;
11
+ /** inserts into the name-ordered lists, replacing an entry with the same name */
12
+ export declare function insert(names: any, values: any, name: any, value: any): void;
13
+ /**
14
+ * 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
15
+ * returned headers, and a line break in a value would start another canonical header line. Returns the lowercase name.
16
+ */
17
+ export declare function signHeaderName(name: any, value: any): any;
18
+ /** A header value as it is signed: trimmed, runs of spaces and tabs collapsed. */
19
+ export declare const headerValue: (value: any) => string;
20
+ export declare function amzDatetime(now: any): string;
21
+ export type SignRequest = {
22
+ /**
23
+ * - GET, HEAD, PUT, POST, DELETE
24
+ */
25
+ method: string;
26
+ url: URL;
27
+ /**
28
+ * - default 's3'
29
+ */
30
+ service?: string;
31
+ /**
32
+ * - for example us-west-2
33
+ */
34
+ region?: string;
35
+ /**
36
+ * - hashed into x-amz-content-sha256; an empty or missing body is the hash of an empty string
37
+ */
38
+ body?: string;
39
+ /**
40
+ * - send UNSIGNED-PAYLOAD as x-amz-content-sha256 instead of hashing the body
41
+ */
42
+ unsignedPayload?: boolean;
43
+ /**
44
+ * - extra headers to sign and return; names are lowercased in the result
45
+ */
46
+ signHeaders?: Record<string, string>;
47
+ /**
48
+ * - signing time in milliseconds since the epoch; defaults to the current time
49
+ */
50
+ now?: number;
51
+ /**
52
+ * - presigned URLs: lifetime in seconds, default 3600
53
+ */
54
+ expires?: number;
55
+ /**
56
+ * - SigV4a: the regions the signature is valid for, 'us-east-1,us-west-2' or '*'
57
+ */
58
+ regionSet?: string;
59
+ };
60
+ export type Credentials = {
61
+ accessKeyId: string;
62
+ secretAccessKey: string;
63
+ /**
64
+ * - temporary credentials: signed and returned as x-amz-security-token
65
+ */
66
+ sessionToken?: string;
67
+ };
68
+ export type AuthHeaders = Record<string, string>;
69
+ export type Primitives = {
70
+ /**
71
+ * - SHA-256 as lowercase hex
72
+ */
73
+ hexHash: (data: string | Uint8Array) => string;
74
+ /**
75
+ * - HMAC-SHA-256 as bytes
76
+ */
77
+ hmac: (key: string | Uint8Array, data: string) => Uint8Array;
78
+ /**
79
+ * - HMAC-SHA-256 as lowercase hex
80
+ */
81
+ hmacHex: (key: Uint8Array, data: string) => string;
82
+ /**
83
+ * - the entry's one signing-key cache, shared by every symmetric signer
84
+ */
85
+ getSigningKey: ReturnType<typeof createKeys>;
86
+ };
87
+ /**
88
+ * @typedef {Object} SignRequest
89
+ * @property {string} method - GET, HEAD, PUT, POST, DELETE
90
+ * @property {URL} url
91
+ * @property {string} [service] - default 's3'
92
+ * @property {string} [region] - for example us-west-2
93
+ * @property {string} [body] - hashed into x-amz-content-sha256; an empty or missing body is the hash of an empty string
94
+ * @property {boolean} [unsignedPayload] - send UNSIGNED-PAYLOAD as x-amz-content-sha256 instead of hashing the body
95
+ * @property {Record<string, string>} [signHeaders] - extra headers to sign and return; names are lowercased in the result
96
+ * @property {number} [now] - signing time in milliseconds since the epoch; defaults to the current time
97
+ * @property {number} [expires] - presigned URLs: lifetime in seconds, default 3600
98
+ * @property {string} [regionSet] - SigV4a: the regions the signature is valid for, 'us-east-1,us-west-2' or '*'
99
+ */
100
+ /**
101
+ * @typedef {Object} Credentials
102
+ * @property {string} accessKeyId
103
+ * @property {string} secretAccessKey
104
+ * @property {string} [sessionToken] - temporary credentials: signed and returned as x-amz-security-token
105
+ */
106
+ /** @typedef {Record<string, string>} AuthHeaders */
107
+ /**
108
+ * @typedef {Object} Primitives
109
+ * @property {(data: string | Uint8Array) => string} hexHash - SHA-256 as lowercase hex
110
+ * @property {(key: string | Uint8Array, data: string) => Uint8Array} hmac - HMAC-SHA-256 as bytes
111
+ * @property {(key: Uint8Array, data: string) => string} hmacHex - HMAC-SHA-256 as lowercase hex
112
+ * @property {ReturnType<typeof createKeys>} getSigningKey - the entry's one signing-key cache, shared by every symmetric signer
113
+ */
114
+ /** The payload hash for x-amz-content-sha256. The hash function may return a promise, which is then the result. */
115
+ export declare function payloadHashOf(request: any, hexHash: any): any;
116
+ /** Everything up to the hashing: the headers to return, the canonical request and the credential scope. */
117
+ export declare function prepare(request: any, credentials: any, payloadHash: any, scheme?: {
118
+ algorithm: string;
119
+ scope: (date: any, region: any, service: any) => string;
120
+ }): {
121
+ headers: {
122
+ 'x-amz-date': string;
123
+ 'x-amz-content-sha256': any;
124
+ } | {
125
+ 'x-amz-content-sha256'?: undefined;
126
+ 'x-amz-date': string;
127
+ };
128
+ canonicalRequest: string;
129
+ datetime: string;
130
+ date: string;
131
+ region: any;
132
+ service: any;
133
+ credentialScope: string;
134
+ signedHeaders: any;
135
+ algorithm: string;
136
+ };
137
+ export declare const stringToSign: (prepared: any, canonicalRequestHash: any) => string;
138
+ /** Adds the authorization header to the headers to return. */
139
+ export declare function authorize(prepared: any, accessKeyId: any, signature: any): any;
140
+ export declare function createKeys(hmac: any): (secretAccessKey: any, date: any, region: any, service: any) => any;
141
+ /**
142
+ * @param {Primitives} primitives
143
+ * @returns {(request: SignRequest, credentials: Credentials) => AuthHeaders}
144
+ */
145
+ export declare function createSigner({ hexHash, hmacHex, getSigningKey }: Primitives): (request: SignRequest, credentials: Credentials) => AuthHeaders;
package/types/bun.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export declare const getAuthHeaders: (request: import("./base.js").SignRequest, credentials: import("./base.js").Credentials) => import("./base.js").AuthHeaders;
2
+ export declare const presign: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string;
3
+ export declare const getAuthHeadersV4a: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => import('./base.js').AuthHeaders;
4
+ export declare const presignV4a: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string;
5
+ export declare const chunkSigner: (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => import("./chunks.js").ChunkSigner;
6
+ export declare const eventSigner: (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => import("./events.js").EventSigner;
@@ -0,0 +1,38 @@
1
+ export declare const utf8: TextEncoder;
2
+ /** The seed signature, timestamp and scope of a signed request, from the headers getAuthHeaders returned. */
3
+ export declare function seedOf(authHeaders: any): {
4
+ signature: string;
5
+ datetime: any;
6
+ date: string;
7
+ region: string;
8
+ service: string;
9
+ scope: string;
10
+ };
11
+ export declare const chunkStringToSign: (datetime: any, scope: any, previous: any, payloadHash: any) => string;
12
+ export declare const trailerStringToSign: (datetime: any, scope: any, previous: any, trailerHash: any) => string;
13
+ /** Trailing headers in canonical form: lowercase names in name order, each line ending in a newline. */
14
+ export declare function canonicalTrailers(trailers: any): string;
15
+ /** The bytes of one chunk on the wire: size in hex, signature, the data. */
16
+ export declare function frameChunk(signature: any, data: any): Uint8Array<any>;
17
+ /** The last chunk (no data), with the signed trailing headers when there are any. */
18
+ export declare function frameEnd(signature: any, trailers: any, trailerSignature: any): Uint8Array<ArrayBuffer>;
19
+ export type ChunkSigner = {
20
+ /**
21
+ * - the bytes to send for one chunk of the body
22
+ */
23
+ chunk: (data: Uint8Array) => Uint8Array;
24
+ /**
25
+ * - the last chunk, with the signed trailing headers when there are any
26
+ */
27
+ end: (trailers?: Record<string, string>) => Uint8Array;
28
+ };
29
+ /**
30
+ * @typedef {Object} ChunkSigner
31
+ * @property {(data: Uint8Array) => Uint8Array} chunk - the bytes to send for one chunk of the body
32
+ * @property {(trailers?: Record<string, string>) => Uint8Array} end - the last chunk, with the signed trailing headers when there are any
33
+ */
34
+ /**
35
+ * @param {import('./base.js').Primitives} primitives
36
+ * @returns {(authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => ChunkSigner}
37
+ */
38
+ export declare function createChunkSigner({ hexHash, hmacHex, getSigningKey }: import('./base.js').Primitives): (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => ChunkSigner;
@@ -0,0 +1,5 @@
1
+ import crypto from 'node:crypto';
2
+ export declare const ecdsa: {
3
+ key(d: any): crypto.KeyObject;
4
+ sign: (key: any, text: any) => string;
5
+ };
@@ -0,0 +1,25 @@
1
+ /** The :date header of an event: name, type 8 (timestamp), milliseconds since the epoch as 8 bytes. */
2
+ export declare function dateHeader(ms: any): Uint8Array<ArrayBuffer>;
3
+ /** The signed event: prelude, the :date and :chunk-signature headers, the inner message as payload, checksums. */
4
+ export declare function frameEvent(dateBytes: any, signatureHex: any, inner: any): Uint8Array<any>;
5
+ export declare const eventStringToSign: (datetime: any, scope: any, previous: any, headersHash: any, payloadHash: any) => string;
6
+ export declare const eventScope: (ms: any, region: any, service: any) => {
7
+ datetime: string;
8
+ scope: string;
9
+ date: string;
10
+ };
11
+ export type EventSigner = {
12
+ /**
13
+ * - the signed event to send for one encoded message
14
+ */
15
+ sign: (message: Uint8Array, now?: number) => Uint8Array;
16
+ };
17
+ /**
18
+ * @typedef {Object} EventSigner
19
+ * @property {(message: Uint8Array, now?: number) => Uint8Array} sign - the signed event to send for one encoded message
20
+ */
21
+ /**
22
+ * @param {import('./base.js').Primitives} primitives
23
+ * @returns {(authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => EventSigner}
24
+ */
25
+ export declare function createEventSigner({ hexHash, hmacHex, getSigningKey }: import('./base.js').Primitives): (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => EventSigner;
@@ -0,0 +1,6 @@
1
+ export declare const getAuthHeaders: (request: import("./base.js").SignRequest, credentials: import("./base.js").Credentials) => import("./base.js").AuthHeaders;
2
+ export declare const presign: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string;
3
+ export declare const getAuthHeadersV4a: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => import('./base.js').AuthHeaders;
4
+ export declare const presignV4a: (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string;
5
+ export declare const chunkSigner: (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => import("./chunks.js").ChunkSigner;
6
+ export declare const eventSigner: (authHeaders: import('./base.js').AuthHeaders, credentials: import('./base.js').Credentials) => import("./events.js").EventSigner;
@@ -0,0 +1,27 @@
1
+ export declare const encodeQueryPart: (text: any) => string;
2
+ /**
3
+ * 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
4
+ * signed; S3 URLs are always UNSIGNED-PAYLOAD, other services sign the body hash. `extra` is appended to the authentication parameters.
5
+ */
6
+ export declare function preparePresign(request: any, credentials: any, payloadHash: any, scheme?: {
7
+ algorithm: string;
8
+ scope: (date: any, region: any, service: any) => string;
9
+ }, extra?: string): {
10
+ canonicalRequest: string;
11
+ datetime: string;
12
+ date: string;
13
+ region: any;
14
+ service: any;
15
+ credentialScope: string;
16
+ signedHeaders: string;
17
+ algorithm: string;
18
+ base: any;
19
+ query: string;
20
+ };
21
+ /** The URL to hand out: the original URL with the authentication query parameters and the signature appended. */
22
+ export declare const presignedUrl: (prepared: any, signature: any) => string;
23
+ /**
24
+ * @param {import('./base.js').Primitives} primitives
25
+ * @returns {(request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string}
26
+ */
27
+ export declare function createPresigner({ hexHash, hmacHex, getSigningKey }: import('./base.js').Primitives): (request: import('./base.js').SignRequest, credentials: import('./base.js').Credentials) => string;