{"_id":"@amrshbib/react-native-hmac","name":"@amrshbib/react-native-hmac","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@amrshbib/react-native-hmac","version":"0.0.1","description":"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.","main":"lib/index.js","types":"lib/index.d.ts","source":"src/index.ts","react-native":"src/index.ts","sideEffects":false,"exports":{".":{"types":"./lib/index.d.ts","react-native":"./src/index.ts","default":"./lib/index.js"},"./package.json":"./package.json"},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","clean":"rm -rf lib android/build android/.gradle","prepare":"npm run build"},"keywords":["react-native","hmac","hmac-sha256","sha256","signature","auth","authentication","security","socket.io","fetch","native","kotlin","swift"],"repository":{"type":"git","url":"git+https://github.com/amrshbib/react-native-hmac.git"},"homepage":"https://github.com/amrshbib/react-native-hmac#readme","bugs":{"url":"https://github.com/amrshbib/react-native-hmac/issues"},"author":"","license":"MIT","peerDependencies":{"react":"*","react-native":"*"},"devDependencies":{"@types/react":"*","typescript":"^5.0.0"},"engines":{"node":">=16"},"gitHead":"b4fef3a63dcd151ae6732dea5a02cdbecb1954c8","_id":"@amrshbib/react-native-hmac@0.0.1","_nodeVersion":"25.2.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-ojAiMxZjuMx95mlCeFjsfQy7vcJCmTMGbH4V+n+n5dTiOKPjWBcbZtzK9atExUKNskW2C9LQuNfITmh8lUzlNA==","shasum":"b268955a8182ccee253c8425a42dd0d333f248e0","tarball":"https://registry.npmjs.org/@amrshbib/react-native-hmac/-/react-native-hmac-0.0.1.tgz","fileCount":15,"unpackedSize":37187,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICbtqb9F8Gdn7Nzqkej/QavVwH7jMOZaKpzlQ1WQZgmAAiEAzdQ4gHipht7MFVw/npJxyR4N4AP9Jz/ZvRVsgyNbm3k="}]},"_npmUser":{"name":"amrshbib","email":"amr.shbib@gmail.com"},"directories":{},"maintainers":[{"name":"amrshbib","email":"amr.shbib@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/react-native-hmac_0.0.1_1779288717984_0.7315426438203698"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-20T14:51:57.810Z","0.0.1":"2026-05-20T14:51:58.125Z","modified":"2026-05-20T14:51:58.442Z"},"maintainers":[{"name":"amrshbib","email":"amr.shbib@gmail.com"}],"description":"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.","homepage":"https://github.com/amrshbib/react-native-hmac#readme","keywords":["react-native","hmac","hmac-sha256","sha256","signature","auth","authentication","security","socket.io","fetch","native","kotlin","swift"],"repository":{"type":"git","url":"git+https://github.com/amrshbib/react-native-hmac.git"},"bugs":{"url":"https://github.com/amrshbib/react-native-hmac/issues"},"license":"MIT","readme":"# react-native-hmac\n\nNative HMAC-SHA256 signer for React Native. Returns a fresh, replay-resistant\nsignature you can attach to **any** transport — Socket.IO, fetch / axios,\nSignalR, raw WebSocket, gRPC-Web, MQTT — anywhere you need to prove that a\nrequest came from your app.\n\nThe signing secret lives only in compiled native resources (Android string\nresource / iOS `Info.plist`). The JS layer **never holds it**, so it cannot be\nextracted from the Hermes/JSC bundle.\n\n- **One synchronous function.** `getSignature()` returns `{ signature, timestamp, version }`\n  — no Promise, no `await`. HMAC over the small canonical payload completes in\n  under a millisecond, so the cost of blocking the JS thread is negligible.\n- **Transport-agnostic.** Zero coupling to any specific networking library.\n- **Native signing.** `javax.crypto.Mac` on Android, `CryptoKit` on iOS.\n- **Zero JS crypto dependencies.** No `crypto-js`, no `react-native-quick-crypto`.\n- **Zero secret exposure to JS.** No `react-native-config`, no `.env` bundling.\n- **Replay-resistant.** Every signature includes a fresh timestamp.\n\n> **Requires Hermes or on-device JSC.** The native method is declared as a\n> blocking synchronous bridge call. It does **not** work in the Chrome remote\n> JS debugger. It works fine in Hermes (default in modern RN), in the\n> Hermes/JSC debugger, and on real devices.\n\n---\n\n## Quick start\n\n```sh\nyarn add react-native-hmac\ncd ios && pod install\n```\n\n```ts\nimport { getSignature } from \"react-native-hmac\";\n\nconst { signature, timestamp, version } = getSignature();\n// → { signature: \"ab12…\", timestamp: 1716220000000, version: \"v1\" }\n```\n\nThat's the whole client-side API. No await, no promise, no setup beyond\nconfiguring the native secret once (below).\n\n---\n\n## Configure the native secret (one-time per app)\n\nGenerate the secret:\n\n```sh\nopenssl rand -hex 32\n```\n\n### Android — one line\n\nAdd to `android/local.properties` (already in `.gitignore`):\n\n```properties\nHMAC_SECRET=...your-generated-secret...\n```\n\nThat's it. The library's own `build.gradle` reads this value at build time and\ninjects it as the Android string resource `react_native_hmac_secret`, which is\nmerged into your APK's `resources.arsc`. **Nothing to add to your app's\n`build.gradle`.**\n\nAlternative sources (checked in order):\n1. `-PHMAC_SECRET=...` Gradle property — handy for CI.\n2. `HMAC_SECRET` environment variable.\n3. `HMAC_SECRET` key in `android/local.properties`.\n\nTo override per-flavor or per-build-type, declare your own `resValue` with the\nsame name in `android/app/build.gradle` — app-level resValues win over\nlibrary-level ones during Android's resource merge.\n\n### iOS\n\n1. Create `ios/Secrets.xcconfig` (gitignored):\n\n    ```\n    REACT_NATIVE_HMAC_SECRET = ...your-generated-secret...\n    ```\n\n2. Reference it from your target's xcconfig.\n3. Add to `ios/<YourApp>/Info.plist`:\n\n    ```xml\n    <key>ReactNativeHmacSecret</key>\n    <string>$(REACT_NATIVE_HMAC_SECRET)</string>\n    ```\n\n---\n\n## Recipes — same `getSignature()`, any transport\n\n### Socket.IO\n\n```ts\nimport io from \"socket.io-client\";\nimport { getSignature } from \"react-native-hmac\";\n\nconst socket = io(url, {\n  auth: (cb) => cb(getSignature()),\n});\n```\n\nThe function form of `auth` is called by socket.io-client on every (re)connect,\nso the timestamp is always fresh.\n\n### Fetch\n\n```ts\nimport { getSignature } from \"react-native-hmac\";\n\nconst { signature, timestamp, version } = getSignature();\nconst res = await fetch(url, {\n  headers: {\n    \"x-hmac-signature\": signature,\n    \"x-hmac-timestamp\": String(timestamp),\n    \"x-hmac-version\": version,\n  },\n});\n```\n\n### Axios (request interceptor)\n\n```ts\nimport axios from \"axios\";\nimport { getSignature } from \"react-native-hmac\";\n\nconst api = axios.create({ baseURL: \"https://api.example.com\" });\napi.interceptors.request.use((config) => {\n  const { signature, timestamp, version } = getSignature();\n  config.headers[\"x-hmac-signature\"] = signature;\n  config.headers[\"x-hmac-timestamp\"] = String(timestamp);\n  config.headers[\"x-hmac-version\"] = version;\n  return config;\n});\n```\n\n### SignalR\n\n```ts\nimport { HubConnectionBuilder } from \"@microsoft/signalr\";\nimport { getSignature } from \"react-native-hmac\";\n\nconst connection = new HubConnectionBuilder()\n  .withUrl(url, {\n    accessTokenFactory: () => {\n      const { signature, timestamp } = getSignature();\n      return `${timestamp}.${signature}`;\n    },\n  })\n  .build();\n```\n\n### Raw WebSocket\n\n```ts\nimport { getSignature } from \"react-native-hmac\";\n\nconst { signature, timestamp } = getSignature();\nconst ws = new WebSocket(`${url}?ts=${timestamp}&sig=${signature}`);\n```\n\n### Reusable signer with bound claims\n\n```ts\nimport { createSigner } from \"react-native-hmac\";\n\nconst signWithTenant = createSigner({ claims: { tenantId: \"t_42\" } });\n\nconst a = signWithTenant();  // fresh timestamp, same claim bound\nconst b = signWithTenant();  // fresh timestamp, same claim bound\n```\n\n### Startup health-check\n\n```ts\nimport { isConfigured } from \"react-native-hmac\";\n\nif (!isConfigured()) {\n  // The native secret wasn't wired up in this build.\n}\n```\n\n---\n\n## API\n\n```ts\ngetSignature(options?: SignOptions): Signature\ncreateSigner(options?: SignOptions): () => Signature\nisConfigured(): boolean\nbuildPayloadString(timestamp: number, claims?): string\n\nclass HmacNotLinkedError      // app not rebuilt\nclass HmacSecretMissingError  // native resource not populated\nclass HmacSignError           // anything else\n```\n\nEverything is synchronous. Errors are thrown synchronously — wrap in try/catch\nif you want graceful degradation; otherwise let them propagate so missing\nconfiguration fails loud and early.\n\n### Options\n\n```ts\ntype SignOptions = {\n  /** Bound INTO the HMAC. The verifier must receive the same keys/values\n   *  and recompute the canonical payload to validate. */\n  claims?: Record<string, string | number | boolean>;\n};\n\ntype Signature = {\n  signature: string;   // lowercase hex\n  timestamp: number;   // epoch ms\n  version: \"v1\";\n};\n```\n\n### Canonical payload\n\nThe native side hashes one of:\n\n```\nv1:<timestamp>\nv1:<timestamp>:<sortedJsonClaims>\n```\n\nClaims are JSON-stringified with keys sorted alphabetically and no whitespace\n— any conformant JSON encoder reproduces the exact bytes, so any verifier on\nthe other end can recompute the same string.\n\n---\n\n## Verifying signatures (any backend / any language)\n\nThe canonical payload format is trivial to reproduce. Node.js example:\n\n```js\nconst { createHmac, timingSafeEqual } = require(\"node:crypto\");\n\nfunction canonicalPayload(timestamp, claims) {\n  if (!claims || Object.keys(claims).length === 0) return `v1:${timestamp}`;\n  const sorted = {};\n  for (const k of Object.keys(claims).sort()) sorted[k] = claims[k];\n  return `v1:${timestamp}:${JSON.stringify(sorted)}`;\n}\n\nfunction verify({ secret, signature, timestamp, claims, windowMs = 60_000 }) {\n  if (Math.abs(Date.now() - timestamp) > windowMs) return false;\n  const expected = createHmac(\"sha256\", secret)\n    .update(canonicalPayload(timestamp, claims))\n    .digest(\"hex\");\n  if (expected.length !== signature.length) return false;\n  return timingSafeEqual(Buffer.from(expected, \"hex\"), Buffer.from(signature, \"hex\"));\n}\n```\n\nUse the **same** secret on both sides — the value in `android/local.properties`\non the device build must equal `process.env.HMAC_SECRET` (or wherever) on your\nbackend.\n\nAlways check the `timestamp` is within a small window of `now` (60 s is a\nsensible default) to prevent replay.\n\n---\n\n## Security FAQ\n\n**Q. Why not just use `crypto-js` and `react-native-config`?**\nBecause both put the secret in your JS bundle. `react-native-config` injects\n`.env` values at *build time* as JS constants; `crypto-js` then needs the\nsecret in memory. An attacker with the APK can run\n`strings index.android.bundle | grep`. This library keeps the secret in\nnative resources and computes HMAC inside Kotlin/Swift — JS never sees the\nkey, even at runtime.\n\n**Q. Can the secret still be extracted?**\nOn a rooted device with Frida, an attacker can hook `Mac.doFinal()` and observe\nthe key in process memory. This is unsolvable for any pure client-side scheme.\nMitigations: root/jailbreak detection, certificate pinning, server-side\nanomaly detection, periodic server-driven secret rotation.\n\n**Q. Why HMAC and not JWT or asymmetric signatures?**\nHMAC is symmetric — one shared secret, simpler ops, ~10× faster signing than\nECDSA. If you also need per-user identity, layer a JWT on top of an\nHMAC-signed request.\n\n**Q. Why is the API synchronous? Doesn't that block the JS thread?**\nThe native bridge call is declared as a *blocking synchronous method*. HMAC\nover a tiny payload completes in well under a millisecond, so the thread is\nunblocked before a single frame is dropped. The benefit is a dramatically\ncleaner API — no `async` contagion through your code just because you wanted\nto sign a request.\n\n**Q. Does this work in Expo Go?**\nNo — Expo Go does not include arbitrary native modules. Use a development\nbuild (`eas build --profile development`) or the bare workflow.\n\n**Q. Does this work in the Chrome remote JS debugger?**\nNo — synchronous native calls aren't supported in the Chrome debugger. Use\nHermes inspector or on-device debugging. (Most modern RN setups use Hermes by\ndefault.)\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-846e4df90cf2affabc1f1f57d3308017"}