Hi% username%. I, regardless of the topic of the report, are constantly asked at conferences the same question - “how to safely store tokens on the user's device?”. I usually try to answer, but time does not allow to fully reveal the topic. With this article I want to completely close this question.
I analyzed a dozen applications to see how they work with tokens. All applications analyzed by me processed critical data and allowed to set a pin-code on an input as additional protection. Let's look at the most common mistakes:
- Sending a PIN code to the API along with RefreshToken to confirm authentication and receive new tokens. - Bad, RefreshToken is unprotected in the local storage, with physical access to the device or backup, you can remove it, as well, malware can do it.
- Save the PIN code to the stack along with RefreshToken, then local pin code verification and send the RefreshToken to the API. - Nightmare, RefreshToken lies unprotected with a pin, which allows them to be extracted, in addition there is another vector suggesting bypass local authentication.
- Unsuccessful encryption RefreshToken pin code that allows you to recover from the ciphertext pin code and RefreshToken. - A special case of a previous error, operated a little more complicated. But we note that this is the right way.
Looking at the frequent errors, you can proceed to thinking through the logic of safe storage of tokens in your application. You should start with the main assets related to authentication / authorization during the operation of the application and put forward some requirements to them:
Credentials - (login + password) - used to authenticate the user to the system.
+ the password is never stored on the device and must be immediately cleared from RAM after being sent to the API
+ are not passed by the GET method in the query parameters of the HTTP request, instead POST requests are used
+ keyboard cache is disabled for password text fields
+ Clipboard is deactivated for text fields that contain a password.
+ The password is not disclosed through the user interface (they use asterisks), the password is also not included in the screenshots
')
AccessToken - used to confirm user authorization.
+ never stored in long-term memory and stored only in RAM
+ are not passed by the GET method in the query parameters of the HTTP request, instead POST requests are used
RefreshToken - used to get the new AccessToken + RefreshToken bundle.
+ is not stored in any form in RAM and should be immediately removed from it after it is received from the API and stored in long-term memory or after it is received from long-term memory and used
+ stored only in encrypted form in long-term memory
+ pin is encrypted using magic and certain rules (the rules will be described below), those if the pin has not been set, then we do not save at all
+ are not passed by the GET method in the query parameters of the HTTP request, instead POST requests are used
PIN - (usually 4 or 6 digit number) - used to encrypt / decrypt RefreshToken.
+ It is never stored anywhere on the device and should be immediately cleaned from RAM after use.
+ never leaves the application limits, those are not transmitted anywhere
+ is used only for encryption / decryption. RefreshToken
OTP is a one-time code for 2FA.
+ OTP is never stored on the device and must be immediately cleaned from RAM after being sent to the API
+ are not passed by the GET method in the query parameters of the HTTP request, instead POST requests are used
+ keyboard cache is disabled for text processing OTP
+ Clipboard is deactivated for text fields that contain OTP
+ OTP does not get into screenshots
+ application removes OTP from the screen when it goes to the background
We now turn to cryptography
magic . The main requirement is that under no circumstances should you allow the implementation of such a RefreshToken encryption mechanism, under which you can validate the result of decryption locally. That is, if the attacker took possession of the ciphertext, he should not be able to pick up the key. The only validator should be an API. This is the only way to restrict key selection attempts and tokens in the event of a Brute-Force attack.
I will give a clear example, let's say we want to encrypt the UUID
aec27f0f-b8a3-43cb-b076-e075a095abfe
such a set of AES / CBC / PKCS5Padding, using the PIN as a key. It seems the algorithm is good, everything is according to the guidelines, but there is a key point here - the key contains very little entropy. Let's see what this leads to:
- Padding - since our token is 36 bytes, and AES is a block encryption mode with a 128-bit block, the algorithm needs to finish the token up to 48 bytes (which is a multiple of 128 bits). In our version, the tail will be added according to the PKCS5Padding standard, i.e. the value of each byte added equals the number of bytes added
01
02 02
03 03 03
04 04 04 04
05 05 05 05 05
06 06 06 06 06 06
etc.
Our last block will look something like this:
... | | 61 62 66 65 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C |
And there is a problem, looking at this padding, we can filter, decrypted with the wrong key, the data (according to the invalid last block) and, thus, determine the valid RefreshToken from the twisted heap. - Predicable token format - even if we make our token multiple of 128 bits (for example, removing hyphens), in order to avoid padding, we will come across the following problem. The problem is that we can collect all the strings from the same heap and determine which of them falls under the UUID format. The UUID in its canonical text form is 32 digits in hexadecimal format separated by a hyphen into 5 groups 8-4-4-4-12
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
where M is a version and N is an option. All this is enough to eliminate tokens decrypted with the wrong key, leaving the appropriate UUID RefreshToken format.
Given all the above, you can go to the implementation, I chose a simple option to generate 64 random bytes and wrap them in base64:
public String createRefreshToken() { byte[] refreshToken = new byte[64]; final SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(refreshToken); return Base64.getUrlEncoder().withoutPadding() .encodeToString(refreshToken); }
Here is an example of such a token:
YmI8rF9pwB1KjJAZKY9JzqsCu3kFz4xt4GkRCzXS9-FS_kbN3-CF9RGiRuuGqwqMo-VxFDhgQNmgjlQFD2GvbA
Now let's see how it looks algorithmically (on Android and iOS, the algorithm will be the same):
private static final String ALGORITHM = "AES"; private static final String CIPHER_SUITE = "AES/CBC/NoPadding"; private static final int AES_KEY_SIZE = 16; private static final int AES_BLOCK_SIZE = 16; public String encryptToken(String token, String pin) { decodedToken = decodeToken(token);
Which lines you should pay attention to:
private static final String CIPHER_SUITE = "AES/CBC/NoPadding";
No padding, well, you remember.
decodedToken = decodeToken(token);
You can't just take and encrypt a token in the base64 view, because this view has a certain format (well, you remember).
byte[] key = kdf.deriveKey(rawPin, salt, AES_KEY_SIZE);
At the output, we obtain a key of AES_KEY_SIZE size, suitable for the AES algorithm. As kdf, you can use any key derivation function recommended by Argon2, SHA-3, Scrypt in case of bad life of pbkdf2 (it is very well paralleled on FPGA).
The final encrypted token can be safely stored on the device and not worry that someone can steal it, whether it is malvar or a subject not burdened by moral principles.
Some more recommendations:
- Exclude tokens from backups.
- On iOS, store the token in keychain with the kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly attribute.
- Do not scatter assets as reviewed in this article (key, pin, password, etc.) throughout the application.
- Erase assets as they become unnecessary, do not keep them in memory longer than necessary.
- Use SecureRandom on Android and SecRandomCopyBytes on iOS to generate random bytes in a cryptographic context.
We considered a number of pitfalls in the storage of tokens, which, in my opinion, every person should be aware of when developing applications that work with critical data. This topic, in which you can get confused at any step, if you have questions, ask them in the comments. Also welcome comments on the text.
References:
CWE-311: Missing Encryption of Sensitive DataCWE-327: Use of a Broken or Risky Cryptographic AlgorithmCWE-327: CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)CWE-598: Information Exposure Through Query Strings in GET Request