Decoding React Native creds on Android
I’ve recently been looking at the storage in different Android apps to see how they protect customer credentials within the app’s sandbox.
There are a variety of schemes around, all the way from no protection to only storing tokens in the system accounts database and everything between.
On one app, I noticed that in the shared_prefs directory the credentials had been stored encrypted, along with what looked like the decryption key. This set of the pen testers sense and I wondered how easy it would be to decrypt the data.
What I had was two files:
As stated above there were two files, RN_KEYCHAIN.xml which contained suspicious strings called “:p” and “:u”, both in base64 format:
(I’ve censored the username as it used one of my domains and I don’t really want too much spam.)
And crypto.KEY_256.xml, which, guessing from the filename has the cryptography key in it (in base64 format):
What we don’t have is the algorithm, or any idea of the data format. We can make a guess that it has a 256 bit key from the name of the key file and that when we decode the base64 string for key we end up with 32 bytes of data, i.e. 256 bits.
A Journey of Discovery
A quick Google led me to a likely looking candidate on github.
If it does use this library we can see decryption code in getGenericPasswordForOptions:
As we can see it gets the username and password from shared_prefs, looking for strings of “u:” and “p:”; base64 decodes it and then passes it to crypto.decrypt. Earlier code shows up that it initialises the crypto object:
This is looking quite close to what we want. Delving further into the code shows that the crypto references above are instantiating Crypto and CrypoCipher from Facebook’s conceal library.
Looking through the conceal code is annoying as they just refer to the bit size of the key rather than refer to the explicit algorithm, the closest hint is made in com.facebook.crypto.CryptoAlgoGcm.java, that it uses a GCM mode cipher. This requires a cipher with a block size of 128 bits, which is most likely to be AES in GCM mode.
This library also shows us that format of the encrypted text:
So, we can see that the initialisation vector (iv) is stored with the crypto text.
At this point, all we can do is attempt to try out our assumptions, which are:
- The data in RN_KEYCHAIN.xml is encrypted credentials
- The encryption key is stored in crypt.KEY_256.xml
- The app uses react-native-keychain
- The cipher is AES GCM with a 256-bit key
- The crypted text includes the IV
I normally use python to try this stuff out as I can do this on the command line without actually writing a program. It was here that I came across another issue: the standard PyCrypto library does not support AES_GCM.
Fortunately, this problem has been noticed before and there is a fork of PyCrypto, called PyCryptodome that supports AES_GCM. One quick install later and I could try out some code:
Yes, Aardvark123 was the password I’d used.
Decoding custom encryption can be quite easy if it uses standard libraries, although it may take a while to work out exactly how to do it.
From a securing that app side, the app made the error of storing the encrypted text alongside the key. This adds a second layer of complexity, but is still not out of the range of decrypting. A much better solution would be to not store the credentials, or to store the key in a different area of the device.
It is much more to not use the app’s sandbox to store credentials, if they must be stored then use operating system facilities, such as the device’s keystore (if it has one) or in the accounts database. Both of these require specific permissions to access and cannot easily be accessed by a normal interactive user.
For greater protection, avoid storing credentials at all, consider only storing an authentication token, or a one-way hash of the password.