@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
JavaScript
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