bericht/ajax/verify_signature.php
Eduard Wisch f37445ac9c
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
feat: Unterschriften-Verifikation + Seiten-Reorder API
ajax/verify_signature.php:
- Liest Metadaten aus .meta.json neben einer Unterschrift-Seite
- Rechnet SHA256 über die zum Signaturzeitpunkt existierenden Seiten
  (erste N laut meta.page_count_at_signing)
- Vergleicht mit dem gespeicherten Hash → verified true/false
- Liefert reason bei Fehlschlag (Seitenanzahl oder Inhalt geändert)

bericht_card.php:
- Seiten mit .meta.json kriegen 🔒-Badge + grünen Rand
- 🔍-Button neben dem Löschen-Button öffnet Verifikations-Modal

editor.js:
- showSignatureVerifyResult-Modal zeigt alle Metadaten,
  beide Hashes, GPS als OpenStreetMap-Link, IP, User, Zeit
- Grün/Rot je nach Verifikationsergebnis

api/pages.php:
- Neuer Action reorder: POST mit {order:[ids]} schreibt neue
  page_order-Werte als Transaktion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
2026-04-09 08:18:51 +02:00

102 lines
3.7 KiB
PHP

<?php
/* Verifiziert eine Unterschrift-Seite: liest den gespeicherten Hash aus der
* zugehörigen .meta.json und rechnet den aktuellen Hash über die Seiten
* die zum Signaturzeitpunkt existierten nach.
*
* POST: pageid, token
*
* Antwort:
* { success: true, verified: true, meta: {...} }
* → Hash stimmt, keine nachträgliche Änderung
* { success: true, verified: false, reason: "...", meta: {...}, current_hash: "..." }
* → Hash stimmt nicht, nachträgliche Änderung erkannt
*/
require_once __DIR__.'/_inc.php';
global $db, $user;
if (!$user->hasRight('bericht', 'read')) bericht_ajax_fail('Permission denied', 403);
$pageid = (int) ($_POST['pageid'] ?? $_GET['pageid'] ?? 0);
if (!$pageid) bericht_ajax_fail('pageid fehlt');
$sqlr = $db->query("SELECT rowid, fk_bericht, source_path FROM ".$db->prefix()."bericht_page WHERE rowid = ".$pageid);
if (!$sqlr || !($row = $db->fetch_object($sqlr))) bericht_ajax_fail('Seite nicht gefunden', 404);
$full = bericht_resolve_data_path($row->source_path);
if (!$full || !file_exists($full)) bericht_ajax_fail('Datei nicht gefunden', 404);
$meta_path = $full.'.meta.json';
if (!file_exists($meta_path)) {
bericht_ajax_ok(array(
'verified' => false,
'reason' => 'Keine Metadaten-Datei — diese Seite ist keine aufgewertete Unterschrift',
));
}
$meta = json_decode(file_get_contents($meta_path), true);
if (!is_array($meta) || empty($meta['chain_hash_sha256'])) {
bericht_ajax_ok(array(
'verified' => false,
'reason' => 'Metadaten ungültig oder kein Hash enthalten',
));
}
// Wir brauchen den Bericht
$bericht = new Bericht($db);
if ($bericht->fetch($row->fk_bericht) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404);
// Die zum Signaturzeitpunkt existierenden Seiten: alle mit page_order < unserer.
// Sort nach page_order wie beim Signieren.
$prev_pages = BerichtPage::fetchAllForBericht($db, $row->fk_bericht);
// Nur die Seiten die VOR der Signatur waren — wir nehmen die ersten N entsprechend meta.page_count_at_signing
$expected_count = (int) ($meta['page_count_at_signing'] ?? 0);
$to_hash = array_slice($prev_pages, 0, $expected_count);
$current_count = 0;
foreach ($prev_pages as $pp) {
if ($pp->id == $row->rowid) break;
$current_count++;
}
// Hash nachrechnen
$hash_ctx = hash_init('sha256');
hash_update($hash_ctx, 'bericht:'.$bericht->id.'|ref:'.$bericht->ref);
foreach ($to_hash as $pp) {
$full_pp = bericht_resolve_data_path($pp->source_path);
if ($full_pp && file_exists($full_pp)) {
hash_update_file($hash_ctx, $full_pp);
}
hash_update($hash_ctx, '|order:'.$pp->page_order.'|note:'.($pp->note ?? ''));
}
$current_hash = hash_final($hash_ctx);
$stored_hash = $meta['chain_hash_sha256'];
$verified = hash_equals($stored_hash, $current_hash);
$result = array(
'verified' => $verified,
'stored_hash' => $stored_hash,
'current_hash' => $current_hash,
'expected_page_count' => $expected_count,
'current_page_count' => $current_count,
'meta' => array(
'signer_name' => $meta['signer_name'] ?? '',
'signed_at' => $meta['signed_at'] ?? '',
'user_login' => $meta['user_login'] ?? '',
'kunde' => $meta['kunde'] ?? '',
'parent_ref' => $meta['parent_ref'] ?? '',
'gps_lat' => $meta['gps_lat'] ?? null,
'gps_lon' => $meta['gps_lon'] ?? null,
'remote_ip' => $meta['remote_ip'] ?? '',
),
);
if (!$verified) {
if ($current_count !== $expected_count) {
$result['reason'] = "Seitenanzahl hat sich geändert ($expected_count$current_count)";
} else {
$result['reason'] = "Hash der Vorseiten stimmt nicht — eine Seite wurde nach der Unterschrift verändert";
}
}
bericht_ajax_ok($result);