12 Jun 2026 · Kamonwan Prasert

Why hashed device IDs still leak identity in Thai fintech apps

Last rainy season a wallet team arrived at the desk proud of having “anonymised” advertising identifiers with SHA-256. Counsel was not proud. Neither were we, once we saw the join table.

Padlock resting on a keyboard

Hashing feels like hygiene. You take a string you are no longer allowed to keep, run it through a one-way function, and tell the warehouse the column is now safe. That story collapses when the input space is small enough to brute-force, or when a second table still holds the preimage.

Thai Android device populations in a single fintech app are not the open internet. A mid-size wallet may see a few million advertising IDs over a year, many of them recycled through the same handset shops. If you also store install timestamps, model names, and the first four digits of a postal code, the hash is a lookup key, not a mask.

The rainy-season join

The team had stopped sending raw advertising_id to BigQuery. They still sent sha256(advertising_id) on every session, plus a support export that listed advertising_id next to ticket numbers for “fraud review.” A contractor rebuilt the preimage in an afternoon by hashing the support list. Measurement and support had become the same identity graph with extra steps.

We did not scold them for trying. We asked them to delete the hashed column from the session table and to stop using advertising identifiers as fraud keys. Fraud can key on account_id after sign-in, or on a short-lived device token that rotates and is not exported to analytics.

What we now refuse in critique

If a property is a hash of a durable device identifier, we treat it as the identifier. Salt does not save you if the salt lives in the same repo as the SDK. Pepper stored in a secrets manager still fails if the warehouse also stores enough quasi-identifiers to invert the set.

For guest funnels we want aggregate counts keyed by day and consent state, not by a stable device token. For signed-in funnels we allow account_id, provided the person can demand deletion and the export path is written down. That is the whole distinction Signal Without Identity keeps repeating.

A narrower test you can run this week

Ask someone who does not work on your app to take a week of hashed device columns plus any public or support-side identifier list. If they can re-identify more than a trivial handful of rows, the column is not anonymised. Publish the result internally. Then delete the column, not the test.

If you want the studio version of this argument, it lives in module 04 of Signal Without Identity. If you only need the inventory, SDK Hygiene Lab is shorter.