Files
AriWeb-Suite/modules/webmaker/lib/awsv4.js
T

33 lines
3.0 KiB
JavaScript

'use strict';
// ============================================================================
// AWS Signature Version 4 서명 (v1.5.36, 요구 5) — Route 53 API 호출에 쓴다. 외부 SDK 없이 node:crypto 만 사용.
//
// 절차(공식 문서 "Signing AWS API requests"): 정규 요청(CanonicalRequest) → 서명 문자열(StringToSign) → 서명 키(kSecret→kDate→kRegion→kService→aws4_request) → HMAC-SHA256.
// Route 53 는 글로벌 서비스라 region 은 us-east-1, host 는 route53.amazonaws.com 이다. 임시 자격 증명(STS)을 쓰면 X-Amz-Security-Token 헤더를 함께 서명한다.
// ============================================================================
const crypto = require('crypto');
const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
const hmac = (k, s) => crypto.createHmac('sha256', k).update(s).digest();
/** RFC 3986 방식 인코딩 (AWS 는 '*' 등도 인코딩해야 한다) */
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
/**
* 서명 헤더 만들기.
* @param {{method:string, host:string, path:string, query?:object, headers?:object, body?:string, service:string, region:string, accessKeyId:string, secretAccessKey:string, sessionToken?:string, date?:Date}} o
* @returns {object} 요청에 붙일 헤더 (Host·X-Amz-Date·Authorization·[X-Amz-Security-Token]·기존 headers)
*/
function sign(o) {
const date = o.date || new Date(); const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, ''); const day = amzDate.slice(0, 8);
const headers = { host: o.host, 'x-amz-date': amzDate, ...(o.sessionToken ? { 'x-amz-security-token': o.sessionToken } : {}), ...Object.fromEntries(Object.entries(o.headers || {}).map(([k, v]) => [k.toLowerCase(), String(v).trim()])) };
const signedNames = Object.keys(headers).sort(); const canonHeaders = signedNames.map((k) => `${k}:${headers[k].replace(/\s+/g, ' ')}\n`).join('');
const canonQuery = Object.entries(o.query || {}).filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k, v]) => [enc(k), enc(String(v))]).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : 1)).map(([k, v]) => `${k}=${v}`).join('&');
const canonPath = o.path.split('/').map((seg) => enc(decodeURIComponent(seg))).join('/') || '/';
const payloadHash = sha256(o.body || '');
const canonical = [o.method.toUpperCase(), canonPath, canonQuery, canonHeaders, signedNames.join(';'), payloadHash].join('\n');
const scope = `${day}/${o.region}/${o.service}/aws4_request`;
const toSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256(canonical)].join('\n');
const kSigning = hmac(hmac(hmac(hmac('AWS4' + o.secretAccessKey, day), o.region), o.service), 'aws4_request');
const signature = crypto.createHmac('sha256', kSigning).update(toSign).digest('hex');
return { ...headers, authorization: `AWS4-HMAC-SHA256 Credential=${o.accessKeyId}/${scope}, SignedHeaders=${signedNames.join(';')}, Signature=${signature}`, 'x-amz-content-sha256': payloadHash };
}
module.exports = { sign, sha256 };