New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@amrshbib/react-native-hmac

Package Overview
Dependencies
Maintainers
1
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amrshbib/react-native-hmac

Native HMAC-SHA256 signer for React Native. The signing secret stays in native code; the JS layer never holds it. Plug-and-play helpers for socket.io, fetch, and axios.

latest
Source
npmnpm
Version
0.0.1
Version published
Maintainers
1
Created
Source

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();
// → { signature: "ab12…", timestamp: 1716220000000, version: "v1" }

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();  // fresh timestamp, same claim bound
const b = signWithTenant();  // fresh timestamp, same claim bound

Startup health-check

import { isConfigured } from "react-native-hmac";

if (!isConfigured()) {
  // The native secret wasn't wired up in this build.
}

API

getSignature(options?: SignOptions): Signature
createSigner(options?: SignOptions): () => Signature
isConfigured(): boolean
buildPayloadString(timestamp: number, claims?): string

class HmacNotLinkedError      // app not rebuilt
class HmacSecretMissingError  // native resource not populated
class HmacSignError           // anything else

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 = {
  /** Bound INTO the HMAC. The verifier must receive the same keys/values
   *  and recompute the canonical payload to validate. */
  claims?: Record<string, string | number | boolean>;
};

type Signature = {
  signature: string;   // lowercase hex
  timestamp: number;   // epoch ms
  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

Keywords

react-native

FAQs

Package last updated on 20 May 2026

Related posts