react-native-hmac
Native HMAC-SHA256 signer for React Native. Returns a fresh, replay-resistant
signature you can attach to any transport — Socket.IO, fetch / axios,
SignalR, raw WebSocket, gRPC-Web, MQTT — anywhere you need to prove that a
request came from your app.
The signing secret lives only in compiled native resources (Android string
resource / iOS Info.plist). The JS layer never holds it, so it cannot be
extracted from the Hermes/JSC bundle.
- One synchronous function.
getSignature() returns { signature, timestamp, version }
— no Promise, no await. HMAC over the small canonical payload completes in
under a millisecond, so the cost of blocking the JS thread is negligible.
- Transport-agnostic. Zero coupling to any specific networking library.
- Native signing.
javax.crypto.Mac on Android, CryptoKit on iOS.
- Zero JS crypto dependencies. No
crypto-js, no react-native-quick-crypto.
- Zero secret exposure to JS. No
react-native-config, no .env bundling.
- Replay-resistant. Every signature includes a fresh timestamp.
Requires Hermes or on-device JSC. The native method is declared as a
blocking synchronous bridge call. It does not work in the Chrome remote
JS debugger. It works fine in Hermes (default in modern RN), in the
Hermes/JSC debugger, and on real devices.
Quick start
yarn add react-native-hmac
cd ios && pod install
import { getSignature } from "react-native-hmac";
const { signature, timestamp, version } = getSignature();
That's the whole client-side API. No await, no promise, no setup beyond
configuring the native secret once (below).
Configure the native secret (one-time per app)
Generate the secret:
openssl rand -hex 32
Android — one line
Add to android/local.properties (already in .gitignore):
HMAC_SECRET=...your-generated-secret...
That's it. The library's own build.gradle reads this value at build time and
injects it as the Android string resource react_native_hmac_secret, which is
merged into your APK's resources.arsc. Nothing to add to your app's
build.gradle.
Alternative sources (checked in order):
-PHMAC_SECRET=... Gradle property — handy for CI.
HMAC_SECRET environment variable.
HMAC_SECRET key in android/local.properties.
To override per-flavor or per-build-type, declare your own resValue with the
same name in android/app/build.gradle — app-level resValues win over
library-level ones during Android's resource merge.
iOS
-
Create ios/Secrets.xcconfig (gitignored):
REACT_NATIVE_HMAC_SECRET = ...your-generated-secret...
-
Reference it from your target's xcconfig.
-
Add to ios/<YourApp>/Info.plist:
<key>ReactNativeHmacSecret</key>
<string>$(REACT_NATIVE_HMAC_SECRET)</string>
Recipes — same getSignature(), any transport
Socket.IO
import io from "socket.io-client";
import { getSignature } from "react-native-hmac";
const socket = io(url, {
auth: (cb) => cb(getSignature()),
});
The function form of auth is called by socket.io-client on every (re)connect,
so the timestamp is always fresh.
Fetch
import { getSignature } from "react-native-hmac";
const { signature, timestamp, version } = getSignature();
const res = await fetch(url, {
headers: {
"x-hmac-signature": signature,
"x-hmac-timestamp": String(timestamp),
"x-hmac-version": version,
},
});
Axios (request interceptor)
import axios from "axios";
import { getSignature } from "react-native-hmac";
const api = axios.create({ baseURL: "https://api.example.com" });
api.interceptors.request.use((config) => {
const { signature, timestamp, version } = getSignature();
config.headers["x-hmac-signature"] = signature;
config.headers["x-hmac-timestamp"] = String(timestamp);
config.headers["x-hmac-version"] = version;
return config;
});
SignalR
import { HubConnectionBuilder } from "@microsoft/signalr";
import { getSignature } from "react-native-hmac";
const connection = new HubConnectionBuilder()
.withUrl(url, {
accessTokenFactory: () => {
const { signature, timestamp } = getSignature();
return `${timestamp}.${signature}`;
},
})
.build();
Raw WebSocket
import { getSignature } from "react-native-hmac";
const { signature, timestamp } = getSignature();
const ws = new WebSocket(`${url}?ts=${timestamp}&sig=${signature}`);
Reusable signer with bound claims
import { createSigner } from "react-native-hmac";
const signWithTenant = createSigner({ claims: { tenantId: "t_42" } });
const a = signWithTenant();
const b = signWithTenant();
Startup health-check
import { isConfigured } from "react-native-hmac";
if (!isConfigured()) {
}
API
getSignature(options?: SignOptions): Signature
createSigner(options?: SignOptions): () => Signature
isConfigured(): boolean
buildPayloadString(timestamp: number, claims?): string
class HmacNotLinkedError
class HmacSecretMissingError
class HmacSignError
Everything is synchronous. Errors are thrown synchronously — wrap in try/catch
if you want graceful degradation; otherwise let them propagate so missing
configuration fails loud and early.
Options
type SignOptions = {
claims?: Record<string, string | number | boolean>;
};
type Signature = {
signature: string;
timestamp: number;
version: "v1";
};
Canonical payload
The native side hashes one of:
v1:<timestamp>
v1:<timestamp>:<sortedJsonClaims>
Claims are JSON-stringified with keys sorted alphabetically and no whitespace
— any conformant JSON encoder reproduces the exact bytes, so any verifier on
the other end can recompute the same string.
Verifying signatures (any backend / any language)
The canonical payload format is trivial to reproduce. Node.js example:
const { createHmac, timingSafeEqual } = require("node:crypto");
function canonicalPayload(timestamp, claims) {
if (!claims || Object.keys(claims).length === 0) return `v1:${timestamp}`;
const sorted = {};
for (const k of Object.keys(claims).sort()) sorted[k] = claims[k];
return `v1:${timestamp}:${JSON.stringify(sorted)}`;
}
function verify({ secret, signature, timestamp, claims, windowMs = 60_000 }) {
if (Math.abs(Date.now() - timestamp) > windowMs) return false;
const expected = createHmac("sha256", secret)
.update(canonicalPayload(timestamp, claims))
.digest("hex");
if (expected.length !== signature.length) return false;
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}
Use the same secret on both sides — the value in android/local.properties
on the device build must equal process.env.HMAC_SECRET (or wherever) on your
backend.
Always check the timestamp is within a small window of now (60 s is a
sensible default) to prevent replay.
Security FAQ
Q. Why not just use crypto-js and react-native-config?
Because both put the secret in your JS bundle. react-native-config injects
.env values at build time as JS constants; crypto-js then needs the
secret in memory. An attacker with the APK can run
strings index.android.bundle | grep. This library keeps the secret in
native resources and computes HMAC inside Kotlin/Swift — JS never sees the
key, even at runtime.
Q. Can the secret still be extracted?
On a rooted device with Frida, an attacker can hook Mac.doFinal() and observe
the key in process memory. This is unsolvable for any pure client-side scheme.
Mitigations: root/jailbreak detection, certificate pinning, server-side
anomaly detection, periodic server-driven secret rotation.
Q. Why HMAC and not JWT or asymmetric signatures?
HMAC is symmetric — one shared secret, simpler ops, ~10× faster signing than
ECDSA. If you also need per-user identity, layer a JWT on top of an
HMAC-signed request.
Q. Why is the API synchronous? Doesn't that block the JS thread?
The native bridge call is declared as a blocking synchronous method. HMAC
over a tiny payload completes in well under a millisecond, so the thread is
unblocked before a single frame is dropped. The benefit is a dramatically
cleaner API — no async contagion through your code just because you wanted
to sign a request.
Q. Does this work in Expo Go?
No — Expo Go does not include arbitrary native modules. Use a development
build (eas build --profile development) or the bare workflow.
Q. Does this work in the Chrome remote JS debugger?
No — synchronous native calls aren't supported in the Chrome debugger. Use
Hermes inspector or on-device debugging. (Most modern RN setups use Hermes by
default.)
License
MIT