jedisct1 / libsodium.js

libsodium compiled to Webassembly and pure JavaScript, with convenient wrappers.
Other
970 stars 138 forks source link

React Native cannot find a secure random number generator #202

Closed max-mapper closed 5 years ago

max-mapper commented 5 years ago

Hi, thank you for all the work on libsodium. I'm trying to load libsodium-wrappers.js and libsodium.js in React Native (android emulator). I random into the r.useBackupModule is not a function error using the current released versions, so I compiled master using emsdk sdk-releases-fastcomp-3b8cff670e9233a6623563add831647e8689a86b-64bit.

I also had to make these edits in libsodium-wrappers.js:

At this point I got the error: Both wasm and asm failed to loadNo secure random number generator found. I was able to fix this by following these steps https://www.npmjs.com/package/react-native-crypto#install and changing back requirexxx('crypto') to require('crypto').

Now I have it all loading and working. Just wanted to share in case anyone else was trying to load the library in React Native, as I couldn't find much info online.

tasn commented 4 years ago

I created a small library to emulate the node crypto API (only randomBytes() and createHash('sha1')). It uses the new expo-random. It's very raw, but it got node-rsa to work without ejecting. Maybe useful for this one too...

moughxyz commented 4 years ago

I was able to solve this using a newer method that relies on the new "extraNodeModules" feature in RN:

  1. npm install --save-dev react-native-randombytes
  2. In metro.config.js, add this to exports:
  resolver: {
    extraNodeModules: {
      crypto: path.resolve(__dirname, 'extra_modules/crypto')
    },
  },
  1. Create file index.js in new extra_modules/crypto folder:
import { randomBytes } from 'react-native-randombytes';

exports.getRandomValues = function getRandomValues(arr) {
    let orig = arr;
    if (arr.byteLength !== arr.length) {
        // Get access to the underlying raw bytes
        arr = new Uint8Array(arr.buffer);
    }
    const bytes = randomBytes(arr.length);
    for (var i = 0; i < bytes.length; i++) {
        arr[i] = bytes[i];
    }

    return orig;
};
exports.randomBytes = exports.rng = exports.pseudoRandomBytes = exports.prng = randomBytes;
  1. In main project index.js, add:
global.crypto = require('crypto');

Everything works fine after that, but I'm getting pretty bad performance on a release build on an iPhone X. Argon2id using only 6mb and 1 iteration takes about 8 seconds. @maxogden do you recall any specific benchmarks in your RN testing?

tasn commented 4 years ago

@mobitar, they say in the libsodium docs that argon performance is bad in JS. Take a look at https://download.libsodium.org/doc/password_hashing and search for "JavaScript".

With that being sad, I haven't tried it myself.

moughxyz commented 4 years ago

Although testing the same code on a browser with 65mb and 4 iterations takes < 1s.

tasn commented 4 years ago

There is no JIT in JavaScript on iOS, so performance of math heavy stuff is pretty bad there. It's the same with RSA (especially keygen) and other math heavy pieces of code. Your best bet is to move your crypto code to a native module.

moughxyz commented 4 years ago

Yeah, probably what I'll do..

So far found:

tasn commented 4 years ago

Please let me know what you end up using. I think I may just end up using swift-sodium and have my own interfaces to javascript. Not sure to be honest.

moughxyz commented 4 years ago

Just gave this a try and it's blazing fast: https://github.com/lyubo/react-native-sodium

67mb, 5 iterations took 400ms on my iPhone X. Same on browser takes 900ms.

max-mapper commented 4 years ago

For our app we ended up doing a custom build of libsodium.js that has wasm as a separate JS bundle from asm.js. Then we ended up using the asm.js only version on RN and the wasm only one on Electron. For our use case it didn't make sense to bundle both and include the fallback mechanism. We are happy with the asm.js bundles performance on RN. It would be nice if there were official separate builds, if there's interest I can look into sending a PR.

moughxyz commented 4 years ago

We are happy with the asm.js bundles performance on RN

I'm assuming you're not using argon2 then? Probably just the encryption algos?

tasn commented 4 years ago

For posterity: I made some benchmarks locally yesterday with the default libsodium pwhash algo, and both opslimit and memlimit set to the "moderate" constant.

  1. libsodium: ~2.5 ops per second.
  2. libsodium.js: ~0.35 ops per second.
  3. libsodium.js (without JIT): I gave up on waiting. :)

For comparison, scrypt, even without JIT, is not as painfully slow.

tasn commented 4 years ago

I finally got it to work with no (major) hacks, no rn-nodeify and no editing of the metro config!

  1. Install the webCrypto implementation for get-random-values: react-native-get-random-values
  2. Fake install deps. They are not actually going to be loaded so can be whatever. You can also take the metroconfig approach of faking the resolving:
    "crypto": "npm:leftpad",
    "fs": "^0.0.1-security",
    "path": "^0.12.7",

    This is needed because metro tries to resolve modules no matter if they are actually used at runtime, so let's just have it load something. Whatever it is.

  3. Shim the document global (because libsodium tries to access it): global.document = global.document || {};

So wherever you are import libsodium from (or e.g. at the top of your index.js) should look like:

import 'react-native-get-random-values';
global.document = global.document || {};

import _sodium from 'libsodium-wrappers';

That's it.

moughxyz commented 3 years ago

Hmm, revisiting this, neither my nor your instructions seem to work anymore. I get:

Error: Both wasm and asm failed to load TypeError: undefined is not an object (evaluating 'I.asm.rc')

It only works if you enable Chrome debugging. Once you turn off the debugger, it no longer works.

tasn commented 3 years ago

@mobitar, here are the instructions I give to my users (other devs): https://docs.etebase.com/installation#on-react-native though as you can see I packaged it in a library that does other things too. To create the library I just followed the instructions I wrote in my previous comment.

moughxyz commented 3 years ago

Wait you actually posted that you had the same exact issue: https://github.com/jedisct1/libsodium.js/issues/249

tasn commented 3 years ago

Sorry, I completely forgot that I had to pin libsodium.js to a lower version due to #249. With the lower version, and the instructions above, it works as expected.