103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
import { createHash, createHmac } from "node:crypto";
|
|
|
|
export type VolcengineSignatureInput = {
|
|
method: "POST" | "GET";
|
|
endpoint: string;
|
|
query: Record<string, string>;
|
|
body: string;
|
|
accessKeyId: string;
|
|
secretAccessKey: string;
|
|
region: string;
|
|
service: string;
|
|
date?: Date;
|
|
};
|
|
|
|
export type SignedVolcengineRequest = {
|
|
url: string;
|
|
headers: Record<string, string>;
|
|
canonicalRequest: string;
|
|
stringToSign: string;
|
|
};
|
|
|
|
export function sha256Hex(value: string | Buffer): string {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
export function signVolcengineRequest(input: VolcengineSignatureInput): SignedVolcengineRequest {
|
|
const endpoint = new URL(input.endpoint);
|
|
const date = input.date || new Date();
|
|
const xDate = formatAmzDate(date);
|
|
const shortDate = xDate.slice(0, 8);
|
|
const bodyHash = sha256Hex(input.body);
|
|
const canonicalQuery = canonicalizeQuery(input.query);
|
|
const canonicalHeaders = [
|
|
`content-type:application/json`,
|
|
`host:${endpoint.host}`,
|
|
`x-content-sha256:${bodyHash}`,
|
|
`x-date:${xDate}`
|
|
].join("\n");
|
|
const signedHeaders = "content-type;host;x-content-sha256;x-date";
|
|
const canonicalRequest = [
|
|
input.method,
|
|
endpoint.pathname || "/",
|
|
canonicalQuery,
|
|
`${canonicalHeaders}\n`,
|
|
signedHeaders,
|
|
bodyHash
|
|
].join("\n");
|
|
const credentialScope = `${shortDate}/${input.region}/${input.service}/request`;
|
|
const stringToSign = [
|
|
"HMAC-SHA256",
|
|
xDate,
|
|
credentialScope,
|
|
sha256Hex(canonicalRequest)
|
|
].join("\n");
|
|
const signingKey = getSigningKey(input.secretAccessKey, shortDate, input.region, input.service);
|
|
const signature = hmacHex(signingKey, stringToSign);
|
|
const authorization = [
|
|
`HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}`,
|
|
`SignedHeaders=${signedHeaders}`,
|
|
`Signature=${signature}`
|
|
].join(", ");
|
|
|
|
endpoint.search = canonicalQuery;
|
|
return {
|
|
url: endpoint.toString(),
|
|
headers: {
|
|
"Authorization": authorization,
|
|
"Content-Type": "application/json",
|
|
"Host": endpoint.host,
|
|
"X-Content-Sha256": bodyHash,
|
|
"X-Date": xDate
|
|
},
|
|
canonicalRequest,
|
|
stringToSign
|
|
};
|
|
}
|
|
|
|
function canonicalizeQuery(query: Record<string, string>): string {
|
|
return Object.entries(query)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
.join("&");
|
|
}
|
|
|
|
function formatAmzDate(date: Date): string {
|
|
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
}
|
|
|
|
function hmac(key: string | Buffer, value: string): Buffer {
|
|
return createHmac("sha256", key).update(value).digest();
|
|
}
|
|
|
|
function hmacHex(key: string | Buffer, value: string): string {
|
|
return createHmac("sha256", key).update(value).digest("hex");
|
|
}
|
|
|
|
function getSigningKey(secretAccessKey: string, date: string, region: string, service: string): Buffer {
|
|
const kDate = hmac(secretAccessKey, date);
|
|
const kRegion = hmac(kDate, region);
|
|
const kService = hmac(kRegion, service);
|
|
return hmac(kService, "request");
|
|
}
|