u
This commit is contained in:
90
scripts/s3-backup/sigv4.mjs
Normal file
90
scripts/s3-backup/sigv4.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
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')}"`;
|
||||
}
|
||||
Reference in New Issue
Block a user