Skip to content

v6.0.0

Choose a tag to compare

@mCodex mCodex released this 28 Apr 15:35
· 27 commits to master since this release

πŸ” v6.0.0 β€” Release Notes (5.6.2 β†’ 6.0.0) πŸš€

react-native-sensitive-info 6 is a from-scratch rewrite on top of Nitro Modules and the React Native New Architecture. It is not a drop-in upgrade from 5.6.2 β€” the API surface is intentionally narrower, fully typed, and metadata-rich. Plan a migration window and use the migration guide.

⚑ TL;DR

  • New runtime: Nitro hybrid object replaces the legacy bridge module. Requires React Native β‰₯ 0.80 with the New Architecture enabled.
  • Promise-based API throughout; every read/write returns rich StorageMetadata.
  • Typed errors (SensitiveInfoError subclasses) with instanceof predicates β€” no more string-matching.
  • First-class React hooks under react-native-sensitive-info/hooks.
  • Versioned key rotation with lazy re-encryption.
  • Defense-in-depth hardening: HMAC integrity tag, AES-GCM AAD binding, setUnlockedDeviceRequired, plaintext zeroization, constant-time comparisons.
  • Tree-shakeable subpath exports (., /hooks, /errors); "sideEffects": false.
  • Windows is no longer supported. v6 targets Android + Apple platforms (iOS, macOS, visionOS, watchOS).
  • First-class Expo config plugin β€” Face ID usage description, biometric permissions, and New Architecture flags wired up automatically. Expo Go is not supported; use a Dev Client or EAS Build.

πŸ€” Why upgrade?

5.6.2 6.0.0
Runtime Legacy bridge module (Paper-compatible) Nitro hybrid object (New Architecture only)
Architecture Old + Fabric (bridge fallback) New Architecture required
API surface Mixed sync/async, manual flag soup (kSecAccessControl*, keystore) Promise-based, typed, single accessControl enum
Return types void writes, raw value reads MutationResult writes, SensitiveInfoItem | null reads with metadata
Errors Plain Error instances, message string-matching Typed subclasses + is*Error predicates, dedicated /errors subpath
Metadata None securityLevel, backend, accessControl, keyVersion, integrityTag, timestamp
Key rotation Not available rotateKeys() + getKeyVersion() with lazy / eager re-encryption
Integrity AES-GCM tag only AES-GCM tag + HMAC-SHA256 over (service, key, version, accessControl, securityLevel, timestamp, ciphertext, iv)
Replay/swap defense None AES-GCM AAD bound to service|key|v<version> (Android); kSecAttrService + kSecAttrAccount (iOS)
Plaintext lifetime Best-effort Zeroized on both platforms after encrypt/decrypt
React hooks None useSecret, useSecretItem, useHasSecret, useSecureStorage, useSecurityAvailability, useKeyRotation, useSecureOperation
Expo support None / community plugin First-party app.plugin.js (Info.plist, AndroidManifest, new-arch flags)
Windows support Yes (legacy) Removed
Bundle size Single CJS entry "sideEffects": false, subpath exports, no default export
Lint/format toolchain ESLint + Prettier Biome 2 (single config)
TypeScript Loose typings Strict, exactOptionalPropertyTypes, declaration maps

✨ Highlights

🏎️ Nitro hybrid core (RN 0.80+ / New Architecture)

The core moves from the legacy module bridge to a Nitro hybrid object. Native calls bypass the JS bridge serialization layer, so secure-storage operations run with significantly lower marshalling overhead and predictable latency.

  • iOS / Apple platforms: Swift + CryptoKit + Keychain, Secure Enclave-gated AES-GCM.
  • Android: Kotlin + Keystore (StrongBox-aware) with EncryptedSharedPreferences software fallback.

πŸ“¦ Promise-based API with metadata

import { setItem, getItem } from 'react-native-sensitive-info'

const result = await setItem('session-token', 'super-secret', {
  service: 'auth',
  accessControl: 'secureEnclaveBiometry',
})
// result.metadata: { securityLevel, backend, accessControl, keyVersion, integrityTag, ... }

const item = await getItem('session-token', { service: 'auth' })
// item: { key, service, value?, metadata } | null

Every read/write returns StorageMetadata, so apps can confirm the actual security level applied (e.g. refuse to store secrets when metadata.securityLevel === 'software').

🚨 Typed errors

import {
  isAuthenticationCanceledError,
  isIntegrityViolationError,
  isKeyInvalidatedError,
} from 'react-native-sensitive-info/errors'

try {
  await getItem('token', { service: 'auth' })
} catch (error) {
  if (isAuthenticationCanceledError(error)) return
  if (isKeyInvalidatedError(error)) {
    // Hardware key invalidated (e.g. biometrics re-enrolled)
  }
  throw error
}

Predicates: isNotFoundError, isAuthenticationCanceledError, isIntegrityViolationError, isKeyInvalidatedError, isRotationFailedError, isInvalidArgumentError.

βš›οΈ React hooks

A focused hooks API ships at react-native-sensitive-info/hooks:

Hook Use case
useSecureStorage List + CRUD over a service
useSecret Single secret + save/delete
useSecretItem Single secret read
useHasSecret Lightweight existence check
useSecurityAvailability Device capability snapshot
useKeyRotation Bump and inspect master-key version
useSecureOperation Wrap arbitrary imperative calls in a state machine

All hooks share the same lifecycle/abort/error pipeline (useAsyncQuery, useMutation) and never tear state on unmount.

πŸ” Versioned key rotation

import { rotateKeys, getKeyVersion } from 'react-native-sensitive-info'

await rotateKeys({ service: 'auth' })                    // lazy, upgrades on next read
await rotateKeys({ service: 'auth', reEncryptEagerly: true }) // eager
const version = await getKeyVersion({ service: 'auth' })
  • iOS: SecItemUpdate preserves existing access-control attributes while bumping keyVersion.
  • Android: mints a fresh per-entry Keystore alias (SensitiveInfo_<hash>_v<version>) and deletes the stale one after a successful rewrite.

πŸ›‘οΈ Defense-in-depth hardening

Applied transparently to new writes and via lazy upgrade on rotation. Backwards compatible β€” entries written by older 6.x RC builds decode without verification and are upgraded on the next write or rotation.

  • HMAC-SHA256 integrity tag bound to every entry's metadata + ciphertext (StorageMetadata.integrityTag). Tampering raises IntegrityViolationError before any biometric prompt is shown.
  • AES-GCM AAD on Android binds ciphertext to service|key|v<version>, defeating cross-entry swap attacks.
  • setUnlockedDeviceRequired(true) on every Android Keystore key (API 28+), mirroring iOS kSecAttrAccessibleWhenUnlocked semantics.
  • Plaintext byte buffers zeroized after use on both platforms.
  • Constant-time HMAC comparison (MessageDigest.isEqual / manual UInt8 XOR fold).

🌳 Tree-shaking & ESM-first packaging

"sideEffects": false everywhere, with three focused subpath entries:

Import Contents
react-native-sensitive-info setItem, getItem, hasItem, deleteItem, getAllItems, clearService, getSupportedSecurityLevels, rotateKeys, getKeyVersion, types
react-native-sensitive-info/hooks Every React hook
react-native-sensitive-info/errors Typed error classes + instanceof predicates

The default export is gone β€” use named imports.

🧩 Expo config plugin

A first-party app.plugin.js ships in the package. Add it to app.json / app.config.ts:

{
  "expo": {
    "plugins": [
      ["react-native-sensitive-info", {
        "faceIDPermission": "Authenticate to unlock your account.",
        "enableNewArchitecture": true
      }]
    ]
  }
}

Handles: NSFaceIDUsageDescription, USE_BIOMETRIC + legacy USE_FINGERPRINT (with maxSdkVersion=28), and New Architecture flags (newArchEnabled, RCT_NEW_ARCH_ENABLED). See docs/EXPO.md.

πŸ’₯ Breaking changes

A migration table for the most common call sites:

5.6.2 6.0.0
Bridge module, Old Architecture compatible Nitro hybrid object, New Architecture required
setItem(key, value, options) returns void setItem(key, value, options) returns Promise<MutationResult>
getItem(key, options) returns the raw value or null getItem(key, options) returns SensitiveInfoItem | null ({ key, service, value, metadata })
getAllItems(options) returns Record<string, string> getAllItems(options) returns SensitiveInfoItem[]
deleteItem(key, options) returns void deleteItem(key, options) returns Promise<boolean>
sharedPreferencesName (Android) / keychainService (iOS) unified as service
kSecAccessControl* strings + Android keystore config object single accessControl: 'secureEnclaveBiometry' | 'biometryCurrentSet' | 'biometryAny' | 'devicePasscode' | 'none'
setInvalidatedByBiometricEnrollment opt-out Always-on; biometric-bound entries raise KeyInvalidatedError deterministically
Errors are plain Error instances Typed SensitiveInfoError subclasses + is*Error predicates
Default + named exports from package root Named exports only; no default export; hooks moved to /hooks
Windows supported Windows removed

πŸ”‘ Access-control flag mapping

5.6.2 (kSecAccessControl…) 6.0.0 (accessControl)
kSecAccessControlBiometryCurrentSet + Secure Enclave class secureEnclaveBiometry
kSecAccessControlBiometryCurrentSet biometryCurrentSet
kSecAccessControlBiometryAny biometryAny
kSecAccessControlDevicePasscode devicePasscode
(no policy) none

🚚 Data migration

5.6.2 β†’ 6.0.0 is not wire-compatible. Existing entries written by 5.6.x are not readable by 6.x because the metadata envelope is different.

Recommended approach:

  1. Easiest: ship a release that simply re-prompts the user to authenticate (most secrets are short-lived by design).
  2. Or: ship a one-time migration that reads with a vendored 5.6 helper and re-writes via 6.x setItem.

See docs/MIGRATION.md for the step-by-step diff-based guide.

πŸ“‹ Requirements

  • React Native β‰₯ 0.80 (New Architecture enabled).
  • Node β‰₯ 18.
  • react-native-nitro-modules as a peer dependency.
  • iOS β‰₯ 13.0 Β· macOS β‰₯ 11.0 Β· visionOS β‰₯ 1.0 Β· watchOS β‰₯ 7.0.
  • Android API 23+ (StrongBox detection requires API 28+).
  • Expo SDK 52+ (Dev Client or EAS Build β€” Expo Go is not supported).

πŸ“₯ Installation

npm install react-native-sensitive-info react-native-nitro-modules
# or
yarn add react-native-sensitive-info react-native-nitro-modules
# or
pnpm add react-native-sensitive-info react-native-nitro-modules

iOS: cd ios && pod install. Expo: add "react-native-sensitive-info" to app.json plugins and run npx expo prebuild --clean.

🧭 Quick API tour

import {
  setItem,
  getItem,
  hasItem,
  deleteItem,
  getAllItems,
  clearService,
  getSupportedSecurityLevels,
  rotateKeys,
  getKeyVersion,
} from 'react-native-sensitive-info'
Method Signature
setItem (key, value, options?) => Promise<MutationResult>
getItem (key, options?) => Promise<SensitiveInfoItem | null>
hasItem (key, options?) => Promise<boolean>
deleteItem (key, options?) => Promise<boolean>
getAllItems (options?) => Promise<SensitiveInfoItem[]>
clearService (options?) => Promise<void>
getSupportedSecurityLevels () => Promise<SecurityAvailability>
rotateKeys (options?) => Promise<RotationResult>
getKeyVersion (options?) => Promise<number>