Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use userbase in React Native #275

Open
Fubinator opened this issue Apr 10, 2021 · 12 comments
Open

Use userbase in React Native #275

Fubinator opened this issue Apr 10, 2021 · 12 comments

Comments

@Fubinator
Copy link
Contributor

Fubinator commented Apr 10, 2021

I asked on Twitter if there was a way to use userbase in React Native. After some time of playing around, however, I still haven't managed to get userbase working. I am opening this issue to share my experience so far and maybe someone can help me find the solution (This is also too much for Twitter 😄).

The proposed approach was to use userbase-js-node with rn-nodeify. In the process, I ran into a few problems, which I'll explain below.

So first things first:
I've had massive problems with using expo in conjunction with rn-nodeify (See here). I ended up ditching expo and used the react-native-cli for creating the project.

After setting up the initial project and installing rn-nodeify, I tried to get userbase-js-node working. The problem here is that with userbase-js-node, node-localstorage is used. node-localstorage simulates localstorage by creating a file on disk, which is not possible in React Native. You could possibly somehow polyfill with react-native-fs, but that also seems a little awkward. @peculiar/webcrypto doesn't seem to work well in React Native either.

I ended up playing around with userbase's JavaScript SDK, which took me the furthest. I was able to polyfill the localstorage with react-native-sync-localstorage to the point that userbase stopped complaining.

Initialising the app does not throw any errors. I suspect this is because no session exists. However, as soon as I call userbase.signUp I get the following error: WebCryptoUnavailable: The WebCrypto API is unavailable. Please make sure your website uses https.
According to userbase's source code this error is thrown when window.crypto.subtle is not available. When I logged that in my app it was indeed undefined. So I figured I needed a polyfill for window.crypto.subtle and came across react-native-webview-crypto. This does polyfill crypto.subtle, but now I get the following error:

Userbase error. Please report this to support@userbase.com.

 stringify error InvalidAccessError: key is not extractable

I am stuck at this point. I think the error comes directly from window.crypto.subtle, but I don't find much on google when I search for it directly. Also, I'm not sure if I'm too naive about polyfilling. Replacing everything that is not available with different libraries doesn't seem to be the best approach.

If anyone has any further suggestions or possibly even userbase in React Native running, I would be very happy if they could point me in the right direction.

EDIT: According to this a key is not extractable, when there is a boolean flag set to false when calling window.crypto.subtle.generateKey(). However, when searching in the repo, I can't find any occurrences where the flag is set to false.

EDIT2: After some debugging, it seems like the error occurs here:

const encryptionKey = await window.crypto.subtle.deriveKey(
hkdf.getParams(PASSWORD_BASED_ENCRYPTION_KEY, salt),
hkdfKey,
getEncryptionKeyParams(),
!KEY_IS_EXTRACTABLE,
KEY_WILL_BE_USED_TO
)

For testing purposes I've set KEY_IS_EXTRACTABLE to true. As soon as I set the flag, I can create a user. Now it seems that the websocket is broken, as I get the following error: WebSocket error: Can't find variable: DOMException

EDIT3: The last error was a little bit misleading. It actually occurs here:

const hmacKey = await window.crypto.subtle.deriveKey(
hkdf.getParams(HMAC_KEY_NAME, salt),
masterKey,
{
name: ALGORITHM_NAME,
hash: {
name: sha256.HASH_ALGORITHM_NAME
},
length: 512 // need to explicitly set length for WebCrypto node polyfill (see: https://github.com/PeculiarVentures/webcrypto-core/issues/31)
},
KEY_IS_EXTRACTABLE,
KEY_WILL_BE_USED_TO_SIGN
)

Again, after I set the key to be extractable, the error is gone. I can even log in and get a session. I won't try any further from here, as I don't know what cryptographic complications this presents either. However, maybe someone knows if the problem is with the implementation of react-native-webview-crypto.

@j-berman
Copy link
Collaborator

Oh man, I'm sorry for this rabbit hole you've gone down. That twitter response was me. The most challenging aspects of getting userbase-js-node working were dealing with similar things. My recommendation would be to turn back unless you're prepared to keep putting time into this. You're making good progress even if it feels crappy to run into error after error.

On the crypto issue: to me, this looks like an implementation error with react-native-webview-crypto mishandling the extraction somehow, I'm not exactly sure. I ran into an implementation error with the node web crypto polyfill as well, and submitted the issue (can see I linked to it in that comment in your EDIT3), so definitely not out of the question. In any case, that boolean isn't a big deal. It's saying the raw key can be loaded into memory shared with the executing code via the function exportKey. But usernames and passwords (and the user's seed) is already loaded into shared memory anyway, and those are functionally what enable anyone to retrieve all of a user's cryptographic keys. So extractability, or lack thereof, is really not providing meaningful protection imo.

On the websocket polyfill: perhaps react-native-websocket will work?

I believe in the end, would need polyfills for all of these to make sure everything works. Could help serve as a guide maybe.

@Fubinator
Copy link
Contributor Author

No worries about the rabbit hole 😄 It actually gave me a deeper understanding of how userbase works internally. I think I'll continue to look into it a bit more as I have some time to do so. I will definitely take a closer look at the react-native-webview-crypto implementation, as it seems to be the biggest problem. If I find a solution or encounter any other problems that someone could help me with, I would comment in here again. Maybe it would help someone else for future reference.

@jneterer
Copy link
Contributor

@Fubinator I would definitely be interested in React Native support for Userbase. I'm currently playing around with Userbase in Ionic. I like React Native significantly more than Ionic, so if you need help with this let me know, I'd for sure want to assist when I have the time!

@Fubinator
Copy link
Contributor Author

@jneterer I'll briefly describe my current evaluation of the problem and what we need to make it work:
The main problem is the polyfill of the WebCrypto API. I have tried several polyfills, all of which failed in different places:

  1. isomorphic-webcrypto: This exposes the Microsoft Research Library. It throws Error: Could not find an appropriate alg. I trusted the error message and think that some algorithm is simply not implemented here.

  2. react-native-webview-crypto: I described that in the initial post. Here errors are thrown when deriveKey is executed. This could possibly be a bug in the library or in webview-crypto, on which react-native-webview-crypto is based.

  3. @peculiar/webcrypto + rn-nodeify: This library is also used in userbase-js-node to polyfill the Crypto Web API. Errors are thrown here that I haven't quite located yet.

So what is the current status? I think there are two options: Either we continue to look for bugs in the libraries and try to reproduce them minimally so that we eventually write bug reports, or we find another library that works out of the box. Another problem is that all libraries so far have been very slow.

My current polyfill setup looks like this:

import './shim.js'; // rn-nodeify shims

const {Crypto} = require('@peculiar/webcrypto');
import localStorage from 'react-native-sync-localstorage';

export default async function initialize(): Promise<void> {
  // crypto
  global.crypto = new Crypto();

  // localStorage
  await localStorage.getAllFromLocalStorage();
  global.localStorage = localStorage;

  // sessionStorage
  // https://gist.github.com/juliocesar/926500#gistcomment-1620487
  global.sessionStorage = {
    _data: {},
    setItem: function (id: string, val: any) {
      return (this._data[id] = String(val));
    },
    getItem: function (id: string) {
    return Object.prototype.hasOwnProperty.call(this._data, 'id')
        ? this._data[id]
        : undefined;
    },
    removeItem: function (id: string) {
      return delete this._data[id];
    },
    clear: function () {
      return (this._data = {});
    },
  };

  // DOMException
  global.DOMException = require('domexception');
}

I hope this is a good overview to try out for yourself.

@Fubinator
Copy link
Contributor Author

So after a little bit of further investigation I managed to sign in using the @peculiar/webcrypto lib. There is one problem with the crypto.createSign function in conjunction with rn-nodeify. rn-nodeify uses browserify-sign. It calls the crypto.createSign function with SHA256 as algorithm name. browserify-sign then looks for the algorithm and throws an error if it is not found (See here). Adding an uppercase definition for the algorithm here lets me sign in.

In my opinion, browserify-sign should allow the algorithm name to be uppercase, as in node the following works:

const crypto = require('crypto');

console.log(crypto.createSign('SHA256'));

I've created an issue on browserify-sign and I might provide a PR. We might be near to a solution for the crypto problem over here.

@jneterer
Copy link
Contributor

@Fubinator this is really encouraging to hear! How was the speed of signing in?

@jneterer
Copy link
Contributor

@j-berman I do have one question when it comes to mobile apps built with Userbase. If I've set remember me to local storage and the user at some point deletes the app, then they want to sign into the web app but they've forgotten their password, will they be able to since they've deleted the app? Or will they have to reinstall and reset it there? The question comes from this line in the Forgot Password docs "Recovery will not be possible if the user loses access to all previously used devices". Is it the physical device that is important, or the browser/app?

@Fubinator
Copy link
Contributor Author

Fubinator commented Apr 18, 2021

@jneterer Sign in speed is not good. I stopped the time and it's taking ~28 seconds.

BTW: It is possible to change the name of the SHA-256 algorithm in userbase.js directly so that it works. Just change the constant to "sha-256" over here:

const HASH_ALGORITHM_NAME = 'SHA-256'

Maybe that would be a possibility if everything else doesn't work.

@Fubinator
Copy link
Contributor Author

I've created a repository with a little bit of explanation and the current status:
https://github.com/Fubinator/react-native-userbase-poc

I think this is better than spamming my intermediate steps in here all the time 😄

@jneterer
Copy link
Contributor

@Fubinator great idea, I'll check your repo out!

@j-berman
Copy link
Collaborator

@j-berman I do have one question when it comes to mobile apps built with Userbase. If I've set remember me to local storage and the user at some point deletes the app, then they want to sign into the web app but they've forgotten their password, will they be able to since they've deleted the app? Or will they have to reinstall and reset it there? The question comes from this line in the Forgot Password docs "Recovery will not be possible if the user loses access to all previously used devices". Is it the physical device that is important, or the browser/app?

For apps that are end-to-end encrypted, if the user deletes the app and all the app's data, this will likely delete the user's encryption key too which is stored locally when you use the local storage option. If the user loses both their password and the encryption key, the user wouldn't be able to access their encrypted data.

@dancomanlive
Copy link

Any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants