The right way

Let’s discuss several improvement points for our authentication approach.

Firstly, the main flaw of “ordinary hashes” (and even “salted ordinary hashes”) is relatively high speed of a brute-force attack (about billions of hashes per minute). To eliminate this flaw we’ve got to use a special KDF-function like PBKDF2 which is natively supported by the Android Framework. Of course, there is some difference between KDF functions and you’ll probably want to choose the other one, but it’s out of this article scope. I’ll give you several useful links about this topic at the end of the article.

Secondly, we have no user data encryption at this point. There are a lot of ways to implement it and I’ll show the simplest and the most reliable one. It’ll be a set of two libraries and some code around them.

Let’s write a PBKDF2 key creating factory to begin with.

Now armed with this factory we’ve got to refactor our savePin() and pinIsValid() methods:

Thus, we’ve just mitigated the main flaw of our previous solution. It’s good, and now we’ve got to add user data encryption. To implement it, we’ll take these libraries:

Tink — A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.

Jetpack Security — Read and write encrypted files and shared preferences by following security best practices.

To get a good encrypted storage, we’ve got to write such code:

That’s all. Later, we can work with it as if it were regular SharedPreferences , but all data will be encrypted. Now we can easily replace the previous implementation.

Let’s summarize the subtotal. We have quite a secure key derived from a PIN, and a fairly reliable approach to store it. That looks cool, but not enough. What if we assume that the attacker has got access to our device and has extracted the whole data from it. In theory, he has all components to decrypt the data at this moment. To solve this problem, we’ve got to achieve two things:

a PIN isn’t stored at all

encryption operations are based on the PIN

How can we achieve these goals without rewriting the whole code? It’s easy! Insofar as we’re using Tink, we can apply its encryption feature named as associated data.

Associated data to be authenticated, but not encrypted. Associated data is optional, so this parameter can be null. In this case the null value is equivalent to an empty (zero-length) byte array. For successful decryption the same associatedData must be provided along with the ciphertext.

That’s it! We can use a PIN as associated data to achieve our designated goals. Thus, possibility or impossibility to decrypt the user data will act as an indicator of the PIN correctness. This scheme usually works as follows:

If a user enters an incorrect PIN, you’ll receive GeneralSecurityException when trying to decrypt the access token. So, the final implementation might look like this:

Nice result! Now we are not storing the PIN anymore, and all data is encrypted by default. Of course, there are a lot of ways to improve this implementation if you want to. I’ve just shown the basic principle.