// AWS SigV4 for the browser app: the same canonical form as core/s3.js // (path-style, us-east-1, UNSIGNED-PAYLOAD for bodies), on Web Crypto. // Browsers set Host themselves, so it is signed but not returned. const subtle = globalThis.crypto.subtle; const enc = new TextEncoder(); export const REGION = 'us-east-1'; export const SERVICE = 's3'; const hex = (b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, '0')).join(''); const sha256 = async (s) => hex(await subtle.digest('SHA-256', typeof s === 'string' ? enc.encode(s) : s)); async function hmac(key, msg) { const k = await subtle.importKey('raw', typeof key === 'string' ? enc.encode(key) : key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); return new Uint8Array(await subtle.sign('HMAC', k, enc.encode(msg))); } export function uriEncode(str, keepSlash) { const out = encodeURIComponent(str).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()); return keepSlash ? out.replace(/%2F/g, '/') : out; } export function canonicalQuery(query) { return Object.keys(query).sort().map((k) => `${uriEncode(k)}=${uriEncode(String(query[k] ?? ''))}`).join('&'); } export const amzDate = (d = new Date()) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); export function objectPath(bucket, key) { return '/' + uriEncode(bucket) + (key != null ? '/' + uriEncode(key, true) : ''); } const signingKeys = new Map(); async function signingKey(secret, date) { const id = `${date}:${secret}`; let k = signingKeys.get(id); if (!k) { k = await hmac(await hmac(await hmac(await hmac('AWS4' + secret, date), REGION), SERVICE), 'aws4_request'); signingKeys.clear(); signingKeys.set(id, k); } return k; } // Headers to send (lower-case, without host) for a signed request. export async function sign({ host, accessKeyId, secretKey }, method, path, query = {}, headers = {}, { now = amzDate(), payloadHash = 'UNSIGNED-PAYLOAD' } = {}) { const date = now.slice(0, 8); const h = { ...headers, host, 'x-amz-date': now, 'x-amz-content-sha256': payloadHash }; const lower = Object.fromEntries(Object.entries(h).map(([k, v]) => [k.toLowerCase(), String(v).trim()])); const names = Object.keys(lower).sort(); const signed = names.join(';'); const canonical = [method, path, canonicalQuery(query), names.map((n) => `${n}:${lower[n]}\n`).join(''), signed, payloadHash].join('\n'); const scope = `${date}/${REGION}/${SERVICE}/aws4_request`; const toSign = ['AWS4-HMAC-SHA256', now, scope, await sha256(canonical)].join('\n'); const sig = hex(await hmac(await signingKey(secretKey, date), toSign)); lower.authorization = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signed}, Signature=${sig}`; delete lower.host; return lower; } // A presigned GET (for