A ring buffer made of audio files
The app runs as an Android foreground service of the microphone type, with a persistent notification, so Android lets it record with the screen off. Audio is encoded as 16 kHz mono AAC and written as short pieces (segments) of a couple of minutes each. When the total passes the window you chose, the oldest piece is deleted. Keeping copies a range of pieces out of the loop into protected storage.
- Crash-safe pieces. Each piece is a self-contained file, so if Android kills the app, everything up to the last piece survives and the service restarts itself. The gap is visible in the chain.
- Plays anywhere. A kept recording is joined into one standard
.m4a, and its original pieces travel with it in the evidence pack. - Scheduled windows are protected. Pieces inside a scheduled keep's window are exempt from pruning until it's kept, however long the hearing runs.
What it gets you: the moment was recorded before anyone knew it mattered, and you only spend storage on what you choose to keep.
Unchanged, made on this phone, and existed by a certain time.
Each layer stands alone and is checked independently. An attacker would have to defeat all of them, including ones run by organisations that have nothing to do with us.
A SHA-256 hash chain
Each audio piece is hashed with SHA-256, a fingerprint that changes completely if a single bit of the audio does. Each piece's link then hashes the previous link together with that fingerprint, the piece's name and its start and end times. It's the same idea that holds a blockchain together, without the blockchain.
link = sha256( prev | sha256(audio) | name | startMs | endMs | ctx ) prev 3a6a5288…ec88b audio 9f04c1d7…b2e10 link 40caf634…45096
ctx commits to the optional surroundings for that piece (see Where and who), so they're as tamper-evident as the audio.
What it gets you: nobody, including you, can cut out a sentence, swap a section or reorder the audio without it showing. Gaps from when recording stopped are visible, not hidden.
Signed in hardware, attested by Google
On first run the app creates an ECDSA P-256 key in the Android Keystore, inside the phone's trusted execution environment, or a StrongBox security chip where the phone has one. The private key can sign but can never be read out, not even by the app. Every chain link is signed with it.
The key comes with a key attestation certificate chain, rooted in Google, that describes it from the hardware's point of view:
key held in trusted execution environment made by package com.tonyandfam.legalrecorder app signed by 5d4d11cb…efb7af bootloader locked, verified boot revocation list not revoked
The verifier checks the chain against Google's attestation roots and its live revocation list, and that the attested app signature is ours.
What it gets you: the recording provably came from this app, on one particular physical phone running unmodified Android. A copy of the app on a rooted phone, or a script on a laptop, can't produce these signatures.
RFC 3161 trusted timestamps
A phone's clock can be set to anything. So every ten minutes, and on every keep, the app sends the latest chain link (only the hash) to a public timestamp authority: Sectigo, with DigiCert as a fallback. The authority signs a token saying "I saw this hash at this time", following RFC 3161, the standard used to timestamp code signatures and signed PDFs. Kept recordings get their joined recording.m4a timestamped too.
authority Sectigo Public Time Stamping Signer time 2026-10-02 22:43:53 UTC for hash 40caf634…45096 signature valid, chains to trusted root
No signal?
Requests queue on the phone (Android WorkManager) and go out as soon as there's a connection. Because a link commits to everything before it, one later stamp still covers the whole recording up to then.
What it gets you: an independent, commercial certificate authority vouches that the recording existed by a given time, so it can't have been made up afterwards. The token is signed, so it can be checked years later without contacting anyone.
An append-only Merkle-tree log
The same hashes, signed by the phone's key, also go to the LegalRecorder log. Every ten minutes the server gathers the new entries into a Merkle tree and gets the tree's root timestamped by the same authorities. The phone then fetches its Merkle path: the few sibling hashes that lead from its entry up to the root. It's the design behind Certificate Transparency, which keeps the web's HTTPS certificates honest.
What it gets you: a second, independent record of when each recording existed, that the people running it (us) can't quietly rewrite. Adding a backdated entry would change a root that's already been timestamped by someone else.
The evidence pack
Share sends two things: a playable recording.m4a, and a .zip evidence pack that carries everything needed to check it, offline, forever.
recording.m4a | The kept recording, trimmed to the range you chose. |
|---|---|
| Original pieces | The untouched audio pieces it was cut from, which the chain hashes cover. They run a little past the trimmed recording at each end. |
| Manifest | The chain: every piece's hash, times, link and the phone's signature on it, plus the key attestation. |
proof/ | The RFC 3161 tokens and the log's Merkle paths and batch timestamps. |
surroundings/ | Only if turned on, and only the fields you chose to include. The rest go in as digests. |
What it gets you: the proof travels with the recording. Email it, put it on a USB stick or file it with the court; it doesn't depend on our servers, our company or the app still existing.
Verify in the browser, or with OpenSSL
The verify page
Drop the pack on the verify page. Every check runs in your browser with its built-in Web Crypto; the recording is never uploaded. You get plain-language results, a map if location was included, and the satellite and hum analyses.
Offline, for experts
tools/verify.py needs only Python and OpenSSL, and reads trusted roots from the repository. An expert witness can audit every step.
$ python3 tools/verify.py pack.zip [PASS] 9 of 9 original audio pieces unchanged [PASS] The hash chain is intact (9 links) [PASS] Phone key attested by Google, held in the TEE [PASS] Chain timestamped by Sectigo, 22:43:53 UTC [PASS] In the LegalRecorder log, batch #2
What it gets you: nobody has to take our word for anything. The other side's expert can check your recording with their own tools.
Context that's as hard to fake as the audio.
Each is a separate switch in Settings. While recording, observations are sealed into the same chain as the audio, so they get the same signatures and timestamps.
Location, cells, Wi-Fi and Bluetooth
Every two minutes
- A GPS fix and a network fix, with accuracy, satellites used and Android's mock-location flag
- The mobile cells in range and a Wi-Fi scan
- A short Bluetooth LE scan of nearby devices
Cross-checked
A GPS fix's time comes from the satellites, which also checks the phone's clock. The verify page can compare the Wi-Fi networks and cells against BeaconDB, an open database of where networks are, through a server endpoint that keeps nothing.
Selective disclosure
Observations are sealed per audio piece. The link's ctx is a hash over one salted digest per field:
ctx = sha256( "=" sha256(salt | field | data) "\n" ... ) # fields sorted by name
When sharing you pick which fields to include. The rest go in as digests only: the chain still verifies, and you can reveal them later. The random salt stops anyone guessing a withheld field (say, which cell tower) by trying candidates against its digest.
What it gets you: you can prove where you were without giving away more than you need to, and you can't be accused of adding location afterwards: it was sealed while recording.
Recomputing the position from the signals
A location fix is only the phone's answer, and an app can fake it. So every ten minutes the app can save fifteen seconds of the GPS chip's raw inputs (Android GnssMeasurement): each satellite's signal transmit time, Doppler shift, carrier phase and strength, plus the receiver clock and gain.
- Physics checks. The verifier checks the measurements behave like real signals: distance and Doppler agree, distances are physically possible, strengths vary between satellites, receiver gain is steady.
- Independent position. It solves the position from the measurements and the satellites' published orbits and compares it with the location the phone reported. On a real Pixel 7 log, that lands within about 10 m, as does RTKLIB run on the RINEX export.
- Offline too.
node tools/gnss.mjs pack.zip --rinex out.obsexports standard RINEX for anyone's GNSS software.
What it gets you: a mock-location app can't fake this. It would take a radio transmitter that fakes every satellite at once, consistent with real orbits for that day.
Two phones vouching for each other
Each phone broadcasts a short token over Bluetooth LE, and logs the tokens it hears from other LegalRecorder phones.
token = sha256(secret)[0..16) # fresh random secret every 10 minutes phone A commits secret in its signed chain, broadcasts token phone B logs "heard token" in its own timestamped chain later A reveals secret → sha256(secret) matches B's log ✓
B's timestamped record of hearing the token, plus A's signed commitment to the secret behind it, shows the two phones were within Bluetooth range at that time. Tokens are random and change every ten minutes, so they can't be used to track anyone.
The one way to fake it is a relay: someone near A passing the token to someone near B in real time.
What it gets you: two independent recordings that corroborate each other, from two different phones with two different keys.
The hum of the power grid
Mains power hums at about 50 Hz, but the exact frequency drifts by millihertz from moment to moment, in step across a whole grid, and grid operators log it. Recordings made near anything mains-powered pick up that hum. Matching it is an established forensic technique.
"Analyse the hum" on the verify page, or node tools/enf.mjs pack.zip --reference grid.csv offline, traces the hum once a second, flags jumps, and finds where the trace matches a grid frequency log. Phone microphones often filter out 50 Hz itself, so it looks at harmonics up to the 4th too.
It only works if something mains-powered was near the phone.
What it gets you: a date and time written into the sound itself by the national grid, completely independent of the phone, the app and us.
Only hashes leave the phone
A SHA-256 hash is one-way: it reveals nothing about the audio, and the audio can't be reconstructed from it. That's all the timestamp authorities and our log ever see.
What leaves the phone
- SHA-256 hashes of the audio
- The phone key's signature on each
- Whatever you choose to share, when you share it
What never does
- The audio, unless you share a recording yourself
- Your names, tags and notes
- Calendar contents (calendar sync reads them on the phone)
- Anything to an advertiser or analytics service
Online timestamps can be turned off in Settings. Recordings then rely on the hash chain and the phone's signature alone.
What the proof shows, and what it doesn't
Evidence is only as good as the claims made about it. These are ours.
It shows
- The audio hasn't changed since it was recorded
- It existed by the time of its timestamp
- It was made on a particular phone, by this app
- Where the recording has gaps, for instance when it was stopped
It doesn't show
- Who is speaking, or what was meant
- What was said outside the part that was kept
- The exact start time beyond the phone's clock; timestamps bound it from above
- Anything from a time the recorder was off
- Whether a court will admit it. That is for the court to decide.