@raeygzz/react-native-nacl-jsi
v0.8.7
Published
Sodium for React Native with JSI binding
Maintainers
Readme
@raeygzz/react-native-nacl-jsi
Sodium library for React Native with JSI bindings.
Fork notice. This is a maintained fork of
react-native-nacl-jsiby PACE, published to npm as@raeygzz/react-native-nacl-jsi. It adds worklet-runtime support, an off-main-thread async streaming decrypt, native disk↔disk chunk crypto, and 16 KB page alignment, and is verified against React Native 0.85 with the New Architecture. See Fork additions.
Features
- ⚡️ High-performance integration of the Sodium library, written in C++
- 🔗 Synchronous calls without the bridge, via JSI
- 🧩 Compatible with the New Architecture (Fabric / TurboModules); verified on RN 0.85
- 🧵 Installable on a
react-native-workletsruntime so crypto can run off the main JS thread - 🚀 Off-main-thread async streaming decrypt (
decryptBase64ChunkAsync) and native disk↔disk chunk crypto - 📦 16 KB native-page-aligned Android build (Google Play requirement for Android 15+)
Installation
This package requires Hermes and, because the worklet-runtime bindings link against it,
react-native-worklets as a peer dependency.
# npm
npm install @raeygzz/react-native-nacl-jsi react-native-worklets
# yarn
yarn add @raeygzz/react-native-nacl-jsi react-native-worklets
npx pod-installIf you consume this under the unscoped name react-native-nacl-jsi, install it via an alias:
yarn add react-native-nacl-jsi@npm:@raeygzz/react-native-nacl-jsiPrecompiled binaries of libsodium are shipped in the package
(libsodium/build.tar.gz) and extracted automatically by the package's postinstall.
No libsodium build step is required on the consumer side.
Peer dependencies
| Package | Range | Why |
| --- | --- | --- |
| react-native | >=0.71.0 | JSI host object registration |
| react-native-worklets | >=0.8.3 | installNaClOnWorkletRuntime + the native worklet-runtime headers the Android/iOS build links against |
| react | * | React Native peer |
| @stablelib/base64, @stablelib/hex, @stablelib/utf8 | >=1.0.x | Encoding helpers |
| hash-wasm | >=4.11.0 | Hashing helpers |
| tweetnacl | >=1.0.3 | JS fallback helpers |
Source compilation (optional)
Precompiled libsodium binaries are linked by default. To recompile libsodium yourself,
run npm run rebuild in the package directory — the source is downloaded and signature-verified
before compilation.
macOS prerequisites
- libtool, autoconf, automake (macports / homebrew)
- Xcode (12 or newer)
Android prerequisites
- Android NDK, CMake, LLDB
npm run rebuildUsage
Encoding / decoding
All crypto functions take and return Uint8Array. Helper functions convert to and from
UTF-8, hexadecimal, and base64 strings.
import {
secretboxGenerateKey,
secretboxSeal,
secretboxOpen,
decodeUtf8,
encodeUtf8,
} from '@raeygzz/react-native-nacl-jsi';
const key = secretboxGenerateKey();
const encrypted = secretboxSeal(decodeUtf8('encrypt me'), key);
const decrypted = secretboxOpen(encrypted, key);
console.log(encodeUtf8(decrypted));Fork additions
Run nacl on a worklet runtime
installNaClOnWorkletRuntime installs every nacl JSI binding on a WorkletRuntime
created with react-native-worklets's createWorkletRuntime. After the call, every nacl
global (secretboxSeal, secretboxOpen, boxOpen, fromHex, toHex, fromBase64, …)
is callable from inside a 'worklet' function scheduled on that runtime — so heavy crypto
runs off the main JS thread.
function installNaClOnWorkletRuntime(workletRuntime: unknown): void;import { createWorkletRuntime } from 'react-native-worklets';
import { installNaClOnWorkletRuntime } from '@raeygzz/react-native-nacl-jsi';
const runtime = createWorkletRuntime('nacl-crypto');
installNaClOnWorkletRuntime(runtime); // throws if the native build predates this APIThrows if the native side is missing the
__installNaclOnWorkletRuntimeglobal — runpod install/ rebuild the Android app after installing so the native bindings are present.
Off-thread chunk crypto (advanced globals)
For large-file (chunked) upload/download and streaming, the package installs low-level
host functions directly on the runtime's globalThis. They keep plaintext/ciphertext out
of JS memory (paths, offsets, and a hex key are the only values that cross the JSI boundary),
and are typically scheduled on the worklet runtime above. The on-wire byte format
(additionalData ++ nonce(24) ++ ciphertext ++ mac(16)) is identical to secretboxSeal /
secretboxOpen, so chunks are interchangeable across paths.
// Off-main async streaming decrypt (main runtime only; requires a JS CallInvoker).
// base64-decode + crypto_secretbox_open on a background thread; ArrayBuffer allocated
// back on the JS thread. Falls back to the main-thread decrypt when absent.
declare function decryptBase64ChunkAsync(
cipherB64: string,
keyHex: string,
additionalDataLength: number
): Promise<ArrayBuffer>;
// Disk→disk: read `readLength` bytes at `srcOffset`, zero-pad to `paddedLength`, seal,
// and write `additionalData ++ nonce ++ ciphertext ++ mac` to `destPath`. Returns bytes written.
declare function encryptChunkToFile(
srcPath: string,
srcOffset: number,
readLength: number,
paddedLength: number,
additionalDataHex: string,
keyHex: string,
destPath: string
): number;
// Disk→disk: open an encrypted chunk file, strip `additionalDataLength` prefix bytes, and
// pwrite up to `maxPlaintextBytes` of plaintext into `destPath` at `destOffset`. Returns bytes written.
declare function decryptChunkFileToFile(
srcEncPath: string,
additionalDataLength: number,
keyHex: string,
destPath: string,
destOffset: number,
maxPlaintextBytes: number
): number;
// Disk→memory: decrypt an encrypted chunk file and return the plaintext as base64.
declare function decryptChunkFileToMemory(
srcEncPath: string,
additionalDataLength: number,
keyHex: string
): string;encryptChunkToFile, decryptChunkFileToFile, and decryptChunkFileToMemory are installed
on both the main and worklet runtimes; decryptBase64ChunkAsync is installed only on the
main runtime and only when a JS CallInvoker was captured at init (otherwise it is absent
and callers fall back to a main-thread decrypt).
API reference
Public-key authenticated encryption (box)
function boxGenerateKey(): KeyPair;
type KeyPair = {
publicKey: Uint8Array;
secretKey: Uint8Array;
};
function boxSeal(
message: Uint8Array,
recipientPublicKey: Uint8Array,
senderSecretKey: Uint8Array
): Uint8Array;
function boxOpen(
encryptedMessage: Uint8Array,
senderPublicKey: Uint8Array,
recipientSecretKey: Uint8Array
): Uint8Array;Secret-key authenticated encryption (secretbox)
function secretboxGenerateKey(): Uint8Array;
function secretboxSeal(message: Uint8Array, secretKey: Uint8Array): Uint8Array;
function secretboxOpen(
encryptedMessage: Uint8Array,
secretKey: Uint8Array
): Uint8Array;Signature
function signGenerateKey(): KeyPair;
function signDetached(
messageToSign: Uint8Array,
secretKey: Uint8Array
): Uint8Array;
function signVerifyDetached(
message: Uint8Array,
publicKey: Uint8Array,
signature: Uint8Array
): boolean;Password hashing (Argon2id)
function argon2idHash(
password: Uint8Array,
iterations: BigInt,
memoryLimit: BigInt
): Promise<string>;
function argon2idVerify(hash: string, password: Uint8Array): Promise<boolean>;Key derivation (Argon2id)
function argon2idDeriveKey(
key: Uint8Array,
salt: Uint8Array,
keyLength: number,
iterations: BigInt,
memoryLimit: BigInt
): Promise<Uint8Array>;AES256-GCM
function aesGenerateKey(): Uint8Array;
function aesEncrypt(message: Uint8Array, key: Uint8Array): Promise<AesResult>;
type AesResult = {
encrypted: Uint8Array;
iv: Uint8Array;
};
function aesDecrypt(
cipherText: Uint8Array,
key: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array>;Random bytes
function getRandomBytes(size: number | BigInt): Uint8Array;Contributing
See the contributing guide to learn how to contribute to the repository and the development workflow.
Credits
Originally authored by Rémy Dautriche (@remydautriche) for PACE. Maintained as a fork by Nuvola Digital.
License
MIT
