A Bullet-Proof Approach to Storing Sensitive User Data in React Native
Keeping data on mobile devices secure is crucial if you’re working on a product that stores sensitive information about your user.
But did you know that by default, the local storage mechanism in React Native (AsyncStorage) is unencrypted?
On iOS, the main storage library for React Native is based on the Apple File System, using FileManager to read and store files on the device unencrypted.
On Android, AsyncStorage
uses either the key-value store RocksDB or SQLite, based on which one is available. The data in there is unencrypted.
TL;DR — just show me the code!
The repository is available here. Feel free to fork and raise issues:
https://github.com/marcelkalveram/react-native-bulletproof-redux-provider
A hybrid approach to safely store user data
Of course, there is a way on both platforms to store data more securely: Keychain and Keystore, both abstracted away and nicely encapsulated by the powerful library react-native-keychain.
But none of them is made for storing huge chunks of data. Here are two bits of context from their respective documentation:
Use Keychain to securely store small chunks of data on behalf of the user.
The Keystore lets you store cryptographic keys in a container to make it more difficult to extract from the device.
As you can see, not a place where you’d store huge blobs of sensitive data.
With that in mind, how would we go about combining Keychain/Keystore and AsyncStorage
to create a hybrid that benefits from the best of both worlds: lots of storage space and ultra-secure encryption?
Here’s a high-level idea. We’ll go into the implementation details later:
- Create a secure key using
react-native-keychain
. That’s the key we’ll use to encrypt our data. 🔑 - Turn our store data into an easily encrypt-able format, ie. using
JSON.stringify()
. - Encrypt our data using the generated secure key 🔐
- Write the encrypted blob of data to our unencrypted store.
This turns your unencrypted AsyncStorage
into a safe vault that can only be unlocked by the person owning that key. In most cases and unless someone got their phone stolen and compromised that’s the primary user of the device.
Adding Redux to the mix
While this approach can be applied to any storage solution, I’m going to explain how this can be achieved using Redux, Redux Persist and Redux Persist’s “Transform Encrypt” plugin.
Leveraging redux-persist
With redux-persist
, implementing this encryption flow gets a whole lot simpler because the encryption/decryption mechanism can be easily plugged in via the redux-persist-transform-encrypt plugin.
The crux here is to not use any randomly generated static key (defined at build time) and could potentially be reverse-engineered and accessed by intruders. Instead, we use a random key that:
- we generate at runtime, when the app gets launched for the first time.
- we store in our secure
Keystore
/Keychain
vault. - we reuse for every subsequent use of our app.
The fact that the secure random key has a high degree of randomness, is not available at build time and can’t be easily guessed or recreated makes the encrypted data in your AsyncStorage
much safer, and completely useless to anyone without it.
Putting it into practice
It’s easier to understand this flow by visualising it at a slightly higher level of abstraction. I’ve split this process up into two “Gate” components, an idea I shamelessly stole from redux-persist
(which contains a so-called PersistGate,
which we’re also going to talk about briefly later).
The “Gate” here implies that the component won’t render its children before it hasn’t completed its internal state logic.
Here’s what each of them does:
EncryptionGate
: takes care of retrieving the secure key- includes a key generator to generate and persist the key on the device. In our example, this is based on
react-native-keychain
.
- includes a key generator to generate and persist the key on the device. In our example, this is based on
StoreGate
: configures the store, using the secure key for encryption- includes a store generator, similar to a
configureStore
method for Redux. In our case, we’re usingredux-persist
for persistence.
- includes a store generator, similar to a
Without further ado, let’s look at the implementation details:
<EncryptionGate />
The first component in our encryption flow completes the following tasks:
- retrieves encryption key using a key generator
- generates an object with two props: an
isFresh
flag and the securekey
. - passes this object into its children, in our case the
StoreGate
.
Here’s the corresponding React code:
export const EncryptionGate = ({children}) => {
const [encryptionKey, setEncryptionKey] = useState({
isFresh: false,
key: null,
});
useEffect(() =>
(async () => {
const {isFresh, key} = await **getEncryptionKey**();
setEncryptionKey({isFresh, key});
})(),
[]);
if (!encryptionKey.key) {
return null;
}
return children(encryptionKey);
};
You may have noticed there’s a new function call in here: getEncryptionKey()
. So what’s that?
This is the place where we generate the persistent secure key on the user’s phone. It means that once it’s been generated, this method will return the same key every time until the user deletes the app or resets the phone.
Before we move on to its implementation details, let’s have a look at how this function fits in to the overall encryption flow.
Here’s the implementation of the getEncryptionKey
method:
import * as Keychain from 'react-native-keychain';
import {generateSecureRandom} from 'react-native-securerandom';
import binaryToBase64 from 'react-native/Libraries/Utilities/binaryToBase64';
// Unique non-sensitive ID which we use to save the store passwordconst ENCRYPTIONKEY = 'UNIQUEID';
export const getEncryptionKey = async () => {
// check for existing credentials
const existingCredentials = await Keychain.getGenericPassword();
if (existingCredentials) {
return { isFresh: false, key: existingCredentials.password };
}
// generate new credentials based on random string
const randomBytes = await generateSecureRandom(32);
const randomBytesString = binaryToBase64(randomBytes);
const hasSetCredentials = await **Keychain**.setGenericPassword(ENCRYPTIONKEY, randomBytesString);
if (hasSetCredentials) {
return { isFresh: true, key: randomBytesString };
}
};
The most interesting bits here are the calls to Keychain
to get an already existing password, and to set a new one if it doesn’t.
Before setting the new password though, we have to create it first. This is where two new function calls come in that we haven’t covered yet:
generateSecureRandom
: generates a secure random key of 32 bytes.binaryToBase64
: converts these bytes to a base64 string. This is our new, cryptographically-secure password. 🎊
The function getEncryptionKey
then returns an object with two keys: isFresh
and key
. This is identical to the property that EncryptionGate
passes on to the next component, the StoreGate
. Let’s look at that next.
<StoreGate />
This component has the following objectives:
- it takes an object and uses its
key
prop for encryption. - it generates the store using a
generateStore
function. - when it finishes, it passes the
store
andpersistor
into its children.
We also include one extra check here:
If the key
turns out to be fresh and there’s already data in our store, we have a problem: the data in our store can not be decrypted anymore, since a newly generated key won’t match the previous one.
export const StoreGate = ({encryptionKey, children}) => {
const [hasData, setHasData] = useState(false);
useEffect(() => (async () {
setHasData(await AsyncStorage.getItem(storageKey));
})(),
[]);
// hasData hasn't been set, so don't return anything if (hasData === false) {
return null;
}
// if the encryption key is fresh, we need to flush AsyncStorage
if (encryptionKey.isFresh && hasData !== null) {
clearStore();
}
return children(generateStore(encryptionKey, hasData));
};
We need to take a closer look at generateStore()
because that’s the place where the actual Redux store configuration takes place, including the encryption transform which makes all this magic possible ✨.
It’s very similar to what a configureStore()
call does in many applications, only that it accepts and returns some additional parameters: encryptionKey
, hasData
go in. And an object with two props (store
, persistor
) comes out.
Let’s take a look under the hood:
export const configureStore = (encryptionKey, hasData) => {
const encryptionTransform = createEncryptor({secretKey: encryptionKey});
const config = {
key: 'root',
storage: AsyncStorage,
transforms: [encryptionTransform],
});
const persistedReducer = persistReducer(config, rootReducer);
const store = createStore(
persistedReducer,
initialState
);
const persistor = persistStore(store);
return { store, persistor };
};
We’re setting up the encryption using the createEncryptor
function from redux-persist-transform-encrypt
and pass that into the transforms field of the config
object. That’s the configuration we’ll use for the persisted reducer.
That persistedReducer
gets passed into Redux’s createStore
, and the resulting store gets passed back into the persistStore
call of redux-persist
.
The end result is a persisted store
object that’ll automatically sync our reducer with the encrypted AsyncStorage
, and a persistor
object that allows us to run additional operations on the store
, such as purge
, flush
or pause.
Sounds complicated? Let’s take a step back again and look at how all of this ties in to the overall component structure:
<EncryptionGate>
{(encryptionKey) => (
<StoreGate encryptionKey={encryptionKey}>
{(store, persistor) => (
...missing store logic goes here...
)}
</StoreGate>
)}
</EncryptionGate>
Putting everything together
We’re almost there. The last piece in our module is the actual context provider that wraps our store and makes it available to the rest of the app. Let’s take a look at the components we have so far and how everything fits together:
<EncryptionGate>
{(encryptionKey) => (
<StoreGate encryptionKey={encryptionKey}>
{(store, persistor) => (
<ReduxProvider store={store}>
<PersistGate
persistor={persistor}
loading={null}
onBeforeLift={onBeforeLift(store)}
>
{children}
</PersistGate>
</ReduxProvider>
)}
</StoreGate>
)}
</EncryptionGate>
I usually call this file BootstrapPersistence
to encapsulate all the store-related logic and then include it in the Bootstrap
, App
or index.js
.
You may have noticed that there’s yet another new piece in our component cascade: PersistGate
. It’s part of redux-persist
and allows you to run additional code before lifting the curtains for the the rest of your app’s code. You may remember that this is where I took inspiration from for those other Gate
components.
You can now wrap the rest of your app inside this scaffold and be confident that all your reducers are properly and securely persisted on the device.
Aren’t we reinventing the wheel here?
Of course, this kind of persistence is very specific to the kind of apps that require a very high level of security. And unless you’re working in a clinical environment, aviation or nuclear energy (anyone?), you may as well get away with much simpler solutions.
- redux-persist-encrypted-async-storage: a simple encrypted wrapper around
AsyncStorage
without any Redux dependencies. Be aware though that it’s usinguuidv4
which is slightly less secure than the library we’re using above,react-native-securerandom
. - react-native-secure-storage: another wrapper for
AsyncStorage
which uses a native implementation ofreact-native-keychain
as its primary storage mechanism, so you get some performance gains, but the amount of data you can store is limited to that of KeyStore/Keychain. This library also has a plugin for redux-persist. - expo-secure-store: the Expo-based version of an encrypted store, based on the same principles as
react-native-secure-storage
. As mentioned in there docs as well, size limit for a value is 2048 bytes.
As you can see there are two limitations in those alternative libraries: you either don’t have a totally secure hashing algorithm (uuidv4
should be ok for most use cases though) or the amount of data you can store is limited.
I hope that explains why I came up with a slightly modified and unique way of encrypting data on the phone.
What do you think?
I’d be excited to hear how you’d use or modify this approach to fit your specific needs. Is it something you’d use in your project, does it achieve the goal of encrypting user data, or are there any loopholes I missed?
Remember, I’ve published the code in a public repository, so you can already go ahead and try it out without having to recreate any of the above yourself:
marcelkalveram/react-native-bulletproof-redux-provider
Thanks for bearing with me until here and looking forward to hearing your feedback. 👋
Originally published in The Startup on December 22, 2020.