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,832 @@
|
|
|
1
|
+
# EC (ECDH) Encryption — Implementation Reference
|
|
2
|
+
|
|
3
|
+
Covers both **EC + React Native JS** and **EC + Native** modes.
|
|
4
|
+
The interceptor structure, request mapping, and secure storage are identical to RSA modes.
|
|
5
|
+
Only the key exchange mechanism and envelope shape differ.
|
|
6
|
+
|
|
7
|
+
This document covers two distinct EC backend contracts:
|
|
8
|
+
|
|
9
|
+
- Per-request EC envelope: the client generates an ephemeral key pair per encrypted request.
|
|
10
|
+
- Session-init EC: the client performs a session handshake first, derives one session AES key, and reuses that key for subsequent encrypted APIs.
|
|
11
|
+
|
|
12
|
+
Do not implement one while the backend expects the other.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contract Check First
|
|
17
|
+
|
|
18
|
+
Before copying any EC code from this reference, confirm all of the following with the backend or a known-good client:
|
|
19
|
+
|
|
20
|
+
1. Is the EC flow per-request or session-init?
|
|
21
|
+
2. What exact endpoint establishes the EC session, if any?
|
|
22
|
+
3. Is `encrypted_shared_aes_key` authoritative, or is it present but ignored by the reference client?
|
|
23
|
+
4. What exact HKDF `info` string is used?
|
|
24
|
+
5. Is AES-GCM AAD required, and if yes what canonical JSON must be used?
|
|
25
|
+
6. Is the encrypted response payload standard base64 or base64url?
|
|
26
|
+
7. Does the response IV come from an explicit `iv` field, the `nonce` field, or some other backend rule?
|
|
27
|
+
|
|
28
|
+
This project exposed a backend that does **not** follow the generic per-request EC contract below. It uses a session-init contract instead.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Session-Init EC Variant
|
|
33
|
+
|
|
34
|
+
Use this variant when the backend exposes an auth/session initialization endpoint and then expects subsequent APIs to use a derived session AES key.
|
|
35
|
+
|
|
36
|
+
### Working implementation sequence for this project
|
|
37
|
+
|
|
38
|
+
This project required the following concrete order. If the backend contract matches this pattern, implement it exactly in this order instead of adapting the generic per-request sample:
|
|
39
|
+
|
|
40
|
+
1. Generate a fresh client P-256 key pair.
|
|
41
|
+
2. Call `initEncryptionSession` with the real generated `client_ecdh_pub`, stable `device_id`, `user_id`, `api_version`, and `platform`.
|
|
42
|
+
3. Read `key_id`, `server_ecdh_pub`, `salt`, and any other session metadata from the init response.
|
|
43
|
+
4. Derive the session AES key directly from ECDH + HKDF.
|
|
44
|
+
5. Call `createSession` using the `key_id` from the init response.
|
|
45
|
+
6. Persist only the auth token; keep the derived AES key in memory.
|
|
46
|
+
7. Encrypt/decrypt business APIs with that cached session AES key.
|
|
47
|
+
|
|
48
|
+
If a stored auth token exists later, do not assume the in-memory encryption session still exists after app restart. Re-establish the encryption session if the business API requires it.
|
|
49
|
+
|
|
50
|
+
### Confirmed integration fixes from this project
|
|
51
|
+
|
|
52
|
+
- Send the real generated `client_ecdh_pub` from the same ECDH instance whose private key is used for HKDF derivation.
|
|
53
|
+
- Do not assume `encrypted_shared_aes_key` must be decrypted. In this backend, the working Flutter client ignored it and derived the session key directly.
|
|
54
|
+
- Base64url fields such as `server_ecdh_pub` and `salt` must be normalized and padded before decoding.
|
|
55
|
+
- Keep the session `device_id` stable. Do not derive HKDF with one device ID and send encrypted requests with another.
|
|
56
|
+
- Confirm whether the login flow depends on both `initEncryptionSession` and `createSession`; in this project both were required.
|
|
57
|
+
- The session key derivation was:
|
|
58
|
+
- `sharedSecret = ECDH(clientPrivateKey, serverPublicKey)`
|
|
59
|
+
- `prk = HMAC-SHA256(salt, sharedSecret)`
|
|
60
|
+
- `sessionKey = HMAC-SHA256(prk, "HDFC-LI-Session:v1|{keyId}|{deviceId}" || 0x01)[0:32]`
|
|
61
|
+
- The backend required AES-GCM AAD with this exact canonical JSON shape:
|
|
62
|
+
- `{"api_ver":"v1","device_id":"DEV1001","key_id":"LI-DEV1001-EMP1005"}`
|
|
63
|
+
- The `device_id` used in init, HKDF context, AAD, and encrypted requests must be the same stable session device identifier.
|
|
64
|
+
- `encryptedResponse` was standard base64 of `ciphertext || 16-byte tag`.
|
|
65
|
+
- Nonce had to be exactly 12 characters for this backend. Treat nonce length and character set as contract data, not a local preference.
|
|
66
|
+
- The response IV contract had to be verified separately from payload decoding; do not infer it from the per-request EC example.
|
|
67
|
+
- In React Native/Hermes, use a verified base64 encoder for public keys and payloads; do not assume every `Buffer` implementation behaves identically.
|
|
68
|
+
- In React Native, sliced `Buffer` values handed to native crypto code may need explicit byte copies if typed-array views cause incorrect native reads.
|
|
69
|
+
|
|
70
|
+
### Session-init TypeScript sketch
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
function buildSessionAad(keyId: string, deviceId: string, apiVersion = 'v1'): Buffer {
|
|
74
|
+
return Buffer.from(
|
|
75
|
+
`{"api_ver":"${apiVersion}","device_id":"${deviceId}","key_id":"${keyId}"}`,
|
|
76
|
+
'utf8',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function deriveSessionKey(
|
|
81
|
+
clientEcdh: ReturnType<typeof createECDH>,
|
|
82
|
+
serverPubBase64Url: string,
|
|
83
|
+
saltBase64Url: string,
|
|
84
|
+
keyId: string,
|
|
85
|
+
deviceId: string,
|
|
86
|
+
apiVersion = 'v1',
|
|
87
|
+
): Buffer {
|
|
88
|
+
const serverPub = Buffer.from(urlSafeBase64ToStd(serverPubBase64Url), 'base64');
|
|
89
|
+
const salt = Buffer.from(urlSafeBase64ToStd(saltBase64Url), 'base64');
|
|
90
|
+
const sharedSecret = clientEcdh.computeSecret(serverPub) as Buffer;
|
|
91
|
+
const contextInfo = `HDFC-LI-Session:${apiVersion}|${keyId}|${deviceId}`;
|
|
92
|
+
return hkdfSha256(sharedSecret, salt, Buffer.from(contextInfo, 'utf8'));
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Session-init implementation notes
|
|
97
|
+
|
|
98
|
+
- Persist only the auth token. Keep the derived session AES key in memory.
|
|
99
|
+
- If the app restarts and only the token remains, you may need to re-establish the encryption session before encrypted APIs can work again.
|
|
100
|
+
- Do not mix a stored JWT `deviceId` claim with the encryption session `device_id` if the backend expects a fixed session device identifier.
|
|
101
|
+
|
|
102
|
+
### Session-init implementation checklist
|
|
103
|
+
|
|
104
|
+
- [ ] `initEncryptionSession` sends the actual freshly generated `client_ecdh_pub`
|
|
105
|
+
- [ ] `createSession` runs only after `initEncryptionSession` succeeds
|
|
106
|
+
- [ ] `createSession` uses the returned `key_id`
|
|
107
|
+
- [ ] nonce generation matches the backend's exact length and character rules
|
|
108
|
+
- [ ] base64url fields are normalized and padded before decoding
|
|
109
|
+
- [ ] session key derivation uses the exact backend `info` string
|
|
110
|
+
- [ ] AAD bytes match the backend's canonical JSON exactly
|
|
111
|
+
- [ ] encrypted response decoding uses the correct base64 alphabet
|
|
112
|
+
- [ ] response IV source is confirmed from contract or reference client
|
|
113
|
+
- [ ] stored auth token reuse does not bypass required encryption-session re-init
|
|
114
|
+
|
|
115
|
+
### Session-init nonce guidance
|
|
116
|
+
|
|
117
|
+
Do not hardcode a generic nonce policy for session-init backends. Confirm one of these explicitly:
|
|
118
|
+
|
|
119
|
+
- 12 random raw bytes, then standard base64 encoded
|
|
120
|
+
- 12 random raw bytes, then base64url encoded
|
|
121
|
+
- 12-character alphanumeric string
|
|
122
|
+
- another backend-specific format
|
|
123
|
+
|
|
124
|
+
This project required a 12-character alphanumeric nonce. Using a different alphabet or length caused request rejection or decryption mismatches.
|
|
125
|
+
|
|
126
|
+
**CRITICAL**: For session-init EC, the nonce IS the AES-GCM IV. The nonce generator MUST use a CSPRNG (`randomBytes` from quick-crypto), NOT `Math.random()`. A predictable IV under AES-GCM breaks both confidentiality and authenticity. See the `generateNonce()` implementation in the auth.service.ts sample below.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## EC Envelope Shape
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"data": "base64(ciphertext + 16-byte GCM auth_tag)",
|
|
135
|
+
"ephemeral_public_key": "base64(65-byte uncompressed P-256 ephemeral public key)",
|
|
136
|
+
"iv": "base64(12-byte IV)",
|
|
137
|
+
"nonce": "appId^unix_timestamp_ms.random_hex_16",
|
|
138
|
+
"key_id": "ec-v1"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
No `key` field — the derived AES key is never transmitted.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Step 1: Add EC Types to `types.ts`
|
|
147
|
+
|
|
148
|
+
Add alongside existing RSA types (do not remove them):
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// src/services/security/types.ts
|
|
152
|
+
|
|
153
|
+
// Per-request cryptographic material stored in the in-memory request map.
|
|
154
|
+
// Used by both RSA and EC modes (aesKey is the session AES key regardless of how it was obtained).
|
|
155
|
+
// Use Uint8Array (not Buffer) — the `buffer` polyfill's Buffer and quick-crypto's Buffer are
|
|
156
|
+
// different classes. Uint8Array is the safe common supertype.
|
|
157
|
+
export type RequestCryptoMaterial = {
|
|
158
|
+
aesKey: Uint8Array; // 32-byte AES-256-GCM session key (random for RSA, HKDF-derived for EC)
|
|
159
|
+
requestIv: Uint8Array; // 12-byte IV used for this request's encryption (for reference only; response uses server IV)
|
|
160
|
+
keyId?: string; // key_id from the active public key (for audit/logging)
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type EcPublicKeyResponse = {
|
|
164
|
+
public_key: string; // base64(65-byte uncompressed P-256 point: 0x04 || X(32) || Y(32))
|
|
165
|
+
key_id: string;
|
|
166
|
+
expires_at?: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Per-request EC envelope shape
|
|
170
|
+
export type EcEncryptedEnvelope = {
|
|
171
|
+
data: string; // base64(ciphertext + 16-byte GCM auth_tag)
|
|
172
|
+
ephemeral_public_key: string; // base64(65-byte uncompressed ephemeral P-256 public key)
|
|
173
|
+
iv: string; // base64(12-byte IV)
|
|
174
|
+
nonce: string; // Per-request: appId^unix_timestamp_ms.random_hex_16
|
|
175
|
+
// Session-init: backend-specific (e.g. 12-char alphanumeric)
|
|
176
|
+
key_id?: string;
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Step 2: Create EC Key Service (`ec-key.service.ts`)
|
|
183
|
+
|
|
184
|
+
Mirrors `RsaKeyService`. Validates the 65-byte uncompressed EC point on every fetch.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// src/services/security/ec-key.service.ts
|
|
188
|
+
import axios from 'axios';
|
|
189
|
+
import Config from 'react-native-config';
|
|
190
|
+
import type { EcPublicKeyResponse } from './types';
|
|
191
|
+
|
|
192
|
+
const DEFAULT_EC_KEY_PATH = '/api/security/ec-public-key';
|
|
193
|
+
const DEFAULT_RETRY_COUNT = 3;
|
|
194
|
+
|
|
195
|
+
export class EcKeyService {
|
|
196
|
+
private cachedKey: EcPublicKeyResponse | null = null;
|
|
197
|
+
|
|
198
|
+
constructor(private readonly apiBaseUrl: string) {}
|
|
199
|
+
|
|
200
|
+
getCached(): EcPublicKeyResponse | null { return this.cachedKey; }
|
|
201
|
+
clearCache(): void { this.cachedKey = null; }
|
|
202
|
+
|
|
203
|
+
async getPublicKey(accessToken?: string): Promise<EcPublicKeyResponse> {
|
|
204
|
+
if (this.cachedKey) return this.cachedKey;
|
|
205
|
+
return this.refreshPublicKey(accessToken);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async refreshPublicKey(accessToken?: string): Promise<EcPublicKeyResponse> {
|
|
209
|
+
const configured = Config.EC_PUBLIC_KEY_ENDPOINT;
|
|
210
|
+
const url = configured && /^https:\/\//i.test(configured)
|
|
211
|
+
? configured
|
|
212
|
+
: `${this.apiBaseUrl.replace(/\/$/, '')}${DEFAULT_EC_KEY_PATH}`;
|
|
213
|
+
|
|
214
|
+
const retries = Number(Config.EC_KEY_RETRY || DEFAULT_RETRY_COUNT);
|
|
215
|
+
let lastError: unknown;
|
|
216
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const response = await axios.get<EcPublicKeyResponse>(url, {
|
|
219
|
+
timeout: 10000,
|
|
220
|
+
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
|
221
|
+
});
|
|
222
|
+
if (!response.data?.public_key) throw new Error('EC public key response missing public_key');
|
|
223
|
+
const decoded = Buffer.from(response.data.public_key, 'base64');
|
|
224
|
+
if (decoded.length !== 65 || decoded[0] !== 0x04) {
|
|
225
|
+
throw new Error('Invalid EC public key: must be 65-byte uncompressed P-256 point');
|
|
226
|
+
}
|
|
227
|
+
this.cachedKey = response.data;
|
|
228
|
+
return response.data;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
lastError = error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw lastError instanceof Error ? lastError : new Error('Failed to fetch EC public key');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Step 3: Create EC Crypto Utility (`ec-crypto.util.ts`)
|
|
241
|
+
|
|
242
|
+
The code below is the generic per-request EC implementation. Do not use it for a session-init backend.
|
|
243
|
+
|
|
244
|
+
### Critical: Hermes Buffer Compatibility
|
|
245
|
+
|
|
246
|
+
React Native with Hermes has these known incompatibilities with crypto code:
|
|
247
|
+
|
|
248
|
+
1. **`Buffer.toString('base64')` is broken** — the `buffer` polyfill relies on `base64-js` internals that fail with Hermes typed arrays. Use the `toBase64()` helper below instead.
|
|
249
|
+
2. **Two `Buffer` classes exist** — `buffer` package's `Buffer` and `react-native-quick-crypto`'s `Buffer`. They are NOT interchangeable. `setAAD()` requires quick-crypto's `Buffer`.
|
|
250
|
+
3. **`Buffer.slice()` returns views** — quick-crypto's native C++ code ignores `byteOffset` on typed-array views, reading wrong data. Always use `byteCopy()` before passing sliced buffers to crypto operations.
|
|
251
|
+
|
|
252
|
+
### Required helpers (include in every EC crypto module)
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Must import BOTH buffer packages
|
|
256
|
+
import {
|
|
257
|
+
createCipheriv,
|
|
258
|
+
createDecipheriv,
|
|
259
|
+
createECDH,
|
|
260
|
+
createHmac,
|
|
261
|
+
randomBytes,
|
|
262
|
+
Buffer as QCBuffer, // quick-crypto's Buffer — needed for setAAD()
|
|
263
|
+
} from 'react-native-quick-crypto';
|
|
264
|
+
import { Buffer } from 'buffer'; // Standard buffer polyfill — used for concat, from, etc.
|
|
265
|
+
|
|
266
|
+
// btoa is available in Hermes (RN 0.73+) but not in TS lib types
|
|
267
|
+
declare function btoa(input: string): string;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Hermes-safe base64 encoding.
|
|
271
|
+
* DO NOT use Buffer.toString('base64') — it is broken on Hermes.
|
|
272
|
+
*/
|
|
273
|
+
export function toBase64(buf: Uint8Array): string {
|
|
274
|
+
const bytes = new Uint8Array(buf);
|
|
275
|
+
let binary = '';
|
|
276
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
277
|
+
binary += String.fromCharCode(bytes[i]);
|
|
278
|
+
}
|
|
279
|
+
return btoa(binary);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Convert any Buffer-like value from quick-crypto to a standard Buffer. */
|
|
283
|
+
function toBuffer(input: unknown): Buffer {
|
|
284
|
+
if (Buffer.isBuffer(input)) return input;
|
|
285
|
+
return Buffer.from(input as Uint8Array);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a byte COPY (not a view) from a Buffer slice.
|
|
290
|
+
* quick-crypto's native code ignores byteOffset on typed-array views,
|
|
291
|
+
* reading the ENTIRE parent ArrayBuffer instead of just the slice.
|
|
292
|
+
* Passing `new Uint8Array(view)` guarantees a fresh ArrayBuffer.
|
|
293
|
+
*/
|
|
294
|
+
function byteCopy(buf: Buffer | Uint8Array, start?: number, end?: number): Uint8Array {
|
|
295
|
+
const view = start !== undefined ? buf.slice(start, end) : buf;
|
|
296
|
+
return new Uint8Array(view);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert URL-safe base64 to standard base64.
|
|
301
|
+
* Servers may return keys/salt with `-` and `_` instead of `+` and `/`.
|
|
302
|
+
*/
|
|
303
|
+
function urlSafeBase64ToStd(input: string): string {
|
|
304
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
305
|
+
return normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### HKDF-SHA256 (used by both per-request and session-init)
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const AES_ALGO = 'aes-256-gcm';
|
|
313
|
+
const AES_KEY_SIZE = 32;
|
|
314
|
+
const IV_SIZE = 12;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* HKDF-SHA256: Extract + single-block Expand.
|
|
318
|
+
* PRK = HMAC-SHA256(salt, IKM)
|
|
319
|
+
* OKM = HMAC-SHA256(PRK, info || 0x01)[0:32]
|
|
320
|
+
*
|
|
321
|
+
* All args are byteCopy'd before use — quick-crypto may misread views.
|
|
322
|
+
*/
|
|
323
|
+
function hkdfSha256(ikm: Buffer, salt: Buffer, info: Buffer): Buffer {
|
|
324
|
+
const prk = toBuffer(
|
|
325
|
+
createHmac('sha256', byteCopy(salt)).update(byteCopy(ikm)).digest(),
|
|
326
|
+
);
|
|
327
|
+
const t1 = toBuffer(
|
|
328
|
+
createHmac('sha256', byteCopy(prk))
|
|
329
|
+
.update(byteCopy(Buffer.concat([info, Buffer.from([0x01])])))
|
|
330
|
+
.digest(),
|
|
331
|
+
);
|
|
332
|
+
return t1.slice(0, AES_KEY_SIZE);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Full per-request implementation (React Native JS mode)
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
export async function ecEncryptPayload(
|
|
340
|
+
payload: unknown,
|
|
341
|
+
serverEcPublicKeyBase64: string,
|
|
342
|
+
nonce: string,
|
|
343
|
+
keyId?: string,
|
|
344
|
+
): Promise<{ envelope: EcEncryptedEnvelope; material: RequestCryptoMaterial }> {
|
|
345
|
+
const ecdh = createECDH('prime256v1');
|
|
346
|
+
ecdh.generateKeys();
|
|
347
|
+
const ephemeralPublicKeyBytes = toBuffer(ecdh.getPublicKey()); // 65 bytes
|
|
348
|
+
|
|
349
|
+
const serverPublicKeyBytes = Buffer.from(serverEcPublicKeyBase64, 'base64');
|
|
350
|
+
const sharedSecret = toBuffer(ecdh.computeSecret(serverPublicKeyBytes as any));
|
|
351
|
+
|
|
352
|
+
// HKDF: salt = nonce bytes, info = 'enc' (per-request contract)
|
|
353
|
+
const aesKey = hkdfSha256(
|
|
354
|
+
sharedSecret,
|
|
355
|
+
Buffer.from(nonce, 'utf8'),
|
|
356
|
+
Buffer.from('enc', 'utf8'),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const iv = toBuffer(randomBytes(IV_SIZE));
|
|
360
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
361
|
+
|
|
362
|
+
// byteCopy every arg before handing to quick-crypto cipher
|
|
363
|
+
const cipher = createCipheriv(AES_ALGO, byteCopy(aesKey), byteCopy(iv));
|
|
364
|
+
const encrypted = toBuffer(cipher.update(byteCopy(plaintext)));
|
|
365
|
+
const finalBlock = toBuffer(cipher.final());
|
|
366
|
+
const authTag = toBuffer(cipher.getAuthTag());
|
|
367
|
+
|
|
368
|
+
const data = Buffer.concat([encrypted, finalBlock, authTag]);
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
envelope: {
|
|
372
|
+
data: toBase64(data), // NOT data.toString('base64')
|
|
373
|
+
ephemeral_public_key: toBase64(ephemeralPublicKeyBytes), // NOT .toString('base64')
|
|
374
|
+
iv: toBase64(iv), // NOT iv.toString('base64')
|
|
375
|
+
nonce,
|
|
376
|
+
key_id: keyId,
|
|
377
|
+
},
|
|
378
|
+
material: { aesKey, requestIv: iv, keyId },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ECDH + HKDF only — for EC + Native mode (AES runs natively)
|
|
383
|
+
export async function ecDeriveKey(
|
|
384
|
+
serverEcPublicKeyBase64: string,
|
|
385
|
+
nonce: string,
|
|
386
|
+
): Promise<{ derivedAesKeyBase64: string; ephemeralPublicKeyBase64: string }> {
|
|
387
|
+
const ecdh = createECDH('prime256v1');
|
|
388
|
+
ecdh.generateKeys();
|
|
389
|
+
const ephemeralPublicKeyBytes = toBuffer(ecdh.getPublicKey());
|
|
390
|
+
const serverPublicKeyBytes = Buffer.from(serverEcPublicKeyBase64, 'base64');
|
|
391
|
+
const sharedSecret = toBuffer(ecdh.computeSecret(serverPublicKeyBytes as any));
|
|
392
|
+
const aesKey = hkdfSha256(sharedSecret, Buffer.from(nonce, 'utf8'), Buffer.from('enc', 'utf8'));
|
|
393
|
+
return {
|
|
394
|
+
derivedAesKeyBase64: toBase64(aesKey), // NOT aesKey.toString('base64')
|
|
395
|
+
ephemeralPublicKeyBase64: toBase64(ephemeralPublicKeyBytes), // NOT .toString('base64')
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Per-request EC: Response decryption — identical to RSA mode
|
|
400
|
+
export { decryptResponsePayload, isEncryptedResponse } from './crypto.util';
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Session-init EC: Full encrypt/decrypt functions
|
|
404
|
+
|
|
405
|
+
These are the WORKING implementations for session-init EC backends. They differ from
|
|
406
|
+
per-request EC in critical ways: the nonce is the IV, AAD is mandatory, and the
|
|
407
|
+
session AES key is reused across requests.
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
/**
|
|
411
|
+
* Build canonical AAD for AES-GCM.
|
|
412
|
+
* Keys MUST be alphabetically sorted. The backend must produce the exact same JSON.
|
|
413
|
+
*/
|
|
414
|
+
export function buildSessionAad(
|
|
415
|
+
keyId: string,
|
|
416
|
+
deviceId: string,
|
|
417
|
+
apiVersion = 'v1',
|
|
418
|
+
): Buffer {
|
|
419
|
+
// Alphabetical key order is part of the contract
|
|
420
|
+
return Buffer.from(
|
|
421
|
+
`{"api_ver":"${apiVersion}","device_id":"${deviceId}","key_id":"${keyId}"}`,
|
|
422
|
+
'utf8',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Derive a session AES-256 key from ECDH shared secret + server salt.
|
|
428
|
+
* Called once after encryption session init.
|
|
429
|
+
*
|
|
430
|
+
* IMPORTANT: The `info` string is backend-specific.
|
|
431
|
+
* This project uses "HDFC-LI-Session:{apiVersion}|{keyId}|{deviceId}".
|
|
432
|
+
* Do NOT assume "enc" — confirm with the backend team.
|
|
433
|
+
*/
|
|
434
|
+
export function deriveSessionKey(
|
|
435
|
+
clientEcdh: ReturnType<typeof createECDH>,
|
|
436
|
+
serverEcdhPubBase64: string,
|
|
437
|
+
saltBase64: string,
|
|
438
|
+
keyId: string,
|
|
439
|
+
deviceId: string,
|
|
440
|
+
apiVersion = 'v1',
|
|
441
|
+
): Buffer {
|
|
442
|
+
// Normalize base64url → standard base64 (with padding)
|
|
443
|
+
const serverPubBytes = Buffer.from(urlSafeBase64ToStd(serverEcdhPubBase64), 'base64');
|
|
444
|
+
const sharedSecret = toBuffer(clientEcdh.computeSecret(serverPubBytes as any));
|
|
445
|
+
const saltBytes = Buffer.from(urlSafeBase64ToStd(saltBase64), 'base64');
|
|
446
|
+
|
|
447
|
+
// Backend-specific info string — confirm this with your backend
|
|
448
|
+
const contextInfo = `HDFC-LI-Session:${apiVersion}|${keyId}|${deviceId}`;
|
|
449
|
+
return hkdfSha256(sharedSecret, saltBytes, Buffer.from(contextInfo, 'utf8'));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Encrypt a payload with a pre-derived session AES key.
|
|
454
|
+
* Uses nonce (12 chars) as the IV (not a random IV).
|
|
455
|
+
* Returns base64(ciphertext || 16-byte GCM auth tag).
|
|
456
|
+
*/
|
|
457
|
+
export function sessionEncrypt(
|
|
458
|
+
payload: unknown,
|
|
459
|
+
aesKey: Buffer,
|
|
460
|
+
nonce: string,
|
|
461
|
+
aad?: Buffer,
|
|
462
|
+
): string {
|
|
463
|
+
const iv = Buffer.from(nonce, 'utf8');
|
|
464
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
465
|
+
|
|
466
|
+
const cipher = createCipheriv(AES_ALGO, byteCopy(aesKey), byteCopy(iv));
|
|
467
|
+
// AAD must be set with QCBuffer.from() — the buffer polyfill's Buffer
|
|
468
|
+
// is a different class and setAAD will silently fail or throw.
|
|
469
|
+
if (aad && aad.length > 0) {
|
|
470
|
+
cipher.setAAD(QCBuffer.from(byteCopy(aad)));
|
|
471
|
+
}
|
|
472
|
+
const encrypted = toBuffer(cipher.update(byteCopy(plaintext)));
|
|
473
|
+
const finalBlock = toBuffer(cipher.final());
|
|
474
|
+
const authTag = toBuffer(cipher.getAuthTag());
|
|
475
|
+
|
|
476
|
+
const data = Buffer.concat([encrypted, finalBlock, authTag]);
|
|
477
|
+
return toBase64(data);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Decrypt a server response with a pre-derived session AES key.
|
|
482
|
+
* Input: base64(ciphertext || 16-byte GCM auth tag).
|
|
483
|
+
*
|
|
484
|
+
* Response IV source varies by backend:
|
|
485
|
+
* - This project: the nonce string (UTF-8 bytes) is the IV.
|
|
486
|
+
* - Other backends may return a separate `iv` field in the response.
|
|
487
|
+
*
|
|
488
|
+
* If decryption fails, this function probes multiple IV/AAD combinations
|
|
489
|
+
* to help identify the correct contract. Once confirmed, remove the probe
|
|
490
|
+
* and hardcode the working strategy.
|
|
491
|
+
*/
|
|
492
|
+
export function sessionDecrypt(
|
|
493
|
+
encryptedBase64: string,
|
|
494
|
+
aesKey: Buffer,
|
|
495
|
+
nonce: string,
|
|
496
|
+
aad?: Buffer,
|
|
497
|
+
): string {
|
|
498
|
+
const decoded = Buffer.from(encryptedBase64, 'base64');
|
|
499
|
+
if (decoded.length < 16) {
|
|
500
|
+
throw new Error(`Response too short: ${decoded.length} bytes`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// byteCopy every slice — Buffer.slice() returns views, not copies
|
|
504
|
+
const authTag = byteCopy(decoded, decoded.length - 16);
|
|
505
|
+
const ciphertext = byteCopy(decoded, 0, decoded.length - 16);
|
|
506
|
+
|
|
507
|
+
// Build IV candidates to probe
|
|
508
|
+
const ivCandidates: Array<{ label: string; iv: Buffer }> = [
|
|
509
|
+
{ label: 'utf8-nonce', iv: Buffer.from(nonce, 'utf8') },
|
|
510
|
+
];
|
|
511
|
+
try {
|
|
512
|
+
const decodedNonce = Buffer.from(urlSafeBase64ToStd(nonce), 'base64');
|
|
513
|
+
if (decodedNonce.length > 0) {
|
|
514
|
+
ivCandidates.push({ label: 'base64-nonce', iv: decodedNonce });
|
|
515
|
+
}
|
|
516
|
+
} catch { /* ignore invalid base64 */ }
|
|
517
|
+
ivCandidates.push({ label: 'zero-iv', iv: Buffer.alloc(12, 0) });
|
|
518
|
+
|
|
519
|
+
const aadCandidates: Array<{ label: string; aadValue?: Buffer }> = [
|
|
520
|
+
{ label: 'with-aad', aadValue: aad },
|
|
521
|
+
{ label: 'without-aad', aadValue: undefined },
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
let lastError: unknown;
|
|
525
|
+
for (const ivCandidate of ivCandidates) {
|
|
526
|
+
for (const aadCandidate of aadCandidates) {
|
|
527
|
+
try {
|
|
528
|
+
const decipher = createDecipheriv(
|
|
529
|
+
AES_ALGO,
|
|
530
|
+
byteCopy(aesKey),
|
|
531
|
+
byteCopy(ivCandidate.iv),
|
|
532
|
+
);
|
|
533
|
+
if (aadCandidate.aadValue && aadCandidate.aadValue.length > 0) {
|
|
534
|
+
decipher.setAAD(QCBuffer.from(byteCopy(aadCandidate.aadValue)));
|
|
535
|
+
}
|
|
536
|
+
decipher.setAuthTag(QCBuffer.from(authTag));
|
|
537
|
+
|
|
538
|
+
const decrypted = toBuffer(decipher.update(ciphertext));
|
|
539
|
+
const finalBlock = toBuffer(decipher.final());
|
|
540
|
+
const plaintext = Buffer.concat([decrypted, finalBlock]).toString('utf8');
|
|
541
|
+
|
|
542
|
+
// Log which strategy worked so you can hardcode it later.
|
|
543
|
+
// NEVER log the plaintext itself — only the strategy labels.
|
|
544
|
+
console.log('[sessionDecrypt] SUCCESS via', ivCandidate.label, aadCandidate.label);
|
|
545
|
+
return plaintext;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
lastError = error;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
throw lastError instanceof Error ? lastError : new Error('Session decrypt failed');
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Session-init auth service pattern (`auth.service.ts`)
|
|
557
|
+
|
|
558
|
+
For session-init EC, the encryption lifecycle is owned by a stateful service class,
|
|
559
|
+
not by generic interceptors. The service holds the derived session AES key in memory
|
|
560
|
+
and uses it for all encrypted API calls within that session.
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// src/services/auth.service.ts
|
|
564
|
+
import axios, { AxiosInstance } from 'axios';
|
|
565
|
+
import { createECDH, randomBytes } from 'react-native-quick-crypto';
|
|
566
|
+
import { Buffer } from 'buffer';
|
|
567
|
+
import {
|
|
568
|
+
deriveSessionKey,
|
|
569
|
+
sessionEncrypt,
|
|
570
|
+
sessionDecrypt,
|
|
571
|
+
buildSessionAad,
|
|
572
|
+
toBase64,
|
|
573
|
+
} from './security/ec-crypto.util';
|
|
574
|
+
|
|
575
|
+
const DEFAULT_ENCRYPTION_DEVICE_ID = 'DEV1001';
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Generate a cryptographically secure random alphanumeric nonce.
|
|
579
|
+
* MUST use CSPRNG (randomBytes) — not Math.random() — because for
|
|
580
|
+
* session-init EC the nonce IS the AES-GCM IV. A predictable IV
|
|
581
|
+
* under AES-GCM completely breaks confidentiality and authenticity.
|
|
582
|
+
*/
|
|
583
|
+
function generateNonce(length = 12): string {
|
|
584
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
585
|
+
const bytes = randomBytes(length);
|
|
586
|
+
let result = '';
|
|
587
|
+
for (let i = 0; i < length; i++) {
|
|
588
|
+
result += chars[bytes[i] % chars.length];
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
class AuthApiService {
|
|
594
|
+
private authClient: AxiosInstance;
|
|
595
|
+
private sessionAesKey: Buffer | null = null;
|
|
596
|
+
private encryptionKeyId: string | null = null;
|
|
597
|
+
private sessionDeviceId: string | null = null;
|
|
598
|
+
private sessionApiVersion = 'v1';
|
|
599
|
+
|
|
600
|
+
constructor(baseUrl: string) {
|
|
601
|
+
this.authClient = axios.create({
|
|
602
|
+
baseURL: baseUrl,
|
|
603
|
+
timeout: 30_000,
|
|
604
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Step 1: Initialize encryption session.
|
|
610
|
+
* Generates ECDH key pair, sends public key to server, derives session key.
|
|
611
|
+
*/
|
|
612
|
+
async initEncryptionSession(userId: string, deviceId?: string) {
|
|
613
|
+
const devId = deviceId || DEFAULT_ENCRYPTION_DEVICE_ID;
|
|
614
|
+
|
|
615
|
+
// Generate a REAL ECDH key pair — the private key is needed for HKDF
|
|
616
|
+
const ecdh = createECDH('prime256v1');
|
|
617
|
+
ecdh.generateKeys();
|
|
618
|
+
const clientPubKeyBase64 = toBase64(new Uint8Array(ecdh.getPublicKey() as any));
|
|
619
|
+
|
|
620
|
+
const response = await this.authClient.post(
|
|
621
|
+
'/api/v1/auth-services/api/auth/encryption/session/init',
|
|
622
|
+
{
|
|
623
|
+
client_ecdh_pub: clientPubKeyBase64, // MUST be the real generated key
|
|
624
|
+
device_id: devId,
|
|
625
|
+
user_id: userId,
|
|
626
|
+
api_version: 'v1',
|
|
627
|
+
platform: 'web',
|
|
628
|
+
session_duration_minutes: 120,
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const data = response.data;
|
|
633
|
+
|
|
634
|
+
// Derive session key directly from ECDH + HKDF.
|
|
635
|
+
// encrypted_shared_aes_key from server is IGNORED (confirmed from Flutter reference).
|
|
636
|
+
this.sessionAesKey = deriveSessionKey(
|
|
637
|
+
ecdh, data.server_ecdh_pub, data.salt, data.key_id, devId,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
this.encryptionKeyId = data.key_id;
|
|
641
|
+
this.sessionDeviceId = devId;
|
|
642
|
+
return data;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Step 2: Create auth session (uses key_id from init).
|
|
647
|
+
*/
|
|
648
|
+
async createSession(body: CreateSessionRequest) {
|
|
649
|
+
// ... standard auth call using this.encryptionKeyId ...
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Step 3: Encrypted business API call (example — replace with your actual API).
|
|
654
|
+
*/
|
|
655
|
+
async exampleApiTesting(token: string, payload: Record<string, unknown>) {
|
|
656
|
+
if (!this.encryptionKeyId || !this.sessionAesKey) {
|
|
657
|
+
throw new Error('Encryption session not initialized');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const nonce = generateNonce(); // 12-char alphanumeric
|
|
661
|
+
const aad = buildSessionAad(
|
|
662
|
+
this.encryptionKeyId,
|
|
663
|
+
this.sessionDeviceId!,
|
|
664
|
+
this.sessionApiVersion,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// Replace the payload and endpoint with your actual API contract
|
|
668
|
+
const encryptedData = sessionEncrypt(payload, this.sessionAesKey, nonce, aad);
|
|
669
|
+
|
|
670
|
+
const response = await this.authClient.post('/api/v1/your-service/your-endpoint', {
|
|
671
|
+
encryptedRequest: encryptedData,
|
|
672
|
+
keyId: this.encryptionKeyId,
|
|
673
|
+
}, {
|
|
674
|
+
headers: {
|
|
675
|
+
'X-Nonce': nonce,
|
|
676
|
+
'X-Device-ID': this.sessionDeviceId,
|
|
677
|
+
Authorization: `Bearer ${token}`,
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Decrypt response
|
|
682
|
+
if (response.data?.encryptedResponse) {
|
|
683
|
+
const decrypted = sessionDecrypt(
|
|
684
|
+
response.data.encryptedResponse,
|
|
685
|
+
this.sessionAesKey,
|
|
686
|
+
response.data.nonce || nonce,
|
|
687
|
+
aad,
|
|
688
|
+
);
|
|
689
|
+
return JSON.parse(decrypted);
|
|
690
|
+
}
|
|
691
|
+
return response.data;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## Step 4: Wire EC into API Service (`api.ts`)
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// New env variables read at module level
|
|
702
|
+
const ENCRYPTION_MODEL = (Config.ENCRYPTION_MODEL || 'rsa').toLowerCase(); // 'rsa' or 'ec'
|
|
703
|
+
|
|
704
|
+
// In constructor — instantiate EC key service alongside RSA key service:
|
|
705
|
+
private ecKeyService: EcKeyService;
|
|
706
|
+
|
|
707
|
+
// In setAuthToken — prefetch the active key service:
|
|
708
|
+
if (ENCRYPTION_ENABLED && token) {
|
|
709
|
+
if (ENCRYPTION_MODEL === 'ec') {
|
|
710
|
+
void this.ecKeyService.refreshPublicKey(token).catch(() => undefined);
|
|
711
|
+
} else {
|
|
712
|
+
void this.rsaKeyService.refreshPublicKey(token).catch(() => undefined);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// In request interceptor — branch on ENCRYPTION_MODEL (JS layer only):
|
|
717
|
+
// Note: use ENCRYPTION_LAYER === 'js' guard so EC+Native falls through to the native block below.
|
|
718
|
+
if (ENCRYPTION_MODEL === 'ec' && ENCRYPTION_LAYER === 'js') {
|
|
719
|
+
const keyData = await this.ecKeyService.getPublicKey(this.token || undefined);
|
|
720
|
+
const requestId = buildNonce(APP_ID);
|
|
721
|
+
const { envelope, material } = await ecEncryptPayload(
|
|
722
|
+
nextConfig.data, keyData.public_key, requestId, keyData.key_id,
|
|
723
|
+
);
|
|
724
|
+
this.requestMaterials.set(requestId, material);
|
|
725
|
+
nextConfig.headers.set('X-Request-Id', requestId);
|
|
726
|
+
if (keyData.key_id) {
|
|
727
|
+
nextConfig.headers.set('X-Key-Id', keyData.key_id);
|
|
728
|
+
}
|
|
729
|
+
// HMAC signing (EC mode — no `key` field, trailing `|` in canonical string)
|
|
730
|
+
if (Config.HMAC_ENABLED === 'true') {
|
|
731
|
+
const signature = buildHmacSignature(
|
|
732
|
+
{ method: nextConfig.method ?? 'POST', url: nextConfig.url ?? '',
|
|
733
|
+
nonce: requestId, keyId: keyData.key_id, envelope },
|
|
734
|
+
Config.HMAC_SHARED_SECRET ?? '',
|
|
735
|
+
);
|
|
736
|
+
nextConfig.headers.set('X-Payload-Signature', signature);
|
|
737
|
+
nextConfig.headers.set('X-Signature-Algorithm', 'HMAC-SHA256');
|
|
738
|
+
}
|
|
739
|
+
nextConfig.data = envelope;
|
|
740
|
+
nextConfig.metadata = { ...nextConfig.metadata, requestId };
|
|
741
|
+
} else if (ENCRYPTION_MODEL === 'rsa') {
|
|
742
|
+
// existing RSA JS path (unchanged)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// In request interceptor — branch on ENCRYPTION_MODEL (Native layer):
|
|
746
|
+
if (ENCRYPTION_MODEL === 'ec' && ENCRYPTION_LAYER === 'native') {
|
|
747
|
+
const keyData = await this.ecKeyService.getPublicKey(this.token || undefined);
|
|
748
|
+
const requestId = buildNonce(APP_ID);
|
|
749
|
+
const { derivedAesKeyBase64, ephemeralPublicKeyBase64 } = await ecDeriveKey(
|
|
750
|
+
keyData.public_key, requestId,
|
|
751
|
+
);
|
|
752
|
+
const { ciphertext, iv } = await nativeEncryptAES(JSON.stringify(nextConfig.data), derivedAesKeyBase64);
|
|
753
|
+
nextConfig.data = {
|
|
754
|
+
data: ciphertext,
|
|
755
|
+
ephemeral_public_key: ephemeralPublicKeyBase64,
|
|
756
|
+
iv,
|
|
757
|
+
nonce: requestId,
|
|
758
|
+
key_id: keyData.key_id,
|
|
759
|
+
};
|
|
760
|
+
this.requestMaterials.set(requestId, {
|
|
761
|
+
aesKey: Buffer.from(derivedAesKeyBase64, 'base64'),
|
|
762
|
+
requestIv: Buffer.from(iv, 'base64'),
|
|
763
|
+
keyId: keyData.key_id,
|
|
764
|
+
});
|
|
765
|
+
nextConfig.metadata = { ...nextConfig.metadata, requestId };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// On KEY_ID_NOT_FOUND (401) — refresh the active key service:
|
|
769
|
+
if (ENCRYPTION_MODEL === 'ec') {
|
|
770
|
+
return this.ecKeyService.refreshPublicKey(this.token || undefined)
|
|
771
|
+
.then(() => this.instance.request(retryConfig));
|
|
772
|
+
} else {
|
|
773
|
+
return this.rsaKeyService.refreshPublicKey(this.token || undefined)
|
|
774
|
+
.then(() => this.instance.request(retryConfig));
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
## Step 5: Environment Variables
|
|
781
|
+
|
|
782
|
+
```
|
|
783
|
+
ENCRYPTION_MODEL=ec
|
|
784
|
+
EC_PUBLIC_KEY_ENDPOINT=https://api.example.com/api/security/ec-public-key
|
|
785
|
+
EC_KEY_RETRY=3
|
|
786
|
+
|
|
787
|
+
# EC + Native mode only:
|
|
788
|
+
ENCRYPTION_LAYER=native
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## Deliverables Summary
|
|
794
|
+
|
|
795
|
+
### EC + React Native JS
|
|
796
|
+
|
|
797
|
+
| File | Action |
|
|
798
|
+
|------|--------|
|
|
799
|
+
| `src/services/security/types.ts` | Add `EcPublicKeyResponse`, `EcEncryptedEnvelope` |
|
|
800
|
+
| `src/services/security/ec-key.service.ts` | Create |
|
|
801
|
+
| `src/services/security/ec-crypto.util.ts` | Create (`ecEncryptPayload`, `ecDeriveKey`) |
|
|
802
|
+
| `src/services/api.ts` | Add `ENCRYPTION_MODEL` routing |
|
|
803
|
+
| `.env` / `.env.example` | Add EC env vars |
|
|
804
|
+
|
|
805
|
+
### EC + Native
|
|
806
|
+
|
|
807
|
+
All EC JS deliverables above, plus:
|
|
808
|
+
|
|
809
|
+
| File | Action |
|
|
810
|
+
|------|--------|
|
|
811
|
+
| `src/services/security/ec-crypto.util.ts` | Use `ecDeriveKey` (no AES in JS) |
|
|
812
|
+
| `src/services/api.ts` | Call `ecDeriveKey` → pass `derivedAesKeyBase64` to existing `nativeEncryptAES` |
|
|
813
|
+
| `.env` | Add `ENCRYPTION_LAYER=native` |
|
|
814
|
+
|
|
815
|
+
No new Kotlin or Swift files required — existing `NativeCryptoModule` is reused.
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Security Requirements (EC mode)
|
|
820
|
+
|
|
821
|
+
- EC public key MUST be validated on every fetch: decoded length == 65, first byte == `0x04`.
|
|
822
|
+
- Ephemeral EC key pair is discarded immediately after `computeSecret()` returns.
|
|
823
|
+
- Derived AES key is NEVER transmitted — only the 65-byte ephemeral public key is sent.
|
|
824
|
+
- HKDF salt MUST be the per-request nonce to bind the derived key to this specific request.
|
|
825
|
+
- On `KEY_ID_NOT_FOUND` (401): refresh EC public key and retry exactly once — no loop.
|
|
826
|
+
|
|
827
|
+
Additional requirements for session-init EC backends:
|
|
828
|
+
|
|
829
|
+
- Do not unwrap `encrypted_shared_aes_key` unless the backend contract explicitly requires it.
|
|
830
|
+
- Normalize and pad base64url fields before decoding.
|
|
831
|
+
- Treat AAD as part of the contract, not an optional add-on.
|
|
832
|
+
- Confirm whether the response IV comes from `iv`, `nonce`, or another field before implementing decryption.
|