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], service: credential[3], 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 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}/${region}/s3/aws4_request`; const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, hash(canonical)].join('\n'); const signingKey = hmac( hmac( hmac(hmac(`AWS4${secretKey}`, parsed.date), region), 's3' ), 'aws4_request' ); const expected = hmac(signingKey, stringToSign, 'hex'); return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parsed.signature, 'hex')); } export function etagFor(data) { return `"${crypto.createHash('md5').update(data).digest('hex')}"`; }