Files

65 lines
2.9 KiB
JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto'
import { config } from '../../config.js'
/**
* Formular-Token des Webshops (Plan 2026-10-05-webshop.md, Block F): "<Ausstellzeit base36>.<HMAC>" je siteKey, wie das
* Formular-Token des Terminplaners, aber mit eigenem Schluessel (Beschriftung 'shop-form', abgeleitet aus config.sessionSecret):
* ein Terminplaner-Token gilt im Shop nicht und umgekehrt. Das Token beweist nur Alter und Herkunft (kein Geheimnis fuer den
* Besucher). Zu schnell abgeschickt (config.shop.formMinSekunden, Standard 3) = Bot, aelter als 2 Stunden = Seite neu laden.
*/
const FORM_MAX_ALTER_MS = 2 * 60 * 60 * 1000
const FORM_TOKEN_MAX_LAENGE = 100
const MAC_RE = /^[A-Za-z0-9_-]{43}$/
const ZEIT_RE = /^[0-9a-z]{1,13}$/
const MIN_STANDARD_SEKUNDEN = 3
/** Mindestalter in Sekunden; wird bei jedem Aufruf gelesen (Tests aendern den Wert). */
export const formMinSekunden = () => {
const wert = config.shop?.formMinSekunden
return Number.isFinite(wert) && wert >= 0 ? wert : MIN_STANDARD_SEKUNDEN
}
function schluessel() {
const geheimnis = config.sessionSecret
if (typeof geheimnis !== 'string' || geheimnis.length < 32) {
throw new Error('Shop-Token: SESSION_SECRET fehlt oder ist zu kurz (mindestens 32 Zeichen).')
}
return createHmac('sha256', geheimnis).update('shop-form').digest()
}
const zeitMs = (now, was) => {
const ms = now instanceof Date ? now.getTime() : Number(now)
if (!Number.isFinite(ms) || ms < 0) throw new TypeError(`${was}: now ist kein gueltiger Zeitpunkt`)
return ms
}
function formMac(siteKey, zeit) {
return createHmac('sha256', schluessel()).update(`${siteKey}|${zeit}`).digest('base64url')
}
/** Formular-Token fuer ein Widget dieses siteKeys, ausgestellt zum Zeitpunkt now. */
export function formToken(siteKey, now = new Date()) {
if (typeof siteKey !== 'string' || siteKey === '') throw new TypeError('formToken: siteKey fehlt')
const zeit = zeitMs(now, 'formToken').toString(36)
return `${zeit}.${formMac(siteKey, zeit)}`
}
/** 'ok' | 'zu_schnell' (juenger als formMinSekunden) | 'abgelaufen' (aelter als 2 h) | 'ungueltig' (fremd/gefaelscht). */
export function pruefeFormToken(siteKey, token, now = new Date()) {
if (typeof siteKey !== 'string' || siteKey === '') return 'ungueltig'
if (typeof token !== 'string' || token.length > FORM_TOKEN_MAX_LAENGE) return 'ungueltig'
const teile = token.split('.')
if (teile.length !== 2) return 'ungueltig'
const [zeit, mac] = teile
if (!ZEIT_RE.test(zeit) || !MAC_RE.test(mac)) return 'ungueltig'
const erwartet = Buffer.from(formMac(siteKey, zeit))
const gegeben = Buffer.from(mac)
if (gegeben.length !== erwartet.length || !timingSafeEqual(gegeben, erwartet)) return 'ungueltig'
const alter = zeitMs(now, 'pruefeFormToken') - parseInt(zeit, 36)
if (alter > FORM_MAX_ALTER_MS) return 'abgelaufen'
if (alter < formMinSekunden() * 1000) return 'zu_schnell'
return 'ok'
}