Moin! 👋
LevelDB is everywhere — you just don't see it. If your workflow starts and ends with SQLite, you're walking past a surprising amount of data on every acquisition. Chrome, Electron-based desktop apps, iOS, Android — LevelDB shows up constantly in real cases, often unnoticed, and often holding data that other tools quietly skip over.
With the current release, the crush LevelDB viewer has grown significantly. This post walks through what it can do — and more importantly, what it means for your investigation.
What Is LevelDB — and Why Should I Care?
LevelDB is an on-disk key-value store developed by Google. Unlike SQLite, it does not
present itself as a single file with a recognisable header — instead, a LevelDB database
occupies an entire folder, containing a mix of files with .log, .ldb,
and metadata files named MANIFEST-######, CURRENT, and LOG.
If you have ever opened an acquisition and seen a folder full of files like that and moved on,
there is a good chance you left data on the table.
The forensic value of LevelDB depends on what the application chose to store there — and that varies enormously. In Chrome and Chromium-based browsers you will find browsing history fragments, sync metadata, IndexedDB entries from web applications, session state, and local storage data. Electron apps like Signal Desktop, Discord, or WhatsApp Desktop use LevelDB for app configuration, cached user data, and UI state.
On top of that, there is a structural property worth understanding: deleted records are not immediately gone. Every write to LevelDB — including deletions — is appended as a new entry with a sequence number. A deletion creates a "tombstone" entry: the key remains, but the value is empty. In .ldb files, superseded or deleted records are eventually cleaned out during compaction — but until that happens, they can still be read. In .log files, the active write log, everything is there. A tombstone without a value is not nothing: the key alone can tell you that something existed and was deliberately removed.
Alex Caithness from CCL wrote the definitive primer on this in 2020 — if you have not read it yet, it is required reading before going further:
- Hang on! That's not SQLite! Chrome, Electron and LevelDB — CCL Solutions Group (September 2020)
- IndexedDB on Chromium — CCL Solutions Group (October 2020)
The open-source Python library that came out of that research — ccl_chrome_indexeddb — is also the foundation that the crush LevelDB viewer builds on. Full credit to Alex and the CCL team for that work.
Where to Find LevelDB
LevelDB shows up more often than most people expect — and the pattern is consistent enough that you can learn to spot it without memorising paths.
On iOS and Android, look for any Chromium-based browser (Chrome, Edge, Brave) and any app built on a Chromium WebView or Electron runtime. The LevelDB stores typically live under the app's data container, in subdirectories named IndexedDB, Local Storage/leveldb, or Sync Data/LevelDB. The folder name is usually your first hint.
On desktop systems — Windows, Linux, macOS — the same logic applies, but the surface area is larger than most people assume. Every Chromium-based browser and every Electron app on the system has its own LevelDB stores. On Linux alone, a standard workstation running Chrome, Signal Desktop, Discord, and Slack will have dozens of LevelDB databases under ~/.config/. It is easy to treat LevelDB as a mobile-only concern. It is not.
The reliable indicator is always the folder structure: a directory containing a CURRENT file, one or more MANIFEST-###### files, and a mix of .log and .ldb or .sst files is a LevelDB database — regardless of what the folder itself is named.
For this post, I am using the Spotify Client App from an iPhone acquisition (Public iOS 17 Image from Josh Hickman) as the example throughout. The path to the LevelDB looks like this:
/private/var/mobile/Containers/Data/Application/E81161FB-2689-4DF6-B98B-C8F95B056440/Library/Application Support/PersistentCache/Users/7jlf2hrgerxu6n0pp8l35r5h0-user/primary.ldb/
Opening a LevelDB Database in crush
LevelDB databases don't announce themselves — there is no single file to click on.
A LevelDB database is a plain directory, typically named LevelDB (or sometimes
leveldb), containing a mix of .log, .ldb, .sst, and metadata
files. There is no special label or icon that distinguishes it from any other folder in the
file panel. The .sst (Sorted String Table) extension is functionally identical to
.ldb — it is an older naming convention still used by some non-Chrome LevelDB
implementations. crush handles both.
To open it in crush, right-click the folder in the file panel and select Open. A single left-click only selects the folder — the right-click menu is what triggers crush to read the directory structure and open the LevelDB viewer. That is the step that is easy to miss the first time.
No extraction needed — crush reads directly from inside ZIP or TAR archives, or from a folder on disk.
The Overview Tab
The first tab is the Overview. Before looking at any records, this is where I orient myself.
It shows the MANIFEST metadata for the database: the comparator in use, the last sequence
number, the log number, and the files that make up the database grouped by compaction level.
crush parses all MANIFEST-* files present in the directory — not just
the current one. The active MANIFEST is labelled (current); older ones expose
compaction history from before the last recovery, potentially referencing file numbers that
are no longer on disk. The CURRENT pointer itself is shown as a separate entry,
and where present, the prev_log_number field identifies the WAL log file that
preceded a recovery — useful context when you are trying to reconstruct the write history
of a database that has been through a crash or a clean shutdown cycle.
The last sequence number tells you how many write operations have been performed on this database across its lifetime — including updates and deletions, not just inserts. A low number (like 0 or close to it) means the database is fresh or barely touched. A high number is a signal that there has been a lot of activity — and potentially a lot of deleted data worth looking at.
The compaction level of the files tells you how mature the database is.
Fresh data lives in .log files and level-0 .ldb files;
as the database ages and compaction runs, data moves to higher levels and old or deleted
records get cleaned out. A database where everything is still at level 0 has had very
little compaction — which is good for us, because deleted records are more likely to
still be present.
The Files Tab
The Files tab gives you a per-file breakdown: filename, type
(Log or Ldb), compaction level, size in bytes, and counts of live, deleted, and unknown
records per file. For .ldb and .sst table files, the tab also
shows the Smallest Key and Largest Key boundaries sourced
directly from the MANIFEST VersionEdit entries — decoded as UTF-8 where possible, hex
otherwise. At a glance, you can see which files cover which key ranges without opening a
single record.
Files containing deleted records are colour-coded red. That is your first triage signal — if a file is red, it is worth a closer look. A file with a high deleted record count relative to its live records tells its own story.
The distinction between Live, Deleted, and Unknown is worth understanding:
- Live — the record is the current version of a key and has a value.
- Deleted — a tombstone entry. The key is present, the value is empty. Something was here.
- Unknown — records in
.ldbfiles where the state cannot be determined without reading the full compaction context. These are worth inspecting manually.
The Records Tab — Live and Deleted in Context
The Records tab shows all records from all files in a single table. This is where the examination happens.
The filter toolbar lets you narrow down to All / Live / Deleted / Unknown records. Deleted records appear inline in red, directly alongside the live data — not in a separate view. That matters: seeing what is gone in the context of what is still there is often where the interesting questions start.
Each row shows:
- The sequence number — the order in which this record was written. Lower sequence numbers are older writes.
- The offset — the byte position within the source file where this record begins, displayed in hex. This is your anchor for cross-referencing in a hex editor and for court testimony: if you need to point to exactly where in the evidence file a record lives, this is how you do it. The offset is included in CSV exports.
- A UTF-8 text preview and a hex preview of the key.
- A UTF-8 text preview and a hex preview of the value.
- The state — Live, Deleted, or Unknown.
Selecting any row feeds the full key and value bytes into a split hex pane below the table —
separate Key and Value tabs, so you can inspect each
independently without them running together. For records from .ldb or
.sst table files, a third Internal Key tab is also available,
showing the complete LevelDB internal key: the user key plus its 8-byte sequence
number and type suffix. This is the raw on-disk representation — useful when you need to
verify exactly what LevelDB stored, or when the type byte tells you something about how
the record was written.
Search and Filter
The Search box filters rows case-insensitively across all columns — key text, value text, state. It combines with the state filter buttons, so you can ask questions like "show me only deleted records whose key contains this prefix" in two clicks. For databases with thousands of records, this is how you find signal in the noise quickly.
The LOG Files
LevelDB writes its own operational log to files named LOG and LOG.old —
plain text, not to be confused with the binary .log write-ahead log files.
crush surfaces these in dedicated tabs, one per file that exists, with the full content
displayed in a monospace view and a Find toolbar for searching within the
log. This is where LevelDB records its own compaction events, file opens, and error
conditions — useful background when you are trying to understand what happened to a database
and when.
BLOB Inspector — When the Value Is Protobuf
In the LevelDB used as the example here, roughly 95% of the values are Protobuf-encoded binary data. That is typical for LevelDBs — the apps use Protobuf extensively for serialising entity data before it goes into the LevelDB store.
Right-clicking any row in the Records table offers Inspect Key…, Inspect Value…, and for table-file records, Inspect Internal Key… — all opening the shared BLOB Inspector dialog.
In Auto mode, the inspector tries to detect the format automatically.
For valid Protobuf data it will decode it and display the wire-decoded fields in
protoc --decode_raw style — field numbers, wire types, and values, rendered
as formatted text. PNG, JPEG, GIF, binary plists, XML, Android Binary XML, and JSON are
also detected automatically in Auto mode via magic bytes or content sniffing.
If auto-detection does not find a match, you can manually select from the available decode modes: Hex, UTF-8, Latin-1, Base64, Plist, XML, JSON, Protobuf (schema-less), Android Binary XML, or Image. This covers the formats you are most likely to encounter in LevelDB values across Chrome, Electron apps, and Android databases.
The inspector opens as a non-modal window — the rest of the UI stays fully interactive while it is open, and you can have multiple inspectors open at the same time. That matters when you are comparing values across records without constantly opening and closing the dialog.
The BLOB Inspector is shared across all viewers in crush — SQLite, LevelDB, Realm. Any improvement to the inspector (a new decode mode, a UI fix) benefits all three at once. If you have been following the SQLite and Realm deep dives, the workflow here is identical: right-click, inspect, decode.
Further Reading
- Hang on! That's not SQLite! Chrome, Electron and LevelDB — Alex Caithness, CCL Solutions Group (2020)
- IndexedDB on Chromium — Alex Caithness, CCL Solutions Group (2020)
- LevelDB Coding Scheme — Chromium IndexedDB documentation — Chromium Project
- ccl_chrome_indexeddb — Alex Caithness, CCL Solutions Group (open source)
Happy examining. 🐢