Photo by Franck - Free-Hotspot.com on Unsplash

A Bullet-Proof Approach to Storing Sensitive User Data in React Native

Marcel Kalveram
The Startup
Published in
9 min readDec 22, 2020

--

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:

  1. Create a secure key usingreact-native-keychain . That’s the key we’ll use to encrypt our data. 🔑
  2. Turn our store data into an easily encrypt-able format, ie. using JSON.stringify().
  3. Encrypt our data using the generated secure key 🔐
  4. 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.

A high-level overview of the component involved in this secure storage mechanism

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.
  • 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 using redux-persist for persistence.

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.

Bird’s eye view of the EncryptionGate and getEncryptionKey method

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 password
const ENCRYPTION_KEY = 'UNIQUE_ID';
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(ENCRYPTION_KEY, 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 and persistor 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.

The Store Gate and generateStore function at a glance

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 using uuidv4 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 of react-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:

Thanks for bearing with me until here and looking forward to hearing your feedback. 👋

--

--

Marcel Kalveram
The Startup

JavaScript, React and React Native. Building and optimising mobile experiences for web & native.