Skip to content

About

Read-only reader for Hive boxes, the key-value store Flutter apps write, for digital forensics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

hive-parser

Read-only reader for Hive boxes, the key-value store Flutter apps write, for digital forensics. One module, no dependencies, which is what lets the LEAPP tools vendor it as a single file.

python -m hive_parser dump BOX.hive            summary and every frame in file order
python -m hive_parser dump --live BOX.hive     only what the app itself reads back
python -m hive_parser dump --json BOX.hive     machine readable
python -m hive_parser dump --carve BOX.hive    scan past a damaged region
python -m hive_parser hex BOX.hive             annotated byte walkthrough of a frame
python -m hive_parser dump DIRECTORY/          every .hive found underneath

As a library:

from hive_parser import read_frames, live_entries, hive_entries

frames = read_frames('settings.hive')   # every frame, in file order
live = live_entries(frames)             # what the app reads back
live, superseded = hive_entries('settings.hive')

Why the default output is frames, not a dict

A box is an append-only log. Writing a key appends a frame instead of editing the old one, and a deletion appends a frame carrying the key and no value. Reading through the Hive API replays that log and returns only the final state, so a tool that prints a key/value dict throws away most of what the file holds.

The default is therefore every frame in file order, with a summary that names the gap:

Frames      11 on disk
Live keys   3   (what the app itself would read back)
Superseded  8 frame(s) hold values that were later overwritten
Deletions   0

--live gives the collapsed view when that is what you want.

A deletion and a stored null are different findings and are reported as such. read_frames sets deleted on the tombstone; live_entries drops the key, matching what the app sees, while a stored null stays with its value of None.

What it will not tell you

Whether the box was compacted. By default Hive rewrites a box once more than 60 frames have been superseded or deleted and that count exceeds 15 percent of the live entry count. An overwrite counts towards it, not just a delete, and an app can supply its own strategy or call compact() directly. The rewrite leaves no marker, so this reader reports what is present and never claims what used to be.

What a developer type's fields are called. Values at type id 32 and above are written by the app's own TypeAdapter. A HiveObject carries {field index: value} and the id, because the field names live only in the app's compiled Dart. On disk the id is the app's declared id plus 32, so 34 is @HiveType(typeId: 2) in the source.

An adapter generated by hive_generator writes a field count, then per field an index byte and a type-tagged value, so its payload describes itself and comes back as a HiveObject. A hand-written adapter calling the raw writers emits none of that, and this reader does not decode it: its layout is only in the app's Dart code, so it comes back as a HiveRaw holding the bytes as they are. HiveRaw is deliberately not a dict, so a caller that tells a record from a scalar with isinstance(value, dict) skips an undecoded blob rather than reporting it as a record.

Damaged boxes

A box has no header and no magic bytes, so recovery leans entirely on the per-frame CRC32.

By default the walk stops at the first frame whose checksum does not match, which is what tells a real frame from the tail of a partly overwritten file. --carve scans every offset for CRC-valid frames instead, which resynchronises past a damaged region and reads a fragment that does not start on a frame boundary. That is opt-in on purpose: a scan can pick up a frame from an older, overwritten generation of the box, and the sequential walk is what the writer actually produced.

One value that does not parse costs its own frame and no others. A frame truncated by the end of the file is still reported, with whatever of its key was readable, because that names the write that was in flight.

Encrypted boxes

Hive's AES-256-CBC mode encrypts only values. Keys are written in the clear, so an encrypted box still yields its key namespace, frame sizes and write ordering. The frame CRC is seeded with CRC32(SHA-256(key)), so every checksum fails without the key.

Such a box is read by following the frame chain rather than stopping at the first bad checksum, so the whole key namespace is returned, with values and type ids left as None because they are ciphertext. The test is deliberately stricter than "the first checksum failed": the chain must land exactly on the end of the file and every key must parse, so a box with one corrupt frame is reported as damaged rather than as encrypted.

This reader does not decrypt. Keys are usually recoverable from the app's own storage rather than a vault: shared_prefs on Android, NSUserDefaults or a plist on iOS, sometimes a config file beside the box.

Format

Read from isar/hive at v2.2.3, commit 470473ffc1ba39f6c90f31ababe0ee63b76b69fe: binary/frame.dart for the type vocabulary, binary/binary_reader_impl.dart for frame and value parsing, crypto/hive_aes_cipher.dart for the cipher and key CRC, box/default_compaction_strategy.dart for the thresholds, and registry/type_registry_impl.dart for the 32 offset. The set type ids 13, 14, 15 and 19 are from hive_ce at 8cb526c78b6f.

Two points worth carrying into a report:

An integer is stored as a float64. Values beyond 2^53 cannot round-trip exactly.

A plain Dart DateTime is written as type 18, with an explicit is_utc flag, not type 16. Every Hive 2.x release registers the timezone adapter first and findAdapterForValue returns on the first runtime-type match. Type 16 records no zone at all, so a type 16 value came from an older writer and must not be rendered as UTC. HiveDateTime carries zone_recorded so the two cannot be confused.

Tests

python -m unittest discover -s tests -v

The suite builds its buffers by hand, so it carries no sample data. It covers every built-in type, both DateTime adapters, BigInt, the hive_ce sets, developer types, append-only history, deletions, and the damage paths. CI runs 3.10 through 3.14, and a skipped test fails the job.

During development the reader was also checked against Hive's own conformance vectors (hive/test/generated/frames.g.dart at the commit above), which it reads cleanly, and diffed against real Flutter app boxes.

Licence

MIT.

About

Read-only reader for Hive boxes, the key-value store Flutter apps write, for digital forensics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages