import * as errors from "./errors.ts";import { bin2hex, getScope, makeDateLong, makeDateShort, sha256digestHex } from "./helpers.ts";
const signV4Algorithm = "AWS4-HMAC-SHA256";
export async function signV4(request: { headers: Headers; method: string; path: string; accessKey: string; secretKey: string; region: string; date: Date;}): Promise<string> { if (!request.accessKey) { throw new errors.AccessKeyRequiredError("accessKey is required for signing"); } if (!request.secretKey) { throw new errors.SecretKeyRequiredError("secretKey is required for signing"); }
const sha256sum = request.headers.get("x-amz-content-sha256"); if (sha256sum === null) { throw new Error( "Internal S3 client error - expected x-amz-content-sha256 header, but it's missing.", ); }
const signedHeaders = getHeadersToSign(request.headers); const canonicalRequest = getCanonicalRequest( request.method, request.path, request.headers, signedHeaders, sha256sum, ); const stringToSign = await getStringToSign( canonicalRequest, request.date, request.region, ); const signingKey = await getSigningKey( request.date, request.region, request.secretKey, ); const credential = getCredential( request.accessKey, request.region, request.date, ); const signature = bin2hex(await sha256hmac(signingKey, stringToSign)) .toLowerCase();
return `${signV4Algorithm} Credential=${credential}, SignedHeaders=${ signedHeaders.join(";").toLowerCase() }, Signature=${signature}`;}
export async function presignV4(request: { protocol: "http:" | "https:"; headers: Headers; method: string; path: string; accessKey: string; secretKey: string; region: string; date: Date; expirySeconds: number;}): Promise<string> { if (!request.accessKey) { throw new errors.AccessKeyRequiredError("accessKey is required for signing"); } if (!request.secretKey) { throw new errors.SecretKeyRequiredError("secretKey is required for signing"); } if (request.expirySeconds < 1) { throw new errors.InvalidExpiryError("expirySeconds cannot be less than 1 seconds"); } if (request.expirySeconds > 604800) { throw new errors.InvalidExpiryError("expirySeconds cannot be greater than 7 days"); } if (!request.headers.has("Host")) { throw new Error("Internal error: host header missing"); }
const resource = request.path.split("?")[0]; const queryString = request.path.split("?")[1]; const iso8601Date = makeDateLong(request.date); const signedHeaders = getHeadersToSign(request.headers); const credential = getCredential(request.accessKey, request.region, request.date); const hashedPayload = "UNSIGNED-PAYLOAD";
const newQuery = new URLSearchParams(queryString); newQuery.set("X-Amz-Algorithm", signV4Algorithm); newQuery.set("X-Amz-Credential", credential); newQuery.set("X-Amz-Date", iso8601Date); newQuery.set("X-Amz-Expires", request.expirySeconds.toString()); newQuery.set("X-Amz-SignedHeaders", signedHeaders.join(";").toLowerCase()); const newPath = resource + "?" + newQuery.toString().replace("+", "%20");
const canonicalRequest = getCanonicalRequest(request.method, newPath, request.headers, signedHeaders, hashedPayload); const stringToSign = await getStringToSign(canonicalRequest, request.date, request.region); const signingKey = await getSigningKey(request.date, request.region, request.secretKey); const signature = bin2hex(await sha256hmac(signingKey, stringToSign)).toLowerCase(); const presignedUrl = `${request.protocol}//${request.headers.get("Host")}${newPath}&X-Amz-Signature=${signature}`; return presignedUrl;}
function getHeadersToSign(headers: Headers): string[] {
const ignoredHeaders = [ "authorization", "content-length", "content-type", "user-agent", ]; const headersToSign = []; for (const key of headers.keys()) { if (ignoredHeaders.includes(key.toLowerCase())) { continue; } headersToSign.push(key); } headersToSign.sort(); return headersToSign;}
const CODES = { A: "A".charCodeAt(0), Z: "Z".charCodeAt(0), a: "a".charCodeAt(0), z: "z".charCodeAt(0), "0": "0".charCodeAt(0), "9": "9".charCodeAt(0), "/": "/".charCodeAt(0),};const ALLOWED_BYTES = "-._~".split("").map((s) => s.charCodeAt(0));
function awsUriEncode(string: string, allowSlashes = false) { const bytes: Uint8Array = new TextEncoder().encode(string); let encoded = ""; for (const byte of bytes) { if ( (byte >= CODES.A && byte <= CODES.Z) || (byte >= CODES.a && byte <= CODES.z) || (byte >= CODES["0"] && byte <= CODES["9"]) || (ALLOWED_BYTES.includes(byte)) || (byte == CODES["/"] && allowSlashes) ) { encoded += String.fromCharCode(byte); } else { encoded += "%" + byte.toString(16).padStart(2, "0").toUpperCase(); } } return encoded;}
function getCanonicalRequest( method: string, path: string, headers: Headers, headersToSign: string[], payloadHash: string,): string { const headersArray = headersToSign.reduce<string[]>((acc, headerKey) => { const val = `${headers.get(headerKey)}`.replace(/ +/g, " "); acc.push(`${headerKey.toLowerCase()}:${val}`); return acc; }, []);
const requestResource = path.split("?")[0]; let requestQuery = path.split("?")[1]; if (requestQuery) { requestQuery = requestQuery .split("&") .sort() .map((element) => element.indexOf("=") === -1 ? element + "=" : element) .join("&"); } else { requestQuery = ""; }
const canonical = []; canonical.push(method.toUpperCase()); canonical.push(awsUriEncode(requestResource, true)); canonical.push(requestQuery); canonical.push(headersArray.join("\n") + "\n"); canonical.push(headersToSign.join(";").toLowerCase()); canonical.push(payloadHash); return canonical.join("\n");}
async function getStringToSign( canonicalRequest: string, requestDate: Date, region: string,): Promise<string> { const hash = await sha256digestHex(canonicalRequest); const scope = getScope(region, requestDate); const stringToSign = []; stringToSign.push(signV4Algorithm); stringToSign.push(makeDateLong(requestDate)); stringToSign.push(scope); stringToSign.push(hash); return stringToSign.join("\n");}
async function getSigningKey( date: Date, region: string, secretKey: string,): Promise<Uint8Array> { const dateLine = makeDateShort(date); const hmac1 = await sha256hmac("AWS4" + secretKey, dateLine); const hmac2 = await sha256hmac(hmac1, region); const hmac3 = await sha256hmac(hmac2, "s3"); return await sha256hmac(hmac3, "aws4_request");}
function getCredential(accessKey: string, region: string, requestDate: Date) { return `${accessKey}/${getScope(region, requestDate)}`;}
async function sha256hmac( secretKey: Uint8Array | string, data: Uint8Array | string,): Promise<Uint8Array> { const enc = new TextEncoder(); const keyObject = await crypto.subtle.importKey( "raw", secretKey instanceof Uint8Array ? secretKey : enc.encode(secretKey), { name: "HMAC", hash: { name: "SHA-256" } }, false, ["sign", "verify"], ); const signature = await crypto.subtle.sign( "HMAC", keyObject, data instanceof Uint8Array ? data : enc.encode(data), ); return new Uint8Array(signature);}
export const _internalMethods = { awsUriEncode, getHeadersToSign, getCanonicalRequest, getStringToSign, getSigningKey, getCredential, sha256hmac,};