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,668 @@
|
|
|
1
|
+
# Platform: React JS (Web) — Production Crypto (Approach 7)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
React JS web projects on approach 7 use the **built-in Web Crypto API** (`window.crypto.subtle`) for all cryptographic operations.
|
|
6
|
+
No external crypto packages are required. All crypto is native to the browser.
|
|
7
|
+
|
|
8
|
+
Supports two modes:
|
|
9
|
+
- **RSA-OAEP** (default) — per-request AES-256-GCM key wrapped with the server's RSA public key. Full request/response encryption. Per-request key isolation.
|
|
10
|
+
- **ECDH-Session** — ECDH P-256 + HKDF-SHA256 session key derivation + AES-256-GCM with AAD. No RSA. Session key derived once per session. Use when integrating with ECDH-Session / HDFC-LI partner APIs.
|
|
11
|
+
|
|
12
|
+
> **Approach 7 vs Approach 1 for React:**
|
|
13
|
+
> Approach 1 is a basic `crypto.subtle` encrypt-only interceptor with no response decryption, no RSA key management, and no session handling.
|
|
14
|
+
> Approach 7 is the full production-grade React JS equivalent of React Native approach 4 — it adds per-request key isolation, response decryption, RSA key service (fetch/cache/retry), in-memory request mapping with guaranteed cleanup, browser-safe storage, and ECDH-Session support.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Prerequisite: Web Crypto API Availability
|
|
19
|
+
|
|
20
|
+
`crypto.subtle` is only available on **secure origins** (HTTPS or `localhost`).
|
|
21
|
+
It is `undefined` on plain HTTP. Verify at startup:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
if (typeof window.crypto?.subtle === 'undefined') {
|
|
25
|
+
throw new Error('crypto.subtle unavailable — app must be served over HTTPS or localhost');
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Framework Detection (Env Var Prefix)
|
|
32
|
+
|
|
33
|
+
Detect from `package.json` before implementation:
|
|
34
|
+
|
|
35
|
+
| Framework | Env prefix | Detection |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Vite (React) | `VITE_` | `vite` in devDependencies |
|
|
38
|
+
| Create React App | `REACT_APP_` | `react-scripts` in dependencies |
|
|
39
|
+
| Next.js | `NEXT_PUBLIC_` | `next` in dependencies |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## File Naming Convention (Approach 7)
|
|
44
|
+
|
|
45
|
+
| Role | Path |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Crypto utilities | `src/services/security/crypto-utils.ts` |
|
|
48
|
+
| AES service | `src/services/security/aes.service.ts` |
|
|
49
|
+
| RSA service | `src/services/security/rsa.service.ts` |
|
|
50
|
+
| RSA key management | `src/services/security/rsa-key.service.ts` |
|
|
51
|
+
| HMAC service | `src/services/security/hmac.service.ts` |
|
|
52
|
+
| Browser storage | `src/services/security/storage.service.ts` |
|
|
53
|
+
| Interceptor | `src/services/security/encryption.interceptor.ts` |
|
|
54
|
+
| Config / feature flags | `src/services/security/encryption-config.ts` |
|
|
55
|
+
| TypeScript types | `src/services/security/types.ts` |
|
|
56
|
+
| ECDH Base64URL util | `src/core/security/ecdh-session/base64url.ts` |
|
|
57
|
+
| ECDH AAD builder | `src/core/security/ecdh-session/aad.ts` |
|
|
58
|
+
| ECDH key pair + derivation | `src/core/security/ecdh-session/ecdhSession.ts` |
|
|
59
|
+
| AES-GCM with AAD | `src/core/security/ecdh-session/aesGcm.ts` |
|
|
60
|
+
| ECDH session service | `src/core/security/ecdh-session/ecdhSessionService.ts` |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Mode A: RSA-OAEP
|
|
65
|
+
|
|
66
|
+
### Crypto Utilities (`crypto-utils.ts`)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// No Buffer dependency — pure browser APIs
|
|
70
|
+
export const bufferToBase64 = (buffer: ArrayBuffer): string => {
|
|
71
|
+
const bytes = new Uint8Array(buffer);
|
|
72
|
+
let binary = '';
|
|
73
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
74
|
+
return btoa(binary);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const base64ToBuffer = (base64: string): ArrayBuffer => {
|
|
78
|
+
const binary = atob(base64);
|
|
79
|
+
const bytes = new Uint8Array(binary.length);
|
|
80
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
81
|
+
return bytes.buffer;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const generateNonce = (appId: string): string => {
|
|
85
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(8));
|
|
86
|
+
const hex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
87
|
+
return `${appId}^${Date.now()}.${hex}`;
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### AES Service (`aes.service.ts`)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { bufferToBase64, base64ToBuffer } from './crypto-utils';
|
|
95
|
+
|
|
96
|
+
export interface AESKeyMaterial {
|
|
97
|
+
cryptoKey: CryptoKey; // for encrypt/decrypt calls
|
|
98
|
+
rawBytes: Uint8Array; // for RSA-OAEP wrapping
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const generateAESKey = async (): Promise<AESKeyMaterial> => {
|
|
102
|
+
const cryptoKey = await crypto.subtle.generateKey(
|
|
103
|
+
{ name: 'AES-GCM', length: 256 },
|
|
104
|
+
true,
|
|
105
|
+
['encrypt', 'decrypt'],
|
|
106
|
+
);
|
|
107
|
+
const rawBytes = new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKey));
|
|
108
|
+
return { cryptoKey, rawBytes };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Web Crypto AES-GCM appends the 16-byte auth tag automatically
|
|
112
|
+
export const encryptAESGCM = async (
|
|
113
|
+
plaintext: string, key: CryptoKey, iv: Uint8Array,
|
|
114
|
+
): Promise<ArrayBuffer> => {
|
|
115
|
+
const data = new TextEncoder().encode(plaintext);
|
|
116
|
+
return crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, data);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const decryptAESGCM = async (
|
|
120
|
+
cipherWithTag: ArrayBuffer, key: CryptoKey, iv: Uint8Array,
|
|
121
|
+
): Promise<string> => {
|
|
122
|
+
// Throws DOMException on auth tag mismatch — treat as tamper detection
|
|
123
|
+
const plainBuffer = await crypto.subtle.decrypt(
|
|
124
|
+
{ name: 'AES-GCM', iv, tagLength: 128 }, key, cipherWithTag,
|
|
125
|
+
);
|
|
126
|
+
return new TextDecoder().decode(plainBuffer);
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### RSA Service (`rsa.service.ts`)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { base64ToBuffer } from './crypto-utils';
|
|
134
|
+
|
|
135
|
+
// Input: PEM (strip headers) or raw Base64 DER → SPKI format required by crypto.subtle
|
|
136
|
+
export const importRSAPublicKey = async (pemOrBase64: string): Promise<CryptoKey> => {
|
|
137
|
+
const base64 = pemOrBase64
|
|
138
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
139
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
140
|
+
.replace(/\s/g, '');
|
|
141
|
+
return crypto.subtle.importKey(
|
|
142
|
+
'spki',
|
|
143
|
+
base64ToBuffer(base64),
|
|
144
|
+
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
|
145
|
+
false,
|
|
146
|
+
['encrypt'],
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const encryptRSAOAEP = async (rsaKey: CryptoKey, aesKeyBytes: Uint8Array): Promise<ArrayBuffer> =>
|
|
151
|
+
crypto.subtle.encrypt({ name: 'RSA-OAEP' }, rsaKey, aesKeyBytes);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### TypeScript Types (`types.ts`)
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
export interface EncryptedEnvelope {
|
|
158
|
+
data: string; // base64(ciphertext + 16-byte GCM auth_tag)
|
|
159
|
+
key: string; // base64(RSA-OAEP encrypted AES key bytes)
|
|
160
|
+
iv: string; // base64(12-byte IV)
|
|
161
|
+
nonce: string; // appId^unix_timestamp_ms.random_hex
|
|
162
|
+
key_id: string; // RSA key version identifier
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface EncryptedResponse {
|
|
166
|
+
data: string; // base64(ciphertext + 16-byte GCM auth_tag)
|
|
167
|
+
iv: string; // base64(new server-generated 12-byte IV — never the request IV)
|
|
168
|
+
nonce?: string;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Request Interceptor Pattern
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// src/services/security/encryption.interceptor.ts
|
|
176
|
+
import { generateAESKey, encryptAESGCM, decryptAESGCM } from './aes.service';
|
|
177
|
+
import { encryptRSAOAEP } from './rsa.service';
|
|
178
|
+
import { bufferToBase64, base64ToBuffer, generateNonce } from './crypto-utils';
|
|
179
|
+
import { getRSAPublicKey, getCurrentKeyId, refreshRSAPublicKey } from './rsa-key.service';
|
|
180
|
+
import { ENCRYPTION_ENABLED, EXCLUDED_PATHS, APP_ID, HMAC_ENABLED, HMAC_SECRET } from './encryption-config';
|
|
181
|
+
|
|
182
|
+
const ENCRYPT_METHODS = new Set(['post', 'put', 'patch']);
|
|
183
|
+
const requestMap = new Map<string, { cryptoKey: CryptoKey; iv: Uint8Array }>();
|
|
184
|
+
|
|
185
|
+
export const applyProductionEncryptionInterceptor = (axiosInstance: AxiosInstance): void => {
|
|
186
|
+
|
|
187
|
+
axiosInstance.interceptors.request.use(async (config) => {
|
|
188
|
+
if (!ENCRYPTION_ENABLED) return config;
|
|
189
|
+
if (!config.method || !ENCRYPT_METHODS.has(config.method.toLowerCase())) return config;
|
|
190
|
+
if (!config.data) return config;
|
|
191
|
+
if (config.data instanceof FormData) return config;
|
|
192
|
+
if (EXCLUDED_PATHS.some(path => config.url?.includes(path))) return config;
|
|
193
|
+
|
|
194
|
+
const rsaPublicKey = await getRSAPublicKey();
|
|
195
|
+
const { cryptoKey, rawBytes } = await generateAESKey();
|
|
196
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
197
|
+
const nonce = generateNonce(APP_ID);
|
|
198
|
+
const requestId = crypto.randomUUID();
|
|
199
|
+
|
|
200
|
+
const plaintext = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
|
|
201
|
+
const cipherWithTag = await encryptAESGCM(plaintext, cryptoKey, iv);
|
|
202
|
+
const encryptedKey = await encryptRSAOAEP(rsaPublicKey, rawBytes);
|
|
203
|
+
|
|
204
|
+
requestMap.set(requestId, { cryptoKey, iv });
|
|
205
|
+
|
|
206
|
+
config.data = {
|
|
207
|
+
data: bufferToBase64(cipherWithTag),
|
|
208
|
+
key: bufferToBase64(encryptedKey),
|
|
209
|
+
iv: bufferToBase64(iv),
|
|
210
|
+
nonce,
|
|
211
|
+
key_id: getCurrentKeyId(),
|
|
212
|
+
};
|
|
213
|
+
config.headers['X-Request-ID'] = requestId;
|
|
214
|
+
|
|
215
|
+
if (HMAC_ENABLED && HMAC_SECRET) {
|
|
216
|
+
const canonical = `${(config.method ?? 'POST').toUpperCase()}|${config.url}|${nonce}|${config.data.data}`;
|
|
217
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
218
|
+
'raw', new TextEncoder().encode(HMAC_SECRET),
|
|
219
|
+
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
|
|
220
|
+
);
|
|
221
|
+
const sig = await crypto.subtle.sign('HMAC', keyMaterial, new TextEncoder().encode(canonical));
|
|
222
|
+
config.headers['X-Request-Signature'] = bufferToBase64(sig);
|
|
223
|
+
config.headers['X-Nonce'] = nonce;
|
|
224
|
+
}
|
|
225
|
+
return config;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
axiosInstance.interceptors.response.use(
|
|
229
|
+
async (response) => {
|
|
230
|
+
if (!ENCRYPTION_ENABLED) return response;
|
|
231
|
+
const requestId = response.config.headers['X-Request-ID'] as string;
|
|
232
|
+
const mapping = requestMap.get(requestId);
|
|
233
|
+
if (requestId) requestMap.delete(requestId); // always clean up
|
|
234
|
+
|
|
235
|
+
if (!mapping || !response.data?.data || !response.data?.iv) return response;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const responseIv = new Uint8Array(base64ToBuffer(response.data.iv));
|
|
239
|
+
const cipherWithTag = base64ToBuffer(response.data.data);
|
|
240
|
+
const plaintext = await decryptAESGCM(cipherWithTag, mapping.cryptoKey, responseIv);
|
|
241
|
+
response.data = JSON.parse(plaintext);
|
|
242
|
+
} catch (err: unknown) {
|
|
243
|
+
if (err instanceof DOMException) {
|
|
244
|
+
throw new Error('ERR_CRYPTO_TAG_MISMATCH: payload tampered or wrong key');
|
|
245
|
+
}
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
return response;
|
|
249
|
+
},
|
|
250
|
+
async (error) => {
|
|
251
|
+
const requestId = error.config?.headers?.['X-Request-ID'];
|
|
252
|
+
if (requestId) requestMap.delete(requestId); // cleanup on error
|
|
253
|
+
|
|
254
|
+
if (error.response?.status === 401 &&
|
|
255
|
+
error.response?.data?.error?.code === 'KEY_ID_NOT_FOUND') {
|
|
256
|
+
await refreshRSAPublicKey();
|
|
257
|
+
return axiosInstance.request(error.config);
|
|
258
|
+
}
|
|
259
|
+
return Promise.reject(error);
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### RSA Key Service (`rsa-key.service.ts`)
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { importRSAPublicKey } from './rsa.service';
|
|
269
|
+
import { RSA_KEY_ENDPOINT, RSA_KEY_RETRIES } from './encryption-config';
|
|
270
|
+
|
|
271
|
+
let cachedKey: CryptoKey | null = null;
|
|
272
|
+
let currentKeyId = '';
|
|
273
|
+
|
|
274
|
+
export const getRSAPublicKey = async (): Promise<CryptoKey> => {
|
|
275
|
+
if (cachedKey) return cachedKey;
|
|
276
|
+
return fetchAndCacheRSAPublicKey();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const fetchAndCacheRSAPublicKey = async (retries = RSA_KEY_RETRIES): Promise<CryptoKey> => {
|
|
280
|
+
for (let i = 1; i <= retries; i++) {
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch(RSA_KEY_ENDPOINT, { credentials: 'include' });
|
|
283
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
284
|
+
const { public_key, key_id } = await res.json();
|
|
285
|
+
cachedKey = await importRSAPublicKey(public_key);
|
|
286
|
+
currentKeyId = key_id;
|
|
287
|
+
return cachedKey;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (i === retries) throw new Error(`ERR_RSA_KEY_UNAVAILABLE: ${err}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
throw new Error('ERR_RSA_KEY_UNAVAILABLE');
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const refreshRSAPublicKey = async (): Promise<void> => {
|
|
296
|
+
cachedKey = null;
|
|
297
|
+
currentKeyId = '';
|
|
298
|
+
await fetchAndCacheRSAPublicKey();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const getCurrentKeyId = (): string => currentKeyId;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Config + Feature Flags (`encryption-config.ts`)
|
|
305
|
+
|
|
306
|
+
Auto-generated with the correct env prefix for the detected framework:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Vite:
|
|
310
|
+
export const ENCRYPTION_ENABLED = import.meta.env.VITE_ENCRYPTION_ENABLED === 'true';
|
|
311
|
+
export const RSA_KEY_ENDPOINT = import.meta.env.VITE_RSA_KEY_ENDPOINT ?? '/api/security/public-key';
|
|
312
|
+
export const APP_ID = import.meta.env.VITE_APP_ID ?? 'web-app';
|
|
313
|
+
export const HMAC_ENABLED = import.meta.env.VITE_HMAC_ENABLED === 'true';
|
|
314
|
+
export const HMAC_SECRET = import.meta.env.VITE_HMAC_SECRET ?? '';
|
|
315
|
+
export const RSA_KEY_RETRIES = 3;
|
|
316
|
+
export const EXCLUDED_PATHS: string[] = [];
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// CRA: replace VITE_ with REACT_APP_ and import.meta.env with process.env
|
|
321
|
+
// Next.js: replace VITE_ with NEXT_PUBLIC_ and import.meta.env with process.env
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Browser Storage (`storage.service.ts`)
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// sessionStorage — cleared on tab close; acceptable for short-lived tokens
|
|
328
|
+
export const setToken = (key: string, value: string): void => sessionStorage.setItem(key, value);
|
|
329
|
+
export const getToken = (key: string): string | null => sessionStorage.getItem(key);
|
|
330
|
+
export const removeToken = (key: string): void => sessionStorage.removeItem(key);
|
|
331
|
+
export const clearAllTokens = (): void => sessionStorage.clear();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
> HTTP-only cookie alternative (recommended for auth tokens): backend sets `Set-Cookie: HttpOnly; Secure; SameSite=Strict`.
|
|
335
|
+
> Configure Axios with `{ withCredentials: true }`. No client-side token storage code needed.
|
|
336
|
+
|
|
337
|
+
### Module Wiring (`axiosInstance.ts`)
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import axios from 'axios';
|
|
341
|
+
import { applyProductionEncryptionInterceptor } from '../services/security/encryption.interceptor';
|
|
342
|
+
import { getRSAPublicKey } from '../services/security/rsa-key.service';
|
|
343
|
+
import { ENCRYPTION_ENABLED } from '../services/security/encryption-config';
|
|
344
|
+
|
|
345
|
+
const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL });
|
|
346
|
+
applyProductionEncryptionInterceptor(axiosInstance);
|
|
347
|
+
// ENCRYPTION_INTERCEPTOR_PLACEHOLDER
|
|
348
|
+
|
|
349
|
+
export default axiosInstance;
|
|
350
|
+
|
|
351
|
+
// Bootstrap: call in App.tsx or auth provider before first API call
|
|
352
|
+
export const bootstrapEncryption = async (): Promise<void> => {
|
|
353
|
+
if (ENCRYPTION_ENABLED) await getRSAPublicKey();
|
|
354
|
+
};
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### .env Files
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
VITE_ENCRYPTION_ENABLED=true
|
|
361
|
+
VITE_RSA_KEY_ENDPOINT=https://<your-api-host>/api/security/public-key
|
|
362
|
+
VITE_APP_ID=com.yourapp.web
|
|
363
|
+
VITE_HMAC_ENABLED=false
|
|
364
|
+
VITE_HMAC_SECRET=
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Set `VITE_ENCRYPTION_ENABLED=false` in `.env.development` and `true` in `.env.production`.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Mode B: ECDH-Session
|
|
372
|
+
|
|
373
|
+
Use when integrating with ECDH-Session / HDFC-LI partner APIs.
|
|
374
|
+
Web Crypto API natively supports ECDH + HKDF — no manual RFC 5869 HMAC chain required.
|
|
375
|
+
Session key derived once per session from ECDH + HKDF-SHA256; IV is random per request.
|
|
376
|
+
|
|
377
|
+
### ECDH-Session Questions (Additional, all blocking)
|
|
378
|
+
|
|
379
|
+
Ask before implementation — no safe defaults:
|
|
380
|
+
1. **Session init endpoint URL** — e.g. `POST <API_BASE_URL>/api/security/session/init`
|
|
381
|
+
2. **HKDF info prefix** — default `HDFC-LI-Session:v1`
|
|
382
|
+
3. **AAD field order** — default `{"api_ver":"v1","device_id":"<deviceId>","key_id":"<keyId>"}` (alphabetical, no spaces)
|
|
383
|
+
4. **API version string** for AAD `api_ver` — default `v1`
|
|
384
|
+
|
|
385
|
+
### Base64URL Utility (`base64url.ts`)
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
export const toBase64URL = (buffer: ArrayBuffer): string => {
|
|
389
|
+
const bytes = new Uint8Array(buffer);
|
|
390
|
+
let binary = '';
|
|
391
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
392
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export const fromBase64URL = (str: string): ArrayBuffer => {
|
|
396
|
+
const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
397
|
+
const padded = b64.padEnd(b64.length + (4 - (b64.length % 4)) % 4, '=');
|
|
398
|
+
const binary = atob(padded);
|
|
399
|
+
const bytes = new Uint8Array(binary.length);
|
|
400
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
401
|
+
return bytes.buffer;
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### AAD Builder (`aad.ts`)
|
|
406
|
+
|
|
407
|
+
Fixed alphabetical key order — never use `JSON.stringify`:
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
export const buildAAD = (apiVersion: string, deviceId: string, keyId: string): ArrayBuffer =>
|
|
411
|
+
new TextEncoder().encode(
|
|
412
|
+
`{"api_ver":"${apiVersion}","device_id":"${deviceId}","key_id":"${keyId}"}`
|
|
413
|
+
);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### ECDH Key Pair + HKDF Session Key (`ecdhSession.ts`)
|
|
417
|
+
|
|
418
|
+
Web Crypto API handles HKDF natively in one `deriveKey` call:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { toBase64URL, fromBase64URL } from './base64url';
|
|
422
|
+
|
|
423
|
+
export const generateECDHKeyPair = async (): Promise<{ privateKey: CryptoKey; publicKeyBase64URL: string }> => {
|
|
424
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
425
|
+
{ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'],
|
|
426
|
+
);
|
|
427
|
+
const publicKeyBytes = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
|
428
|
+
return { privateKey: keyPair.privateKey, publicKeyBase64URL: toBase64URL(publicKeyBytes) };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export const computeSessionAESKey = async (
|
|
432
|
+
privateKey: CryptoKey,
|
|
433
|
+
serverPublicKeyBase64URL: string,
|
|
434
|
+
saltBase64URL: string,
|
|
435
|
+
keyId: string,
|
|
436
|
+
deviceId: string,
|
|
437
|
+
infoPrefix = 'HDFC-LI-Session:v1',
|
|
438
|
+
): Promise<CryptoKey> => {
|
|
439
|
+
const serverPublicKey = await crypto.subtle.importKey(
|
|
440
|
+
'raw', fromBase64URL(serverPublicKeyBase64URL),
|
|
441
|
+
{ name: 'ECDH', namedCurve: 'P-256' }, false, [],
|
|
442
|
+
);
|
|
443
|
+
const sharedBits = await crypto.subtle.deriveBits(
|
|
444
|
+
{ name: 'ECDH', public: serverPublicKey }, privateKey, 256,
|
|
445
|
+
);
|
|
446
|
+
const hkdfKey = await crypto.subtle.importKey('raw', sharedBits, { name: 'HKDF' }, false, ['deriveKey']);
|
|
447
|
+
return crypto.subtle.deriveKey(
|
|
448
|
+
{
|
|
449
|
+
name: 'HKDF', hash: 'SHA-256',
|
|
450
|
+
salt: fromBase64URL(saltBase64URL),
|
|
451
|
+
info: new TextEncoder().encode(`${infoPrefix}|${keyId}|${deviceId}`),
|
|
452
|
+
},
|
|
453
|
+
hkdfKey,
|
|
454
|
+
{ name: 'AES-GCM', length: 256 },
|
|
455
|
+
false, // derived key is non-extractable — never stored
|
|
456
|
+
['encrypt', 'decrypt'],
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### AES-GCM with AAD (`aesGcm.ts`)
|
|
462
|
+
|
|
463
|
+
Format: `Base64URL(IV || ciphertext || authTag)` — same as the React Native ECDH-Session skill:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import { toBase64URL, fromBase64URL } from './base64url';
|
|
467
|
+
|
|
468
|
+
const IV_BYTES = 12;
|
|
469
|
+
const TAG_BYTES = 16;
|
|
470
|
+
|
|
471
|
+
export const encryptAESGCM = async (plaintext: string, aesKey: CryptoKey, aad: ArrayBuffer): Promise<string> => {
|
|
472
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
473
|
+
const data = new TextEncoder().encode(plaintext);
|
|
474
|
+
const cipherWithTag = await crypto.subtle.encrypt(
|
|
475
|
+
{ name: 'AES-GCM', iv, additionalData: aad, tagLength: 128 }, aesKey, data,
|
|
476
|
+
);
|
|
477
|
+
const combined = new Uint8Array(IV_BYTES + cipherWithTag.byteLength);
|
|
478
|
+
combined.set(iv, 0);
|
|
479
|
+
combined.set(new Uint8Array(cipherWithTag), IV_BYTES);
|
|
480
|
+
return toBase64URL(combined.buffer);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const decryptAESGCM = async (payloadBase64URL: string, aesKey: CryptoKey, aad: ArrayBuffer): Promise<string> => {
|
|
484
|
+
const combined = new Uint8Array(fromBase64URL(payloadBase64URL));
|
|
485
|
+
if (combined.length < IV_BYTES + TAG_BYTES + 1) throw new Error('ERR_CRYPTO_PAYLOAD_TOO_SHORT');
|
|
486
|
+
const iv = combined.slice(0, IV_BYTES);
|
|
487
|
+
const cipherWithTag = combined.slice(IV_BYTES);
|
|
488
|
+
// DOMException thrown on auth tag failure — caller must handle as tamper detection
|
|
489
|
+
const plainBuffer = await crypto.subtle.decrypt(
|
|
490
|
+
{ name: 'AES-GCM', iv, additionalData: aad, tagLength: 128 }, aesKey, cipherWithTag,
|
|
491
|
+
);
|
|
492
|
+
return new TextDecoder().decode(plainBuffer);
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### ECDH Session Service (`ecdhSessionService.ts`)
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { generateECDHKeyPair, computeSessionAESKey } from './ecdhSession';
|
|
500
|
+
|
|
501
|
+
const STORAGE_KEY = 'ecdh_session';
|
|
502
|
+
|
|
503
|
+
export interface SessionMaterial {
|
|
504
|
+
privateKeyJwk: JsonWebKey;
|
|
505
|
+
serverPublicKeyBase64URL: string;
|
|
506
|
+
saltBase64URL: string;
|
|
507
|
+
keyId: string;
|
|
508
|
+
deviceId: string;
|
|
509
|
+
apiVersion: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let _sessionAESKey: CryptoKey | null = null;
|
|
513
|
+
let _sessionMaterial: SessionMaterial | null = null;
|
|
514
|
+
|
|
515
|
+
export const getSessionAESKey = (): CryptoKey => {
|
|
516
|
+
if (!_sessionAESKey) throw new Error('ERR_SESSION_NOT_INITIALIZED');
|
|
517
|
+
return _sessionAESKey;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const getSessionMaterial = (): SessionMaterial => {
|
|
521
|
+
if (!_sessionMaterial) throw new Error('ERR_SESSION_NOT_INITIALIZED');
|
|
522
|
+
return _sessionMaterial;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const getOrCreateDeviceId = (): string => {
|
|
526
|
+
let id = sessionStorage.getItem('device_id');
|
|
527
|
+
if (!id) { id = crypto.randomUUID(); sessionStorage.setItem('device_id', id); }
|
|
528
|
+
return id;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export const restoreSession = async (): Promise<boolean> => {
|
|
532
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
533
|
+
if (!raw) return false;
|
|
534
|
+
const mat: SessionMaterial = JSON.parse(raw);
|
|
535
|
+
const privateKey = await crypto.subtle.importKey(
|
|
536
|
+
'jwk', mat.privateKeyJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'],
|
|
537
|
+
);
|
|
538
|
+
_sessionMaterial = mat;
|
|
539
|
+
_sessionAESKey = await computeSessionAESKey(
|
|
540
|
+
privateKey, mat.serverPublicKeyBase64URL, mat.saltBase64URL, mat.keyId, mat.deviceId,
|
|
541
|
+
);
|
|
542
|
+
return true;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export const initSession = async (sessionInitUrl: string, apiVersion = 'v1'): Promise<void> => {
|
|
546
|
+
const deviceId = getOrCreateDeviceId();
|
|
547
|
+
const { privateKey, publicKeyBase64URL } = await generateECDHKeyPair();
|
|
548
|
+
|
|
549
|
+
const res = await fetch(sessionInitUrl, {
|
|
550
|
+
method: 'POST',
|
|
551
|
+
headers: { 'Content-Type': 'application/json' },
|
|
552
|
+
credentials: 'include',
|
|
553
|
+
body: JSON.stringify({ clientPublicKey: publicKeyBase64URL }),
|
|
554
|
+
});
|
|
555
|
+
if (!res.ok) throw new Error(`ERR_SESSION_INIT_FAILED: HTTP ${res.status}`);
|
|
556
|
+
const { serverPublicKey, salt, keyId } = await res.json();
|
|
557
|
+
|
|
558
|
+
_sessionAESKey = await computeSessionAESKey(privateKey, serverPublicKey, salt, keyId, deviceId);
|
|
559
|
+
|
|
560
|
+
const mat: SessionMaterial = {
|
|
561
|
+
privateKeyJwk: await crypto.subtle.exportKey('jwk', privateKey),
|
|
562
|
+
serverPublicKeyBase64URL: serverPublicKey,
|
|
563
|
+
saltBase64URL: salt, keyId, deviceId, apiVersion,
|
|
564
|
+
};
|
|
565
|
+
_sessionMaterial = mat;
|
|
566
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(mat));
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export const clearSession = (): void => {
|
|
570
|
+
_sessionAESKey = null;
|
|
571
|
+
_sessionMaterial = null;
|
|
572
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
573
|
+
sessionStorage.removeItem('device_id');
|
|
574
|
+
};
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### ECDH-Session Interceptor Pattern
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
// Add to axiosClient.ts
|
|
581
|
+
import { getSessionAESKey, getSessionMaterial } from '../security/ecdh-session/ecdhSessionService';
|
|
582
|
+
import { buildAAD } from '../security/ecdh-session/aad';
|
|
583
|
+
import { encryptAESGCM, decryptAESGCM } from '../security/ecdh-session/aesGcm';
|
|
584
|
+
|
|
585
|
+
instance.interceptors.request.use(async (config) => {
|
|
586
|
+
if (!ENCRYPTION_ENABLED) return config;
|
|
587
|
+
if (!config.method || !ENCRYPT_METHODS.has(config.method.toLowerCase())) return config;
|
|
588
|
+
if (!config.data) return config;
|
|
589
|
+
const aesKey = getSessionAESKey();
|
|
590
|
+
const mat = getSessionMaterial();
|
|
591
|
+
const aad = buildAAD(mat.apiVersion, mat.deviceId, mat.keyId);
|
|
592
|
+
const plaintext = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
|
|
593
|
+
config.data = { encryptedRequest: await encryptAESGCM(plaintext, aesKey, aad), keyId: mat.keyId };
|
|
594
|
+
config.headers['X-Device-ID'] = mat.deviceId;
|
|
595
|
+
config.headers['X-Timestamp'] = new Date().toISOString();
|
|
596
|
+
return config;
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
instance.interceptors.response.use(
|
|
600
|
+
async (response) => {
|
|
601
|
+
if (!ENCRYPTION_ENABLED || !response.data?.encryptedResponse) return response;
|
|
602
|
+
const aesKey = getSessionAESKey();
|
|
603
|
+
const mat = getSessionMaterial();
|
|
604
|
+
const aad = buildAAD(mat.apiVersion, mat.deviceId, mat.keyId);
|
|
605
|
+
try {
|
|
606
|
+
response.data = JSON.parse(await decryptAESGCM(response.data.encryptedResponse, aesKey, aad));
|
|
607
|
+
} catch (err: unknown) {
|
|
608
|
+
if (err instanceof DOMException) {
|
|
609
|
+
clearSession();
|
|
610
|
+
throw new Error('ERR_CRYPTO_TAG_MISMATCH: session invalid — re-init required');
|
|
611
|
+
}
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
return response;
|
|
615
|
+
},
|
|
616
|
+
(error) => Promise.reject(error),
|
|
617
|
+
);
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### App Bootstrap (ECDH-Session)
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
// In App.tsx or auth provider:
|
|
624
|
+
import { restoreSession, initSession } from './core/security/ecdh-session/ecdhSessionService';
|
|
625
|
+
|
|
626
|
+
async function bootstrapSecurity(): Promise<void> {
|
|
627
|
+
const restored = await restoreSession();
|
|
628
|
+
if (!restored) await initSession(import.meta.env.VITE_SESSION_INIT_URL);
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Key Differences from Approach 1 (React Basic Web Crypto)
|
|
635
|
+
|
|
636
|
+
| Feature | Approach 1 | Approach 7 |
|
|
637
|
+
|---|---|---|
|
|
638
|
+
| Response decryption | No | Yes — per-request key map |
|
|
639
|
+
| RSA key management | Manual/static | Auto fetch/cache/retry service |
|
|
640
|
+
| Per-request key isolation | No | Yes — `CryptoKey` map per requestId |
|
|
641
|
+
| Browser-safe storage | Not included | `sessionStorage` + HTTP-only cookie guidance |
|
|
642
|
+
| ECDH-Session mode | No | Yes — ECDH P-256 + HKDF-SHA256 + AES-GCM with AAD |
|
|
643
|
+
| HMAC signing | No | Yes — Web Crypto HMAC-SHA256 |
|
|
644
|
+
| Framework detection | No | Yes — Vite/CRA/Next.js prefix |
|
|
645
|
+
| `crypto.subtle` availability check | No | Yes — HTTPS gate at startup |
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Security Rules (Approach 7)
|
|
650
|
+
|
|
651
|
+
- Use `crypto.getRandomValues` for all random material — never `Math.random()`.
|
|
652
|
+
- Never reuse IV values.
|
|
653
|
+
- `DOMException` from `crypto.subtle.decrypt` = auth tag failure — trigger secure logout, do not retry.
|
|
654
|
+
- Never store AES keys, IVs, or derived ECDH session keys in any storage.
|
|
655
|
+
- ECDH-Session: private key exported as JWK to `sessionStorage` only; derived AES key never stored.
|
|
656
|
+
- Never fall back to `localStorage` for tokens or session material.
|
|
657
|
+
- Enforce HTTPS — `crypto.subtle` is unavailable on plain HTTP origins.
|
|
658
|
+
- `tsconfig.json` must include `"lib": ["DOM"]` for `crypto.subtle` typings.
|
|
659
|
+
- No `crypto-js`, `node-forge`, or Buffer polyfill — use `Uint8Array` + `TextEncoder`/`TextDecoder` + `btoa`/`atob`.
|
|
660
|
+
- CORS: ensure backend `Access-Control-Allow-Headers` includes `X-Request-ID`, `X-Nonce`, `X-Request-Signature`, `X-Device-ID`, `X-Timestamp`.
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## Build Command
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
npm run build
|
|
668
|
+
```
|