UNPKG

@acusti/aws-signature-v4

Version:

A lightweight isomorphic module to generate request headers that fulfill the AWS SigV4 signing process

185 lines (184 loc) 5.92 kB
const universalBtoa = (text) => { try { return btoa(text); } catch (err) { return Buffer.from(text).toString("base64"); } }; const subtleCrypto = globalThis.crypto.subtle; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const DEFAULT_ALGORITHM = "SHA-256"; const DEFAULT_SERVICE = "appsync"; const REGION = typeof process === "undefined" ? "" : process.env.REGION ?? ""; const decodeArrayBuffer = (buffer, encoding) => { const uint8Array = new Uint8Array(buffer); switch (encoding) { case "base64": return universalBtoa(String.fromCharCode(...uint8Array)); case "hex": return uint8Array.reduce((a, b) => a + b.toString(16).padStart(2, "0"), ""); default: return decoder.decode(uint8Array); } }; const encrypt = async (payload) => { const keyArray = typeof payload.key === "string" ? encoder.encode(payload.key) : payload.key; const algorithm = { hash: payload.algorithm ?? DEFAULT_ALGORITHM, name: "HMAC" }; const key = await subtleCrypto.importKey("raw", keyArray, algorithm, false, ["sign"]); const signature = await subtleCrypto.sign("hmac", key, encoder.encode(payload.data)); if (!payload.encoding) return new Uint8Array(signature); return decodeArrayBuffer(signature, payload.encoding); }; const hash = async ({ algorithm = DEFAULT_ALGORITHM, data, encoding }) => { const _hash = await subtleCrypto.digest({ name: algorithm }, encoder.encode(data)); return decodeArrayBuffer(_hash, encoding); }; const getNormalizedHeaders = (headers) => Object.keys(headers).map((key) => ({ key: key.toLowerCase(), value: headers[key] })).sort((a, b) => a.key < b.key ? -1 : 1); const getCanonicalHeaders = (headers) => { const normalizedHeaders = getNormalizedHeaders(headers); if (!normalizedHeaders.length) return ""; return normalizedHeaders.reduce((acc, { key, value }) => { value = value ? value.trim().replace(/\s{2,}/g, " ") : ""; return acc + key + ":" + value + "\n"; }, ""); }; const getSignedHeaders = (headers) => getNormalizedHeaders(headers).map(({ key }) => key).join(";"); const getCanonicalString = async (resource, fetchOptions) => { const url = new URL(resource); url.searchParams.sort(); let bodyHash = ""; if (fetchOptions.headers["x-amz-content-sha256"] === "UNSIGNED-PAYLOAD") { bodyHash = "UNSIGNED-PAYLOAD"; } else { const body = typeof fetchOptions.body === "string" ? fetchOptions.body : ""; bodyHash = await hash({ data: body, encoding: "hex" }); } return [ fetchOptions.method, url.pathname, url.searchParams.toString(), getCanonicalHeaders(fetchOptions.headers), getSignedHeaders(fetchOptions.headers), bodyHash ].join("\n"); }; const getRegionFromResource = (resource) => { const { host } = new URL(resource); const matched = host.match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com$/); const region = matched && matched[2]; return region ?? ""; }; const getCredentialScope = ({ dateString, region, service }) => `${dateString}/${region}/${service}/aws4_request`; const getStringToSign = async ({ algorithm, canonicalString, dateTimeString, scope }) => [ algorithm, dateTimeString, scope, await hash({ data: canonicalString, encoding: "hex" }) ].join("\n"); const getSigningKey = async ({ dateString, region, secretAccessKey, service }) => { const key = "AWS4" + secretAccessKey; const keyDate = await encrypt({ data: dateString, key }); const keyRegion = await encrypt({ data: region, key: keyDate }); const keyService = await encrypt({ data: service, key: keyRegion }); return await encrypt({ data: "aws4_request", key: keyService }); }; const getSignature = async (signingKey, stringToSign) => await encrypt({ data: stringToSign, encoding: "hex", key: signingKey }); const getAuthorizationHeader = ({ accessKeyId, algorithm, scope, signature, signedHeaders }) => [ algorithm + " Credential=" + accessKeyId + "/" + scope, "SignedHeaders=" + signedHeaders, "Signature=" + signature ].join(", "); const getHeadersWithAuthorization = async (resource, fetchOptions, { accessKeyId, region = REGION || getRegionFromResource(resource), secretAccessKey, service = DEFAULT_SERVICE, sessionToken }) => { const date = /* @__PURE__ */ new Date(); const dateTimeString = date.toISOString().replace(/[:-]|\.\d{3}/g, ""); const dateString = dateTimeString.substring(0, 8); const algorithm = "AWS4-HMAC-SHA256"; const { host } = new URL(resource); const headers = fetchOptions.headers ?? {}; headers.host = host; headers["x-amz-date"] = dateTimeString; if (!headers.accept) { headers.accept = "*/*"; } if (!headers["content-type"]) { headers["content-type"] = "application/json; charset=UTF-8"; } if (sessionToken) { headers["x-amz-security-token"] = sessionToken; } if (typeof fetchOptions.body !== "string") { headers["x-amz-content-sha256"] = "UNSIGNED-PAYLOAD"; } delete headers.authorization; delete headers.date; const scope = getCredentialScope({ dateString, region, service }); const signingKey = await getSigningKey({ dateString, region, secretAccessKey, service }); const canonicalString = await getCanonicalString(resource, { ...fetchOptions, headers }); const stringToSign = await getStringToSign({ algorithm, canonicalString, dateTimeString, scope }); headers.authorization = getAuthorizationHeader({ accessKeyId, algorithm, scope, signature: await getSignature(signingKey, stringToSign), signedHeaders: getSignedHeaders(headers) }); delete headers.host; return headers; }; export { getAuthorizationHeader, getCanonicalHeaders, getCanonicalString, getCredentialScope, getHeadersWithAuthorization, getRegionFromResource, getSignature, getSignedHeaders, getSigningKey, getStringToSign }; //# sourceMappingURL=index.js.map