pruefeWebhookSignatur function
Prueft die Signatur einer eingehenden Zustellung.
secrets ist das Secret aus createPartnerWebhook -- es verlaesst den
Server genau einmal. Mehrere erlauben den Schluesselwechsel: es reicht, wenn
einer passt.
rumpf ist der rohe Rumpf: die Bytes, wie sie ankamen. Nicht das
Ergebnis von jsonDecode, und nichts, was danach wieder zusammengesetzt
wurde.
jetztSek ist injizierbar -- kein Test dieses Pakets haengt an der Wanduhr.
Wirft nie: jede Ausnahme im Inneren wird zur Ablehnung.
Implementation
WebhookPruefung pruefeWebhookSignatur({
required List<String> secrets,
required String? signaturKopf,
required List<int> rumpf,
int? jetztSek,
int toleranzSek = kWebhookToleranzSek,
}) {
try {
final gueltige = secrets.where((s) => s.isNotEmpty).toList(growable: false);
if (gueltige.isEmpty) return const WebhookPruefung.nein(WebhookAblehnung.secretFehlt);
if (signaturKopf == null || signaturKopf.trim().isEmpty) {
return const WebhookPruefung.nein(WebhookAblehnung.kopfFehlt);
}
final kopf = leseSignaturKopf(signaturKopf);
if (kopf == null) return const WebhookPruefung.nein(WebhookAblehnung.kopfUnbrauchbar);
final jetzt = jetztSek ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
// In BEIDE Richtungen: eine vorgehende Uhr auf der Gegenseite darf ein
// altes Ereignis nicht wieder gueltig machen.
if ((jetzt - kopf.t).abs() > toleranzSek) {
return const WebhookPruefung.nein(WebhookAblehnung.zeitfenster);
}
final nachricht = <int>[...utf8.encode('${kopf.t}.'), ...rumpf];
for (final secret in gueltige) {
final soll = Hmac(sha256, utf8.encode(secret)).convert(nachricht).bytes;
for (final v1 in kopf.v1) {
final ist = hexZuBytes(v1);
if (ist != null && gleichZeitkonstant(ist, soll)) {
return WebhookPruefung.ok(kopf.t);
}
}
}
return const WebhookPruefung.nein(WebhookAblehnung.signatur);
} on Object {
// Punkt 4 im Bibliothekskommentar: eine Ausnahme ist eine Ablehnung, nie
// ein Ja.
return const WebhookPruefung.nein(WebhookAblehnung.signatur);
}
}