| 1 | package com.rbctcsworld.ecommerce.audit; | |
| 2 | ||
| 3 | import java.nio.charset.StandardCharsets; | |
| 4 | import java.security.MessageDigest; | |
| 5 | import java.security.NoSuchAlgorithmException; | |
| 6 | import java.time.LocalDateTime; | |
| 7 | import java.time.format.DateTimeFormatter; | |
| 8 | import java.util.HexFormat; | |
| 9 | import java.util.List; | |
| 10 | ||
| 11 | /** | |
| 12 | * The hash chain that makes the audit trail tamper-evident. | |
| 13 | * | |
| 14 | * hash(row) = SHA-256 of the previous row's hash and every field of this row. Each part is written as | |
| 15 | * "length:value" (null as "-1:"), so no two different rows can produce the same text ("ab"+"c" vs "a"+"bc"). | |
| 16 | * Changing a value, deleting a row or swapping two rows changes a hash that a later row depends on. | |
| 17 | */ | |
| 18 | public final class AuditChain { | |
| 19 | ||
| 20 | public static final String GENESIS = "0".repeat(64); | |
| 21 | private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); | |
| 22 | ||
| 23 | private AuditChain() { | |
| 24 | } | |
| 25 | ||
| 26 | public static String hash(String prevHash, long seq, LocalDateTime occurredAt, String actor, String entityType, | |
| 27 | long entityId, String action, String field, String oldValue, String newValue) { | |
| 28 | StringBuilder s = new StringBuilder(); | |
| 29 | for (String part : new String[]{prevHash, Long.toString(seq), occurredAt.format(TIME), actor, entityType, | |
| 30 | Long.toString(entityId), action, field, oldValue, newValue}) { | |
| 31 |
2
1. hash : removed conditional - replaced equality check with false → KILLED 2. hash : removed conditional - replaced equality check with false → KILLED |
s.append(part == null ? -1 : part.length()).append(':').append(part == null ? "" : part).append('|'); |
| 32 | } | |
| 33 | try { | |
| 34 | byte[] digest = MessageDigest.getInstance("SHA-256").digest(s.toString().getBytes(StandardCharsets.UTF_8)); | |
| 35 |
1
1. hash : replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hash → KILLED |
return HexFormat.of().formatHex(digest); |
| 36 | } catch (NoSuchAlgorithmException e) { | |
| 37 | throw new IllegalStateException("SHA-256 is part of every JVM", e); | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | static String hashOf(AuditRecord r) { | |
| 42 |
1
1. hashOf : replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hashOf → KILLED |
return hash(r.prevHash(), r.seq(), r.occurredAt(), r.actor(), r.entityType(), r.entityId(), r.action(), |
| 43 | r.field(), r.oldValue(), r.newValue()); | |
| 44 | } | |
| 45 | ||
| 46 | /** Result of checking the whole trail. brokenAtSeq = the first row that does not fit (null when valid). */ | |
| 47 | public record Verification(boolean valid, int entries, Long brokenAtSeq, String reason) { | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Checks rows in seq order: numbering without gaps, each row linked to the one before, each hash correct, | |
| 52 | * and the last row is the one the head points to (otherwise the newest rows were removed). | |
| 53 | */ | |
| 54 | public static Verification verify(List<AuditRecord> rows, long headSeq, String headHash) { | |
| 55 | Verification chain = verify(rows); | |
| 56 |
2
1. verify : removed conditional - replaced equality check with false → KILLED 2. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
if (!chain.valid()) return chain; |
| 57 |
2
1. verify : Replaced integer subtraction with addition → KILLED 2. verify : removed conditional - replaced equality check with false → KILLED |
String lastHash = rows.isEmpty() ? GENESIS : rows.get(rows.size() - 1).hash(); |
| 58 |
2
1. verify : removed conditional - replaced equality check with false → KILLED 2. verify : removed conditional - replaced equality check with false → KILLED |
if (rows.size() != headSeq || !lastHash.equals(headHash)) { |
| 59 |
2
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED 2. verify : Replaced long addition with subtraction → KILLED |
return new Verification(false, rows.size(), (long) rows.size() + 1, "the newest rows are missing"); |
| 60 | } | |
| 61 |
1
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
return chain; |
| 62 | } | |
| 63 | ||
| 64 | /** Checks rows in seq order: numbering without gaps, each row linked to the one before, each hash correct. */ | |
| 65 | public static Verification verify(List<AuditRecord> rows) { | |
| 66 | String expectedPrev = GENESIS; | |
| 67 | long expectedSeq = 1; | |
| 68 | for (AuditRecord r : rows) { | |
| 69 |
1
1. verify : removed conditional - replaced equality check with false → KILLED |
if (r.seq() != expectedSeq) { |
| 70 |
1
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
return new Verification(false, rows.size(), r.seq(), "row " + expectedSeq + " is missing"); |
| 71 | } | |
| 72 |
1
1. verify : removed conditional - replaced equality check with false → KILLED |
if (!r.prevHash().equals(expectedPrev)) { |
| 73 |
1
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
return new Verification(false, rows.size(), r.seq(), "not linked to the row before"); |
| 74 | } | |
| 75 |
1
1. verify : removed conditional - replaced equality check with false → KILLED |
if (!hashOf(r).equals(r.hash())) { |
| 76 |
1
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
return new Verification(false, rows.size(), r.seq(), "content does not match its hash"); |
| 77 | } | |
| 78 | expectedPrev = r.hash(); | |
| 79 |
1
1. verify : Replaced long addition with subtraction → KILLED |
expectedSeq++; |
| 80 | } | |
| 81 |
1
1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED |
return new Verification(true, rows.size(), null, null); |
| 82 | } | |
| 83 | } | |
Mutations | ||
| 31 |
1.1 2.2 |
|
| 35 |
1.1 |
|
| 42 |
1.1 |
|
| 56 |
1.1 2.2 |
|
| 57 |
1.1 2.2 |
|
| 58 |
1.1 2.2 |
|
| 59 |
1.1 2.2 |
|
| 61 |
1.1 |
|
| 69 |
1.1 |
|
| 70 |
1.1 |
|
| 72 |
1.1 |
|
| 73 |
1.1 |
|
| 75 |
1.1 |
|
| 76 |
1.1 |
|
| 79 |
1.1 |
|
| 81 |
1.1 |