diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..3258ed4 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,57 @@ +/** + * Edge-compatible session signer/verifier using Web Crypto API. + */ + +async function getSessionKey() { + const secret = process.env.CAPTCHA_SECRET || 'dev_secret_meow'; + const encoder = new TextEncoder(); + return await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify'] + ); +} + +export async function signSession() { + const expires = Date.now() + 24 * 60 * 60 * 1000; // 24 hours validity + const data = `admin.${expires}`; + const encoder = new TextEncoder(); + const key = await getSessionKey(); + + const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(data)); + const signatureArray = Array.from(new Uint8Array(signatureBuffer)); + const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + return `${data}.${signatureHex}`; +} + +export async function verifySession(token) { + if (!token) return false; + + const parts = token.split('.'); + if (parts.length !== 3) return false; + + const [user, expires, signatureHex] = parts; + if (user !== 'admin') return false; + + // Check if token expired + if (Date.now() > parseInt(expires, 10)) return false; + + const data = `${user}.${expires}`; + const encoder = new TextEncoder(); + const key = await getSessionKey(); + + // Convert hex string back to Uint8Array + const signatureBytes = new Uint8Array( + signatureHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)) + ); + + try { + // Verify the HMAC signature ensures the token hasn't been tampered with + return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(data)); + } catch { + return false; + } +}