devendra-skill-package 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/skills/boiler-project-generator/SKILL.md +103 -0
- package/.github/skills/boiler-project-generator/assets/templates/android/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/angular/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/flutter/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/ios/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/react/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/README.md +3 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/api-setup.ts +76 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/component-primary-button.tsx +29 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/env-config.env +4 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-auth-screen.tsx +45 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-dashboard-screen.tsx +45 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-profile-screen.tsx +45 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/hook-use-app-boot.ts +11 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/navigation-setup.tsx +25 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/project-structure.md +24 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-hooks.ts +5 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-session-slice.ts +29 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-store.ts +15 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/theme-index.ts +23 -0
- package/.github/skills/boiler-project-generator/assets/templates/react-native/utils-helpers.ts +7 -0
- package/.github/skills/boiler-project-generator/metadata.yaml +29 -0
- package/.github/skills/boiler-project-generator/references/android.md +19 -0
- package/.github/skills/boiler-project-generator/references/angular.md +14 -0
- package/.github/skills/boiler-project-generator/references/flutter.md +17 -0
- package/.github/skills/boiler-project-generator/references/ios.md +17 -0
- package/.github/skills/boiler-project-generator/references/react-native.md +103 -0
- package/.github/skills/boiler-project-generator/references/react.md +17 -0
- package/.github/skills/boiler-project-generator/scripts/angular.js +43 -0
- package/.github/skills/boiler-project-generator/scripts/generate-project.mjs +107 -0
- package/.github/skills/boiler-project-generator/scripts/init.js +105 -0
- package/.github/skills/boiler-project-generator/scripts/react-native.js +1041 -0
- package/.github/skills/boiler-project-generator/scripts/react.js +32 -0
- package/.github/skills/clean-architecture-generator/SKILL.md +235 -0
- package/.github/skills/clean-architecture-generator/references/architecture-core.md +126 -0
- package/.github/skills/clean-architecture-generator/references/platform-angular.md +374 -0
- package/.github/skills/clean-architecture-generator/references/platform-flutter.md +366 -0
- package/.github/skills/clean-architecture-generator/references/platform-react-native.md +590 -0
- package/.github/skills/clean-architecture-generator/references/platform-react.md +261 -0
- package/.github/skills/code-test-coverage-analyzer/SKILL.md +1358 -0
- package/.github/skills/code-test-coverage-analyzer/assets/config-questionnaire.md +118 -0
- package/.github/skills/code-test-coverage-analyzer/assets/conflict-resolution-protocol.md +99 -0
- package/.github/skills/code-test-coverage-analyzer/assets/coverage-calculation-rules.md +294 -0
- package/.github/skills/code-test-coverage-analyzer/assets/multi-strategy-search.md +145 -0
- package/.github/skills/code-test-coverage-analyzer/assets/output-file-contracts.md +651 -0
- package/.github/skills/code-test-coverage-analyzer/assets/status-definitions.md +273 -0
- package/.github/skills/code-test-coverage-analyzer/examples/example-invocation.md +208 -0
- package/.github/skills/code-test-coverage-analyzer/references/android-patterns.md +229 -0
- package/.github/skills/code-test-coverage-analyzer/references/angular-patterns.md +177 -0
- package/.github/skills/code-test-coverage-analyzer/references/backend-patterns.md +383 -0
- package/.github/skills/code-test-coverage-analyzer/references/flutter-patterns.md +254 -0
- package/.github/skills/code-test-coverage-analyzer/references/ios-patterns.md +228 -0
- package/.github/skills/code-test-coverage-analyzer/references/react-native-patterns.md +431 -0
- package/.github/skills/code-test-coverage-analyzer/references/react-patterns.md +206 -0
- package/.github/skills/code-test-coverage-analyzer/scripts/detect-platform.ps1 +261 -0
- package/.github/skills/code-test-coverage-analyzer/scripts/scan-repository.ps1 +242 -0
- package/.github/skills/code-test-coverage-analyzer/templates/brd-compliance-row.md +107 -0
- package/.github/skills/code-test-coverage-analyzer/templates/test-case-row.md +130 -0
- package/.github/skills/code-test-coverage-analyzer/templates/user-story-compliance-row.md +170 -0
- package/.github/skills/custom-font-integrator/SKILL.md +373 -0
- package/.github/skills/custom-font-integrator/assets/FontTestScreen.js +150 -0
- package/.github/skills/custom-font-integrator/references/example-info-plist.xml +21 -0
- package/.github/skills/custom-font-integrator/scripts/setup-fonts.sh +99 -0
- package/.github/skills/encryption-decryption-utility/Instructions.md +282 -0
- package/.github/skills/encryption-decryption-utility/README.md +222 -0
- package/.github/skills/encryption-decryption-utility/SKILL.md +645 -0
- package/.github/skills/encryption-decryption-utility/assets/encryption-config-template.json +13 -0
- package/.github/skills/encryption-decryption-utility/assets/platform-matrix.json +180 -0
- package/.github/skills/encryption-decryption-utility/metadata.yaml +51 -0
- package/.github/skills/encryption-decryption-utility/references/android-crypto.md +157 -0
- package/.github/skills/encryption-decryption-utility/references/angular-crypto.md +130 -0
- package/.github/skills/encryption-decryption-utility/references/approach-1-webcrypto.md +95 -0
- package/.github/skills/encryption-decryption-utility/references/approach-2-rsa-aes-cbc.md +111 -0
- package/.github/skills/encryption-decryption-utility/references/approach-3-aes-sharedkey.md +114 -0
- package/.github/skills/encryption-decryption-utility/references/architecture.md +233 -0
- package/.github/skills/encryption-decryption-utility/references/backend-alignment.md +506 -0
- package/.github/skills/encryption-decryption-utility/references/dependency-guidance.md +234 -0
- package/.github/skills/encryption-decryption-utility/references/ec-implementation.md +832 -0
- package/.github/skills/encryption-decryption-utility/references/error-handling-playbook.md +234 -0
- package/.github/skills/encryption-decryption-utility/references/flutter-crypto.md +105 -0
- package/.github/skills/encryption-decryption-utility/references/ios-crypto.md +338 -0
- package/.github/skills/encryption-decryption-utility/references/native-rsa-implementation.md +373 -0
- package/.github/skills/encryption-decryption-utility/references/node-crypto.md +86 -0
- package/.github/skills/encryption-decryption-utility/references/react-crypto.md +105 -0
- package/.github/skills/encryption-decryption-utility/references/react-native-crypto.md +1001 -0
- package/.github/skills/encryption-decryption-utility/references/react-web-crypto.md +668 -0
- package/.github/skills/encryption-decryption-utility/references/security-checklist.md +176 -0
- package/.github/skills/encryption-decryption-utility/references/web-crypto.md +74 -0
- package/.github/skills/encryption-decryption-utility/scripts/detect.js +128 -0
- package/.github/skills/encryption-decryption-utility/scripts/install.js +1713 -0
- package/.github/skills/encryption-decryption-utility/scripts/setup.js +800 -0
- package/.github/skills/encryption-decryption-utility/template.md +241 -0
- package/.github/skills/encryption-decryption-utility/templates/config-questionnaire.md +467 -0
- package/.github/skills/encryption-decryption-utility/templates/delivery-checklist.md +206 -0
- package/.github/skills/encryption-decryption-utility/templates/implementation-plan.md +304 -0
- package/.github/skills/feature-generator/SKILL.MD +2741 -0
- package/.github/skills/feature-generator/assets/fingerprint-schema.json +385 -0
- package/.github/skills/feature-generator/assets/spec-template.md +172 -0
- package/.github/skills/feature-generator/assets/transform-rules.json +82 -0
- package/.github/skills/feature-generator/references/api-envelope-patterns.md +327 -0
- package/.github/skills/feature-generator/references/screen-logic-patterns.md +399 -0
- package/.github/skills/feature-generator/references/state-library-patterns.md +464 -0
- package/.github/skills/figma-ui-mapper/README.md +34 -0
- package/.github/skills/figma-ui-mapper/SKILL.md +101 -0
- package/.github/skills/figma-ui-mapper/assets/templates/component-map-template.md +30 -0
- package/.github/skills/figma-ui-mapper/assets/templates/generation-report-template.md +49 -0
- package/.github/skills/figma-ui-mapper/metadata.yaml +22 -0
- package/.github/skills/figma-ui-mapper/references/android.md +11 -0
- package/.github/skills/figma-ui-mapper/references/angular.md +18 -0
- package/.github/skills/figma-ui-mapper/references/capacitor.md +18 -0
- package/.github/skills/figma-ui-mapper/references/common.md +23 -0
- package/.github/skills/figma-ui-mapper/references/expo.md +18 -0
- package/.github/skills/figma-ui-mapper/references/flutter.md +18 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/angular/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/capacitor/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/expo/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/flutter/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/ionic/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/kotlin-multiplatform/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/native-android/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/native-ios/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/next/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/nuxt/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/react/rules.md +19 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/react-native/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/svelte/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/frameworks/vue/rules.md +23 -0
- package/.github/skills/figma-ui-mapper/references/ionic.md +18 -0
- package/.github/skills/figma-ui-mapper/references/ios.md +11 -0
- package/.github/skills/figma-ui-mapper/references/kotlin-multiplatform.md +18 -0
- package/.github/skills/figma-ui-mapper/references/native-android.md +18 -0
- package/.github/skills/figma-ui-mapper/references/native-ios.md +18 -0
- package/.github/skills/figma-ui-mapper/references/next.md +18 -0
- package/.github/skills/figma-ui-mapper/references/nuxt.md +18 -0
- package/.github/skills/figma-ui-mapper/references/react-figma-guideline.md +21 -0
- package/.github/skills/figma-ui-mapper/references/react-native.md +18 -0
- package/.github/skills/figma-ui-mapper/references/react.md +18 -0
- package/.github/skills/figma-ui-mapper/references/script-usage.md +46 -0
- package/.github/skills/figma-ui-mapper/references/svelte.md +18 -0
- package/.github/skills/figma-ui-mapper/references/vue.md +18 -0
- package/.github/skills/figma-ui-mapper/references/workflow.md +74 -0
- package/.github/skills/figma-ui-mapper/scripts/generate-page-through-figma.js +635 -0
- package/.github/skills/figma-ui-mapper/scripts/generate-react-page-through-figma.js +4 -0
- package/.github/skills/frontend-task-breakdown/SKILL.md +734 -0
- package/.github/skills/md-file-converter/SKILL.md +291 -0
- package/.github/skills/md-file-converter/config/formats.json +114 -0
- package/.github/skills/md-file-converter/extractors/extract-docx.sh +120 -0
- package/.github/skills/md-file-converter/extractors/extract-eml.py +108 -0
- package/.github/skills/md-file-converter/extractors/extract-pdf.sh +97 -0
- package/.github/skills/md-file-converter/extractors/extract-pptx.py +143 -0
- package/.github/skills/md-file-converter/extractors/extract-xlsx.py +175 -0
- package/.github/skills/md-file-converter/scripts/check-tools.sh +47 -0
- package/.github/skills/md-file-converter/scripts/extract-archive.sh +83 -0
- package/.github/skills/md-file-converter/scripts/extract-ocr.sh +77 -0
- package/.github/skills/md-file-converter/scripts/get-metadata.sh +57 -0
- package/.github/skills/md-file-converter/templates/output.md +73 -0
- package/.github/skills/md-file-converter/templates/unsupported-object.md +6 -0
- package/.github/skills/mockAPI-contract-generator/SKILL.md +607 -0
- package/.github/skills/test-case-validator/CHANGELOG.md +155 -0
- package/.github/skills/test-case-validator/CONFIGURATION.md +130 -0
- package/.github/skills/test-case-validator/EXAMPLES.md +50 -0
- package/.github/skills/test-case-validator/QUALITY_SCORING.md +98 -0
- package/.github/skills/test-case-validator/README.md +111 -0
- package/.github/skills/test-case-validator/REPORT_TEMPLATE.md +116 -0
- package/.github/skills/test-case-validator/SKILL.md +209 -0
- package/.github/skills/test-case-validator/SYSTEM_PROMPT.md +127 -0
- package/.github/skills/test-case-validator/TRACEABILITY_ENGINE.md +152 -0
- package/.github/skills/test-case-validator/VALIDATION_RULES.md +411 -0
- package/.github/skills/test-case-validator/config/default.config.yaml +62 -0
- package/.github/skills/test-case-validator/docs/ARCHITECTURE.md +66 -0
- package/.github/skills/test-case-validator/docs/EXTENDING.md +85 -0
- package/.github/skills/test-case-validator/docs/FAQ.md +88 -0
- package/.github/skills/test-case-validator/docs/VERSIONING.md +55 -0
- package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-brd.md +43 -0
- package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-test-cases.csv +9 -0
- package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-user-story.md +29 -0
- package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/output-report.md +303 -0
- package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/input-spec.md +43 -0
- package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/input-test-files-index.md +56 -0
- package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/output-report.md +274 -0
- package/.github/skills/test-case-validator/prompts/00-applicability-resolution.md +61 -0
- package/.github/skills/test-case-validator/prompts/01-extraction-user-story.md +58 -0
- package/.github/skills/test-case-validator/prompts/02-extraction-brd.md +64 -0
- package/.github/skills/test-case-validator/prompts/03-extraction-test-cases.md +92 -0
- package/.github/skills/test-case-validator/prompts/04-traceability-matrix.md +41 -0
- package/.github/skills/test-case-validator/prompts/05-coverage-analysis.md +46 -0
- package/.github/skills/test-case-validator/prompts/06-missing-scenario-detection.md +32 -0
- package/.github/skills/test-case-validator/prompts/07-duplicate-detection.md +36 -0
- package/.github/skills/test-case-validator/prompts/08-risk-analysis.md +39 -0
- package/.github/skills/test-case-validator/prompts/09-gated-wiring-validation.md +53 -0
- package/.github/skills/test-case-validator/prompts/10-observation-report.md +56 -0
- package/.github/skills/test-case-validator/prompts/11-final-report-assembly.md +57 -0
- package/.github/skills/test-case-validator/templates/coverage-table.md +25 -0
- package/.github/skills/test-case-validator/templates/executive-summary.md +38 -0
- package/.github/skills/test-case-validator/templates/final-summary-block.md +87 -0
- package/.github/skills/test-case-validator/templates/observation-entry.md +41 -0
- package/.github/skills/test-case-validator/templates/rtm-row.md +25 -0
- package/.github/skills/validation-engine-builder/README.md +50 -0
- package/.github/skills/validation-engine-builder/SKILL.md +241 -0
- package/.github/skills/validation-engine-builder/references/core.md +158 -0
- package/.github/skills/validation-engine-builder/references/platform-angular.md +166 -0
- package/.github/skills/validation-engine-builder/references/platform-flutter.md +118 -0
- package/.github/skills/validation-engine-builder/references/platform-react-native.md +170 -0
- package/.github/skills/validation-engine-builder/references/platform-react.md +94 -0
- package/.github/skills/validation-engine-builder/scripts/apply-to-project.js +80 -0
- package/.github/skills/validation-engine-builder/scripts/generate-angular.js +271 -0
- package/.github/skills/validation-engine-builder/scripts/generate-flutter.js +298 -0
- package/.github/skills/validation-engine-builder/scripts/generate-react-native.js +217 -0
- package/.github/skills/validation-engine-builder/scripts/generate-react.js +163 -0
- package/.github/skills/validation-engine-builder/scripts/install-deps.js +53 -0
- package/.github/skills/validation-engine-builder/scripts/shared/detect-platform.js +68 -0
- package/.github/skills/validation-engine-builder/scripts/shared/npm-deps.js +103 -0
- package/.github/skills/validation-engine-builder/scripts/shared/rhf-hook.js +43 -0
- package/.github/skills/validation-engine-builder/scripts/shared/ts-messages-regex.js +62 -0
- package/.github/skills/validation-engine-builder/scripts/shared/util.js +44 -0
- package/.github/skills/validation-engine-builder/scripts/shared/yup-schemas.js +80 -0
- package/.github/skills/validation-engine-builder/template.md +73 -0
- package/.github/skills/validation-engine-builder/templates/messages.json +22 -0
- package/.github/skills/validation-engine-builder/templates/regex.json +16 -0
- package/.github/skills/validation-engine-builder/templates/schemas.json +48 -0
- package/bin/index.js +145 -0
- package/package.json +22 -0
- package/readme.md +0 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
# Platform: React Native
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
React Native supports six encryption approaches:
|
|
6
|
+
|
|
7
|
+
| Approach | Mode | Crypto | Libraries |
|
|
8
|
+
|----------|------|--------|-----------|
|
|
9
|
+
| 2 | Basic RSA+AES-CBC | jsencrypt + crypto-js | axios, jsencrypt, crypto-js, react-native-get-random-values |
|
|
10
|
+
| 3 | Basic AES-CBC shared key | crypto-js | axios, crypto-js, react-native-get-random-values |
|
|
11
|
+
| 4 | **Production JS** | AES-256-GCM + RSA-OAEP, per-request ephemeral keys, response decryption, keychain | react-native-quick-crypto, react-native-keychain |
|
|
12
|
+
| 5 | **Production Native** | OS-level crypto (javax.crypto.Cipher / CryptoKit) + RSA-OAEP, same JS interceptor shape | react-native-aes-crypto, react-native-rsa-native, react-native-keychain |
|
|
13
|
+
| 6 | **EC (ECDH+AES-256-GCM)** | ECDH key exchange (P-256) + HKDF-SHA256 + AES-256-GCM; per-request or session-init; EC + Native variant reuses NativeCryptoModule for AES | react-native-quick-crypto (createECDH, randomBytes), react-native-keychain, buffer |
|
|
14
|
+
|
|
15
|
+
> **Approach 1 (Web Crypto API) is NOT supported** — `crypto.subtle` is absent from the React Native JS runtime.
|
|
16
|
+
|
|
17
|
+
Approaches 2 and 3 are suitable for low-to-moderate sensitivity APIs or prototypes.
|
|
18
|
+
**Approaches 4, 5, and 6 are required for production apps** handling PII, financial data, or any sensitive user information.
|
|
19
|
+
|
|
20
|
+
**Approach 6 (EC)** provides forward secrecy: each request uses an ephemeral P-256 key pair. The derived AES key is never transmitted. See `references/ec-implementation.md` for full implementation details including Hermes Buffer compatibility helpers (`toBase64`, `byteCopy`, `QCBuffer`), HKDF-SHA256, and session-init variant code.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Approaches 2 & 3 — Basic (AES-CBC)
|
|
25
|
+
|
|
26
|
+
### File Naming Convention
|
|
27
|
+
|
|
28
|
+
| Role | Path |
|
|
29
|
+
|---------------|-------------------------------------------|
|
|
30
|
+
| Config | `src/encryption/encryption.config.ts` |
|
|
31
|
+
| Service | `src/encryption/encryption.service.ts` |
|
|
32
|
+
| Interceptor | `src/encryption/encryption.interceptor.ts`|
|
|
33
|
+
| Module wiring | `src/api/axiosInstance.ts` |
|
|
34
|
+
|
|
35
|
+
### Module Wiring
|
|
36
|
+
|
|
37
|
+
Import `react-native-get-random-values` at the very top of `index.js` (app entry point) — this must happen before any crypto-js import.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// index.js
|
|
41
|
+
import 'react-native-get-random-values'; // MUST be first import
|
|
42
|
+
import { AppRegistry } from 'react-native';
|
|
43
|
+
import App from './App';
|
|
44
|
+
import { name as appName } from './app.json';
|
|
45
|
+
AppRegistry.registerComponent(appName, () => App);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/api/axiosInstance.ts
|
|
50
|
+
import axios from 'axios';
|
|
51
|
+
import { applyEncryptionInterceptor } from '../encryption/encryption.interceptor';
|
|
52
|
+
const axiosInstance = axios.create({});
|
|
53
|
+
applyEncryptionInterceptor(axiosInstance);
|
|
54
|
+
// ENCRYPTION_INTERCEPTOR_PLACEHOLDER
|
|
55
|
+
export default axiosInstance;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Interceptor Pattern (Approaches 2 & 3)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
|
62
|
+
import { EncryptionService } from './encryption.service';
|
|
63
|
+
import { encryptionConfig } from './encryption.config';
|
|
64
|
+
|
|
65
|
+
const encryptionService = new EncryptionService();
|
|
66
|
+
|
|
67
|
+
export function applyEncryptionInterceptor(instance: AxiosInstance): void {
|
|
68
|
+
const cfg = encryptionConfig.encryption;
|
|
69
|
+
instance.interceptors.request.use(
|
|
70
|
+
async (config: InternalAxiosRequestConfig) => {
|
|
71
|
+
if (!cfg.enabled) return config;
|
|
72
|
+
if (!config.data) return config;
|
|
73
|
+
if (config.data instanceof FormData) return config;
|
|
74
|
+
if (cfg.excludeUrls?.some((u: string) => config.url?.startsWith(u))) return config;
|
|
75
|
+
try {
|
|
76
|
+
config.data = await encryptionService.encrypt(config.data);
|
|
77
|
+
config.headers['Content-Type'] = 'application/json';
|
|
78
|
+
} catch {}
|
|
79
|
+
return config;
|
|
80
|
+
},
|
|
81
|
+
(error: unknown) => Promise.reject(error)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Dependencies (Approaches 2 & 3)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install axios jsencrypt crypto-js react-native-get-random-values
|
|
90
|
+
npm install --save-dev @types/crypto-js
|
|
91
|
+
npx pod-install # bare React Native only
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Approach 4 — Production React Native JS Mode (AES-256-GCM)
|
|
97
|
+
|
|
98
|
+
### Architecture
|
|
99
|
+
|
|
100
|
+
- AES-256-GCM with 32-byte per-request ephemeral key, 12-byte IV, 16-byte auth tag
|
|
101
|
+
- RSA-OAEP (SHA-256) key wrapping — server's public key fetched from API at startup
|
|
102
|
+
- Per-request in-memory key map (`requestId → { aesKey, iv }`) — cleared immediately after response
|
|
103
|
+
- Response decryption using the same per-request AES key with a fresh server-generated IV
|
|
104
|
+
- Secure token storage via `react-native-keychain`
|
|
105
|
+
- Feature flag: `ENCRYPTION_ENABLED`
|
|
106
|
+
|
|
107
|
+
### File Naming Convention
|
|
108
|
+
|
|
109
|
+
| Role | Path |
|
|
110
|
+
|------------------------|-----------------------------------------------|
|
|
111
|
+
| Types | `src/services/security/types.ts` |
|
|
112
|
+
| Encryption service | `src/services/security/encryption.service.ts` |
|
|
113
|
+
| Decryption service | `src/services/security/decryption.service.ts` |
|
|
114
|
+
| RSA key service | `src/services/security/rsa-key.service.ts` |
|
|
115
|
+
| Keychain storage | `src/services/security/keychain.service.ts` |
|
|
116
|
+
| HMAC service | `src/services/security/hmac.service.ts` |
|
|
117
|
+
| Encryption config | `src/services/security/encryption-config.ts` |
|
|
118
|
+
| Request interceptor | `src/services/security/encryption.interceptor.ts` |
|
|
119
|
+
| Module wiring | `src/api/axiosInstance.ts` |
|
|
120
|
+
|
|
121
|
+
### TypeScript Envelope Types
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// src/services/security/types.ts
|
|
125
|
+
export interface EncryptedEnvelope {
|
|
126
|
+
data: string; // base64(ciphertext + 16-byte GCM auth_tag)
|
|
127
|
+
key: string; // base64(RSA-OAEP encrypted AES key bytes)
|
|
128
|
+
iv: string; // base64(12-byte IV)
|
|
129
|
+
nonce: string; // appId^unix_timestamp_ms.random_hex_16
|
|
130
|
+
key_id: string; // RSA key version identifier from key endpoint
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface EncryptedResponse {
|
|
134
|
+
data: string; // base64(ciphertext + 16-byte GCM auth_tag)
|
|
135
|
+
iv: string; // base64(new server-generated 12-byte IV — never the request IV)
|
|
136
|
+
nonce?: string; // optional server nonce
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Encryption Service (Approach 4)
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// src/services/security/encryption.service.ts
|
|
144
|
+
import { randomBytes, createCipheriv } from 'react-native-quick-crypto';
|
|
145
|
+
|
|
146
|
+
export const encryptPayload = async (
|
|
147
|
+
plaintext: string,
|
|
148
|
+
rsaPublicKeyPem: string
|
|
149
|
+
): Promise<{ ciphertext: string; aesKey: Buffer; iv: Buffer }> => {
|
|
150
|
+
const aesKey = randomBytes(32);
|
|
151
|
+
const iv = randomBytes(12);
|
|
152
|
+
const cipher = createCipheriv('aes-256-gcm', aesKey, iv);
|
|
153
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
154
|
+
const tag = (cipher as any).getAuthTag() as Buffer;
|
|
155
|
+
const ciphertext = Buffer.concat([encrypted, tag]).toString('base64');
|
|
156
|
+
return { ciphertext, aesKey, iv };
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### RSA Key Service (Approach 4)
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// src/services/security/rsa-key.service.ts
|
|
164
|
+
import axios from 'axios';
|
|
165
|
+
|
|
166
|
+
let cachedPublicKey: string | null = null;
|
|
167
|
+
let cachedKeyId: string | null = null;
|
|
168
|
+
const MAX_RETRIES = 3;
|
|
169
|
+
|
|
170
|
+
export const getRsaPublicKey = async (
|
|
171
|
+
endpoint: string
|
|
172
|
+
): Promise<{ publicKey: string; keyId: string }> => {
|
|
173
|
+
if (cachedPublicKey && cachedKeyId) return { publicKey: cachedPublicKey, keyId: cachedKeyId };
|
|
174
|
+
let lastError: Error | null = null;
|
|
175
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
const { data } = await axios.get(endpoint);
|
|
178
|
+
cachedPublicKey = data.public_key;
|
|
179
|
+
cachedKeyId = data.key_id;
|
|
180
|
+
return { publicKey: data.public_key, keyId: data.key_id };
|
|
181
|
+
} catch (e: any) {
|
|
182
|
+
lastError = e;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`RSA key fetch failed after ${MAX_RETRIES} attempts: ${lastError?.message}`);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const invalidateRsaKey = () => {
|
|
189
|
+
cachedPublicKey = null;
|
|
190
|
+
cachedKeyId = null;
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Decryption Service (Approach 4)
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// src/services/security/decryption.service.ts
|
|
198
|
+
import { createDecipheriv } from 'react-native-quick-crypto';
|
|
199
|
+
|
|
200
|
+
export const decryptResponse = (
|
|
201
|
+
ciphertextBase64: string,
|
|
202
|
+
ivBase64: string,
|
|
203
|
+
aesKey: Buffer
|
|
204
|
+
): string => {
|
|
205
|
+
const combined = Buffer.from(ciphertextBase64, 'base64');
|
|
206
|
+
const tag = combined.slice(combined.length - 16);
|
|
207
|
+
const ciphertext = combined.slice(0, combined.length - 16);
|
|
208
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
209
|
+
const decipher = createDecipheriv('aes-256-gcm', aesKey, iv);
|
|
210
|
+
(decipher as any).setAuthTag(tag);
|
|
211
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
212
|
+
return decrypted.toString('utf8');
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Keychain Storage Service (Approach 4)
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// src/services/security/keychain.service.ts
|
|
220
|
+
import * as Keychain from 'react-native-keychain';
|
|
221
|
+
|
|
222
|
+
export const storeToken = async (key: string, value: string): Promise<void> => {
|
|
223
|
+
await Keychain.setGenericPassword(key, value, { service: key });
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const getToken = async (key: string): Promise<string | null> => {
|
|
227
|
+
const result = await Keychain.getGenericPassword({ service: key });
|
|
228
|
+
return result ? result.password : null;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const deleteToken = async (key: string): Promise<void> => {
|
|
232
|
+
await Keychain.resetGenericPassword({ service: key });
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### HMAC Service (Approach 4 — when enabled)
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// src/services/security/hmac.service.ts
|
|
240
|
+
import { createHmac } from 'react-native-quick-crypto';
|
|
241
|
+
|
|
242
|
+
export const buildCanonicalString = (
|
|
243
|
+
method: string,
|
|
244
|
+
url: string,
|
|
245
|
+
nonce: string,
|
|
246
|
+
encryptedData: string
|
|
247
|
+
): string => `${method.toUpperCase()}|${url}|${nonce}|${encryptedData}`;
|
|
248
|
+
|
|
249
|
+
export const signRequest = (canonical: string, hmacSecret: string): string => {
|
|
250
|
+
if (!hmacSecret) throw new Error('HMAC_SECRET is not configured');
|
|
251
|
+
const hmac = createHmac('sha256', hmacSecret);
|
|
252
|
+
hmac.update(canonical);
|
|
253
|
+
return hmac.digest('base64');
|
|
254
|
+
};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Encryption Config (Approach 4)
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// src/services/security/encryption-config.ts
|
|
261
|
+
export const EXCLUDED_PATHS: string[] = [];
|
|
262
|
+
// Add paths that must NOT be encrypted, e.g. '/api/health', '/api/auth/register'
|
|
263
|
+
|
|
264
|
+
export const ENCRYPTION_ENABLED = process.env.ENCRYPTION_ENABLED === 'true';
|
|
265
|
+
export const RSA_KEY_ENDPOINT = process.env.RSA_KEY_ENDPOINT ?? '<API_BASE_URL>/api/security/public-key';
|
|
266
|
+
export const HMAC_ENABLED = process.env.HMAC_ENABLED === 'true';
|
|
267
|
+
export const HMAC_SECRET = process.env.HMAC_SECRET ?? '';
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Request + Response Interceptors (Approach 4)
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// src/services/security/encryption.interceptor.ts
|
|
274
|
+
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
|
275
|
+
import { randomBytes, publicEncrypt } from 'react-native-quick-crypto';
|
|
276
|
+
import { encryptPayload } from './encryption.service';
|
|
277
|
+
import { decryptResponse } from './decryption.service';
|
|
278
|
+
import { getRsaPublicKey, invalidateRsaKey } from './rsa-key.service';
|
|
279
|
+
import { buildCanonicalString, signRequest } from './hmac.service';
|
|
280
|
+
import { ENCRYPTION_ENABLED, EXCLUDED_PATHS, RSA_KEY_ENDPOINT, HMAC_ENABLED, HMAC_SECRET } from './encryption-config';
|
|
281
|
+
|
|
282
|
+
// In-memory request map: requestId → { aesKey, iv }
|
|
283
|
+
const requestMap = new Map<string, { aesKey: Buffer; iv: Buffer }>();
|
|
284
|
+
|
|
285
|
+
export function applyProductionEncryptionInterceptor(instance: AxiosInstance): void {
|
|
286
|
+
// ── Request interceptor ──────────────────────────────────────────────────────
|
|
287
|
+
instance.interceptors.request.use(
|
|
288
|
+
async (config: InternalAxiosRequestConfig) => {
|
|
289
|
+
if (!ENCRYPTION_ENABLED) return config;
|
|
290
|
+
if (!config.data) return config;
|
|
291
|
+
if (config.data instanceof FormData) return config;
|
|
292
|
+
if (EXCLUDED_PATHS.some(p => config.url?.includes(p))) return config;
|
|
293
|
+
const method = (config.method ?? '').toUpperCase();
|
|
294
|
+
if (!['POST', 'PUT', 'PATCH'].includes(method)) return config;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const { publicKey, keyId } = await getRsaPublicKey(RSA_KEY_ENDPOINT);
|
|
298
|
+
const plaintext = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
|
|
299
|
+
const { ciphertext, aesKey, iv } = await encryptPayload(plaintext, publicKey);
|
|
300
|
+
|
|
301
|
+
// RSA-OAEP wrap the AES key
|
|
302
|
+
const encryptedKey = publicEncrypt({ key: publicKey, padding: 4 /* RSA_PKCS1_OAEP_PADDING */ }, aesKey).toString('base64');
|
|
303
|
+
|
|
304
|
+
const requestId = randomBytes(16).toString('hex');
|
|
305
|
+
const nonce = `${(config as any).appId ?? 'app'}^${Date.now()}.${randomBytes(8).toString('hex')}`;
|
|
306
|
+
|
|
307
|
+
requestMap.set(requestId, { aesKey, iv });
|
|
308
|
+
(config as any).__requestId = requestId;
|
|
309
|
+
|
|
310
|
+
config.data = { data: ciphertext, key: encryptedKey, iv: iv.toString('base64'), nonce, key_id: keyId };
|
|
311
|
+
config.headers['Content-Type'] = 'application/json';
|
|
312
|
+
config.headers['X-Request-Id'] = requestId;
|
|
313
|
+
|
|
314
|
+
if (HMAC_ENABLED) {
|
|
315
|
+
const canonical = buildCanonicalString(method, config.url ?? '', nonce, ciphertext);
|
|
316
|
+
const signature = signRequest(canonical, HMAC_SECRET);
|
|
317
|
+
config.headers['X-Request-Signature'] = signature;
|
|
318
|
+
config.headers['X-Nonce'] = nonce;
|
|
319
|
+
}
|
|
320
|
+
} catch (err: any) {
|
|
321
|
+
// Never block the request; log at debug only
|
|
322
|
+
if (__DEV__) console.debug('[EncryptionInterceptor] encrypt error:', err?.message);
|
|
323
|
+
}
|
|
324
|
+
return config;
|
|
325
|
+
},
|
|
326
|
+
(error: unknown) => Promise.reject(error)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// ── Response interceptor ─────────────────────────────────────────────────────
|
|
330
|
+
instance.interceptors.response.use(
|
|
331
|
+
(response: AxiosResponse) => {
|
|
332
|
+
const requestId = (response.config as any).__requestId;
|
|
333
|
+
if (!requestId || !requestMap.has(requestId)) return response;
|
|
334
|
+
const { aesKey } = requestMap.get(requestId)!;
|
|
335
|
+
requestMap.delete(requestId); // always clean up
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const encResp = response.data;
|
|
339
|
+
if (encResp?.data && encResp?.iv) {
|
|
340
|
+
response.data = JSON.parse(decryptResponse(encResp.data, encResp.iv, aesKey));
|
|
341
|
+
}
|
|
342
|
+
} catch (err: any) {
|
|
343
|
+
// Auth tag mismatch or tamper — trigger session reset
|
|
344
|
+
if (__DEV__) console.debug('[EncryptionInterceptor] decrypt error:', err?.message);
|
|
345
|
+
// Caller should handle session reset based on error type
|
|
346
|
+
throw Object.assign(new Error('DECRYPTION_AUTH_TAG_FAILURE'), { isAuthTagFailure: true });
|
|
347
|
+
}
|
|
348
|
+
return response;
|
|
349
|
+
},
|
|
350
|
+
async (error: any) => {
|
|
351
|
+
const requestId = error?.config?.__requestId;
|
|
352
|
+
if (requestId) requestMap.delete(requestId); // always clean up
|
|
353
|
+
|
|
354
|
+
// KEY_ID_NOT_FOUND: refresh key and retry exactly once
|
|
355
|
+
if (error?.response?.status === 401 && error?.response?.data?.code === 'KEY_ID_NOT_FOUND') {
|
|
356
|
+
invalidateRsaKey();
|
|
357
|
+
return instance.request(error.config);
|
|
358
|
+
}
|
|
359
|
+
return Promise.reject(error);
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Dependencies (Approach 4)
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npm install react-native-quick-crypto react-native-keychain
|
|
369
|
+
# RSA library — choose one compatible with your RN version:
|
|
370
|
+
# npm install react-native-rsa-native (for RSA-OAEP support)
|
|
371
|
+
npx pod-install # bare React Native
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Install rules:**
|
|
375
|
+
- Prefer pinning exact versions in production environments.
|
|
376
|
+
- Validate iOS pod install and Android Gradle sync after install.
|
|
377
|
+
- Confirm crypto methods available at runtime before shipping.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Approach 5 — Production Native Mode (OS-level crypto)
|
|
382
|
+
|
|
383
|
+
All of approach 4 applies. The only difference is the crypto backend:
|
|
384
|
+
- AES-256-GCM and RSA-OAEP operations run inside `javax.crypto.Cipher` (Android) and CryptoKit (iOS).
|
|
385
|
+
- Per-request AES keys are **generated in JS** using `randomBytes(32)` and **passed as parameters** into native calls — they are never stored in Android Keystore or iOS Secure Enclave.
|
|
386
|
+
|
|
387
|
+
### Additional Files (beyond approach 4)
|
|
388
|
+
|
|
389
|
+
| Role | Path |
|
|
390
|
+
|-----------------------|----------------------------------------------------------------------------|
|
|
391
|
+
| Android module | `android/app/src/main/java/.../NativeCryptoModule.kt` |
|
|
392
|
+
| Android package | `android/app/src/main/java/.../NativeCryptoPackage.kt` |
|
|
393
|
+
| iOS module | `ios/NativeCryptoModule.swift` |
|
|
394
|
+
| iOS bridge | `ios/NativeCryptoModule.m` |
|
|
395
|
+
| JS bridge interface | `src/services/security/native-crypto.module.ts` |
|
|
396
|
+
|
|
397
|
+
### NativeCryptoModule.kt (Android)
|
|
398
|
+
|
|
399
|
+
```kotlin
|
|
400
|
+
package com.example.security
|
|
401
|
+
|
|
402
|
+
import android.util.Base64
|
|
403
|
+
import com.facebook.react.bridge.*
|
|
404
|
+
import javax.crypto.Cipher
|
|
405
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
406
|
+
import javax.crypto.spec.SecretKeySpec
|
|
407
|
+
import java.security.KeyFactory
|
|
408
|
+
import java.security.spec.X509EncodedKeySpec
|
|
409
|
+
|
|
410
|
+
class NativeCryptoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
411
|
+
override fun getName() = "NativeCryptoModule"
|
|
412
|
+
|
|
413
|
+
@ReactMethod
|
|
414
|
+
fun encryptAES(plaintext: String, keyBase64: String, promise: Promise) {
|
|
415
|
+
try {
|
|
416
|
+
val keyBytes = Base64.decode(keyBase64, Base64.NO_WRAP)
|
|
417
|
+
require(keyBytes.size == 32) { "AES key must be 32 bytes" }
|
|
418
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
419
|
+
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyBytes, "AES"))
|
|
420
|
+
val cipherWithTag = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
|
|
421
|
+
val result = WritableNativeMap().apply {
|
|
422
|
+
putString("ciphertext", Base64.encodeToString(cipherWithTag, Base64.NO_WRAP))
|
|
423
|
+
putString("iv", Base64.encodeToString(cipher.iv, Base64.NO_WRAP))
|
|
424
|
+
}
|
|
425
|
+
promise.resolve(result)
|
|
426
|
+
} catch (e: Exception) { promise.reject("ERR_ENCRYPT", e.message, e) }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
@ReactMethod
|
|
430
|
+
fun decryptAES(ciphertextBase64: String, ivBase64: String, keyBase64: String, promise: Promise) {
|
|
431
|
+
try {
|
|
432
|
+
val keyBytes = Base64.decode(keyBase64, Base64.NO_WRAP)
|
|
433
|
+
require(keyBytes.size == 32) { "AES key must be 32 bytes" }
|
|
434
|
+
val cipherWithTag = Base64.decode(ciphertextBase64, Base64.NO_WRAP)
|
|
435
|
+
val iv = Base64.decode(ivBase64, Base64.NO_WRAP)
|
|
436
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
437
|
+
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(keyBytes, "AES"), GCMParameterSpec(128, iv))
|
|
438
|
+
promise.resolve(String(cipher.doFinal(cipherWithTag), Charsets.UTF_8))
|
|
439
|
+
} catch (e: Exception) { promise.reject("ERR_DECRYPT", e.message, e) }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@ReactMethod
|
|
443
|
+
fun encryptRSA(publicKeyPem: String, dataBase64: String, promise: Promise) {
|
|
444
|
+
try {
|
|
445
|
+
val pem = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\n", "").trim()
|
|
446
|
+
val pubKey = KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(Base64.decode(pem, Base64.NO_WRAP)))
|
|
447
|
+
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
|
|
448
|
+
cipher.init(Cipher.ENCRYPT_MODE, pubKey)
|
|
449
|
+
promise.resolve(Base64.encodeToString(cipher.doFinal(Base64.decode(dataBase64, Base64.NO_WRAP)), Base64.NO_WRAP))
|
|
450
|
+
} catch (e: Exception) { promise.reject("ERR_RSA_ENCRYPT", e.message, e) }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### NativeCryptoPackage.kt (Android)
|
|
456
|
+
|
|
457
|
+
```kotlin
|
|
458
|
+
package com.example.security
|
|
459
|
+
|
|
460
|
+
import com.facebook.react.ReactPackage
|
|
461
|
+
import com.facebook.react.bridge.NativeModule
|
|
462
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
463
|
+
import com.facebook.react.uimanager.ViewManager
|
|
464
|
+
|
|
465
|
+
class NativeCryptoPackage : ReactPackage {
|
|
466
|
+
override fun createNativeModules(ctx: ReactApplicationContext): List<NativeModule> =
|
|
467
|
+
listOf(NativeCryptoModule(ctx))
|
|
468
|
+
override fun createViewManagers(ctx: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
> Register `NativeCryptoPackage` in `MainApplication.kt` / `MainApplication.java` `getPackages()`.
|
|
473
|
+
|
|
474
|
+
### NativeCryptoModule.swift (iOS)
|
|
475
|
+
|
|
476
|
+
```swift
|
|
477
|
+
// NativeCryptoModule.swift
|
|
478
|
+
import CryptoKit
|
|
479
|
+
import Foundation
|
|
480
|
+
|
|
481
|
+
@objc(NativeCryptoModule)
|
|
482
|
+
class NativeCryptoModule: NSObject {
|
|
483
|
+
|
|
484
|
+
@objc func encryptAES(_ plaintext: String, keyBase64: String,
|
|
485
|
+
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
486
|
+
do {
|
|
487
|
+
guard let data = plaintext.data(using: .utf8),
|
|
488
|
+
let keyData = Data(base64Encoded: keyBase64), keyData.count == 32 else {
|
|
489
|
+
return reject("ERR_INVALID_INPUT", "Invalid plaintext or key", nil)
|
|
490
|
+
}
|
|
491
|
+
let key = SymmetricKey(data: keyData)
|
|
492
|
+
let nonce = try AES.GCM.Nonce()
|
|
493
|
+
let sealed = try AES.GCM.seal(data, using: key, nonce: nonce)
|
|
494
|
+
resolve(["ciphertext": (sealed.ciphertext + sealed.tag).base64EncodedString(),
|
|
495
|
+
"iv": Data(nonce).base64EncodedString()])
|
|
496
|
+
} catch { reject("ERR_ENCRYPT", error.localizedDescription, error) }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@objc func decryptAES(_ ciphertextBase64: String, ivBase64: String, keyBase64: String,
|
|
500
|
+
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
501
|
+
do {
|
|
502
|
+
guard let combined = Data(base64Encoded: ciphertextBase64),
|
|
503
|
+
let ivData = Data(base64Encoded: ivBase64),
|
|
504
|
+
let keyData = Data(base64Encoded: keyBase64),
|
|
505
|
+
keyData.count == 32, combined.count > 16 else {
|
|
506
|
+
return reject("ERR_INVALID_INPUT", "Invalid inputs", nil)
|
|
507
|
+
}
|
|
508
|
+
let key = SymmetricKey(data: keyData)
|
|
509
|
+
let nonce = try AES.GCM.Nonce(data: ivData)
|
|
510
|
+
let box = try AES.GCM.SealedBox(nonce: nonce, ciphertext: combined.dropLast(16), tag: combined.suffix(16))
|
|
511
|
+
guard let plain = String(data: try AES.GCM.open(box, using: key), encoding: .utf8) else {
|
|
512
|
+
return reject("ERR_DECODE", "UTF-8 decode failed", nil)
|
|
513
|
+
}
|
|
514
|
+
resolve(plain)
|
|
515
|
+
} catch { reject("ERR_DECRYPT", error.localizedDescription, error) }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@objc func encryptRSA(_ publicKeyPEM: String, dataBase64: String,
|
|
519
|
+
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
520
|
+
guard let data = Data(base64Encoded: dataBase64) else {
|
|
521
|
+
return reject("ERR_INVALID_INPUT", "Invalid data", nil)
|
|
522
|
+
}
|
|
523
|
+
let pem = publicKeyPEM
|
|
524
|
+
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
|
|
525
|
+
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
526
|
+
.replacingOccurrences(of: "\n", with: "")
|
|
527
|
+
guard let keyData = Data(base64Encoded: pem),
|
|
528
|
+
let secKey = SecKeyCreateWithData(keyData as CFData,
|
|
529
|
+
[kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary, nil) else {
|
|
530
|
+
return reject("ERR_KEY_LOAD", "Failed to load RSA key", nil)
|
|
531
|
+
}
|
|
532
|
+
var error: Unmanaged<CFError>?
|
|
533
|
+
guard let encrypted = SecKeyCreateEncryptedData(secKey, .rsaEncryptionOAEPSHA256, data as CFData, &error) else {
|
|
534
|
+
return reject("ERR_RSA_ENCRYPT", error?.takeRetainedValue().localizedDescription ?? "RSA encrypt failed", nil)
|
|
535
|
+
}
|
|
536
|
+
resolve((encrypted as Data).base64EncodedString())
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### NativeCryptoModule.m (iOS bridge)
|
|
542
|
+
|
|
543
|
+
```objc
|
|
544
|
+
// NativeCryptoModule.m
|
|
545
|
+
#import <React/RCTBridgeModule.h>
|
|
546
|
+
@interface RCT_EXTERN_MODULE(NativeCryptoModule, NSObject)
|
|
547
|
+
RCT_EXTERN_METHOD(encryptAES:(NSString *)plaintext keyBase64:(NSString *)keyBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
548
|
+
RCT_EXTERN_METHOD(decryptAES:(NSString *)ciphertextBase64 ivBase64:(NSString *)ivBase64 keyBase64:(NSString *)keyBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
549
|
+
RCT_EXTERN_METHOD(encryptRSA:(NSString *)publicKeyPEM dataBase64:(NSString *)dataBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
550
|
+
@end
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### JS Bridge Interface (Approach 5)
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
// src/services/security/native-crypto.module.ts
|
|
557
|
+
import { NativeModules } from 'react-native';
|
|
558
|
+
const { NativeCryptoModule } = NativeModules;
|
|
559
|
+
|
|
560
|
+
if (!NativeCryptoModule) {
|
|
561
|
+
throw new Error('[NativeCryptoModule] Native module not registered. Verify MainApplication and Xcode wiring.');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export const nativeEncryptAES = (plaintext: string, keyBase64: string): Promise<{ ciphertext: string; iv: string }> =>
|
|
565
|
+
NativeCryptoModule.encryptAES(plaintext, keyBase64);
|
|
566
|
+
|
|
567
|
+
export const nativeDecryptAES = (ciphertextBase64: string, ivBase64: string, keyBase64: string): Promise<string> =>
|
|
568
|
+
NativeCryptoModule.decryptAES(ciphertextBase64, ivBase64, keyBase64);
|
|
569
|
+
|
|
570
|
+
export const nativeEncryptRSA = (publicKeyPEM: string, dataBase64: string): Promise<string> =>
|
|
571
|
+
NativeCryptoModule.encryptRSA(publicKeyPEM, dataBase64);
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Feature Flags (Approach 5)
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
ENCRYPTION_ENABLED=true
|
|
578
|
+
ENCRYPTION_LAYER=native # rn = JS mode (approach 4), native = OS-level (approach 5)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
> The interceptor reads `ENCRYPTION_LAYER` at startup to choose the JS or native crypto path. This allows toggling between modes without a code change.
|
|
582
|
+
|
|
583
|
+
**Security architecture note:** All crypto is asynchronous and non-blocking. SSL pinning is recommended for high-security environments — pin both the current and a backup certificate fingerprint to prevent MITM during certificate rotation.
|
|
584
|
+
|
|
585
|
+
### Dependencies (Approach 5)
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
npm install react-native-aes-crypto react-native-rsa-native react-native-keychain
|
|
589
|
+
npx pod-install
|
|
590
|
+
# Android: verify minSdkVersion >= 23 in android/app/build.gradle
|
|
591
|
+
# iOS: verify deployment target >= 13.0 in ios/Podfile
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**iOS CommonCrypto fallback (if deployment target < 13.0):**
|
|
595
|
+
|
|
596
|
+
If the iOS deployment target is below 13.0, CryptoKit is unavailable. Use `CommonCrypto` instead:
|
|
597
|
+
- AES-256-GCM via `CCCryptorGCM` (`kCCAlgorithmAES`, `kCCModeGCM`)
|
|
598
|
+
- RSA-OAEP via `SecKeyCreateEncryptedData` with `.rsaEncryptionOAEPSHA256` (available since iOS 11)
|
|
599
|
+
|
|
600
|
+
Update the `NativeCryptoModule.swift` to detect OS version at runtime:
|
|
601
|
+
```swift
|
|
602
|
+
if #available(iOS 13.0, *) {
|
|
603
|
+
// CryptoKit path
|
|
604
|
+
} else {
|
|
605
|
+
// CommonCrypto path using CCCryptorGCM
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
> Prefer raising the deployment target to >= 13.0 for new projects. CommonCrypto is only for legacy app maintenance.
|
|
610
|
+
|
|
611
|
+
**Install rules:**
|
|
612
|
+
- Prefer pinning exact versions in production environments.
|
|
613
|
+
- Validate iOS pod install and Android Gradle sync after install.
|
|
614
|
+
- Confirm native modules appear in `NativeModules` in JS.
|
|
615
|
+
- Confirm AES encrypt + decrypt round-trip produces correct plaintext on both platforms before shipping.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Request Envelope (Approaches 4 & 5 — Identical Shape)
|
|
620
|
+
|
|
621
|
+
```json
|
|
622
|
+
{
|
|
623
|
+
"data": "base64(ciphertext + 16-byte auth_tag)",
|
|
624
|
+
"key": "base64(RSA-OAEP encrypted 32-byte AES key)",
|
|
625
|
+
"iv": "base64(12-byte IV)",
|
|
626
|
+
"nonce": "appId^unix_timestamp_ms.random_hex_16",
|
|
627
|
+
"key_id": "v1"
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## Backend Alignment (Approaches 4 & 5)
|
|
632
|
+
|
|
633
|
+
**Purpose:** This section is written for the backend team. It defines the exact contract the server must implement.
|
|
634
|
+
|
|
635
|
+
### RSA Public Key Endpoint
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
GET /api/security/public-key
|
|
639
|
+
Authorization: Bearer <access_token> (if route is authenticated)
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Response — 200 OK:**
|
|
643
|
+
|
|
644
|
+
```json
|
|
645
|
+
{
|
|
646
|
+
"public_key": "<PEM or Base64-encoded DER RSA public key>",
|
|
647
|
+
"key_id": "v1",
|
|
648
|
+
"expires_at": "2026-07-01T00:00:00Z"
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
| Field | Type | Required | Description |
|
|
653
|
+
|--------------|--------|----------|--------------------------------------------------------------|
|
|
654
|
+
| `public_key` | string | yes | PEM (`-----BEGIN PUBLIC KEY-----…`) preferred; or Base64 DER |
|
|
655
|
+
| `key_id` | string | yes | Version tag used for key rotation tracking |
|
|
656
|
+
| `expires_at` | string | no | ISO 8601 UTC. Client may use to schedule proactive key refresh. |
|
|
657
|
+
|
|
658
|
+
- PEM is preferred (self-describing).
|
|
659
|
+
- The key must be a **public key** (`BEGIN PUBLIC KEY`), not a certificate (`BEGIN CERTIFICATE`).
|
|
660
|
+
|
|
661
|
+
The client retries with exponential back-off (default 3 attempts). If unavailable, the client **blocks all encrypted calls** and surfaces an error.
|
|
662
|
+
|
|
663
|
+
### Crypto Parameters (Non-Negotiable)
|
|
664
|
+
|
|
665
|
+
| Parameter | Required Value | Notes |
|
|
666
|
+
|------------------|-----------------------------|--------------------------------------------------|
|
|
667
|
+
| Symmetric cipher | AES-256-GCM | 256-bit key, authenticated mode |
|
|
668
|
+
| AES key size | 32 bytes | Generated fresh per request on the client |
|
|
669
|
+
| IV/Nonce size | 12 bytes | Generated fresh per request, never reused |
|
|
670
|
+
| Auth tag size | 16 bytes (128 bits) | Appended to ciphertext |
|
|
671
|
+
| Asymmetric | RSA-OAEP SHA-256 (mandatory)| PKCS1 v1.5 only for legacy with written approval — **not acceptable for new deployments in 2026** |
|
|
672
|
+
| RSA key size | >= 2048 bits | 4096 bits recommended for new deployments |
|
|
673
|
+
| Encoding | Base64 (no line breaks) | |
|
|
674
|
+
|
|
675
|
+
> Do not negotiate algorithm parameters at runtime. Mismatches cause auth tag failures.
|
|
676
|
+
|
|
677
|
+
### Encrypted Request Contract
|
|
678
|
+
|
|
679
|
+
Every POST / PUT / PATCH body the client sends replaces the original payload with:
|
|
680
|
+
|
|
681
|
+
```json
|
|
682
|
+
{
|
|
683
|
+
"data": "<Base64(ciphertext + auth_tag)>",
|
|
684
|
+
"key": "<Base64(RSA-OAEP encrypted AES key)>",
|
|
685
|
+
"iv": "<Base64(12-byte IV)>",
|
|
686
|
+
"nonce": "<appId>^<unix_timestamp_ms>.<random_hex_16>",
|
|
687
|
+
"key_id": "v1"
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
| Field | Type | Description |
|
|
692
|
+
|----------|--------|--------------------------------------------------------------------------|
|
|
693
|
+
| `data` | string | Base64 of AES-256-GCM(plaintext_json). Last 16 bytes are the auth tag — appended automatically by AES-GCM. |
|
|
694
|
+
| `key` | string | Base64 of RSA-OAEP(aes_key_bytes). Encrypted with the public key from the key endpoint. |
|
|
695
|
+
| `iv` | string | Base64 of the 12-byte IV used for AES-GCM. Must be passed to decryption step. |
|
|
696
|
+
| `nonce` | string | Anti-replay identifier. Format: `<appId>^<epoch_ms>.<16-char random hex>`. |
|
|
697
|
+
| `key_id` | string | Identifies which RSA key pair to use for unwrapping. Must match the `key_id` from public key endpoint. |
|
|
698
|
+
|
|
699
|
+
Client sends `Content-Type: application/json` — the envelope is valid JSON.
|
|
700
|
+
|
|
701
|
+
### Backend Decryption Steps
|
|
702
|
+
|
|
703
|
+
```
|
|
704
|
+
1. Read key_id → select matching RSA private key from secure storage.
|
|
705
|
+
2. Base64-decode key → RSA-OAEP-decrypt (SHA-256) → 32-byte AES key.
|
|
706
|
+
3. Base64-decode iv → 12-byte IV.
|
|
707
|
+
4. Base64-decode data → ciphertext+tag (last 16 bytes = auth tag; remaining = ciphertext).
|
|
708
|
+
5. AES-256-GCM decrypt(key, iv, ciphertext, tag) → if auth tag FAILS → return 400.
|
|
709
|
+
6. Parse UTF-8 JSON → original request payload.
|
|
710
|
+
7. Validate nonce (anti-replay — see below).
|
|
711
|
+
8. Process business logic with the original payload.
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Nonce / Anti-Replay Validation
|
|
715
|
+
|
|
716
|
+
Format: `<appId>^<unix_timestamp_ms>.<16-char random hex>`
|
|
717
|
+
|
|
718
|
+
Example: `com.myapp^1745189241337.a3f9c2e1d8b047f6`
|
|
719
|
+
|
|
720
|
+
Backend validation rules:
|
|
721
|
+
1. Parse: split on `^`, then split the right side on `.`.
|
|
722
|
+
2. Timestamp check: reject if `|server_time_ms - timestamp_ms| > 300000` (5-minute window, configurable).
|
|
723
|
+
3. Nonce store: reject if the exact nonce value has been seen before within the window (prevents replay).
|
|
724
|
+
|
|
725
|
+
### Encrypted Response Contract
|
|
726
|
+
|
|
727
|
+
The server MUST use the **same AES key** from the request (the client stores it in memory keyed by `requestId`) and a **freshly generated IV** (not the request IV):
|
|
728
|
+
|
|
729
|
+
```json
|
|
730
|
+
{
|
|
731
|
+
"data": "base64(ciphertext + auth_tag)",
|
|
732
|
+
"iv": "base64(NEW server-generated 12-byte IV)",
|
|
733
|
+
"nonce": "server_nonce"
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Step-by-step server-side response encryption:**
|
|
738
|
+
```
|
|
739
|
+
1. Retrieve the AES key that was decrypted from the incoming request.
|
|
740
|
+
2. Generate a new cryptographically random 12-byte IV (never reuse the request IV).
|
|
741
|
+
3. Serialize response JSON to UTF-8 bytes.
|
|
742
|
+
4. Encrypt with AES-256-GCM(aes_key, new_iv, response_bytes) → ciphertext + auth_tag.
|
|
743
|
+
5. Return { data: base64(ciphertext || auth_tag), iv: base64(new_iv), nonce: server_nonce }.
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
> **Critical:** Reusing the request IV with the same AES key is a security violation.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Security Checklist (Approaches 4 & 5)
|
|
751
|
+
|
|
752
|
+
### Crypto (Both Modes)
|
|
753
|
+
- [ ] AES key: 32 bytes. IV: 12 bytes. Auth tag: 16 bytes.
|
|
754
|
+
- [ ] AES mode is GCM — no other mode acceptable.
|
|
755
|
+
- [ ] Random generation uses cryptographically secure `randomBytes` (JS) or OS-provided `SecRandomCopyBytes` / `KeyGenerator` (Native).
|
|
756
|
+
- [ ] RSA padding: RSA-OAEP SHA-256. PKCS1 v1.5 only for legacy with written approval.
|
|
757
|
+
|
|
758
|
+
### Key Handling (Both Modes)
|
|
759
|
+
- [ ] RSA public key format validated (PEM/Base64 as expected).
|
|
760
|
+
- [ ] RSA key source is HTTPS — never HTTP.
|
|
761
|
+
- [ ] RSA key cached in memory only; retry bounded at 3.
|
|
762
|
+
- [ ] AES key and IV are never persisted to disk or AsyncStorage.
|
|
763
|
+
- [ ] Request key mapping cleared immediately after response processing (success or failure).
|
|
764
|
+
- [ ] Block encrypted endpoints when RSA key is unavailable after retries.
|
|
765
|
+
|
|
766
|
+
### Key Handling (Native Mode Only)
|
|
767
|
+
- [ ] Per-request AES keys generated in JS using `randomBytes(32)` and passed as base64 to native calls — NOT stored in Android Keystore.
|
|
768
|
+
- [ ] AES key lives only in the JS request map for one request/response cycle; deleted immediately after.
|
|
769
|
+
- [ ] RSA public key fetched from API over HTTPS; cached in memory; not stored in Keystore.
|
|
770
|
+
- [ ] No RSA private key exists on the client — private key resides on the server.
|
|
771
|
+
|
|
772
|
+
### Transport and API (Both Modes)
|
|
773
|
+
- [ ] Encryption applied to POST/PUT/PATCH by default.
|
|
774
|
+
- [ ] GET excluded unless explicitly configured.
|
|
775
|
+
- [ ] Request mapping safe under concurrent requests (each has its own `requestId`).
|
|
776
|
+
- [ ] Request map entry deleted immediately after each response (success or failure).
|
|
777
|
+
- [ ] Missing request map entry handled with secure fallback.
|
|
778
|
+
- [ ] All crypto operations run asynchronously — never block the UI thread.
|
|
779
|
+
- [ ] SSL pinning configured for high-security environments; current and backup certificate fingerprints pinned.
|
|
780
|
+
|
|
781
|
+
### Native Module Wiring (Native Mode Only)
|
|
782
|
+
- [ ] `NativeCryptoModule` registered in `MainApplication.java` / `MainApplication.kt` (Android).
|
|
783
|
+
- [ ] `NativeCryptoModule` bridge `.m` file present and linked in Xcode project (iOS).
|
|
784
|
+
- [ ] `pod install` completed successfully after adding native module.
|
|
785
|
+
- [ ] `NativeModules.NativeCryptoModule` accessible from JS at runtime.
|
|
786
|
+
- [ ] `encryptAES`, `decryptAES`, `encryptRSA` callable and return Promises from JS.
|
|
787
|
+
- [ ] `ENCRYPTION_LAYER` env flag set and read correctly in interceptor setup.
|
|
788
|
+
|
|
789
|
+
### Error Handling (Both Modes)
|
|
790
|
+
- [ ] Invalid auth tag triggers session reset/force logout — not a silent fallback.
|
|
791
|
+
- [ ] Decryption failure does not leak plaintext.
|
|
792
|
+
- [ ] RSA key fetch retry is bounded (default 3).
|
|
793
|
+
- [ ] KEY_ID_NOT_FOUND triggers key refresh and exactly one retry — no retry loop.
|
|
794
|
+
- [ ] Sensitive fields excluded from logs (no keys, IVs, plaintext, auth tags).
|
|
795
|
+
|
|
796
|
+
### Storage (Both Modes)
|
|
797
|
+
- [ ] Tokens stored via `react-native-keychain`.
|
|
798
|
+
- [ ] Sensitive persisted fields stored via keychain.
|
|
799
|
+
- [ ] No secrets stored in plain `AsyncStorage`.
|
|
800
|
+
|
|
801
|
+
### Environment (Both Modes)
|
|
802
|
+
- [ ] `ENCRYPTION_ENABLED` supported.
|
|
803
|
+
- [ ] `ENCRYPTION_LAYER` supported (values: `rn` / `native`).
|
|
804
|
+
- [ ] Production default is secure (encryption enabled, no debug logging).
|
|
805
|
+
- [ ] Development override is explicit and controlled.
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
## Error Handling (Approaches 4 & 5)
|
|
810
|
+
|
|
811
|
+
| Error | Behavior |
|
|
812
|
+
|-------|----------|
|
|
813
|
+
| RSA key fetch failure | Retry up to 3 times; block encrypted calls if all fail |
|
|
814
|
+
| KEY_ID_NOT_FOUND (401) | Invalidate cached key, retry request exactly once |
|
|
815
|
+
| Auth tag mismatch | Trigger force logout / session reset |
|
|
816
|
+
| Missing request map entry | Fail securely; do not retry |
|
|
817
|
+
| Keychain unavailable | Treat as session-expired; force re-login; never fallback to AsyncStorage |
|
|
818
|
+
| Native module undefined (approach 5) | Block all encrypted calls; surface clear setup error |
|
|
819
|
+
| Native encrypt/decrypt rejection (approach 5) | Non-recoverable for that request; log error code only |
|
|
820
|
+
| Decryption parse failure | Catch JSON parse exceptions after decryption; return sanitized error; never log plaintext payload |
|
|
821
|
+
| AES key lifecycle mismatch | Symptom: decryption succeeds but produces garbage, or auth tag fails. Cause: different key used in encrypt vs decrypt, or request map cleared before response arrived. Resolution: ensure request map stores exact `aesKeyBase64` used in encryption and passes it unchanged into decryption; clear only after decryption completes |
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## Build Command
|
|
826
|
+
|
|
827
|
+
```bash
|
|
828
|
+
npx react-native run-android
|
|
829
|
+
# or for Expo:
|
|
830
|
+
npx expo start
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
## Delivery Checklist (Approaches 4 & 5)
|
|
836
|
+
|
|
837
|
+
### Configuration
|
|
838
|
+
- [ ] All configuration questions answered and validated.
|
|
839
|
+
- [ ] Encryption layer selected and recorded (JS mode or Native).
|
|
840
|
+
|
|
841
|
+
### Implementation — JS Mode (Approach 4)
|
|
842
|
+
- [ ] Encryption module implemented (`react-native-quick-crypto`).
|
|
843
|
+
- [ ] Decryption module implemented.
|
|
844
|
+
- [ ] RSA key service implemented (fetch/cache/retry).
|
|
845
|
+
- [ ] Request + response interceptors integrated.
|
|
846
|
+
- [ ] Keychain storage integrated.
|
|
847
|
+
- [ ] Feature flag wired.
|
|
848
|
+
|
|
849
|
+
### Implementation — Native Mode (Approach 5, all of above plus)
|
|
850
|
+
- [ ] Android `NativeCryptoModule.kt` created and registered in `NativeCryptoPackage.kt`.
|
|
851
|
+
- [ ] `NativeCryptoPackage` registered in `MainApplication.java` / `MainApplication.kt`.
|
|
852
|
+
- [ ] iOS `NativeCryptoModule.swift` created with `do/try/catch` — `try!` not present anywhere.
|
|
853
|
+
- [ ] iOS bridge `NativeCryptoModule.m` created and added to Xcode project.
|
|
854
|
+
- [ ] JS bridge interface `native-crypto.module.ts` created.
|
|
855
|
+
- [ ] Interceptors updated to use native bridge calls.
|
|
856
|
+
- [ ] AES key generated in JS, passed into native calls, not generated inside native module.
|
|
857
|
+
- [ ] `ENCRYPTION_LAYER` env flag added and read in interceptor setup.
|
|
858
|
+
- [ ] `pod install` completed (iOS).
|
|
859
|
+
- [ ] Gradle sync completed (Android).
|
|
860
|
+
- [ ] `NativeModules.NativeCryptoModule` confirmed accessible from JS at runtime.
|
|
861
|
+
|
|
862
|
+
### Security
|
|
863
|
+
- [ ] No sensitive logging (no keys, IVs, plaintext, tags in logs).
|
|
864
|
+
- [ ] No persistent AES key/IV storage.
|
|
865
|
+
- [ ] HTTPS enforced for RSA key endpoint and all API traffic.
|
|
866
|
+
- [ ] Auth tag verification enforced; mismatch triggers session reset.
|
|
867
|
+
- [ ] Native mode: AES key never written to native storage; exists only in JS request map.
|
|
868
|
+
|
|
869
|
+
### Operational
|
|
870
|
+
- [ ] Handles concurrent requests correctly (each has its own requestId).
|
|
871
|
+
- [ ] Handles missing key / RSA key refetch flow.
|
|
872
|
+
- [ ] Handles invalid auth tag → force logout flow.
|
|
873
|
+
- [ ] Native mode: tested on physical Android device (not emulator-only).
|
|
874
|
+
- [ ] Native mode: tested on physical iOS device (not simulator-only).
|
|
875
|
+
|
|
876
|
+
### Final Output
|
|
877
|
+
- [ ] Files changed listed (including native module files if applicable).
|
|
878
|
+
- [ ] Dependency changes listed.
|
|
879
|
+
- [ ] Backend alignment spec shared with server team.
|
|
880
|
+
- [ ] Residual risks noted.
|
|
881
|
+
- [ ] Suggested next validation steps included.
|
|
882
|
+
|
|
883
|
+
| Role | Path |
|
|
884
|
+
|----------------|-------------------------------------------------------|
|
|
885
|
+
| Config | `src/encryption/encryption.config.ts` |
|
|
886
|
+
| Service | `src/encryption/encryption.service.ts` |
|
|
887
|
+
| Interceptor | `src/encryption/encryption.interceptor.ts` |
|
|
888
|
+
| Module wiring | `src/api/axiosInstance.ts` |
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## Module Wiring
|
|
893
|
+
|
|
894
|
+
Create a shared Axios instance in `src/api/axiosInstance.ts` and register the interceptor there.
|
|
895
|
+
Import `react-native-get-random-values` at the very top of `index.js` (app entry point) — this must happen before any crypto-js import.
|
|
896
|
+
|
|
897
|
+
### `index.js` (entry point polyfill)
|
|
898
|
+
|
|
899
|
+
```ts
|
|
900
|
+
import 'react-native-get-random-values'; // MUST be first import
|
|
901
|
+
import { AppRegistry } from 'react-native';
|
|
902
|
+
import App from './App';
|
|
903
|
+
import { name as appName } from './app.json';
|
|
904
|
+
|
|
905
|
+
AppRegistry.registerComponent(appName, () => App);
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### `src/api/axiosInstance.ts`
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
import axios from 'axios';
|
|
912
|
+
import { applyEncryptionInterceptor } from '../encryption/encryption.interceptor';
|
|
913
|
+
import { encryptionConfig } from '../encryption/encryption.config';
|
|
914
|
+
|
|
915
|
+
const axiosInstance = axios.create({
|
|
916
|
+
baseURL: encryptionConfig.apiUrl,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
applyEncryptionInterceptor(axiosInstance);
|
|
920
|
+
// ENCRYPTION_INTERCEPTOR_PLACEHOLDER
|
|
921
|
+
|
|
922
|
+
export default axiosInstance;
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
The `// ENCRYPTION_INTERCEPTOR_PLACEHOLDER` comment is the marker used by install.js `patchFile()`.
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## Interceptor Pattern
|
|
930
|
+
|
|
931
|
+
Identical to the React interceptor pattern — Axios request interceptor:
|
|
932
|
+
|
|
933
|
+
```ts
|
|
934
|
+
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
|
935
|
+
import { encryptionService } from './encryption.service';
|
|
936
|
+
import { encryptionConfig } from './encryption.config';
|
|
937
|
+
|
|
938
|
+
export function applyEncryptionInterceptor(instance: AxiosInstance): void {
|
|
939
|
+
const cfg = encryptionConfig.encryption;
|
|
940
|
+
|
|
941
|
+
instance.interceptors.request.use(
|
|
942
|
+
async (config: InternalAxiosRequestConfig) => {
|
|
943
|
+
if (!cfg.enabled) return config;
|
|
944
|
+
if (!config.data) return config;
|
|
945
|
+
if (config.data instanceof FormData) return config;
|
|
946
|
+
if (cfg.excludeUrls?.some(u => config.url?.startsWith(u))) return config;
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
config.data = await encryptionService.encrypt(config.data);
|
|
950
|
+
config.headers['Content-Type'] = 'application/json';
|
|
951
|
+
} catch {}
|
|
952
|
+
|
|
953
|
+
return config;
|
|
954
|
+
},
|
|
955
|
+
(error) => Promise.reject(error)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## URL Exclusion Pattern
|
|
963
|
+
|
|
964
|
+
```ts
|
|
965
|
+
const excludeUrls: string[] = encryptionConfig.encryption.excludeUrls;
|
|
966
|
+
if (excludeUrls.some(prefix => config.url?.startsWith(prefix))) {
|
|
967
|
+
return config;
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
---
|
|
972
|
+
|
|
973
|
+
## Dependencies (Approach 2 or 3)
|
|
974
|
+
|
|
975
|
+
```bash
|
|
976
|
+
npm install axios jsencrypt crypto-js react-native-get-random-values
|
|
977
|
+
npm install --save-dev @types/crypto-js
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
`react-native-get-random-values` provides the `crypto.getRandomValues` polyfill required by `crypto-js` in the React Native JS runtime.
|
|
981
|
+
For bare React Native projects, also run:
|
|
982
|
+
|
|
983
|
+
```bash
|
|
984
|
+
npx pod-install
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Approach 1 is NOT available — `crypto.subtle` is absent from the React Native JS runtime.
|
|
988
|
+
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
## Build Command
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
npx react-native run-android
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
For Expo-managed projects:
|
|
998
|
+
|
|
999
|
+
```bash
|
|
1000
|
+
npx expo start
|
|
1001
|
+
```
|