A tamper-evident, append-only log using SHA-256 hash chains.
Think git commits, but for audit trails. If someone changes a single byte, you'll know exactly where.
Why? β’ Install β’ Quick Start β’ Commands β’ Library β’ Format β’ Security β’ License
Most log files are just text. Anyone with write access can silently edit or delete records β and you'd never know.
cryptlog solves this with a dead-simple idea: every entry includes the SHA-256 hash of the previous one. Change any past record, and the chain breaks at exactly that point.
entry 0 ββhashβββΆ entry 1 ββhashβββΆ entry 2 ββhashβββΆ entry 3
β²
tamper here
β
verify catches it β
No database. No server. No blockchain network.
Just a single binary file with cryptographic integrity you can verify offline.
| Scenario | How cryptlog helps |
|---|---|
| ποΈ Compliance & auditing | Prove records weren't altered after the fact |
| π Incident forensics | Trustworthy timeline of what happened and when |
| π Academic records | Detect unauthorized grade or fee modifications |
| π§Ύ Financial trails | Immutable record of transactions and approvals |
| π Debug journals | Append-only traces that can't be silently edited |
| π¦ Chain of custody | Verify a sequence of events was never reordered |
cargo install cryptloggit clone https://github.com/YOUR_USERNAME/cryptlog.git
cd cryptlog
cargo install --path .cargo build --release
# binary is at ./target/release/cryptlogRequirements: Rust 1.70+ (stable)
# 1. append some audit entries
cryptlog append "user:42 logged in from 192.168.1.1"
cryptlog append "user:42 deleted invoice:991"
cryptlog append "admin:01 exported all records"
# 2. verify the chain β are all records untouched?
cryptlog verify
# β chain intact β 3 entries verified
# 3. view recent entries
cryptlog tailOutput:
cryptlog audit.clog (showing last 3)
# timestamp message hash
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
0 2026-03-26 16:24:57.936 user:42 logged in from 192.168.1.1 d656fc0f93ee
1 2026-03-26 16:24:57.945 user:42 deleted invoice:991 30b8067ccedd
2 2026-03-26 16:24:57.955 admin:01 exported all records abfb117724bb
total: 3
# tamper with the log file
printf '\x00' | dd of=audit.clog bs=1 seek=50 count=1 conv=notrunc 2>/dev/null
# try to verify again
cryptlog verify
# verifying 3 entries... TAMPERED
# β chain broken at entry 0
# ! entries before 0 are valid, entry 0 and beyond are suspectOne flipped byte. Caught instantly. That's the whole point.
| Command | Description | Exit Code |
|---|---|---|
cryptlog append <message> |
Append a new entry to the log | 0 |
cryptlog verify |
Verify the entire hash chain | 0 intact, 2 tampered |
cryptlog tail [n] |
Show last n entries (default: 10) | 0 |
cryptlog range <from_ms> <to_ms> |
Show entries in a unix timestamp range (ms) | 0 |
cryptlog snapshot |
Export hash anchor for external storage | 0 |
cryptlog check-snapshot <snap> |
Verify log against a saved snapshot | 0 match, 2 mismatch |
cryptlog count |
Print total entry count | 0 |
cryptlog help |
Show usage information | 0 |
| Variable | Default | Description |
|---|---|---|
CRYPTLOG_PATH |
audit.clog |
Path to the log file |
# use a custom log file
CRYPTLOG_PATH=/var/log/myapp.clog cryptlog append "system started"
CRYPTLOG_PATH=/var/log/myapp.clog cryptlog verifycryptlog is both a CLI tool and a Rust library. Add it to your project:
cargo add cryptloguse cryptlog::Log;
fn main() -> cryptlog::Result<()> {
// open or create a new log
let mut log = Log::open("my_audit.clog")?;
// append entries (file-locked for concurrency safety)
log.append("user:42 logged in")?;
log.append("user:42 changed password")?;
// verify the chain
log.verify()?;
println!("chain intact β {} entries", log.entry_count());
// stream entries without loading everything into memory
let mut iter = log.entries()?;
while let Some(entry) = iter.next_entry()? {
println!("{}: {}", entry.timestamp, String::from_utf8_lossy(&entry.data));
}
// or load all at once (fine for smaller logs)
let all = log.read_all()?;
// anchor the current state externally
let snap = log.snapshot();
println!("anchor: {}", snap.to_hex());
// later: log.verify_snapshot(&snap) detects full rewrites
Ok(())
}| Method | Description |
|---|---|
Log::open(path) |
Open an existing log or create a new one |
log.append(data) |
Append an entry (file-locked, auto-timestamped, auto-hashed) |
log.verify() |
Walk the chain and verify every hash link (shared lock) |
log.entries() |
Streaming iterator β reads one entry at a time |
log.read_all() |
Read all entries into memory |
log.read_range(from, to) |
Read entries within a microsecond timestamp range |
log.snapshot() |
Export current state for external anchoring |
log.verify_snapshot(&snap) |
Check log against a saved snapshot |
log.entry_count() |
Number of entries in the log |
log.last_hash() |
The SHA-256 hash of the most recent entry |
cryptlog is also published to npm natively as an ultra-fast Rust binding via napi-rs.
npm install @ayush-e4/cryptlog
# or
yarn add @ayush-e4/cryptlogconst { append, verify, count, snapshot, checkSnapshot } = require("@ayush-e4/cryptlog");
const path = "audit.clog";
// 1. Append entries
append(path, "user:42 logged in via Node.js");
append(path, "admin:01 purged cache");
// 2. Verify chain integrity
try {
verify(path);
console.log(`Chain intact: ${count(path)} entries`);
} catch (err) {
console.error("Tampering detected!", err.message);
}
// 3. Output snapshot for external anchoring
const snap = snapshot(path);
console.log("Anchor:", snap);
// 4. Verify against an anchor
if (checkSnapshot(path, snap)) {
console.log("Log matches anchor!");
}Each entry is a self-contained binary record. No external index needed β just walk the file sequentially.
graph TD
classDef magic fill:#1e1e2e,color:#a6adc8,stroke:#89b4fa,stroke-width:2px;
classDef version fill:#1e1e2e,color:#a6adc8,stroke:#f38ba8,stroke-width:2px;
classDef ts fill:#1e1e2e,color:#a6adc8,stroke:#a6e3a1,stroke-width:2px;
classDef len fill:#1e1e2e,color:#a6adc8,stroke:#f9e2af,stroke-width:2px;
classDef data fill:#1e1e2e,color:#cdd6f4,stroke:#cba6f7,stroke-width:2px;
classDef hash fill:#1e1e2e,color:#f5e0dc,stroke:#fab387,stroke-width:2px;
subgraph "Entry N-1"
H1["HASH<br/>(32 bytes)"]:::hash
end
subgraph "Entry N"
M2["MAGIC<br/>'CLOG'"]:::magic -.-> V2["VER<br/>0x01"]:::version
V2 -.-> T2["TIMESTAMP<br/>(8 bytes)"]:::ts
T2 -.-> L2["LEN<br/>(4 bytes)"]:::len
L2 -.-> D2["DATA<br/>(variable)"]:::data
D2 -.-> P2["PREV_HASH<br/>(32 bytes)"]:::hash
H2["HASH: SHA-256<br/>(All fields combined)"]:::hash
P2 -.-> H2
end
H1 ==>|Hard cryptographic link| P2
style H1 stroke-dasharray: 5 5
| Field | Size | Encoding | Description |
|---|---|---|---|
MAGIC |
4 bytes | ASCII | Always CLOG β identifies the file format |
VERSION |
1 byte | uint8 | Format version (0x01) |
TIMESTAMP |
8 bytes | big-endian u64 | Microseconds since Unix epoch |
DATA_LEN |
4 bytes | big-endian u32 | Length of the data payload |
DATA |
variable | raw bytes | Your log message |
PREV_HASH |
32 bytes | raw | SHA-256 of the previous entry (zeros for first) |
HASH |
32 bytes | raw | SHA-256 of all preceding fields in this entry |
- No framing / length-prefix for the whole entry β the fixed-size header +
DATA_LENis enough to walk forward - Microsecond timestamps β more precision than milliseconds, no external dependency
- SHA-256 β battle-tested, fast, 32-byte digests keep entries compact
- Append-only by design β
open()seeks to the end, there is no update/delete API
The included example appends entries, verifies the chain, then intentionally tampers with the file to show detection:
cargo run --example demo=== appending entries ===
entries written: 4
last hash: abfb1177...
=== verifying chain ===
β chain intact β nothing tampered
=== tampering with file ===
flipped byte at offset 140
β caught tampering: ChainBroken { at_entry: 1 }
| Attack | Protection |
|---|---|
| Modify past entries | β Hash chain breaks at tampered entry |
| Reorder entries | β
prev_hash linkage makes reordering detectable |
| Truncate the log | β
snapshot + check-snapshot detects missing entries |
| Full rewrite attack | β External snapshot anchoring catches it |
| Concurrent write corruption | β
Exclusive file locking via fs2 |
| Large file memory exhaustion | β
Streaming entries() iterator reads lazily |
| Limitation | Why | Future fix |
|---|---|---|
| No entry signing | Anyone with write access can append fake entries | Ed25519 signatures per entry |
| No payload encryption | Data is stored in cleartext | Optional AES-256-GCM encryption |
| No compression | Each entry is a full write | Batching / LZ4 compression |
cryptlog is a detection tool, not a prevention tool. It tells you that tampering happened and where. It doesn't stop someone from appending new entries. For authentication, sign entries or restrict file access via OS permissions.
- SHA-256 hash chain with tamper detection
- Colored CLI with
append,verify,tail,range,count - File locking for concurrent access safety
- Streaming iterator for large files
- External snapshot anchoring (
snapshot/check-snapshot) - Comprehensive test suite (22 tests + doc-tests)
- GitHub Actions CI (check, test, clippy, fmt)
-
exportcommand β dump entries as JSON / CSV -
watchmode β tail -f style live monitoring - Ed25519 entry signing (authentication)
- Optional payload encryption (AES-256-GCM)
- Configurable hash algorithms (SHA-512, BLAKE3)
- Entry batching and compression
- Merkle tree witness proofs for individual entries
Contributions are welcome! Feel free to:
- Fork the repo
- Create a feature branch (
git checkout -b feature/awesome) - Commit your changes (
git commit -m 'Add awesome feature') - Push to the branch (
git push origin feature/awesome) - Open a Pull Request
MIT β use it however you want.
Built with π¦ Rust. If this is useful to you, consider giving it a β
