Security12 min read

Four Years of React Native Quick Crypto: From Wallets to Node Parity

How a JSI-based hash-and-HMAC library written for the 2022 Web3 wave grew into a full Node crypto implementation on mobile, with WebCrypto, post-quantum signatures, six phases of security audit, and weekly download counts five times what they were a year ago.

Brad Anderson
Brad AndersonJune 11, 2026
Four Years of React Native Quick Crypto: From Wallets to Node Parity

A library born in the wallet boom

In February 2022, Marc Rousavy committed the first version of what was then called react-native-fast-crypto. The README pitched it plainly: "A fast implementation of Node's crypto module written in C/C++ JSI. FastCrypto can be used as a drop-in replacement for your Web3/Crypto apps."

The timing was not an accident. NFTs, DeFi, and WalletConnect were on every product roadmap, and React Native wallet apps were paying real money in CPU cycles for SHA‑256, HMAC, and PBKDF2 implemented in JavaScript. The pure‑JS polyfills crypto-browserify and react-native-crypto worked, but they were slow enough that derivation flows and message signing visibly stuttered the UI. A native implementation, bridged through JSI instead of the React Native bridge, was the obvious fix.

By the end of July 2022, WalletConnect's logo had been added to the README's sponsor list: receipts that the wallet thesis was correct.

Szymon: the OpenSSL plumbing

Marc's first month of commits scaffolded the project: an Android template, an iOS template, the C++ adapter, a TypedArray shim for poking at jsi::ArrayBuffer instances. The actual cryptography arrived when Szymon Kapała joined a few weeks later.

Szymon's PR #11 was, in his own commit message, "the last multi-commit PR :D", and it set up everything the library would need for years: OpenSSL linkage on both platforms, a worker thread for off-main-thread crypto, a macro layer for JSI ↔ C++ conversions, and the first algorithms, PBKDF2 and HMAC. Over the following months he added hashing, secure random (getRandomValues included), streaming support so HMAC and hashes could be fed chunked input, and a Mocha-based test rig that ran inside the example app.

By the time Szymon stepped back from active work, the library could do everything a typical wallet derivation flow required: PBKDF2 for BIP‑39, SHA‑family hashes, HMAC, and random bytes, all at native speed, all behind a Node-compatible API.

The choice of OpenSSL as the engine predates my involvement, but the logic holds up. Node's own crypto module is a thin layer over OpenSSL, so if the goal is to behave exactly like Node, linking the same library is the shortest path to parity; you inherit its edge cases instead of reverse-engineering them. It is also the most scrutinized cryptographic codebase on the planet, written in C with hand-tuned assembly for the hot paths, which is exactly the "don't roll your own crypto" posture you want in a security library. libsodium would have been the obvious alternative, but in 2022 it covered a narrower surface than Node's crypto, and bending it to match Node's API behavior‑for‑behavior would have meant fighting the library rather than leaning on it. (It still earns a place: RNQC pulls in libsodium as an opt-in dependency for the extended ciphers OpenSSL doesn't ship, like XSalsa20 and XChaCha20.)

Oscar: ciphers and signing

In June 2022 the project picked up its next major contributor. Oscar Franco landed the osp/cipher PR, opening up createCipheriv/createDecipheriv and AES modes. The library renamed from "fast" to "quick" that same month ("fast → quick", in Marc's terse commit) and shipped 0.4.x with the broader API surface.

Oscar followed up with publicEncrypt, then RSA signing, then the first WebCrypto pieces in late 2023, importing ECDSA keys and exporting them as SPKI. That work foreshadowed where the library would head next, but it also exposed the seams of the original architecture: the C++ side had grown a Margelo-specific abstraction layer of HostObjects and hand-rolled JSI conversions, each algorithm wired up by hand, with a worker thread shuttling work off the JS thread. It was fast, and it shipped, but every new algorithm meant another round of boilerplate.

How I got pulled in

I started contributing in early 2024, not because of wallets but because of databases.

The thing I was building needed CRDTs. Conflict-free Replicated Data Types are data structures (shared documents, JSON trees, key-value maps) that multiple devices can edit independently while offline, then merge automatically with no central server and no merge conflicts. They are the foundation of the local-first movement, and they're what powers libraries like Yjs, Automerge, and Loro. The catch: almost every serious CRDT stack needs the same short list of cryptography: Ed25519 to sign document deltas and identify peers, X25519 to negotiate device keys, ChaCha20‑Poly1305 or AES‑GCM to seal replicated payloads, and HKDF to derive everything from a root secret.

None of that was in react-native-quick-crypto when I arrived. The wallet-era algorithms were necessary but not sufficient. So I started filling in the WebCrypto subtle.* surface that Oscar had begun: subtle.digest(), importKey/exportKey across raw and JWK formats, generateKey for ECDSA and ECDH, sign/verify for ECDSA, then encrypt/decrypt for AES and RSA. By the 0.7.x line in mid‑2024, the library covered most of what a modern local-first stack actually called.

Ed25519 itself arrived a few months later, and X25519 shared-secret derivation followed in 2025, the two algorithms that had pulled me into the project in the first place.

Nitro: rewriting the layer underneath

Two and a half years of accumulated JSI plumbing had left the C++ side with a lot of weight: a custom jsi::HostObject hierarchy, hand-written argument unpacking for every method, a worker-thread dispatcher with its own quirks, and a set of bugs that were hard to fix without disturbing everything else.

Then Nitro Modules landed.

Nitro is Marc's framework for declaratively generating C++ bindings from TypeScript specs: the same JSI under the hood, but with code generation handling the boilerplate, async dispatch, hybrid objects, state management across the JS/C++ boundary, and ArrayBuffer zero‑copy paths. From my perspective, it was essentially v2 of the very layer we'd been hand-maintaining.

On August 15, 2024, PR #404 ("refactor: conversion to nitro modules") became 1.0.0-beta.1. The PR description was characteristically terse: "Refactor entire codebase to use new architecture / bridgeless / fabric / nitro-modules. Add benchmarking facility, and add benchmarks for each function ported over to nitro-modules."

It was one of the first non-trivial libraries to ship on Nitro, and it retired most of the technical debt the old Margelo C++ layer had accumulated. Each algorithm became a TypeScript spec plus a C++ implementation behind a generated interface. Throughput went up, especially on small inputs where the bridge crossing had dominated; bug surface went down; and adding new algorithms stopped requiring archaeology.

What runs where: sync by default, threaded when it's heavy

The shape of the runtime is worth a diagram. A Nitro Hybrid Object lives in the JavaScript runtime's own address space, so a call from JS is a direct C++ invocation over JSI, with ArrayBuffers shared by reference instead of copied across a bridge:

Architecture of react-native-quick-crypto: the JavaScript thread makes a direct JSI call into a Nitro Hybrid Object. Light operations run synchronously straight through to OpenSSL, while heavy operations are dispatched to a C++ thread pool that runs them off the JS thread.

Most of the API runs synchronously, right on the JS thread: hashing, HMAC, randomBytes, and the update/digest streaming calls return instantly, the same as any other function call. That is itself a feature. The old bridge-based react-native-crypto was async-only, so even hashing a string meant awaiting a Promise; with RNQC you can write straight-line createHash('sha256').update(data).digest('hex') and mean it.

The expensive operations are the ones that would actually stall a frame: PBKDF2, scrypt, RSA and EC key generation, signing and verification. For those, RNQC follows Node's own two-form convention. The synchronous variant (pbkdf2Sync, generateKeySync) runs inline and blocks the caller; the asynchronous variant (pbkdf2 with a callback or Promise) hands the work to a dedicated C++ thread pool, so a 600 ms key stretch happens off the JS thread and the UI keeps painting at 60fps. The pool is reused across calls rather than spawning a fresh OS thread per operation.

The trade-off is exactly the one you'd reason about in Node: the sync path is marginally faster per call because it skips the dispatch hop, while the async path costs a little latency but never freezes anything. You pick per call site, which is why a few rows in the benchmark table below are explicitly marked (sync).

From "works for wallets" to Node parity

Past 1.0, the goal shifted from "cover what the popular consumers need" to "match Node's crypto module behavior‑for‑behavior." That is a much harder target. Node's crypto has decades of edge cases: PKCS#1 v1.5 implicit rejection, OAEP default hashes, sliced TypedArray byte‑offset semantics, JWK round‑trips that preserve alg/crv/kty validation, DH peer-key checks, the difference between raw-public, raw-private, and raw-seed imports across algorithm families.

The Node parity push picked up dramatically in late 2025 and early 2026, with agentic assistants in the loop. The pace shows up in the commit log:

  • X509Certificate with all 25 methods and properties
  • SHA-3 and cSHAKE in subtle.digest()
  • KMAC128 / KMAC256
  • TurboSHAKE128/256 and KangarooTwelve KT128/KT256
  • DSA and DH key generation
  • EC named-curve matching, on-curve checks, and uncompressed SPKI export
  • randomUUIDv7 (RFC 9562 §5.7) plus a disableEntropyCache option
  • Spec-correct DOMException types throughout the WebCrypto error paths
  • Passphrase-encrypted PEM and DER in createPrivateKey / createPublicKey

Post-quantum, on a phone

OpenSSL 3.6 brought native ML‑KEM, ML‑DSA, and SLH‑DSA, the NIST post-quantum finalists. Mobile is one of the more interesting deployment targets for PQ crypto: the keys and signatures are large by today's standards (ML‑DSA‑87 signatures are 4,627 bytes; SLH‑DSA‑256s signatures are 49,856 bytes), and any pure‑JS implementation would be unusable in practice.

Through late 2025 and into 2026, RNQC added all three:

  • ML‑KEM: full encapsulate/decapsulate, unwrapKey support, plus raw‑public and raw‑seed export/import
  • ML‑DSA: sign, verify, JWK and PKCS#8 import/export
  • SLH‑DSA: sign and verify

All three are usable today on iOS and Android through the same subtle and KeyObject APIs as the classical algorithms.

Six phases of security audit

A library that ships into wallets, local-first databases, and now post-quantum protocols had earned a real audit. In April 2026 we ran a structured, six-phase security pass (Phase 0 through Phase 5), with each phase landing as its own reviewed PR. The PR descriptions read like incident reports because, in some cases, they are.

A short list of what Phase 0 alone fixed:

  • Sliced ArrayBuffer byte-offset bug: timingSafeEqual and AEAD setAAD were passing the entire backing buffer to native instead of the requested view. For sliced or offset buffers, this compared and authenticated the wrong bytes. Also caught and fixed in X509Certificate's string constructor.
  • XSalsa20 keystream restart: crypto_stream_xor always begins at block 0, so calling it once per update() XORed N copies of the same keystream against different plaintext. A catastrophic two-time-pad whenever the streaming Cipheriv API was used. Replaced with crypto_stream_xsalsa20_xor_ic plus a per-instance block counter and 64-byte leftover keystream so chunked updates resume cleanly.
  • DH and ECDH invalid peer keys: EVP_PKEY_derive_set_peer does not call DH_check_pub_key or EVP_PKEY_public_check, so off-curve or small-subgroup peers were silently accepted. Now explicitly checked, with cross-curve attack tests (a P-384 pubkey sent to a P-256 instance) in the regression suite.
  • Bleichenbacher oracle on RSA PKCS#1 v1.5: distinguishable error messages across decrypt failures formed a padding oracle. Fixed by enabling OpenSSL 3.2+ implicit rejection and collapsing every failure path to a single opaque error, matching Node's crypto_cipher.cc policy.

Phases 1 through 5 went after the supporting structure: shared validation helpers, a memory-safety sweep with RAII wrappers for every OpenSSL handle, TypeScript-boundary validation hardening, eleven additional implementation fixes uncovered by expanded test coverage, GitHub Actions hardening (shell-injection fixes in the release workflow, pinned third-party actions, minimum-scope permissions blocks), and a published-artifact trim that cut the npm tarball from 2,133 files to 989 and from 18.7 MB unpacked to 5.6 MB. The audit also added a CI gate that runs bun audit against just the runtime dependency tree on every PR, so the "zero advisories in the runtime tree" property is locked in as a regression test.

How fast is "fast," in 2026?

Top of the Benchmark Suites screen in the example app: each row is a suite, the green number is the headline throughput multiplier over the best pure-JS challenger.

Older benchmark numbers floating around the docs site predate the Nitro rewrite, the native C++ encoding layer that landed in March 2026, and most of the Node-parity work. So we re-ran the suite. The example app in this repo ships a benchmark tab that runs tinybench against the popular pure-JS alternatives: crypto-browserify, the @noble/* family, and the Buffer polyfill path React Native apps end up using when they don't have a native one.

A selection of the more telling results, taken on a recent iPhone simulator:

OperationChallengerSpeedup
DH modp14 key generationcrypto-browserify7,480×
HMAC SHA‑256, 8 MB Buffer@noble/hashes/hmac1,418×
HMAC SHA‑256, 1 MB Buffer@noble/hashes/hmac1,336×
DH modp14 computecrypto-browserify1,022×
utf16le encode, 1 MB (ASCII)Buffer polyfill992×
scrypt N=256 r=8 p=1 (sync)@noble/hashes/scrypt790×
HMAC SHA‑256, 1 MB string@noble/hashes/hmac431×
utf16le encode, 1 MBBuffer polyfill296× (latency)
ECDH P‑256 compute@noble/curves136×
HKDF SHA‑256, 32 B (sync)@noble/hashes/hkdf124×
utf16le decode, 1 MBBuffer polyfill41×
base64 decode, 1 MBBuffer polyfill17×
ECDH P‑256 key generation@noble/curves9.5×
hex encode, 1 MBBuffer polyfill

The headline numbers don't quite land until you see how lopsided the bars actually look. Three of them, in ms latency, lower is better:

crypto-browserify (DH modp14 keygen)
3984.50msbaseline
react-native-quick-crypto
0.53ms7518x
@noble/hashes/hmac (HMAC SHA-256, 8 MB Buffer)
21268.61msbaseline
react-native-quick-crypto
15ms1418x
@noble/curves (ECDH P-256 compute)
37.93msbaseline
react-native-quick-crypto
0.28ms135x

Two patterns are worth pointing out. First, the biggest wins are not in the headline algorithms; they're in the surrounding tape: encoding/decoding, HMAC, key derivation, modular arithmetic. Pure‑JS BigInt is the constant gravity those workloads fight, and OpenSSL's hand‑tuned assembly simply doesn't care. Second, the smaller multipliers (the 2× and 3× wins on short‑input encoding) are the ones that matter most for typical app code, because they show up tens of thousands of times per session in any flow that touches Buffers.

The 992× / 1,418× outliers are not typos. They are the difference between "this is a polyfill" and "this is OpenSSL on the device."

Adoption

The download chart finally caught up to all of this. A year ago, in June 2025, react-native-quick-crypto was averaging roughly 47,000 downloads a week. As of late May 2026, weekly downloads are sitting at ~244,000, and the monthly figure has crossed 851,000. May 21, 2026 alone moved 57,293 copies, a single‑day record.

Roughly 5× growth in twelve months, with visible inflection points around the post-quantum, X.509, and Node‑parity work last fall and winter, and again in mid-May 2026 after the security audit and 1.1.x line shipped. Whatever the next React Native crypto consumer is, whether wallet, CRDT, payments, attestation, or post‑quantum messaging, it seems to be reaching for this library by default now.

What it looks like today

Four years on, react-native-quick-crypto sits in roughly the spot the original README claimed but couldn't yet deliver: a drop-in replacement for Node's crypto module on mobile, plus the WebCrypto subtle.* surface, plus the modern algorithms (Ed25519, X25519, ChaCha20‑Poly1305, SHA‑3, KMAC, TurboSHAKE, and the NIST post-quantum trio) that the next generation of React Native apps will actually call.

The shape of the contributors changed along the way too. The early years were a small group writing C++ by hand. The Nitro rewrite turned a lot of that hand-written plumbing into generated code. And the last year of work has been a mix of human review and agentic assistants: running the security audit, working through Node parity edge cases, producing the test vectors and regression suites that catch each fix.

The through-line, from the first JSI commit to the post-quantum suite, hasn't changed: cryptography on mobile should be fast, correct, and unsurprising to anyone coming from Node. The library's job is to make that the default.

Appendix: full benchmark results

ℹ️

None of this is meant to disparage crypto-browserify, @noble/*, or the Buffer polyfill; they are excellent libraries and perform amazingly well in a server-side Node environment. This library exists because React Native does not have that environment nor a Node crypto implementation at hand, so the benchmark suite is here to show the speedup over the alternative of using a pure-JS library on React Native.

The example app's Benchmarks tab is what produced the table above. Three suites that capture the spread, captured on a recent iPhone simulator:

DH modp14: keygen and compute vs. crypto-browserify. The modular-arithmetic gap is the widest in the suite: pure-JS BigInt versus OpenSSL's tuned big-number routines. modp14 keygen runs ~7,000× faster on RNQC.

DH modp14 key generation and compute vs. crypto-browserify. modp14 keygen is over 7000× faster.

HMAC SHA-256: 1 MB and 8 MB inputs, strings and Buffers, vs. @noble/hashes/hmac and browserify. The Buffer path is where the contrast is starkest: the 8 MB Buffer case is more than 1,400× faster than the next-best JS implementation.

HMAC SHA-256 throughput vs. @noble/hashes/hmac and browserify, across 1 MB and 8 MB strings and Buffers. The Buffer path is more than 1000× faster.

Encoding: hex, base64, and utf16le, encode side, at 32 B and 1 MB vs. the Buffer polyfill. The mundane Buffer-tape every React Native app touches in normal flows. utf16le 1 MB tops out near 1,000×; the small-input cases are 1–2× but show up tens of thousands of times per session.

Encoding benchmarks: hex, base64, and utf16le encode at 32 B and 1 MB vs. the Buffer polyfill. utf16le 1 MB tops out at almost 1000× faster.

Brad Anderson
Brad AndersonMaintainer of react-native-quick-crypto
react-native-quick-cryptoCryptoWeb3WebCryptopost-quantumNitroOpenSSLSecurity

Share this article