All checks were successful
Deploy bericht / deploy (push) Successful in 1s
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]
102 lines
3.7 KiB
PHP
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);
|