---
title: "The hash chain: how tamper detection works"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{The hash chain: how tamper detection works}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(regulog)
```

Every `regulog` entry is cryptographically linked to the entry before it.
This means that any modification to any part of any entry — however subtle
— breaks the chain at that point and is detectable by `verify_log()`.

This vignette explains how the chain is constructed, what it detects, what
it does not detect, and how to verify logs in a production setting.

## 1. The genesis record

When `regulog_init()` is called, a genesis record is created immediately.
It is not a log entry in the usual sense — it carries no user action — but
its SHA-256 hash becomes the anchor for the entire chain.

```{r genesis}
log <- regulog_init(app = "demo", version = "1.0", user = "analyst")

cat("Genesis hash:", log$genesis_hash, "\n")
cat("Last hash:   ", log$last_hash, "\n")
```

The genesis hash incorporates the app name, version, and creation
timestamp. Two sessions with the same app and version but different
creation times will have different genesis hashes.

## 2. How entries are hashed

Each entry hash is computed over a canonical string that includes every
field of that entry plus the hash of the previous entry:

```
h_n = SHA256(
  entry_id | timestamp | app | app_version | user | type |
  <payload fields in sorted key order> | h_{n-1}
)
```

The pipe character `|` is the delimiter. Field values are concatenated in
a fixed, deterministic order — this is what makes the hash reproducible
during verification.

Let us look at what this looks like in practice:

```{r first-entry}
log_action(
  log, "approved", "dataset_v1",
  "All quality checks passed — dataset approved for analysis"
)

entry <- log$entries[[1L]]
cat("Entry ID:   ", entry$entry_id, "\n")
cat("Prev hash:  ", entry$prev_hash, "\n") # = genesis hash
cat("Entry hash: ", entry$entry_hash, "\n")
```

The `prev_hash` of the first entry matches the `genesis_hash`. The chain
has begun.

```{r chain}
log_action(log, "model_fit", "ANCOVA_v1", "Primary ANCOVA fitted per SAP")
log_note(log, "Outlier in subject 042 retained per SAP section 8.3")

cat("Entry 1 hash:", log$entries[[1L]]$entry_hash, "\n")
cat("Entry 2 prev:", log$entries[[2L]]$prev_hash, "\n")
cat("Match:       ", log$entries[[1L]]$entry_hash ==
  log$entries[[2L]]$prev_hash, "\n")
```

Each entry's hash is the `prev_hash` of the next. The chain is intact.

## 3. Verification

`verify_log()` recomputes every entry hash from scratch and checks that:

1. The recomputed hash matches the stored `entry_hash` — confirms
   the entry content has not been modified
2. The stored `prev_hash` matches the preceding entry's `entry_hash` —
   confirms no entries have been inserted, deleted, or reordered

```{r verify-intact}
verify_log(log)
```

The return value is always a list with structured fields:

```{r verify-result}
result <- verify_log(log, verbose = FALSE)
str(result)
```

## 4. What tampering looks like

### 4a. Modifying an entry's content

If any field — reason, action, timestamp, user — is changed after writing,
the recomputed hash will not match the stored `entry_hash`. The entry
fails step 1 of verification.

```{r tamper-content}
# Simulate a reason being altered
original_reason <- log$entries[[1L]]$reason
log$entries[[1L]]$reason <- "ALTERED"

result <- suppressWarnings(verify_log(log, verbose = FALSE))
cat("Intact:       ", result$intact, "\n")
cat("First broken: ", result$first_broken, "\n")
cat("Error:        ", result$errors[[1L]], "\n")

log$entries[[1L]]$reason <- original_reason # restore
```

### 4b. Deleting an entry

If entry 2 is deleted, entry 3's `prev_hash` will no longer match entry
1's `entry_hash`. The chain fails step 2 at entry 3.

```{r tamper-delete}
saved <- log$entries
log$entries <- log$entries[-2L] # remove entry 2

result <- suppressWarnings(verify_log(log, verbose = FALSE))
cat("Intact:       ", result$intact, "\n")
cat("First broken: ", result$first_broken, "\n")

log$entries <- saved # restore
```

### 4c. Breaking the prev_hash directly

```{r tamper-hash}
saved_prev <- log$entries[[2L]]$prev_hash
log$entries[[2L]]$prev_hash <- paste(rep("0", 64L), collapse = "")

result <- suppressWarnings(verify_log(log, verbose = FALSE))
cat("Intact:       ", result$intact, "\n")
cat("First broken: ", result$first_broken, "\n")

log$entries[[2L]]$prev_hash <- saved_prev # restore
verify_log(log, verbose = FALSE)$intact # confirm restored
```

## 5. What the chain does NOT protect against

The hash chain proves that entries have not been modified after writing.
It does not:

- **Authenticate the user** — `user` is a string; `regulog` does not
  verify that the person who set `user = "jsmith"` is actually that
  person. Authentication is the responsibility of the calling system
  (OS login, Shiny Server Pro, Posit Connect).
- **Prevent future entries** — anyone with write access to the `.rlog`
  file can append new entries. The chain only covers what is written;
  it does not prevent additions.
- **Encrypt the content** — `.rlog` files are plain JSON. Do not store
  sensitive data in log entries; store references (file names, IDs) instead.
- **Protect from file deletion** — if the `.rlog` file is deleted, the
  audit trail is gone. Use standard file system controls and backups.

## 6. Verifying a file without a live session

The `.rlog` file is self-contained. Verification does not require the
original `regulog` object — it works directly from the file path:

```{r verify-file, eval = FALSE}
# Can be run by a QC reviewer with no knowledge of the original session
result <- verify_log("logs/trial001_audit.rlog")

# Structured result for programmatic use
if (!result$intact) {
  warning(sprintf(
    "Log integrity failure: %d error(s). First broken entry: #%d",
    length(result$errors), result$first_broken
  ))
}
```

This is the intended QC workflow in regulated environments: the analyst
runs the analysis and produces the `.rlog`, a reviewer verifies the file
independently.

## 7. Working with persistent .rlog files

When `path` is supplied to `regulog_init()`, entries are written to disk
immediately — each `log_action()`, `log_change()`, etc. appends one JSON
line. The file is append-only from `regulog`'s perspective.

```{r persistent, eval = FALSE}
log <- regulog_init(
  app     = "trial-analysis",
  version = "1.0.0",
  user    = "jsmith",
  path    = "logs/trial001_audit.rlog"
)

log_action(log, "data_read", "adsl.sas7bdat", "Reading ADSL")
# ↑ This line is written to disk immediately

# The .rlog file at this point:
# Line 1: {"entry_id":0,"type":"GENESIS",...}
# Line 2: {"entry_id":1,"type":"ACTION","action":"data_read",...}
```

The genesis record is always line 1. Subsequent entries follow in order.
Each line is a complete, self-contained JSON object.

## 8. The NDJSON format

`.rlog` files are newline-delimited JSON (NDJSON). Each line is one entry:

```json
{"entry_id":0,"timestamp":"2026-06-23T10:00:00.000Z","app":"trial-analysis","app_version":"1.0.0","user":"jsmith","type":"GENESIS","prev_hash":"0","entry_hash":"a3f8c2..."}
{"entry_id":1,"timestamp":"2026-06-23T10:01:22.841Z","app":"trial-analysis","app_version":"1.0.0","user":"jsmith","type":"ACTION","action":"data_read","object":"adsl.sas7bdat","reason":"Reading ADSL","prev_hash":"a3f8c2...","entry_hash":"b7d94e..."}
```

This format was chosen deliberately:

- **Human-readable** without specialist software
- **Streamable** — tools like `jq` can filter entries without loading
  the whole file
- **Append-safe** — no file rewriting needed; each entry is one line
- **Portable** — plain text, no binary encoding

## 9. Archival and long-term storage

For regulatory archival, export a signed CSV or JSON before storing:

```{r archival, eval = FALSE}
# Signed CSV — chain_intact and verified_at stamped on every row
export_audit_trail(log,
  format = "csv",
  signed = TRUE,
  path   = "archive/trial001_audit_2026-06-23.csv"
)

# Original .rlog — keep this too; it allows re-verification later
file.copy(
  "logs/trial001_audit.rlog",
  "archive/trial001_audit_2026-06-23.rlog"
)
```

The signed CSV is human-readable and importable into any audit management
system. The `.rlog` file allows the original hash chain to be verified at
any future point using `verify_log()`.

## 10. Hash algorithm

The default algorithm is SHA-256 (`hash_algo = "sha256"`). This is set
at `regulog_init()` and stored with the session — do not change it after
a `.rlog` file is in use, as verification would fail for any entries
written with a different algorithm.

SHA-256 is the standard for regulated environments. If your organisation
requires a different algorithm, pass `hash_algo` to `regulog_init()` —
any algorithm supported by `digest::digest()` is accepted.
