browserforensics
Back to all articles

Chromium LevelDB format for forensics

2026-06-21 · 4 min

LevelDB is a simple key/value store, and Chromium leans on it hard: Local Storage, Session Storage and IndexedDB all live there. Each origin or store gets its own LevelDB directory. Understand the on-disk format and you understand both how to read these artifacts and why deleted values survive long after the user thinks they are gone.

One directory per store

LevelDB is not a single file — it is a directory. Chromium creates one per artifact class, and inside IndexedDB, one per origin:

…\<profile>\Local Storage\leveldb\
…\<profile>\Session Storage\
…\<profile>\IndexedDB\<origin>.indexeddb.leveldb\

Copy the whole directory. A loose .ldb file out of context is much harder to interpret, and LevelDB itself will refuse to open an incomplete set.

The files inside

A LevelDB directory is a small, fixed cast of files:

  • CURRENT — a one-line pointer to the live MANIFEST file.
  • MANIFEST-NNNNNN — the version edit log. It records file metadata: which .ldb files exist, at which level, and their key ranges. This is the index that tells LevelDB which SSTables are actually live.
  • *.log — the write-ahead log backing the in-memory memtable. Recent writes that have not yet been flushed to an SSTable live here.
  • *.ldb / *.sst — Sorted String Tables: immutable, sorted blocks of key/value records. The bulk of the data.
  • LOCK — the process lock file (empty; do not let a live tool grab it).
  • LOG — a human-readable operational log of compactions and flushes.

To read a store correctly you follow CURRENT to the live MANIFEST, learn which SSTables are live from the MANIFEST, then merge those SSTables with the .log memtable — newest sequence number wins per key.

Records, sequence numbers and tombstones

Every record carries a sequence number and a type: a value write or a deletion tombstone. LevelDB never edits records in place. A delete is just a new record with a tombstone type and a higher sequence number; an update is a new value record. The old record stays on disk in its original SSTable until compaction rewrites that file.

This is the forensic gift. Because SSTables are immutable and compaction is lazy, old and deleted values routinely survive in older .ldb and .log files. The live merge hides them — but a raw scan over every file surfaces superseded values and the tombstones that buried them. Until compaction runs, "deleted" Local Storage and IndexedDB entries are often still right there.

Block encoding and key formats

Block data is Snappy-compressed; a per-block trailer flags whether to decompress. Once decoded, the key tells you which artifact you have:

  • Local Storage keys look like _<origin>\x00<key>. The value carries a one-byte encoding marker (UTF-16LE vs Latin-1).
  • Session Storage is namespaced through a namespace -> map -> key indirection; resolve the meta keys before reading values.
  • IndexedDB encodes object stores and indexes into the key space and is considerably more complex; values are V8 structured-clone blobs.

A raw key/value view is invaluable here: it shows you the literal bytes when the higher-level decoder cannot make sense of a fragment.

Reading it in the tool

BrowserForensics reads LevelDB stores entirely client-side — Local and Session Storage, best-effort IndexedDB, plus a raw key/value view for everything else. Nothing leaves the browser. Point it at a copied leveldb directory and it walks CURRENT, the MANIFEST and the SSTable plus .log set the way LevelDB itself would, while exposing the superseded records a live read would discard.

Acquisition notes

  • Collect the entire directory, including .log. The most recent writes may exist only in the memtable log, not yet in any .ldb.
  • Never open a store with a live, read-write tool first — it can trigger compaction and destroy the very superseded records you wanted.
  • Treat the LOG file as a timeline hint: its compaction entries tell you when older values were last at risk of being rewritten.

Further reading