Comparing 4-6 digits from the user with the correct value on storage, how can that be hard? It’s just numbers? Indeed, comparing entered digits with the stored ones is very easy, but security-wise far from trivial.
Let’s define the problem first: we assume a verification system (e.g. a remote server) that somehow needs to make sure that a user entered the correct PIN, before performing a certain action. Sending the PIN to the verification system (even over a secure connection) and then comparing it with a stored one is a security nightmare. First of all, the verification system will see both the entered PIN and the reference PIN. It will also somehow need to store the reference PIN.
So, what does it mean to verify a PIN securely? Ideally only users know their PINs. This implies that a PIN cannot be learned by observing PIN verification and also that the parties involved in PIN verification cannot learn the PIN. As such, one can be reasonably sure that it was you that entered your PIN. There is of course no stopping someone from entering a PIN as a real user would. To mitigate this threat, rate limiting controls prevent brute-forcing (trying all possible PIN codes until hitting the right one). Typical examples of such controls are limiting the number of consecutive failed attempts, and having an exponential back off after each failed attempt.
We’ll explore the two facets of the problem: storing the PIN and sending the PIN to the verification system, and explain why some classical solutions don’t work. Ideally, a solution should avoid storing or processing PIN codes in plain at the verification system. The less the verification system (and thus an attacker breaking into it) can learn, the better.
Storing the reference PIN
An immediate reflex when dealing with PIN codes is grasping for password solutions. So, what about password hashing to protect PIN codes stored on the server? Salted hashing is a popular option for protecting stored passwords. It performs a one-way operation, making it hard(er) to recover the original password from the stored hash. For PIN codes this mechanism does not work, there are simply too few possible values for the PIN, so you can try them all in a split-second. PIN codes are – by definition – extremely weak passwords.
An alternative for storing the PIN codes in plain form is by making sure they are only in plain inside secure hardware. Hardware security modules (HSM) are expensive and tedious to operate.
Learning the PIN
Somehow it seems obvious that you need to send the entered PIN to the server, in order to check it. You can use secure channels (such as TLS) to protect it from eavesdroppers. Be sure to take all additional measures such as for example certificate pinning to avoid Man-in-the-Middle attacks. However, even with these additional measures, the server will still see the entered PIN in plain.
To avoid exposing the PIN to the server and at the same time avoiding storing the PIN on the server, a popular escape route is using the PIN to locally encrypt some data. Instead of using the PIN code, that stored data is used for authentication to the server, after decryption based on the PIN code. When following this route, one must be careful not to open up the possibility of local brute-forcing. If the data is not random (i.e., structured in some way) or an authenticated encryption scheme is used (e.g., AES-GCM), one can try all PINs on the locally stored file to check which one is able to correctly decrypt the data. Afterwards, that data can be used for further authentication and the PIN is not required anymore. On top of that, choosing the correct cryptographic algorithms and implementations, taking care of unexpected properties, is a tedious task. Even the slightest error will create a security disaster.
Let’s use some fancy cryptographic protocol we found online! But are you sure it can be applied to PIN codes? We have seen cases where password-authenticated key agreement was being used with PIN codes instead of passwords. Only problem: they were never designed to deal with PIN codes – extremely weak passwords as you know. If you do not properly understand what these protocols actually do and under what assumptions they are secure, don’t shoehorn them to do what you think they might be able to do.
Finally, it should be impossible for someone in control of the server (attacker or insider) to perform a PIN verification for a given user, without that user’s cooperation. If not, this leaves the server open to brute-force attacks. Even when an HSM is used and rate limiting is enforced inside the HSM, it should not be possible to perform PIN verification without cooperation of the user as otherwise one could try up to the maximum number of attempts for each user, leading to a certain percentage of users’ PINs being compromised.
How can nextAuth help?
Next to our classical authentication solution (which is the better option if you want to do mobile authentication), we also offer separate modules, through a mini-SDK, implementing part of our core functionalities. One of these modules is the PIN verification module. It allows you to securely verify a PIN code inside your mobile app with a server: without the server seeing the entered PIN, without the server storing the reference PIN and without the vulnerability of local brute-forcing on the client or the server! And it also does not require expensive HSMs.