Files
jtlPosSync/scripts/s3-backup/sigv4.mjs
2026-07-15 03:11:30 +02:00

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')}"`;
}