|
| 1 | +# Agents.md — AI Coding Agent Guide for react-native-keychain |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +**react-native-keychain** is a React Native library that provides secure credential storage using iOS Keychain and Android Keystore. It supports biometric authentication (Face ID, Touch ID, fingerprint), multiple encryption strategies, and both the Old and New (TurboModule/Fabric) React Native architectures. |
| 6 | + |
| 7 | +- **Package name**: `react-native-keychain` |
| 8 | +- **Version**: 10.0.0 |
| 9 | +- **License**: MIT |
| 10 | +- **Repository**: https://github.com/oblador/react-native-keychain |
| 11 | +- **Documentation**: https://oblador.github.io/react-native-keychain |
| 12 | +- **Platforms**: iOS 9+, Android API 23+, macOS Catalyst, visionOS 1.0+, tvOS 9+ |
| 13 | + |
| 14 | +## Repository Structure |
| 15 | + |
| 16 | +| Directory / File | Purpose | |
| 17 | +|------------------|---------| |
| 18 | +| `src/` | TypeScript source — public API (`index.ts`), enums, types, option normalization, and the TurboModule spec | |
| 19 | +| `lib/` | Build output (commonjs, ESM, type declarations) — generated by `yarn prepare`, do not edit | |
| 20 | +| `android/` | Android native code (Kotlin). Key areas: `KeychainModule.kt` (RN module), `cipherStorage/` (encryption implementations), `resultHandler/` (biometric prompt flow), `exceptions/` | |
| 21 | +| `ios/RNKeychainManager/` | iOS native code (Obj-C++). Single `.h`/`.mm` pair implementing the Keychain bridge | |
| 22 | +| `KeychainExample/` | Example/test app. `src/App.tsx` is the demo UI; `e2e/` contains Detox E2E tests and helpers | |
| 23 | +| `website/` | Docusaurus documentation site with versioned docs and TypeDoc-generated API reference | |
| 24 | +| `.github/workflows/` | GitHub Actions CI/CD (E2E tests, lint, docs deploy, NPM publish) | |
| 25 | +| `RNKeychain.podspec` | CocoaPods spec for iOS/macOS/tvOS/visionOS | |
| 26 | +| `package.json` | Library manifest, workspace config, and react-native-builder-bob build targets | |
| 27 | + |
| 28 | +## Tech Stack |
| 29 | + |
| 30 | +| Layer | Technology | |
| 31 | +|-------|-----------| |
| 32 | +| **JS/TS** | TypeScript (strict mode), React Native TurboModules | |
| 33 | +| **Android** | Kotlin, Android Keystore, AndroidX Biometric (1.1.0), AndroidX DataStore, Coroutines | |
| 34 | +| **iOS** | Objective-C++ (.mm), Security.framework, LocalAuthentication.framework | |
| 35 | +| **Build** | react-native-builder-bob (commonjs + ESM + typedefs) | |
| 36 | +| **Package Manager** | Yarn 3.6.1 (workspaces) | |
| 37 | +| **Tests** | Detox (E2E), Jest | |
| 38 | +| **Docs** | Docusaurus 3 + TypeDoc | |
| 39 | +| **CI/CD** | GitHub Actions | |
| 40 | +| **Codegen** | React Native Codegen (`RNKeychainSpec`) for New Architecture | |
| 41 | + |
| 42 | +## Architecture |
| 43 | + |
| 44 | +### Native Bridge |
| 45 | + |
| 46 | +The library uses React Native's **TurboModule** system (New Architecture) with backward compatibility for the Bridge (Old Architecture): |
| 47 | + |
| 48 | +1. `src/NativeKeychainManager.ts` defines the `Spec` interface via `TurboModuleRegistry.getEnforcing<Spec>('RNKeychainManager')`. |
| 49 | +2. `src/index.ts` wraps all native calls with TypeScript types and option normalization. |
| 50 | +3. On iOS, `RNKeychainManager.mm` conditionally implements `NativeKeychainManagerSpec` (New Arch) or `RCTBridgeModule` (Old Arch). |
| 51 | +4. On Android, `KeychainModule.kt` is a `@ReactModule` supporting both architectures via conditional source sets (`src/newarch` vs `src/oldarch`). |
| 52 | + |
| 53 | +### Android Encryption Strategy |
| 54 | + |
| 55 | +Android uses a pluggable cipher storage system: |
| 56 | + |
| 57 | +- **AES-GCM** (`CipherStorageKeystoreAesGcm`): Primary cipher with optional biometric auth |
| 58 | +- **AES-GCM No Auth** (`CipherStorageKeystoreAesGcm` without auth): For non-biometric storage |
| 59 | +- **RSA-ECB** (`CipherStorageKeystoreRsaEcb`): Asymmetric encryption with biometric auth |
| 60 | +- **AES-CBC** (`CipherStorageKeystoreAesCbc`): Legacy/deprecated |
| 61 | + |
| 62 | +Result handlers manage the biometric prompt flow: `ResultHandlerInteractiveBiometric` for interactive auth, `ResultHandlerNonInteractive` for no-auth operations. |
| 63 | + |
| 64 | +### iOS Keychain Integration |
| 65 | + |
| 66 | +iOS uses Security.framework directly: |
| 67 | +- `kSecClassGenericPassword` for generic passwords |
| 68 | +- `kSecClassInternetPassword` for internet credentials |
| 69 | +- Supports all `kSecAttrAccessible*` levels and `SecAccessControl` for biometric gates |
| 70 | +- Safari shared web credentials via `SecRequestSharedWebCredential` / `SecAddSharedWebCredential` |
| 71 | + |
| 72 | +## Public API |
| 73 | + |
| 74 | +All functions are exported from `src/index.ts`: |
| 75 | + |
| 76 | +| Function | Description | |
| 77 | +|----------|-------------| |
| 78 | +| `setGenericPassword(username, password, options?)` | Store credentials by service | |
| 79 | +| `getGenericPassword(options?)` | Retrieve credentials by service | |
| 80 | +| `hasGenericPassword(options?)` | Check if credentials exist | |
| 81 | +| `resetGenericPassword(options?)` | Delete credentials by service | |
| 82 | +| `getAllGenericPasswordServices(options?)` | List all service keys | |
| 83 | +| `setInternetCredentials(server, username, password, options?)` | Store credentials by server URL | |
| 84 | +| `getInternetCredentials(server, options?)` | Retrieve credentials by server | |
| 85 | +| `hasInternetCredentials(options)` | Check if internet credentials exist | |
| 86 | +| `resetInternetCredentials(options)` | Delete internet credentials | |
| 87 | +| `getSupportedBiometryType()` | Get device biometry type | |
| 88 | +| `canImplyAuthentication(options?)` | Check auth policy support (iOS) | |
| 89 | +| `getSecurityLevel(options?)` | Get security level (Android) | |
| 90 | +| `isPasscodeAuthAvailable()` | Check passcode auth availability | |
| 91 | +| `requestSharedWebCredentials()` | Get Safari shared credentials (iOS) | |
| 92 | +| `setSharedWebCredentials(server, username, password?)` | Set Safari shared credentials (iOS) | |
| 93 | + |
| 94 | +### Key Enums |
| 95 | + |
| 96 | +- **`ACCESSIBLE`**: When keychain items are accessible (e.g., `WHEN_UNLOCKED`, `AFTER_FIRST_UNLOCK`) |
| 97 | +- **`ACCESS_CONTROL`**: Auth gates (e.g., `BIOMETRY_ANY`, `DEVICE_PASSCODE`, `BIOMETRY_ANY_OR_DEVICE_PASSCODE`) |
| 98 | +- **`AUTHENTICATION_TYPE`**: Biometrics vs passcode authentication |
| 99 | +- **`SECURITY_LEVEL`**: Android security level (`ANY`, `SECURE_SOFTWARE`, `SECURE_HARDWARE`) |
| 100 | +- **`STORAGE_TYPE`**: Android cipher type (`AES_GCM`, `AES_GCM_NO_AUTH`, `RSA`, `AES_CBC`) |
| 101 | +- **`BIOMETRY_TYPE`**: Device biometry (`TOUCH_ID`, `FACE_ID`, `OPTIC_ID`, `FINGERPRINT`, `FACE`, `IRIS`) |
| 102 | +- **`ERROR_CODE`**: Standardized error codes (`PASSCODE_NOT_SET`, `BIOMETRIC_NOT_ENROLLED`, etc.) |
| 103 | + |
| 104 | +### Key Types |
| 105 | + |
| 106 | +- **`SetOptions`**: `accessible`, `securityLevel`, `storage`, `accessControl`, `authenticationPrompt`, `service`, `cloudSync`, `accessGroup` |
| 107 | +- **`GetOptions`**: `accessControl`, `authenticationPrompt`, `service` |
| 108 | +- **`BaseOptions`**: `service`, `server`, `cloudSync`, `accessGroup` |
| 109 | +- **`Result`**: `{ service, storage }` |
| 110 | +- **`UserCredentials`**: `{ username, password, service, storage }` |
| 111 | +- **`AuthenticationPrompt`**: `{ title?, subtitle?, description?, cancel? }` |
| 112 | + |
| 113 | +## Development |
| 114 | + |
| 115 | +### Prerequisites |
| 116 | + |
| 117 | +- Node.js >= 16 |
| 118 | +- Yarn 3.6.1 (uses workspaces) |
| 119 | +- Xcode (for iOS) |
| 120 | +- Android SDK with API 23+ (for Android) |
| 121 | +- CocoaPods (for iOS) |
| 122 | + |
| 123 | +### Setup |
| 124 | + |
| 125 | +```bash |
| 126 | +# Install dependencies (all workspaces) |
| 127 | +yarn install |
| 128 | + |
| 129 | +# Build the library (TypeScript → lib/) |
| 130 | +yarn prepare |
| 131 | + |
| 132 | +# Run typecheck |
| 133 | +yarn typecheck |
| 134 | + |
| 135 | +# Run linter |
| 136 | +yarn lint |
| 137 | + |
| 138 | +# Format code |
| 139 | +yarn format |
| 140 | +``` |
| 141 | + |
| 142 | +### Example App |
| 143 | + |
| 144 | +```bash |
| 145 | +cd KeychainExample |
| 146 | + |
| 147 | +# iOS |
| 148 | +pod install --project-directory=ios |
| 149 | +yarn start # Metro bundler |
| 150 | +# Build & run via Xcode or react-native run-ios |
| 151 | + |
| 152 | +# Android |
| 153 | +yarn start # Metro bundler |
| 154 | +# Build & run via Android Studio or react-native run-android |
| 155 | +``` |
| 156 | + |
| 157 | +### Running E2E Tests |
| 158 | + |
| 159 | +Tests use **Detox** with physical emulator/simulator: |
| 160 | + |
| 161 | +```bash |
| 162 | +cd KeychainExample |
| 163 | + |
| 164 | +# iOS |
| 165 | +yarn test:ios # Builds + runs Detox tests |
| 166 | + |
| 167 | +# Android |
| 168 | +yarn test:android # Builds + runs Detox tests |
| 169 | +``` |
| 170 | + |
| 171 | +Test specs are in `KeychainExample/e2e/testCases/`. They cover: |
| 172 | +- Access control (passcode, biometric) for both credential types |
| 173 | +- Storage types (AES-CBC, AES-GCM, AES-GCM-NO-AUTH, RSA) — Android only |
| 174 | +- Security levels (Any, Software, Hardware) — Android only |
| 175 | + |
| 176 | +E2E tests use biometric simulation: `device.matchFace()` on iOS, ADB fingerprint touch on Android. Passcode on Android is `1111`. |
| 177 | + |
| 178 | +### Build Output |
| 179 | + |
| 180 | +`yarn prepare` (via react-native-builder-bob) generates: |
| 181 | +- `lib/commonjs/` — CommonJS modules (with ESM interop) |
| 182 | +- `lib/module/` — ES modules |
| 183 | +- `lib/typescript/` — Type declarations |
| 184 | + |
| 185 | +## CI/CD Pipelines |
| 186 | + |
| 187 | +| Workflow | Trigger | What it does | |
| 188 | +|----------|---------|-------------| |
| 189 | +| `e2e_tests.yaml` | Push/PR to master | Builds APKs + iOS app, runs Detox E2E tests on Android API 33/34/35 emulators and iOS simulator | |
| 190 | +| `lint_and_types.yaml` | Push/PR to master | Runs ESLint + TypeScript typecheck | |
| 191 | +| `docs.yaml` | Release created | Builds Docusaurus site, deploys to GitHub Pages | |
| 192 | +| `deploy.yaml` | Release created | Builds library via bob, publishes to NPM | |
| 193 | + |
| 194 | +## Coding Conventions |
| 195 | + |
| 196 | +- **TypeScript**: Strict mode, no unused locals/parameters, `verbatimModuleSyntax`, ESNext target |
| 197 | +- **Android**: Kotlin with coroutines, Java 17 compatibility, AndroidX libraries |
| 198 | +- **iOS**: Objective-C++ (.mm files), supports both TurboModule and Bridge |
| 199 | +- **Formatting**: Prettier for JS/TS/JSON/MD files |
| 200 | +- **Linting**: ESLint with `@react-native/eslint-config` |
| 201 | +- **Option normalization**: All public functions normalize options through `normalizeAuthPrompt()` which sets default `title` and `cancel` for auth prompts |
| 202 | + |
| 203 | +## Error Handling |
| 204 | + |
| 205 | +Both platforms use standardized error codes (defined in `ERROR_CODE` enum): |
| 206 | + |
| 207 | +| Code | Meaning | |
| 208 | +|------|---------| |
| 209 | +| `E_PASSCODE_NOT_SET` | Device has no passcode/PIN configured | |
| 210 | +| `E_BIOMETRIC_NOT_ENROLLED` | No biometrics enrolled on device | |
| 211 | +| `E_BIOMETRIC_UNAVAILABLE` | Biometric hardware unavailable | |
| 212 | +| `E_BIOMETRIC_LOCKOUT` | Too many failed biometric attempts | |
| 213 | +| `E_AUTH_CANCELED` | User canceled authentication | |
| 214 | +| `E_AUTH_ERROR` | General authentication error | |
| 215 | +| `E_INVALID_PARAMETERS` | Invalid parameters passed | |
| 216 | +| `E_STORAGE_ACCESS_ERROR` | Keychain/Keystore access error | |
| 217 | +| `E_INTERNAL_ERROR` | Unexpected internal error | |
| 218 | + |
| 219 | +## Common Tasks for Agents |
| 220 | + |
| 221 | +### Adding a new public API method |
| 222 | + |
| 223 | +1. Add the method signature to `src/NativeKeychainManager.ts` (`Spec` interface) |
| 224 | +2. Implement the TypeScript wrapper in `src/index.ts` with JSDoc |
| 225 | +3. Add relevant types to `src/types.ts` if needed |
| 226 | +4. Implement the native method in `ios/RNKeychainManager/RNKeychainManager.mm` |
| 227 | +5. Implement the native method in `android/src/main/java/com/oblador/keychain/KeychainModule.kt` |
| 228 | +6. Add to the default export object in `src/index.ts` |
| 229 | +7. Run `yarn typecheck` to validate TypeScript |
| 230 | +8. Add E2E test coverage in `KeychainExample/e2e/testCases/` |
| 231 | + |
| 232 | +### Adding a new enum value |
| 233 | + |
| 234 | +1. Add the value to the relevant enum in `src/enums.ts` |
| 235 | +2. Handle the new value in the corresponding native code (iOS `.mm` and/or Android `.kt`) |
| 236 | +3. Update the example app (`KeychainExample/src/App.tsx`) if it has a selector for that enum |
| 237 | + |
| 238 | +### Adding a new Android cipher storage |
| 239 | + |
| 240 | +1. Create a new class in `android/src/main/java/com/oblador/keychain/cipherStorage/` implementing `CipherStorage` |
| 241 | +2. Register it in `KeychainModule.kt` |
| 242 | +3. Add a corresponding `STORAGE_TYPE` enum value in `src/enums.ts` |
| 243 | +4. Add E2E test in `storageTypesTest.spec.js` |
| 244 | + |
| 245 | +### Modifying iOS Keychain behavior |
| 246 | + |
| 247 | +1. Edit `ios/RNKeychainManager/RNKeychainManager.mm` |
| 248 | +2. Map any new error domains to standardized `ERROR_CODE` values |
| 249 | +3. Guard platform-specific code with `TARGET_OS_IOS`, `TARGET_OS_VISION`, or `TARGET_OS_UIKITFORMAC` |
| 250 | + |
| 251 | +### Updating documentation |
| 252 | + |
| 253 | +1. Edit Markdown files in `website/docs/` |
| 254 | +2. API docs are auto-generated from TSDoc comments in `src/index.ts` via TypeDoc |
| 255 | +3. For new versions, use Docusaurus versioning (existing versions: 9.0.x, 9.1.x, 9.2.x) |
0 commit comments