The False Allure of Hashing for Anonymization
gravitational.com
gravitational.com
At first glance there didn't seem to be a lot to go on. There was no auditing in the application itself so I focused on the nginx logs. It's amazing how clear of a picture you can create from ip addresses, user agent strings and accessed urls.
Within an hour I could say with a high degree of certainty that the story was something like:
Sales rep makes mistake with record on Friday afternoon
Monday morning - at home, late for work
Receives call from another rep re mistake
Logs in via mobile device to see the issue
Logs in via desktop to fix broken record
Arrives at work 1.5 hours later
Claims dev team had broken the record for the weekend
There's a lot of information lurking in log files (let alone insecure dbs), and that's just the tip of the iceberg of what's stored these days. I dread to think how much personal information is stored in some of the bigger CRM apps these days.Quite frankly I'm glad there's a push to start thinking about this stuff from the outset at the moment.
And even in companies with the best culture, I would expect such things to happen if the cost of a mistake is comparable to a person's yearly salary or above that.
We're seeing more of this with privacy and user data. The author very correctly points out some issues with hashing and "pure" anonymization. It's more correctly considered "pseudonymization" (which is a recommended GDPR technique [1]).
All of which is to say _it's still an improvement over nothing_ and when layered with other techniques can help protect user privacy.
1 - https://blog.varonis.com/gdpr-requirements-list-in-plain-eng...
Notably the Fukushima Nuke plant and Deepwater Horizon disasters did not have defense in depth. One failure each had a zipper effect.
(Of course, defense in depth is a concept from the military, look how medieval castles are constructed for a very visible implementation of it.)
Using crypto hashes to anonymize data is one of those mistakes I've seen several times, and wanted to draw some attention to the issue so that hopefully we can all learn from it.
Let me know if you have any questions.
This is essentially a public key crypto version of HMACs. You have a piece of data and a blinding factor, both are committed into a single point on an elliptic curve, and blinding factor is not exposed as-is (as in case with salt for hashed content), but instead committed into a public key (a point).
The resulting point preserves the algebraic structure, which allows you to sign proofs about it, but w/o leaking the blinding factor. You can even sign a proof to a "designated verifier", which makes the proof useful only to a specific party, but anyone else wouldn't be able to trust it as it could be forged by that other party.
(There can still be good reasons to apply one-way hashing to the random internal UUID, as well: for example, to provide different levels of logs access to different internal users. People who make dashboards get hashed ids, and people who debug logging get raw ids.)
The problem of entropy allowing individual user identification even with all IDs scrubbed is still very real, though, and non-trivial to undertake. One can start by wrapping the query engine with a service which checks that a certain minimum number of people are covered by a given query before returning the results. Or apply differential privacy-type transformations to the output...
In the case of teleport, I think this is a bit more difficult to achieve, because we don't necessarily have our own account database, our common commercial use case is integrated to an identity provider through SAML/OIDC, which I'm not sure would consistently offer a random id per account to use.
While there are many way's we could generate and store the username <-> random id mappings, this adds a certain amount of complexity to get right on a distributed system.
If building a system from scratch with end to end control, I do prefer the random identifier approach.
Just because you can't connect it doesn't mean nobody else can.
It's bcrypt hash is: '$2b$15$qUxzZ5ZF55lMuqiH9GMjQOHkNyee86qd2Vh2kQyF5P3U6JZJx9AEC'
I bet nobody could ever reverse this secure cryptographic hash to figure out what it could be... ;)
... account should be taken of all objective factors, such as the costs of and the amount of time required for identification, taking into consideration the available technology at the time of the processing and technological developments.
The principles of data protection should therefore not apply to anonymous information, namely information which does not relate to an identified or identifiable natural person or to personal data rendered anonymous in such a manner that the data subject is not or no longer identifiable. ...
It's sufficient if one can't reasonably reconnect the data back to the user. It doesn't need to be NSA-proof.
I don't know how you have drawn that it shouldn't be NSA-proof from this text if it literally says "in such a manner that the data subject is not or no longer identifiable."
... To determine whether a natural person is identifiable, account should be taken of all the means reasonably likely to be used ...
The parallel history of cryptography is little more than a history of overconfidence re what counters were thought to be likely, and not. Do we really need to recapitulate that?
If you use something such as AES 256, which is approved for use to encrypt 'top secret' information by the NSA, and through some miracle it turns out that we can easily decrypt such data in 5 years, then I'm pretty sure you can argue in court that you were following best practices and had no reasonable way of predicting this encryption disaster.
“And most importantly, we only collect enough data to fulfill our stated purpose. The fewer data points that we collect, the less opportunity that someone can correlate the data.”
The smaller the domain, the less anonymization works to conceal which user id did an action. However, if you think about it, identity is far more than user id. That is why real anonymity means not storing any unnecessary information from other domains. We have a technique where we use iframes to display a person’s name, friends etc. back to them based on user ids, but the enclosing domain knows only the user ids and their connections.
Additional security could be added by making a session-unique identifier (not based on user, chronological, or external context data) and only having the master lookup table for user to sessions in an elevated security environment.
But if it's not, and your adversary knows the distribution of the input data, then the protection level is pretty close to zero.
> pseudonimization strategy in context of GDPR guidelines.
i.e. Doing the minimal work to meet the guidelines. Effectiveness is incidental to that goal. Maybe I misunderstood the GP tho.
For example taking your example of motor vehicle trips off the top of my head, in order the things that can ID you are:
Driver's License
Name
Vehicle License Plate
Time, Location of trip
Trip Distance
Location of driver residence
Location of driver workplace
If you had a database of these things, you could apply some of the strategies in the article, and a few others to ensure no collisions. Driver's License: Ditch it,
hash it with private key or have a lookup table
somewhere. I'd favor ditching it.
Name: Same as DL number
Vehicle License Plate: Same as DL number
For the above 3, you really may only need a few variables that are less constrained: gender, approximate age, type of vehicle so you could just compute out to those and store only that result. Time, Location of trip: Fudge these +- random time, or +- random distance from start/finish.
Careful not to have it be a dumb random circle, Strava does this, given enough public rides I'm sure people
could figure out where I live. (maybe do this as function of population density?)
Trip Distance: Fudge +- random distance
Location of driver residence: Fudge to begin with, probably ditch if possible
Location of driver workplace: Ditto
The point is think about what you need from the dataset and deliberately mess it up so that you'd have to have the original to piece it together. Often, you don't need the exact input data, but something within a random delta of it, so just keep the stuff within a random delta.Things we do:
- Rotate passwords used to access networks/servers regularly
- 2FA all the things
- Only provide permissions to what a user needs
- Limit it to just time a user needs it
- Logging+security scanning across the backend infrastructure
- Tight monitoring of devices used to access network for patch level
- Keep front-end networking infrastructure redundant and patched
- Multiple levels of auth (vpn pw, vpn 2FA, then public/private key for each server, then 2FA for each server, etc.)
You can only do so much but you can make it so that it's harder to compromise the crown jewels.Thanks for your thoughts!
You can tweak the hash length so that whatever statistics you run out of the hashed data are meaningful (though not exact) despite the collisions, but that running a dictionary attack of plausible usernames returns an overwhelming amount of false positives.
"We are storing PII (Personally Identifiable Information) on 100 million Americans. A data leak could lead to significant material damages, from settlements to an impaired public reputation."
https://www.privitar.com/listing/k-anonymity-an-introduction
that said, bcrypt, PBKDF2, and other time/work-based hashing solutions are still very good options for this.
> Even with something like bcrypt at reasonable work factors, a database of 100,000 anonymous users would take less than a day on a single cpu core to test every bcrypted entry for the string “knisbet” and unmask my secret data.
With my understanding of bcrypt, it's an algorithm that's designed to be slow (and the implementation providing guarantee's to resist attempts to significantly speed it up), and the slowness is tuneable through a work factor. So usually you would target something like 200-500ms. Long enough to be slow if you have to make billions of guesses, but still fast enough that when someone enters their correct password they're not sitting around waiting for you're login to complete.
If my database is something like:
salt1 + alex = aaaaaaa
salt2 + ben = bbbbbbbb
salt3 + kevin = ccccccc
* with 100,000 entriesThis means if I want to find user = kevin in this database, I take the salt + username, and test if it produces a match in the database. Once I get to salt3 + kevin, and produce cccccc, and see that cccccc is in the database, I've now unmasked that user.
At 500ms, a single CPU can test 172,800 entries per day, which could easily scan the database. If it's millions, or tens of millions of users, you do need more resources, and if you want to unmask every user it will take some time, but it becomes plausible for a moderately sophisticated adversary with moderate resources.
The algorithms like bcrypt, scrypt etc are great for passwords, where depending on the password used, you need to make billions of guesses. However, if you reduce the problem space down to millions or thousands of guesses, because you already know the username, the email address, or the date of birth because this is not secret information but public information, these algorithms being slow helps significantly, but not enough to work alone.
If you change the semantics and make the salt a secret that is stored separately, it does make this difficult to attack, but the advice I was given is it would be better to use hmac, which is already designed to work this way based on storing a secret.
This is essentially how HMAC works (HMAC is mentioned in the article). It's generally considered safer to use a 'real' HMAC algorithm instead of rolling your own.
https://en.wikipedia.org/wiki/Pepper_(cryptography)
If you read the article above, you'll see that you still need a salt, since users with very simple passwords will have the same hash: crack one, and you can crack the others for free.
A good rule of thumb with these things is to assume that if there's any sort of indirect link between some person and that server (even if it involves multiple hops across security boundaries - e.g a web request invoking a backend service querying a database that accesses the hash from a stored procedure), it can potentially be compromised. You never know when another Meltdown happens, and what it'll look like.
I find the distinction between information and exformation revealing: Information is the bits we gleaned from the data, exformation is the bits we discarded while reducing the data. The efficacy of an information processing system is in how much it discards while extracting the information we need. The expensive operation is not the recording but the forgetting.
If you want to protect data from being stolen, distill it as soon as possible into the information you need. And destroy the rest. It comes down to the value of being able to re-run the analysis versus the effort to guard the data.
It MAY be more ethically permissible to degrade the context and preserve only the most valuable and least personally identifying data. (Such as saving only the actual search query and a local timestamp, but filtering out anything related to a recognized name that isn't famous)
You probably have a better chance of creating your own secure block cipher than of achieving this goal. In a similar way, your inability to see what's wrong with your scheme is not evidence that it works.
I don't like to be negative, and I'm all for continued research, but at this point the conservative thing to do with data that you need to "anonymize" is delete it.
~~People just aren't the unique snowflakes our mothers told us we are.~~ Most people for example can be uniquely (and easily) identified with just a DOB, first name, and suburb.
Edit: maybe the problem is actually that we are too unique :)
This is so we can evaluate CDN performance and also see how well ISPs are doing in serving content to the user. So it's essentially asking questions about network performance rather than at a macro level of individual users.
As far as IPs are concerned we don't care much after that, other than maybe the odd "how many unique IP addresses were served today" type queries.
We've talked about doing the secret/salt that is rotated periodically, but to be safe you would definitely need to ensure previous salts are destroyed, and not even let people view them or access them when they are live.
Don’t get me wrong, this does make it significantly harder
to attack a leaked database to unmask every user, but the
resources required to do so or target specific users are
within the reach of many adversaries.
I don't see how it's more feasible to reverse hash(known_user+salt) than it is to dereference hash(salt), and even state level actors can't do anything but attempt to brute-force hash(salt). IOW without more behind the author's assertion, I don't buy it that adding more data to the data you want to protect is insufficient protection, even against known targets.What I didn't like was the continual reference to AWS as if it is the only provider available, without qualifying whether it is specifically an AWS product that solves the problem or whether it is an example of using a cloud service to transfer the risk. There are many alternatives to AWS load balancers and Key Management systems, so the advice is tainted sigh
1. Outsiders can't determine an arbitrary UUID, even if they know the original user-details.
2. You can easily destroy a relationship (to limit correlation or to comply with laws like GDPR) by erasing the corresponding row in the lookup table.
3. Insiders can't directly go backwards from UUID to real-name, due to the hashing step. They would need to generate hashes for all the users, and hope that matches still exist in the lookup table.
Passwords are stored as salted hashes for these obvious reasons...
So salts definitely do help. And if you chose your salt well (e.g. global fixed/rotating plus local/temporal) you significantly increase your protection compared to not using a salt at all.
What would make this article great is general ideas on what is a good way to anonymize data. I'm surprised that info is missing, actually.
What would make it world class great is discussion about GDPR ramifications, keeping in mind that one need not necessarily be perfect for GDPR, even if you're FB/Google.
I was trying to avoid the general ideas on what is a good way to anonymize data, because I don't think there are general rules that apply, and I'm not in a position to give authoritative advice on this. The more I dug in, the more I realized this is probably one of the hardest technical problems that exists right now, and there isn't yet a right answer that works (like use scrypt for passwords).
As for GDPR, I think digging into this in more detail would be a great follow up.
If we are wanting information to be readable by some people in some circumstances, that's not anonymisation: that's data protection and an entirely different problem.
You can also truncate the hash after the HMAC to mix the data of different users. It still would be useful for aggregate analytics, abuse protection, rate limiting, etc, but if each user shares an identifier with many others it would be harder to unmask them and make correlations.
How do you anonymously map your UUID to the anonymised signifier? Such that you cannot back it out yourself?
The properties that ensure this make the UUID useless.
Even better, just replace your data with H(randomBytes(16)). Or a random UUID.