Skip to content

fix(obliterate): scrub content-store blobs and log entries for the path (P0-1) - #141

Merged
hyperpolymath merged 2 commits into
mainfrom
fix/obliterate-scrubs-cas
Oct 2, 2026
Merged

hyperpolymath merged 2 commits into
mainfrom
fix/obliterate-scrubs-cas

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

ULTRAPLAN P0-1: jk obliterate now removes the content history, not just the working file.

Defect (0613e69)

obliterate_file (obliteration.rs:352) shredded only the working file. cmd_obliterate never opened the JanusKey store. Every prior version therefore survived as a gzip blob in the content store, and the log entries kept naming the path, so jk undo could bring the "obliterated" content back. The code knew this: TODO(product) at :349.

Change

  • reversible-core/metadata.rs:

    • purge_path removes every log entry whose path or path_secondary matches, saves, and returns the hashes those entries referenced.
    • purge_path_entries does the same but returns the removed entries.
    • referenced_hashes() returns the refcount set.
    • The OperationLog doc now names its two non-append exceptions, prune and obliteration.
  • januskey-cli/obliteration.rs: obliterate_path works in four steps:

    1. Computes the blobs that only this path references (its hashes minus every surviving entry's hashes).
    2. Shreds each blob through the existing ObliterationManager::obliterate. Shared blobs are kept.
    3. Shreds the working file if present.
    4. Purges the log.

    Blobs go before the log, so a failed shred leaves the entries in place and the command can be re-run. Stored path spellings are normalised against the store root, which still works after the file has been deleted. Every shred is recorded in .januskey/obliterations.json. The TODO(product) is removed.

  • main.rs: cmd_obliterate uses obliterate_path when .januskey/ is initialised and keeps the old behaviour otherwise. It now exits non-zero if any path fails; before, it always returned 0.

Evidence

cargo test --workspace: 98 → 106 passed, 0 failed. reversible-core unit tests went 12 → 15, plus the new tests/obliteration_cas_test.rs with 5 tests.

The main test has a positive control. Blobs are gzip-compressed, so a naive byte-grep would pass vacuously.

  • Before obliterating: every recorded hash's content_path exists, retrieve() returns the expected V0 and V1 plaintext, and a decompressing walk of .januskey/ finds both markers.
  • After:
    • the blobs are absent and retrieve errs;
    • 0 log entries name the path;
    • the decompressing walk finds no V0, V1 or V2 marker, and not the filename;
    • undo returns Err.

The other four tests:

  • Shared blob: b's identical-bytes blob survives and b's undo still restores it.
  • Delete then obliterate: history is scrubbed even though the working file is already gone.
  • Untracked absent path: returns Err.
  • CLI (assert_cmd): jk init, modify, obliterate, then jk undo prints "Nothing to undo", with the marker walk before and after.

Mutants, each applied and then reverted:

Mutant Tests failed
Skip the blob shred 4 of 5
Ignore blob sharing only the shared-blob test
Skip the log purge 4 of 5

Clippy warnings in the touched files are unchanged (main 19, obliteration 4, metadata 1). The new test file has 0. rustfmt is clean on all touched files.

Residual traces, stated rather than hidden

  • Hash record: obliterations.json keeps each shredded blob's sha256. For low-entropy content, someone could confirm a guess against it. This is a candidate for J2-3: record a keyed MAC instead of the bare hash.
  • Log rewrite: metadata.json is rewritten with fs::write, so its old bytes, which include the path, may stay in freed blocks. This is the same copy-on-write/SSD caveat the file header already gives.
  • Transactions: transactions/ keeps operation IDs that now point to nothing. They are IDs only, with no content.
  • Audit log: recording into the keyed, HMAC-chained AuditLog needs an unlocked attestation key, so it is deferred to J2-3.
  • Cost: normalise_path makes one canonicalize call per log entry in each of about four passes, so roughly 40k calls at max_history=10000. Correct, but optimisable later.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VxcAoyMQe7CjQwCKL18Mm4

`jk obliterate` only shredded the working file; every prior version stayed
recoverable from .januskey/content (gzip blobs) and the operation log, and
`jk undo` could resurrect it.

- reversible-core MetadataStore: add purge_path / purge_path_entries
  (remove entries whose path or path_secondary matches, persist, return the
  referenced hashes) and referenced_hashes over surviving entries.
- obliteration: add obliterate_path(jk, manager, path, ..) which shreds the
  working file if present, shreds every blob referenced only by the path's
  entries (blobs deduplicated with another path are kept), then purges the
  entries. Blobs are shredded before the log is purged so a failed shred
  leaves the command re-runnable. Stored paths are matched after
  normalisation (relative to root, parent canonicalised). Each shred is
  recorded in .januskey/obliterations.json via the new
  ObliterationManager::record_proof; chaining into the keyed AuditLog is
  deferred to J2-3. Removes the TODO(product).
- jk obliterate: use obliterate_path when the working dir has a
  .januskey/ store (also handles delete-then-obliterate, where the working
  file is already gone); exit non-zero if any path failed.
- tests/obliteration_cas_test.rs: positive-controlled checks that blobs,
  log entries, decompressed plaintext and undo are gone; shared blob kept
  and other path still undoable; delete-then-obliterate; CLI end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a5a62d59-0b6b-427b-90c4-aec2a7b53f65

📥 Commits

Reviewing files that changed from the base of the PR and between 3f4545a and 135d2c4.

📒 Files selected for processing (4)
  • crates/januskey-cli/src/main.rs
  • crates/januskey-cli/src/obliteration.rs
  • crates/januskey-cli/tests/obliteration_cas_test.rs
  • crates/reversible-core/src/metadata.rs
 _________________________________________
< Code review, I will. Find bugs, I must. >
 -----------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 2, 2026 12:14
@hyperpolymath
hyperpolymath disabled auto-merge October 2, 2026 12:14
@hyperpolymath
hyperpolymath merged commit faac22e into main Oct 2, 2026
23 of 38 checks passed
@hyperpolymath
hyperpolymath deleted the fix/obliterate-scrubs-cas branch October 2, 2026 12:14
hyperpolymath added a commit that referenced this pull request Oct 2, 2026
Unblocks the **required** `scan / gitleaks` check, which is red on
`main` (0613e69) and therefore on every open januskey PR, including
ULTRAPLAN P0-1 (#141) and P0-2 (#140).

## Cause
The estate secret scanner mirrors `.adoc` files, which default gitleaks
never scans. Its `generic-api-key` rule matched two **example** UUIDs in
a JSON sample in `docs/security/KEY_LIFECYCLE.adoc`:
- :487 `"key_id": "123e4567-e89b-12d3-a456-426614174000"`
- :488 `"new_key_id": "789e0123-e89b-12d3-a456-426614174000"`

These are documentation placeholders, not secrets.

## Change
Both values are replaced with obvious low-entropy placeholders
(`00000000-0000-4000-8000-000000000001` and `…0002`). One file, 2 lines
changed.

## Evidence
Run locally with gitleaks and the estate baseline
`standards:config/gitleaks/estate-baseline.toml` (origin/main):

| Input | Result |
|---|---|
| original file (positive control) | `leaks found: 2`, generic-api-key
at lines 487 and 488, the same two lines CI reports |
| edited file | `no leaks found` |
| all `.adoc` files in the repo, mirrored | `no leaks found` |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VxcAoyMQe7CjQwCKL18Mm4

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant