91 lines
2.8 KiB
JavaScript
91 lines
2.8 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
function hmac(key, data, encoding) {
|
|
return crypto.createHmac('sha256', key).update(data, 'utf8').digest(encoding);
|
|
}
|
|
|
|
function hash(data) {
|
|
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
|
|
}
|
|
|
|
function parseAuthHeader(header) {
|
|
const parts = Object.fromEntries(
|
|
header.replace(/^AWS4-HMAC-SHA256\s+/, '').split(',').map((part) => {
|
|
const idx = part.indexOf('=');
|
|
const key = part.slice(0, idx).trim();
|
|
const value = part.slice(idx + 1).trim().replace(/^"|"$/g, '');
|
|
return [key, value];
|
|
})
|
|
);
|
|
const credential = parts.Credential.split('/');
|
|
return {
|
|
accessKey: credential[0],
|
|
date: credential[1],
|
|
region: credential[2],
|
|
signedHeaders: parts.SignedHeaders.split(';'),
|
|
signature: parts.Signature,
|
|
};
|
|
}
|
|
|
|
function getHeader(req, name) {
|
|
return req.headers[name.toLowerCase()] || '';
|
|
}
|
|
|
|
function canonicalQuery(query) {
|
|
if (!query) return '';
|
|
const params = new URLSearchParams(query.startsWith('?') ? query.slice(1) : query);
|
|
return [...params.entries()]
|
|
.map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(v)])
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join('&');
|
|
}
|
|
|
|
function canonicalHeaders(req, signedHeaders) {
|
|
return signedHeaders
|
|
.map((name) => `${name}:${getHeader(req, name).trim().replace(/\s+/g, ' ')}`)
|
|
.join('\n');
|
|
}
|
|
|
|
export function verifyRequest(req, body, { accessKey, secretKey, region = 'us-east-1' }) {
|
|
const auth = getHeader(req, 'authorization');
|
|
if (!auth.startsWith('AWS4-HMAC-SHA256')) {
|
|
return false;
|
|
}
|
|
|
|
const parsed = parseAuthHeader(auth);
|
|
if (parsed.accessKey !== accessKey) {
|
|
return false;
|
|
}
|
|
|
|
const regionToUse = parsed.region || region;
|
|
const amzDate = getHeader(req, 'x-amz-date');
|
|
const declaredPayload = getHeader(req, 'x-amz-content-sha256');
|
|
const payloadHash =
|
|
declaredPayload === 'UNSIGNED-PAYLOAD' ? 'UNSIGNED-PAYLOAD' : declaredPayload || hash(body);
|
|
const canonical = [
|
|
req.method,
|
|
req.url.split('?')[0] || '/',
|
|
canonicalQuery(req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''),
|
|
`${canonicalHeaders(req, parsed.signedHeaders)}\n`,
|
|
parsed.signedHeaders.join(';'),
|
|
payloadHash,
|
|
].join('\n');
|
|
|
|
const scope = `${parsed.date}/${regionToUse}/s3/aws4_request`;
|
|
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, hash(canonical)].join('\n');
|
|
const signingKey = hmac(
|
|
hmac(hmac(hmac(`AWS4${secretKey}`, parsed.date), regionToUse), 's3'),
|
|
'aws4_request'
|
|
);
|
|
const expected = hmac(signingKey, stringToSign, 'hex');
|
|
if (expected.length !== parsed.signature.length) {
|
|
return false;
|
|
}
|
|
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parsed.signature, 'hex'));
|
|
}
|
|
|
|
export function etagFor(data) {
|
|
return `"${crypto.createHash('md5').update(data).digest('hex')}"`;
|
|
}
|