mulungood

Elliptic Curve encryption within Expo workflow for local-first apps

Challenges and solutions to using third-party encryption modules with Expo, and our step-by-step instructions on how to get it done.

Frederico Santiago's photoFrederico Santiago

When building local-first apps, we must be specially careful with how we protect data. That is because we keep the data locally within the user’s device. All the encryption and decryption must be local and guarantee that unauthorized people do not have access to sensitive data.

Say we have an user that’s signed into the app with some provider (such as Google) and that provider has sent us the user’s private key so we could do our authentication bit. What if we used that private key and its corresponding public key to encrypt/decrypt this user’s data on the device?

Well, that’s what I set out to do and here’s what I found out:

Expo does not do well with third-party encryption modules and also does not provide the tools for it.

The main problem is that being a react-native workflow, Expo doesn’t support node specific modules (such as crypto) and its own expo-crypto (see the docs) implementation has no encryption algorithms — only hashing, which for a local-first app is not nearly enough. On top of that, any implementation that’s native will be rejected by Expo.

There are solutions such as crypto-browserify (see the docs) for making the node crypto module accessible outside of node, but I needed to do some sort of hacking before I could get an encryption algorithm working with Expo and I’ll show below how I got it done.

My pick for an Elliptic Curve implementation was eth-crypto’s (see the docs). With it, having the user’s private key, I can derive their public key with the publicKeyByPrivateKey method. Then, I can encrypt the data with encryptWithPublicKey and decrypt it later with decryptWithPrivateKey. It all should be very straight-forward.

But then, we run into two main problems. The eth-crypto depends on:

To deal with issue #1, I found out about the patch-package package. With it, we can patch the dependencies from requiring crypto to requiring crypto-browserify and make these patches reproducible within the github repo. I gotta say here that I’m aware that patching modules is not the ideal way, and I agree that it should be avoided when possible, but so far it has been the one solution out of four others I’ve tried that’s actually worked.

So, to get it done, all I did was:

And that’s pretty much all the hacking needed. If you run into any stream module resolving issue (or any other node specific modules), install their browserified versions and patch the dependencies as needed such as in the steps 3-4 above.

Now, to use the elliptic curve encryption algorithm, as mentioned before, we can simply follow the eth-crypto docs and do things like:

const publicKey = EthCrypto.publicKeyByPrivateKey( '0x107be946709e41b7895eea9f2dacf998a0a9124acbb786f0fd1a826101581a07' ); // > 'bf1cc3154424dc22191941d9f4f50b063a2b663a2337e5548abea633c1d06ece...'
const encrypted = await EthCrypto.encryptWithPublicKey( // beware that there's no x0 at public key start 'bf1cc3154424dc22191941d9f4f50b063a2b663a2337e5548abea633c1d06ece...', // publicKey 'foobar' // message ); /* > { iv: '02aeac54cb45283b427bd1a5028552c1', ephemPublicKey: '044acf39ed83c304f19f41ea66615d7a6c0068d5fc48ee181f2fb1091...', ciphertext: '5fbbcc1a44ee19f7499dbc39cfc4ce96', mac: '96490b293763f49a371d3a2040a2d2cb57f246ee88958009fe3c7ef2a38264a1' } */
const message = await EthCrypto.decryptWithPrivateKey( '0x107be946709e41b7895eea9f2dacf998a0a9124acbb786f0fd1a826101581a07', // privateKey { iv: '02aeac54cb45283b427bd1a5028552c1', ephemPublicKey: '044acf39ed83c304f19f41ea66615d7a6c0068d5fc48ee181f2fb1091...', ciphertext: '5fbbcc1a44ee19f7499dbc39cfc4ce96', mac: '96490b293763f49a371d3a2040a2d2cb57f246ee88958009fe3c7ef2a38264a1' } // encrypted-data ); // 'foobar'

And those should work fine with Expo.


UPDATE: While the steps above have worked for me in order to encrypt/decrypt strings given that a private key already exists, I had trouble calling the EthCrypto.createIdentity() method to create both the private and the public keys from scratch. To bypass it, I ended up going all the way up to the @noble/curves module to get a function that would generate the keys. All I did was:

As explicitly named, the return of randomPrivateKey is a UInt8Array. To be able to use it along with the eth-crypto package functions, I turned it into an hex-string and prepended a “0x” to it.


This is an open learning experience for me. If you have any suggestions or questions, feel free to reach me at frederico@mulungood.com