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.
Files changed (223) hide show
  1. package/.github/skills/boiler-project-generator/SKILL.md +103 -0
  2. package/.github/skills/boiler-project-generator/assets/templates/android/README.md +3 -0
  3. package/.github/skills/boiler-project-generator/assets/templates/angular/README.md +3 -0
  4. package/.github/skills/boiler-project-generator/assets/templates/flutter/README.md +3 -0
  5. package/.github/skills/boiler-project-generator/assets/templates/ios/README.md +3 -0
  6. package/.github/skills/boiler-project-generator/assets/templates/react/README.md +3 -0
  7. package/.github/skills/boiler-project-generator/assets/templates/react-native/README.md +3 -0
  8. package/.github/skills/boiler-project-generator/assets/templates/react-native/api-setup.ts +76 -0
  9. package/.github/skills/boiler-project-generator/assets/templates/react-native/component-primary-button.tsx +29 -0
  10. package/.github/skills/boiler-project-generator/assets/templates/react-native/env-config.env +4 -0
  11. package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-auth-screen.tsx +45 -0
  12. package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-dashboard-screen.tsx +45 -0
  13. package/.github/skills/boiler-project-generator/assets/templates/react-native/feature-profile-screen.tsx +45 -0
  14. package/.github/skills/boiler-project-generator/assets/templates/react-native/hook-use-app-boot.ts +11 -0
  15. package/.github/skills/boiler-project-generator/assets/templates/react-native/navigation-setup.tsx +25 -0
  16. package/.github/skills/boiler-project-generator/assets/templates/react-native/project-structure.md +24 -0
  17. package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-hooks.ts +5 -0
  18. package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-session-slice.ts +29 -0
  19. package/.github/skills/boiler-project-generator/assets/templates/react-native/redux-store.ts +15 -0
  20. package/.github/skills/boiler-project-generator/assets/templates/react-native/theme-index.ts +23 -0
  21. package/.github/skills/boiler-project-generator/assets/templates/react-native/utils-helpers.ts +7 -0
  22. package/.github/skills/boiler-project-generator/metadata.yaml +29 -0
  23. package/.github/skills/boiler-project-generator/references/android.md +19 -0
  24. package/.github/skills/boiler-project-generator/references/angular.md +14 -0
  25. package/.github/skills/boiler-project-generator/references/flutter.md +17 -0
  26. package/.github/skills/boiler-project-generator/references/ios.md +17 -0
  27. package/.github/skills/boiler-project-generator/references/react-native.md +103 -0
  28. package/.github/skills/boiler-project-generator/references/react.md +17 -0
  29. package/.github/skills/boiler-project-generator/scripts/angular.js +43 -0
  30. package/.github/skills/boiler-project-generator/scripts/generate-project.mjs +107 -0
  31. package/.github/skills/boiler-project-generator/scripts/init.js +105 -0
  32. package/.github/skills/boiler-project-generator/scripts/react-native.js +1041 -0
  33. package/.github/skills/boiler-project-generator/scripts/react.js +32 -0
  34. package/.github/skills/clean-architecture-generator/SKILL.md +235 -0
  35. package/.github/skills/clean-architecture-generator/references/architecture-core.md +126 -0
  36. package/.github/skills/clean-architecture-generator/references/platform-angular.md +374 -0
  37. package/.github/skills/clean-architecture-generator/references/platform-flutter.md +366 -0
  38. package/.github/skills/clean-architecture-generator/references/platform-react-native.md +590 -0
  39. package/.github/skills/clean-architecture-generator/references/platform-react.md +261 -0
  40. package/.github/skills/code-test-coverage-analyzer/SKILL.md +1358 -0
  41. package/.github/skills/code-test-coverage-analyzer/assets/config-questionnaire.md +118 -0
  42. package/.github/skills/code-test-coverage-analyzer/assets/conflict-resolution-protocol.md +99 -0
  43. package/.github/skills/code-test-coverage-analyzer/assets/coverage-calculation-rules.md +294 -0
  44. package/.github/skills/code-test-coverage-analyzer/assets/multi-strategy-search.md +145 -0
  45. package/.github/skills/code-test-coverage-analyzer/assets/output-file-contracts.md +651 -0
  46. package/.github/skills/code-test-coverage-analyzer/assets/status-definitions.md +273 -0
  47. package/.github/skills/code-test-coverage-analyzer/examples/example-invocation.md +208 -0
  48. package/.github/skills/code-test-coverage-analyzer/references/android-patterns.md +229 -0
  49. package/.github/skills/code-test-coverage-analyzer/references/angular-patterns.md +177 -0
  50. package/.github/skills/code-test-coverage-analyzer/references/backend-patterns.md +383 -0
  51. package/.github/skills/code-test-coverage-analyzer/references/flutter-patterns.md +254 -0
  52. package/.github/skills/code-test-coverage-analyzer/references/ios-patterns.md +228 -0
  53. package/.github/skills/code-test-coverage-analyzer/references/react-native-patterns.md +431 -0
  54. package/.github/skills/code-test-coverage-analyzer/references/react-patterns.md +206 -0
  55. package/.github/skills/code-test-coverage-analyzer/scripts/detect-platform.ps1 +261 -0
  56. package/.github/skills/code-test-coverage-analyzer/scripts/scan-repository.ps1 +242 -0
  57. package/.github/skills/code-test-coverage-analyzer/templates/brd-compliance-row.md +107 -0
  58. package/.github/skills/code-test-coverage-analyzer/templates/test-case-row.md +130 -0
  59. package/.github/skills/code-test-coverage-analyzer/templates/user-story-compliance-row.md +170 -0
  60. package/.github/skills/custom-font-integrator/SKILL.md +373 -0
  61. package/.github/skills/custom-font-integrator/assets/FontTestScreen.js +150 -0
  62. package/.github/skills/custom-font-integrator/references/example-info-plist.xml +21 -0
  63. package/.github/skills/custom-font-integrator/scripts/setup-fonts.sh +99 -0
  64. package/.github/skills/encryption-decryption-utility/Instructions.md +282 -0
  65. package/.github/skills/encryption-decryption-utility/README.md +222 -0
  66. package/.github/skills/encryption-decryption-utility/SKILL.md +645 -0
  67. package/.github/skills/encryption-decryption-utility/assets/encryption-config-template.json +13 -0
  68. package/.github/skills/encryption-decryption-utility/assets/platform-matrix.json +180 -0
  69. package/.github/skills/encryption-decryption-utility/metadata.yaml +51 -0
  70. package/.github/skills/encryption-decryption-utility/references/android-crypto.md +157 -0
  71. package/.github/skills/encryption-decryption-utility/references/angular-crypto.md +130 -0
  72. package/.github/skills/encryption-decryption-utility/references/approach-1-webcrypto.md +95 -0
  73. package/.github/skills/encryption-decryption-utility/references/approach-2-rsa-aes-cbc.md +111 -0
  74. package/.github/skills/encryption-decryption-utility/references/approach-3-aes-sharedkey.md +114 -0
  75. package/.github/skills/encryption-decryption-utility/references/architecture.md +233 -0
  76. package/.github/skills/encryption-decryption-utility/references/backend-alignment.md +506 -0
  77. package/.github/skills/encryption-decryption-utility/references/dependency-guidance.md +234 -0
  78. package/.github/skills/encryption-decryption-utility/references/ec-implementation.md +832 -0
  79. package/.github/skills/encryption-decryption-utility/references/error-handling-playbook.md +234 -0
  80. package/.github/skills/encryption-decryption-utility/references/flutter-crypto.md +105 -0
  81. package/.github/skills/encryption-decryption-utility/references/ios-crypto.md +338 -0
  82. package/.github/skills/encryption-decryption-utility/references/native-rsa-implementation.md +373 -0
  83. package/.github/skills/encryption-decryption-utility/references/node-crypto.md +86 -0
  84. package/.github/skills/encryption-decryption-utility/references/react-crypto.md +105 -0
  85. package/.github/skills/encryption-decryption-utility/references/react-native-crypto.md +1001 -0
  86. package/.github/skills/encryption-decryption-utility/references/react-web-crypto.md +668 -0
  87. package/.github/skills/encryption-decryption-utility/references/security-checklist.md +176 -0
  88. package/.github/skills/encryption-decryption-utility/references/web-crypto.md +74 -0
  89. package/.github/skills/encryption-decryption-utility/scripts/detect.js +128 -0
  90. package/.github/skills/encryption-decryption-utility/scripts/install.js +1713 -0
  91. package/.github/skills/encryption-decryption-utility/scripts/setup.js +800 -0
  92. package/.github/skills/encryption-decryption-utility/template.md +241 -0
  93. package/.github/skills/encryption-decryption-utility/templates/config-questionnaire.md +467 -0
  94. package/.github/skills/encryption-decryption-utility/templates/delivery-checklist.md +206 -0
  95. package/.github/skills/encryption-decryption-utility/templates/implementation-plan.md +304 -0
  96. package/.github/skills/feature-generator/SKILL.MD +2741 -0
  97. package/.github/skills/feature-generator/assets/fingerprint-schema.json +385 -0
  98. package/.github/skills/feature-generator/assets/spec-template.md +172 -0
  99. package/.github/skills/feature-generator/assets/transform-rules.json +82 -0
  100. package/.github/skills/feature-generator/references/api-envelope-patterns.md +327 -0
  101. package/.github/skills/feature-generator/references/screen-logic-patterns.md +399 -0
  102. package/.github/skills/feature-generator/references/state-library-patterns.md +464 -0
  103. package/.github/skills/figma-ui-mapper/README.md +34 -0
  104. package/.github/skills/figma-ui-mapper/SKILL.md +101 -0
  105. package/.github/skills/figma-ui-mapper/assets/templates/component-map-template.md +30 -0
  106. package/.github/skills/figma-ui-mapper/assets/templates/generation-report-template.md +49 -0
  107. package/.github/skills/figma-ui-mapper/metadata.yaml +22 -0
  108. package/.github/skills/figma-ui-mapper/references/android.md +11 -0
  109. package/.github/skills/figma-ui-mapper/references/angular.md +18 -0
  110. package/.github/skills/figma-ui-mapper/references/capacitor.md +18 -0
  111. package/.github/skills/figma-ui-mapper/references/common.md +23 -0
  112. package/.github/skills/figma-ui-mapper/references/expo.md +18 -0
  113. package/.github/skills/figma-ui-mapper/references/flutter.md +18 -0
  114. package/.github/skills/figma-ui-mapper/references/frameworks/angular/rules.md +23 -0
  115. package/.github/skills/figma-ui-mapper/references/frameworks/capacitor/rules.md +23 -0
  116. package/.github/skills/figma-ui-mapper/references/frameworks/expo/rules.md +23 -0
  117. package/.github/skills/figma-ui-mapper/references/frameworks/flutter/rules.md +23 -0
  118. package/.github/skills/figma-ui-mapper/references/frameworks/ionic/rules.md +23 -0
  119. package/.github/skills/figma-ui-mapper/references/frameworks/kotlin-multiplatform/rules.md +23 -0
  120. package/.github/skills/figma-ui-mapper/references/frameworks/native-android/rules.md +23 -0
  121. package/.github/skills/figma-ui-mapper/references/frameworks/native-ios/rules.md +23 -0
  122. package/.github/skills/figma-ui-mapper/references/frameworks/next/rules.md +23 -0
  123. package/.github/skills/figma-ui-mapper/references/frameworks/nuxt/rules.md +23 -0
  124. package/.github/skills/figma-ui-mapper/references/frameworks/react/rules.md +19 -0
  125. package/.github/skills/figma-ui-mapper/references/frameworks/react-native/rules.md +23 -0
  126. package/.github/skills/figma-ui-mapper/references/frameworks/svelte/rules.md +23 -0
  127. package/.github/skills/figma-ui-mapper/references/frameworks/vue/rules.md +23 -0
  128. package/.github/skills/figma-ui-mapper/references/ionic.md +18 -0
  129. package/.github/skills/figma-ui-mapper/references/ios.md +11 -0
  130. package/.github/skills/figma-ui-mapper/references/kotlin-multiplatform.md +18 -0
  131. package/.github/skills/figma-ui-mapper/references/native-android.md +18 -0
  132. package/.github/skills/figma-ui-mapper/references/native-ios.md +18 -0
  133. package/.github/skills/figma-ui-mapper/references/next.md +18 -0
  134. package/.github/skills/figma-ui-mapper/references/nuxt.md +18 -0
  135. package/.github/skills/figma-ui-mapper/references/react-figma-guideline.md +21 -0
  136. package/.github/skills/figma-ui-mapper/references/react-native.md +18 -0
  137. package/.github/skills/figma-ui-mapper/references/react.md +18 -0
  138. package/.github/skills/figma-ui-mapper/references/script-usage.md +46 -0
  139. package/.github/skills/figma-ui-mapper/references/svelte.md +18 -0
  140. package/.github/skills/figma-ui-mapper/references/vue.md +18 -0
  141. package/.github/skills/figma-ui-mapper/references/workflow.md +74 -0
  142. package/.github/skills/figma-ui-mapper/scripts/generate-page-through-figma.js +635 -0
  143. package/.github/skills/figma-ui-mapper/scripts/generate-react-page-through-figma.js +4 -0
  144. package/.github/skills/frontend-task-breakdown/SKILL.md +734 -0
  145. package/.github/skills/md-file-converter/SKILL.md +291 -0
  146. package/.github/skills/md-file-converter/config/formats.json +114 -0
  147. package/.github/skills/md-file-converter/extractors/extract-docx.sh +120 -0
  148. package/.github/skills/md-file-converter/extractors/extract-eml.py +108 -0
  149. package/.github/skills/md-file-converter/extractors/extract-pdf.sh +97 -0
  150. package/.github/skills/md-file-converter/extractors/extract-pptx.py +143 -0
  151. package/.github/skills/md-file-converter/extractors/extract-xlsx.py +175 -0
  152. package/.github/skills/md-file-converter/scripts/check-tools.sh +47 -0
  153. package/.github/skills/md-file-converter/scripts/extract-archive.sh +83 -0
  154. package/.github/skills/md-file-converter/scripts/extract-ocr.sh +77 -0
  155. package/.github/skills/md-file-converter/scripts/get-metadata.sh +57 -0
  156. package/.github/skills/md-file-converter/templates/output.md +73 -0
  157. package/.github/skills/md-file-converter/templates/unsupported-object.md +6 -0
  158. package/.github/skills/mockAPI-contract-generator/SKILL.md +607 -0
  159. package/.github/skills/test-case-validator/CHANGELOG.md +155 -0
  160. package/.github/skills/test-case-validator/CONFIGURATION.md +130 -0
  161. package/.github/skills/test-case-validator/EXAMPLES.md +50 -0
  162. package/.github/skills/test-case-validator/QUALITY_SCORING.md +98 -0
  163. package/.github/skills/test-case-validator/README.md +111 -0
  164. package/.github/skills/test-case-validator/REPORT_TEMPLATE.md +116 -0
  165. package/.github/skills/test-case-validator/SKILL.md +209 -0
  166. package/.github/skills/test-case-validator/SYSTEM_PROMPT.md +127 -0
  167. package/.github/skills/test-case-validator/TRACEABILITY_ENGINE.md +152 -0
  168. package/.github/skills/test-case-validator/VALIDATION_RULES.md +411 -0
  169. package/.github/skills/test-case-validator/config/default.config.yaml +62 -0
  170. package/.github/skills/test-case-validator/docs/ARCHITECTURE.md +66 -0
  171. package/.github/skills/test-case-validator/docs/EXTENDING.md +85 -0
  172. package/.github/skills/test-case-validator/docs/FAQ.md +88 -0
  173. package/.github/skills/test-case-validator/docs/VERSIONING.md +55 -0
  174. package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-brd.md +43 -0
  175. package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-test-cases.csv +9 -0
  176. package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/input-user-story.md +29 -0
  177. package/.github/skills/test-case-validator/examples/example-1-manual-qa-ecommerce/output-report.md +303 -0
  178. package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/input-spec.md +43 -0
  179. package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/input-test-files-index.md +56 -0
  180. package/.github/skills/test-case-validator/examples/example-2-automated-frontend-suite/output-report.md +274 -0
  181. package/.github/skills/test-case-validator/prompts/00-applicability-resolution.md +61 -0
  182. package/.github/skills/test-case-validator/prompts/01-extraction-user-story.md +58 -0
  183. package/.github/skills/test-case-validator/prompts/02-extraction-brd.md +64 -0
  184. package/.github/skills/test-case-validator/prompts/03-extraction-test-cases.md +92 -0
  185. package/.github/skills/test-case-validator/prompts/04-traceability-matrix.md +41 -0
  186. package/.github/skills/test-case-validator/prompts/05-coverage-analysis.md +46 -0
  187. package/.github/skills/test-case-validator/prompts/06-missing-scenario-detection.md +32 -0
  188. package/.github/skills/test-case-validator/prompts/07-duplicate-detection.md +36 -0
  189. package/.github/skills/test-case-validator/prompts/08-risk-analysis.md +39 -0
  190. package/.github/skills/test-case-validator/prompts/09-gated-wiring-validation.md +53 -0
  191. package/.github/skills/test-case-validator/prompts/10-observation-report.md +56 -0
  192. package/.github/skills/test-case-validator/prompts/11-final-report-assembly.md +57 -0
  193. package/.github/skills/test-case-validator/templates/coverage-table.md +25 -0
  194. package/.github/skills/test-case-validator/templates/executive-summary.md +38 -0
  195. package/.github/skills/test-case-validator/templates/final-summary-block.md +87 -0
  196. package/.github/skills/test-case-validator/templates/observation-entry.md +41 -0
  197. package/.github/skills/test-case-validator/templates/rtm-row.md +25 -0
  198. package/.github/skills/validation-engine-builder/README.md +50 -0
  199. package/.github/skills/validation-engine-builder/SKILL.md +241 -0
  200. package/.github/skills/validation-engine-builder/references/core.md +158 -0
  201. package/.github/skills/validation-engine-builder/references/platform-angular.md +166 -0
  202. package/.github/skills/validation-engine-builder/references/platform-flutter.md +118 -0
  203. package/.github/skills/validation-engine-builder/references/platform-react-native.md +170 -0
  204. package/.github/skills/validation-engine-builder/references/platform-react.md +94 -0
  205. package/.github/skills/validation-engine-builder/scripts/apply-to-project.js +80 -0
  206. package/.github/skills/validation-engine-builder/scripts/generate-angular.js +271 -0
  207. package/.github/skills/validation-engine-builder/scripts/generate-flutter.js +298 -0
  208. package/.github/skills/validation-engine-builder/scripts/generate-react-native.js +217 -0
  209. package/.github/skills/validation-engine-builder/scripts/generate-react.js +163 -0
  210. package/.github/skills/validation-engine-builder/scripts/install-deps.js +53 -0
  211. package/.github/skills/validation-engine-builder/scripts/shared/detect-platform.js +68 -0
  212. package/.github/skills/validation-engine-builder/scripts/shared/npm-deps.js +103 -0
  213. package/.github/skills/validation-engine-builder/scripts/shared/rhf-hook.js +43 -0
  214. package/.github/skills/validation-engine-builder/scripts/shared/ts-messages-regex.js +62 -0
  215. package/.github/skills/validation-engine-builder/scripts/shared/util.js +44 -0
  216. package/.github/skills/validation-engine-builder/scripts/shared/yup-schemas.js +80 -0
  217. package/.github/skills/validation-engine-builder/template.md +73 -0
  218. package/.github/skills/validation-engine-builder/templates/messages.json +22 -0
  219. package/.github/skills/validation-engine-builder/templates/regex.json +16 -0
  220. package/.github/skills/validation-engine-builder/templates/schemas.json +48 -0
  221. package/bin/index.js +145 -0
  222. package/package.json +22 -0
  223. package/readme.md +0 -0
@@ -0,0 +1,1713 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.js - Encryption-Decryption Generator Skill
4
+ *
5
+ * Deterministic file writer. Accepts a fully resolved config JSON and writes
6
+ * all generated encryption files into the target project.
7
+ * No interactive prompts. Idempotent: same config always produces same output.
8
+ *
9
+ * Usage:
10
+ * node install.js --config-json <path> [--dry-run]
11
+ * node install.js --help
12
+ *
13
+ * Exit codes:
14
+ * 0 All files written successfully
15
+ * 1 Config JSON missing or invalid
16
+ * 2 File write error
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const args = process.argv.slice(2);
25
+
26
+ // ── Helpers ───────────────────────────────────────────────────────────────────
27
+
28
+ const SKILL_ROOT = path.resolve(__dirname, '..');
29
+ const MATRIX_PATH = path.join(SKILL_ROOT, 'assets', 'platform-matrix.json');
30
+
31
+ function loadJson(filePath, label) {
32
+ if (!fs.existsSync(filePath)) {
33
+ console.error('Error: ' + label + ' not found: ' + filePath);
34
+ process.exit(1);
35
+ }
36
+ try {
37
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
38
+ } catch (e) {
39
+ console.error('Error: ' + label + ' is not valid JSON: ' + e.message);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ function writeFile(filePath, content, dryRun) {
45
+ if (dryRun) {
46
+ console.log('DRY-RUN WRITE ' + filePath);
47
+ console.log(content);
48
+ console.log('---');
49
+ return;
50
+ }
51
+ const dir = path.dirname(filePath);
52
+ try {
53
+ fs.mkdirSync(dir, { recursive: true });
54
+ fs.writeFileSync(filePath, content, 'utf8');
55
+ console.log('WROTE ' + filePath);
56
+ } catch (e) {
57
+ console.error('Error: could not write ' + filePath + ': ' + e.message);
58
+ process.exit(2);
59
+ }
60
+ }
61
+
62
+ function patchFile(filePath, marker, insertContent, dryRun) {
63
+ if (dryRun) {
64
+ console.log('DRY-RUN PATCH ' + filePath);
65
+ return;
66
+ }
67
+ if (!fs.existsSync(filePath)) {
68
+ console.log('SKIP PATCH (file not found): ' + filePath);
69
+ return;
70
+ }
71
+ let content = fs.readFileSync(filePath, 'utf8');
72
+ if (content.includes(insertContent.trim())) {
73
+ console.log('ALREADY PATCHED ' + filePath);
74
+ return;
75
+ }
76
+ try {
77
+ const updated = content.replace(marker, insertContent + '\n' + marker);
78
+ fs.writeFileSync(filePath, updated, 'utf8');
79
+ console.log('PATCHED ' + filePath);
80
+ } catch (e) {
81
+ console.error('Error: could not patch ' + filePath + ': ' + e.message);
82
+ process.exit(2);
83
+ }
84
+ }
85
+
86
+ function scaffoldPath(relPath) {
87
+ const ext = path.extname(relPath);
88
+ const base = relPath.slice(0, relPath.length - ext.length);
89
+ return base + '.scaffold' + ext;
90
+ }
91
+
92
+ // ── Content generators ────────────────────────────────────────────────────────
93
+
94
+ function generateConfig(platform, config) {
95
+ const enc = config.encryptionEnabled ? 'true' : 'false';
96
+ const debugVal = (config.debug === true) ? 'true' : 'false';
97
+
98
+ if (platform === 'android') {
99
+ return [
100
+ 'package com.example.encryption',
101
+ '',
102
+ 'object EncryptionConfig {',
103
+ ' val encryptionEnabled: Boolean = ' + enc,
104
+ ' val approach: Int = ' + (config.approach || 0),
105
+ ' val publicKeyPem: String? = ' + (config.publicKeyPem ? '"' + config.publicKeyPem + '"' : 'null'),
106
+ ' val sharedKey: String? = ' + (config.sharedKey ? '"' + config.sharedKey + '"' : 'null'),
107
+ ' val excludeUrls: List<String> = listOf(' + (config.excludeUrls || []).map(u => '"' + u + '"').join(', ') + ')',
108
+ ' val hmacEnabled: Boolean = ' + (config.hmac && config.hmac.enabled ? 'true' : 'false'),
109
+ ' val debug: Boolean = ' + debugVal,
110
+ '}',
111
+ ].join('\n');
112
+ }
113
+
114
+ if (platform === 'ios') {
115
+ return [
116
+ 'import Foundation',
117
+ '',
118
+ 'struct EncryptionConfig {',
119
+ ' static let encryptionEnabled: Bool = ' + enc,
120
+ ' static let approach: Int = ' + (config.approach || 0),
121
+ ' static let publicKeyPem: String? = ' + (config.publicKeyPem ? '"' + config.publicKeyPem + '"' : 'nil'),
122
+ ' static let sharedKey: String? = ' + (config.sharedKey ? '"' + config.sharedKey + '"' : 'nil'),
123
+ ' static let excludeUrls: [String] = [' + (config.excludeUrls || []).map(u => '"' + u + '"').join(', ') + ']',
124
+ ' static let hmacEnabled: Bool = ' + (config.hmac && config.hmac.enabled ? 'true' : 'false'),
125
+ ' static let debug: Bool = ' + debugVal,
126
+ '}',
127
+ ].join('\n');
128
+ }
129
+
130
+ if (platform === 'flutter') {
131
+ return [
132
+ 'class EncryptionConfig {',
133
+ ' static const bool encryptionEnabled = ' + enc + ';',
134
+ ' static const int approach = ' + (config.approach || 0) + ';',
135
+ ' static const String? publicKeyPem = ' + (config.publicKeyPem ? "'" + config.publicKeyPem + "'" : 'null') + ';',
136
+ ' static const String? sharedKey = ' + (config.sharedKey ? "'" + config.sharedKey + "'" : 'null') + ';',
137
+ ' static const List<String> excludeUrls = [' + (config.excludeUrls || []).map(u => "'" + u + "'").join(', ') + '];',
138
+ ' static const bool hmacEnabled = ' + (config.hmac && config.hmac.enabled ? 'true' : 'false') + ';',
139
+ ' static const bool debug = ' + debugVal + ';',
140
+ '}',
141
+ ].join('\n');
142
+ }
143
+
144
+ // angular / web / node — TypeScript / JS
145
+ const isBrowser = (platform === 'angular' || platform === 'web');
146
+ const exportWord = isBrowser ? 'export const environment' : 'export const encryptionConfig';
147
+ return [
148
+ exportWord + ' = {',
149
+ ' encryption: {',
150
+ ' enabled: ' + enc + ',',
151
+ ' approach: ' + (config.approach || 0) + ',',
152
+ ' publicKeyPem: ' + (config.publicKeyPem ? JSON.stringify(config.publicKeyPem) : 'null') + ',',
153
+ ' privateKeyPem: ' + (config.privateKeyPem ? JSON.stringify(config.privateKeyPem) : 'null') + ',',
154
+ ' sharedKey: ' + (config.sharedKey ? JSON.stringify(config.sharedKey) : 'null') + ',',
155
+ ' excludeUrls: ' + JSON.stringify(config.excludeUrls || []) + ',',
156
+ ' hmac: {',
157
+ ' enabled: ' + (config.hmac && config.hmac.enabled ? 'true' : 'false') + ',',
158
+ ' },',
159
+ ' debug: ' + debugVal + ',',
160
+ ' },',
161
+ '};',
162
+ ].join('\n');
163
+ }
164
+
165
+ function generateService(platform, config) {
166
+ const approach = config.approach || 0;
167
+ const hasHmac = config.hmac && config.hmac.enabled;
168
+
169
+ if (platform === 'android') {
170
+ const androidApproach = config.approach || 3;
171
+ const encryptBody = androidApproach === 2
172
+ ? [
173
+ ' val aesKey = ByteArray(32).also { SecureRandom().nextBytes(it) }',
174
+ ' val iv = ByteArray(16).also { SecureRandom().nextBytes(it) }',
175
+ ' val aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding")',
176
+ ' aesCipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(aesKey, "AES"), IvParameterSpec(iv))',
177
+ ' val encryptedData = aesCipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))',
178
+ ' val pubKeyPem = EncryptionConfig.publicKeyPem?.replace(Regex("-----.*?-----"), "")?.replace("\\\\s".toRegex(), "") ?: return null',
179
+ ' val pubKeyBytes = android.util.Base64.decode(pubKeyPem, android.util.Base64.DEFAULT)',
180
+ ' val rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")',
181
+ ' rsaCipher.init(Cipher.ENCRYPT_MODE, java.security.KeyFactory.getInstance("RSA").generatePublic(java.security.spec.X509EncodedKeySpec(pubKeyBytes)))',
182
+ ' val encryptedKey = rsaCipher.doFinal(aesKey)',
183
+ ' "{\\\"encryptedKey\\\":\\\"${Base64.encodeToString(encryptedKey, Base64.NO_WRAP)}\\\",\\\"iv\\\":\\\"${Base64.encodeToString(iv, Base64.NO_WRAP)}\\\",\\\"encryptedData\\\":\\\"${Base64.encodeToString(encryptedData, Base64.NO_WRAP)}\\\"}"',
184
+ ].join('\n')
185
+ : [
186
+ ' val iv = ByteArray(16).also { SecureRandom().nextBytes(it) }',
187
+ ' val key = deriveKey()',
188
+ ' val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")',
189
+ ' cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))',
190
+ ' val encrypted = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))',
191
+ ' "{\\\"iv\\\":\\\"${Base64.encodeToString(iv, Base64.NO_WRAP)}\\\",\\\"encryptedData\\\":\\\"${Base64.encodeToString(encrypted, Base64.NO_WRAP)}\\\"}"',
192
+ ].join('\n');
193
+ const deriveKeyFn = androidApproach === 3 ? [
194
+ ' private fun deriveKey(): ByteArray {',
195
+ ' val secret = EncryptionConfig.sharedKey ?: ""',
196
+ ' return MessageDigest.getInstance("SHA-256").digest(secret.toByteArray(Charsets.UTF_8))',
197
+ ' }',
198
+ ].join('\n') : '';
199
+ const hmacFn = hasHmac ? [
200
+ ' fun computeHmac(data: String): String {',
201
+ ' val secret = System.getenv("ENCRYPTION_HMAC_SECRET") ?: ""',
202
+ ' val mac = javax.crypto.Mac.getInstance("HmacSHA256")',
203
+ ' mac.init(javax.crypto.spec.SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256"))',
204
+ ' return Base64.encodeToString(mac.doFinal(data.toByteArray(Charsets.UTF_8)), Base64.NO_WRAP)',
205
+ ' }',
206
+ ].join('\n') : '';
207
+ return [
208
+ 'package com.example.encryption',
209
+ '',
210
+ 'import javax.crypto.Cipher',
211
+ 'import javax.crypto.spec.IvParameterSpec',
212
+ 'import javax.crypto.spec.SecretKeySpec',
213
+ 'import java.security.MessageDigest',
214
+ 'import java.security.SecureRandom',
215
+ 'import android.util.Base64',
216
+ '',
217
+ 'object EncryptionService {',
218
+ '',
219
+ ' fun encrypt(plaintext: String): String? {',
220
+ ' if (!EncryptionConfig.encryptionEnabled) return plaintext',
221
+ ' return try {',
222
+ encryptBody,
223
+ ' } catch (e: Exception) {',
224
+ config.debug ? ' if (EncryptionConfig.debug) android.util.Log.d("EncryptionService", "encrypt error", e)' : null,
225
+ ' null',
226
+ ' }',
227
+ ' }',
228
+ '',
229
+ deriveKeyFn,
230
+ hmacFn,
231
+ '}',
232
+ ].filter(l => l !== null).join('\n');
233
+ }
234
+
235
+ if (platform === 'ios') {
236
+ const iosApproach = config.approach || 3;
237
+ const encryptImpl = iosApproach === 1
238
+ ? [
239
+ ' let sealedBox = try AES.GCM.seal(data, using: try deriveSymmetricKey())',
240
+ ' return sealedBox.combined!.base64EncodedString()',
241
+ ].join('\n')
242
+ : [
243
+ ' let key = try deriveAESKey()',
244
+ ' var iv = [UInt8](repeating: 0, count: kCCBlockSizeAES128)',
245
+ ' let _ = SecRandomCopyBytes(kSecRandomDefault, iv.count, &iv)',
246
+ ' let keyBytes = key.withUnsafeBytes { Array($0) }',
247
+ ' var outBytes = [UInt8](repeating: 0, count: data.count + kCCBlockSizeAES128)',
248
+ ' var numBytes = 0',
249
+ ' CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES),',
250
+ ' CCOptions(kCCOptionPKCS7Padding), keyBytes, kCCKeySizeAES256,',
251
+ ' iv, (data as NSData).bytes, data.count, &outBytes, outBytes.count, &numBytes)',
252
+ ' let ivB64 = Data(iv).base64EncodedString()',
253
+ ' let encB64 = Data(outBytes[..<numBytes]).base64EncodedString()',
254
+ ' return ["iv": ivB64, "encryptedData": encB64].description',
255
+ ].join('\n');
256
+ const deriveImpl = iosApproach === 1
257
+ ? [
258
+ ' private static func deriveSymmetricKey() throws -> SymmetricKey {',
259
+ ' let secret = EncryptionConfig.sharedKey ?? ""',
260
+ ' return SymmetricKey(data: SHA256.hash(data: Data(secret.utf8)))',
261
+ ' }',
262
+ ].join('\n')
263
+ : [
264
+ ' private static func deriveAESKey() throws -> Data {',
265
+ ' let secret = EncryptionConfig.sharedKey ?? ""',
266
+ ' return Data(SHA256.hash(data: Data(secret.utf8)))',
267
+ ' }',
268
+ ].join('\n');
269
+ const hmacImpl = hasHmac ? [
270
+ ' static func computeHmac(_ data: String) -> String {',
271
+ ' let secret = ProcessInfo.processInfo.environment["ENCRYPTION_HMAC_SECRET"] ?? ""',
272
+ ' let key = SymmetricKey(data: Data(secret.utf8))',
273
+ ' let mac = HMAC<SHA256>.authenticationCode(for: Data(data.utf8), using: key)',
274
+ ' return Data(mac).base64EncodedString()',
275
+ ' }',
276
+ ].join('\n') : '';
277
+ const imports = iosApproach === 1
278
+ ? ['import Foundation', 'import CryptoKit']
279
+ : ['import Foundation', 'import CryptoKit', 'import CommonCrypto'];
280
+ return [
281
+ ...imports,
282
+ '',
283
+ 'struct EncryptionService {',
284
+ '',
285
+ ' static func encrypt(_ plaintext: String) -> String? {',
286
+ ' guard EncryptionConfig.encryptionEnabled else { return plaintext }',
287
+ ' guard let data = plaintext.data(using: .utf8) else { return nil }',
288
+ ' do {',
289
+ encryptImpl,
290
+ ' } catch {',
291
+ config.debug ? ' if EncryptionConfig.debug { print("[EncryptionService] encrypt error: \\(error)") }' : '',
292
+ ' return nil',
293
+ ' }',
294
+ ' }',
295
+ '',
296
+ deriveImpl,
297
+ hmacImpl,
298
+ '}',
299
+ ].filter(l => l !== null).join('\n');
300
+ }
301
+
302
+ if (platform === 'flutter') {
303
+ return [
304
+ "import 'dart:convert';",
305
+ "import 'dart:typed_data';",
306
+ "import 'package:encrypt/encrypt.dart';",
307
+ "import 'package:pointycastle/digests/sha256.dart';",
308
+ "import 'encryption_config.dart';",
309
+ '',
310
+ 'class EncryptionService {',
311
+ ' static String? encrypt(String plaintext) {',
312
+ ' if (!EncryptionConfig.encryptionEnabled) return plaintext;',
313
+ ' try {',
314
+ ' final key = _deriveKey();',
315
+ " final iv = IV.fromSecureRandom(16);",
316
+ " final encrypter = Encrypter(AES(key, mode: AESMode.cbc));",
317
+ ' final encrypted = encrypter.encrypt(plaintext, iv: iv);',
318
+ " return jsonEncode({'iv': iv.base64, 'encryptedData': encrypted.base64});",
319
+ ' } catch (_) { return null; }',
320
+ ' }',
321
+ '',
322
+ ' static String? decrypt(String payload) {',
323
+ ' if (!EncryptionConfig.encryptionEnabled) return payload;',
324
+ ' try {',
325
+ ' final map = jsonDecode(payload) as Map<String, dynamic>;',
326
+ ' final iv = IV.fromBase64(map[\'iv\'] as String);',
327
+ ' final encrypted = Encrypted.fromBase64(map[\'encryptedData\'] as String);',
328
+ ' final key = _deriveKey();',
329
+ " final encrypter = Encrypter(AES(key, mode: AESMode.cbc));",
330
+ ' return encrypter.decrypt(encrypted, iv: iv);',
331
+ ' } catch (_) { return null; }',
332
+ ' }',
333
+ '',
334
+ ' static Key _deriveKey() {',
335
+ ' final secret = EncryptionConfig.sharedKey ?? \'\';',
336
+ ' final digest = SHA256Digest();',
337
+ ' final bytes = Uint8List.fromList(utf8.encode(secret));',
338
+ ' final hash = digest.process(bytes);',
339
+ ' return Key(hash);',
340
+ ' }',
341
+ '}',
342
+ ].join('\n');
343
+ }
344
+
345
+ // TypeScript (angular / web / node)
346
+ const configImport = platform === 'angular'
347
+ ? "import { environment } from '../../../environments/environment';"
348
+ : platform === 'web'
349
+ ? "import { encryptionConfig } from './encryption.config.js';"
350
+ : "import { encryptionConfig } from './encryption.config';";
351
+ const cfgRef = (platform === 'angular') ? 'environment.encryption' : 'encryptionConfig.encryption';
352
+ const injectable = platform === 'angular' ? "@Injectable({ providedIn: 'root' })\nexport " : 'export ';
353
+ const importBlock = [
354
+ platform === 'angular' ? "import { Injectable } from '@angular/core';" : null,
355
+ configImport,
356
+ approach === 2 ? "import JSEncrypt from 'jsencrypt';" : null,
357
+ approach === 2 || approach === 3 ? "import CryptoJS from 'crypto-js';" : null,
358
+ ].filter(Boolean).join('\n');
359
+ return [
360
+ importBlock,
361
+ '',
362
+ injectable + 'class EncryptionService {',
363
+ '',
364
+ ' async encrypt(payload: object): Promise<object | null> {',
365
+ ' const cfg = ' + cfgRef + ';',
366
+ ' if (!cfg.enabled) return payload;',
367
+ ' try {',
368
+ approach === 1
369
+ ? [
370
+ ' const bodyStr = JSON.stringify(payload);',
371
+ ' const bodyBytes = new TextEncoder().encode(bodyStr);',
372
+ ' const aesKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);',
373
+ ' const iv = crypto.getRandomValues(new Uint8Array(12));',
374
+ ' const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, bodyBytes);',
375
+ ' const rawKey = await crypto.subtle.exportKey("raw", aesKey);',
376
+ ' const pubKey = await this.importPublicKey(cfg.publicKeyPem ?? "");',
377
+ ' const encryptedKey = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, pubKey, rawKey);',
378
+ ' return {',
379
+ ' encryptedKey: this.toBase64(encryptedKey),',
380
+ ' iv: this.toBase64(iv),',
381
+ ' encryptedData: this.toBase64(encryptedData),',
382
+ ' };',
383
+ ].join('\n')
384
+ : approach === 2
385
+ ? [
386
+ " const bodyStr = JSON.stringify(payload);",
387
+ " const aesKey = CryptoJS.lib.WordArray.random(32);",
388
+ " const iv = CryptoJS.lib.WordArray.random(16);",
389
+ " const salt = CryptoJS.lib.WordArray.random(8);",
390
+ " const encrypted = CryptoJS.AES.encrypt(bodyStr, aesKey, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });",
391
+ " const jsEncrypt = new JSEncrypt();",
392
+ " jsEncrypt.setPublicKey(cfg.publicKeyPem ?? '');",
393
+ " const encKey = jsEncrypt.encrypt(CryptoJS.enc.Base64.stringify(aesKey));",
394
+ " if (!encKey) throw new Error('RSA encryption failed');",
395
+ " return {",
396
+ " encryptedKey: encKey,",
397
+ " iv: CryptoJS.enc.Base64.stringify(iv),",
398
+ " salt: CryptoJS.enc.Base64.stringify(salt),",
399
+ " encryptedData: encrypted.toString(),",
400
+ " };",
401
+ ].join('\n')
402
+ : [
403
+ " const key = CryptoJS.SHA256(cfg.sharedKey ?? '');",
404
+ " const iv = CryptoJS.lib.WordArray.random(16);",
405
+ " const encrypted = CryptoJS.AES.encrypt(JSON.stringify(payload), key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });",
406
+ " return {",
407
+ " iv: CryptoJS.enc.Base64.stringify(iv),",
408
+ " encryptedData: encrypted.toString(),",
409
+ " };",
410
+ ].join('\n'),
411
+ ' } catch (err) {',
412
+ config.debug ? " if (cfg.debug) console.debug('[EncryptionService] encrypt error:', err);" : '',
413
+ ' return payload;',
414
+ ' }',
415
+ ' }',
416
+ '',
417
+ approach === 1 ? ' private toBase64(buffer: ArrayBuffer | Uint8Array): string {' : null,
418
+ approach === 1 ? ' return btoa(String.fromCharCode(...new Uint8Array(buffer instanceof ArrayBuffer ? buffer : buffer)));' : null,
419
+ approach === 1 ? ' }' : null,
420
+ approach === 1 ? '' : null,
421
+ approach === 1 ? ' private async importPublicKey(pem: string): Promise<CryptoKey> {' : null,
422
+ approach === 1 ? ' const binaryDer = atob(pem.replace(/-----.*?-----/g, "").replace(/\\s/g, ""));' : null,
423
+ approach === 1 ? ' const buffer = Uint8Array.from(binaryDer, c => c.charCodeAt(0));' : null,
424
+ approach === 1 ? ' return crypto.subtle.importKey("spki", buffer, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);' : null,
425
+ approach === 1 ? ' }' : null,
426
+ '}',
427
+ ].filter(l => l !== null).join('\n');
428
+ }
429
+
430
+ function generateInterceptor(platform, config) {
431
+ const hasHmac = config.hmac && config.hmac.enabled;
432
+ const excludeUrls = config.excludeUrls || [];
433
+
434
+ if (platform === 'android') {
435
+ const excludeCheck = excludeUrls.length > 0
436
+ ? [
437
+ ' val url = chain.request().url.toString()',
438
+ ' val excluded = listOf(' + excludeUrls.map(u => '"' + u + '"').join(', ') + ')',
439
+ ' if (excluded.any { url.startsWith(it) }) return chain.proceed(chain.request())',
440
+ ].join('\n')
441
+ : '';
442
+ return [
443
+ 'package com.example.encryption',
444
+ '',
445
+ 'import okhttp3.Interceptor',
446
+ 'import okhttp3.Response',
447
+ 'import okhttp3.RequestBody.Companion.toRequestBody',
448
+ 'import okhttp3.MediaType.Companion.toMediaType',
449
+ '',
450
+ 'class EncryptionInterceptor : Interceptor {',
451
+ ' override fun intercept(chain: Interceptor.Chain): Response {',
452
+ excludeCheck,
453
+ ' val original = chain.request()',
454
+ ' if (!EncryptionConfig.encryptionEnabled) return chain.proceed(original)',
455
+ ' return try {',
456
+ ' val body = original.body ?: return chain.proceed(original)',
457
+ ' val buffer = okio.Buffer()',
458
+ ' body.writeTo(buffer)',
459
+ ' val plaintext = buffer.readUtf8()',
460
+ ' val encrypted = EncryptionService.encrypt(plaintext) ?: return chain.proceed(original)',
461
+ ' val newBody = encrypted.toRequestBody("application/json".toMediaType())',
462
+ hasHmac ? [
463
+ ' val timestamp = System.currentTimeMillis().toString()',
464
+ ' val nonce = java.util.UUID.randomUUID().toString()',
465
+ ' val hmacSig = EncryptionService.computeHmac("$timestamp.$nonce.$encrypted")',
466
+ ' val newRequest = original.newBuilder()',
467
+ ' .method(original.method, newBody)',
468
+ ' .addHeader("X-Timestamp", timestamp)',
469
+ ' .addHeader("X-Nonce", nonce)',
470
+ ' .addHeader("X-Signature", hmacSig)',
471
+ ' .build()',
472
+ ].join('\n') : ' val newRequest = original.newBuilder().method(original.method, newBody).build()',
473
+ ' chain.proceed(newRequest)',
474
+ ' } catch (e: Exception) {',
475
+ config.debug ? ' if (EncryptionConfig.debug) android.util.Log.d("EncryptionInterceptor", "intercept error", e)' : null,
476
+ ' chain.proceed(original)',
477
+ ' }',
478
+ ' }',
479
+ '}',
480
+ ].filter(l => l !== null).join('\n');
481
+ }
482
+
483
+ if (platform === 'ios') {
484
+ const excludeCheckIos = excludeUrls.length > 0
485
+ ? [
486
+ ' let url = request.url?.absoluteString ?? ""',
487
+ ' let excluded: [String] = [' + excludeUrls.map(u => '"' + u + '"').join(', ') + ']',
488
+ ' if excluded.contains(where: { url.hasPrefix($0) }) { return request }',
489
+ ].join('\n')
490
+ : '';
491
+ const hmacIos = hasHmac ? [
492
+ ' let timestamp = String(Int(Date().timeIntervalSince1970 * 1000))',
493
+ ' let nonce = UUID().uuidString',
494
+ ' let sig = EncryptionService.computeHmac("\\(timestamp).\\(nonce).\\(encrypted)")',
495
+ ' modified.addValue(timestamp, forHTTPHeaderField: "X-Timestamp")',
496
+ ' modified.addValue(nonce, forHTTPHeaderField: "X-Nonce")',
497
+ ' modified.addValue(sig, forHTTPHeaderField: "X-Signature")',
498
+ ].join('\n') : '';
499
+ return [
500
+ 'import Foundation',
501
+ 'import CryptoKit',
502
+ '',
503
+ 'struct EncryptionInterceptor {',
504
+ ' static func intercept(request: URLRequest) -> URLRequest {',
505
+ ' guard EncryptionConfig.encryptionEnabled else { return request }',
506
+ excludeCheckIos,
507
+ ' guard let body = request.httpBody,',
508
+ ' let plaintext = String(data: body, encoding: .utf8) else { return request }',
509
+ ' guard let encrypted = EncryptionService.encrypt(plaintext) else { return request }',
510
+ ' guard let encryptedData = encrypted.data(using: .utf8) else { return request }',
511
+ ' var modified = request',
512
+ ' modified.httpBody = encryptedData',
513
+ hmacIos,
514
+ ' return modified',
515
+ ' }',
516
+ '}',
517
+ ].filter(l => l !== null).join('\n');
518
+ }
519
+
520
+ if (platform === 'flutter') {
521
+ const excludeCheck = excludeUrls.length > 0
522
+ ? [
523
+ " final excludeUrls = EncryptionConfig.excludeUrls;",
524
+ " if (excludeUrls.any((prefix) => options.path.startsWith(prefix))) {",
525
+ " return handler.next(options);",
526
+ " }",
527
+ ].join('\n')
528
+ : '';
529
+ const hmacFlutter = hasHmac ? [
530
+ " final timestamp = DateTime.now().millisecondsSinceEpoch.toString();",
531
+ " final nonce = DateTime.now().microsecondsSinceEpoch.toString();",
532
+ " final hmacSecret = const String.fromEnvironment('ENCRYPTION_HMAC_SECRET');",
533
+ " final sigPayload = '$timestamp.$nonce.${jsonEncode(body)}';",
534
+ " final hmacBytes = Hmac(sha256, utf8.encode(hmacSecret)).convert(utf8.encode(sigPayload)).bytes;",
535
+ " final signature = base64Encode(hmacBytes);",
536
+ " options.headers['X-Timestamp'] = timestamp;",
537
+ " options.headers['X-Nonce'] = nonce;",
538
+ " options.headers['X-Signature'] = signature;",
539
+ ].join('\n') : '';
540
+ return [
541
+ "import 'dart:convert';",
542
+ "import 'dart:typed_data';",
543
+ "import 'package:dio/dio.dart';",
544
+ "import 'package:crypto/crypto.dart';",
545
+ "import 'encryption_config.dart';",
546
+ "import 'encryption_service.dart';",
547
+ '',
548
+ 'class EncryptionInterceptor extends Interceptor {',
549
+ ' @override',
550
+ ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {',
551
+ excludeCheck,
552
+ ' if (!EncryptionConfig.encryptionEnabled) return handler.next(options);',
553
+ ' try {',
554
+ ' final body = options.data;',
555
+ ' if (body == null || body is FormData) return handler.next(options);',
556
+ ' final plaintext = body is String ? body : jsonEncode(body);',
557
+ ' final encrypted = EncryptionService.encrypt(plaintext);',
558
+ ' if (encrypted != null) {',
559
+ ' options.data = encrypted;',
560
+ hmacFlutter,
561
+ ' }',
562
+ ' } catch (e) {',
563
+ config.debug ? " if (EncryptionConfig.debug) debugPrint('[EncryptionInterceptor] error: $e');" : '',
564
+ ' }',
565
+ ' handler.next(options);',
566
+ ' }',
567
+ '}',
568
+ ].filter(l => l !== null).join('\n');
569
+ }
570
+
571
+ if (platform === 'react' || platform === 'react-native') {
572
+ const rnHmacImport = hasHmac ? "import CryptoJS from 'crypto-js';" : null;
573
+ const rnExcludeCheck = excludeUrls.length > 0
574
+ ? " if (cfg.excludeUrls?.some((u: string) => config.url?.startsWith(u))) return config;"
575
+ : null;
576
+ const rnHmacBlock = hasHmac ? [
577
+ " const timestamp = Date.now().toString();",
578
+ " const nonce = Math.random().toString(36).slice(2);",
579
+ " const hmacSecret = process.env['ENCRYPTION_HMAC_SECRET'] ?? '';",
580
+ " const sigPayload = timestamp + '.' + nonce + '.' + JSON.stringify(config.data);",
581
+ " const signature = CryptoJS.HmacSHA256(sigPayload, hmacSecret).toString(CryptoJS.enc.Base64);",
582
+ " config.headers['X-Timestamp'] = timestamp;",
583
+ " config.headers['X-Nonce'] = nonce;",
584
+ " config.headers['X-Signature'] = signature;",
585
+ ].join('\n') : null;
586
+ return [
587
+ "import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';",
588
+ "import { EncryptionService } from './encryption.service';",
589
+ rnHmacImport,
590
+ "import { encryptionConfig } from './encryption.config';",
591
+ '',
592
+ 'const encryptionService = new EncryptionService();',
593
+ '',
594
+ 'export function applyEncryptionInterceptor(instance: AxiosInstance): void {',
595
+ ' const cfg = encryptionConfig.encryption;',
596
+ '',
597
+ ' instance.interceptors.request.use(',
598
+ ' async (config: InternalAxiosRequestConfig) => {',
599
+ ' if (!cfg.enabled) return config;',
600
+ ' if (!config.data) return config;',
601
+ ' if (config.data instanceof FormData) return config;',
602
+ rnExcludeCheck,
603
+ ' try {',
604
+ ' config.data = await encryptionService.encrypt(config.data);',
605
+ " config.headers['Content-Type'] = 'application/json';",
606
+ rnHmacBlock,
607
+ ' } catch {}',
608
+ ' return config;',
609
+ ' },',
610
+ ' (error: unknown) => Promise.reject(error)',
611
+ ' );',
612
+ '}',
613
+ ].filter(l => l !== null).join('\n');
614
+ }
615
+
616
+ // TypeScript (angular / web / node)
617
+ const excludeBlock = excludeUrls.length > 0
618
+ ? [
619
+ ' const excludeUrls: string[] = ' + JSON.stringify(excludeUrls) + ';',
620
+ " if (excludeUrls.some(prefix => url.startsWith(prefix))) {",
621
+ platform === 'angular' ? ' return next(req);' : ' return next(request);',
622
+ ' }',
623
+ ].join('\n')
624
+ : '';
625
+
626
+ const hmacBlock = hasHmac
627
+ ? [
628
+ ' const timestamp = Date.now().toString();',
629
+ " const nonce = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2);",
630
+ " const hmacSecret = process.env['ENCRYPTION_HMAC_SECRET'] ?? '';",
631
+ " const sigPayload = timestamp + '.' + nonce + '.' + JSON.stringify(encrypted);",
632
+ platform === 'angular' || platform === 'web'
633
+ ? [
634
+ " const sigKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(hmacSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);",
635
+ " const sigBuf = await crypto.subtle.sign('HMAC', sigKey, new TextEncoder().encode(sigPayload));",
636
+ " const signature = btoa(String.fromCharCode(...new Uint8Array(sigBuf)));",
637
+ ].join('\n')
638
+ : [
639
+ " const { createHmac } = await import('crypto');",
640
+ " const signature = createHmac('sha256', hmacSecret).update(sigPayload).digest('base64');",
641
+ ].join('\n'),
642
+ ].join('\n')
643
+ : '';
644
+
645
+ if (platform === 'angular') {
646
+ const configImport = "import { environment } from '../../../environments/environment';";
647
+ return [
648
+ "import { Injectable } from '@angular/core';",
649
+ "import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';",
650
+ "import { Observable, from, switchMap, catchError } from 'rxjs';",
651
+ configImport,
652
+ "import { EncryptionService } from './encryption.service';",
653
+ '',
654
+ "@Injectable()",
655
+ 'export class EncryptionInterceptor implements HttpInterceptor {',
656
+ ' constructor(private encryptionService: EncryptionService) {}',
657
+ '',
658
+ ' intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {',
659
+ ' const cfg = environment.encryption;',
660
+ ' const url = req.url;',
661
+ excludeBlock,
662
+ ' if (!cfg.enabled || req.body == null) return next.handle(req);',
663
+ ' if (req.body instanceof FormData) return next.handle(req);',
664
+ ' return from(this.encryptionService.encrypt(req.body as object)).pipe(',
665
+ hasHmac
666
+ ? ' switchMap(encrypted => from((async () => {'
667
+ : ' switchMap(encrypted => next.handle(req.clone({ body: encrypted }))),',
668
+ hasHmac ? hmacBlock : null,
669
+ hasHmac ? " const headers: Record<string, string> = { 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Signature': signature };" : null,
670
+ hasHmac ? ' return req.clone({ body: encrypted, setHeaders: headers });' : null,
671
+ hasHmac ? ' })()).pipe(switchMap(cloned => next.handle(cloned)))),': null,
672
+ ' catchError(err => {',
673
+ " if (cfg.debug) console.debug('[EncryptionInterceptor] error:', err);",
674
+ ' return next.handle(req);',
675
+ ' }),',
676
+ ' );',
677
+ ' }',
678
+ '}',
679
+ ].filter(l => l !== null).join('\n');
680
+ }
681
+
682
+ if (platform === 'node') {
683
+ return [
684
+ "import { Request, Response, NextFunction } from 'express';",
685
+ "import { encryptionConfig } from './encryption.config';",
686
+ "import { EncryptionService } from './encryption.service';",
687
+ '',
688
+ 'const encryptionService = new EncryptionService();',
689
+ '',
690
+ 'export async function encryptionMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {',
691
+ ' const cfg = encryptionConfig.encryption;',
692
+ ' const url = req.path;',
693
+ excludeBlock,
694
+ ' if (!cfg.enabled || req.body == null) return next();',
695
+ ' try {',
696
+ ' req.body = await encryptionService.encrypt(req.body);',
697
+ hasHmac ? hmacBlock : '',
698
+ hasHmac ? " res.setHeader('X-Timestamp', timestamp);\n res.setHeader('X-Nonce', nonce);\n res.setHeader('X-Signature', signature);" : '',
699
+ ' } catch (err) {',
700
+ " if (cfg.debug) console.debug('[EncryptionMiddleware] error:', err);",
701
+ ' }',
702
+ ' next();',
703
+ '}',
704
+ ].join('\n');
705
+ }
706
+
707
+ // web
708
+ return [
709
+ "import { encryptionConfig } from './encryption.config.js';",
710
+ "import { EncryptionService } from './encryption.service.js';",
711
+ '',
712
+ 'const encryptionService = new EncryptionService();',
713
+ '',
714
+ 'export async function encryptRequest(url, options = {}) {',
715
+ ' const cfg = encryptionConfig.encryption;',
716
+ ' const excludeUrls = ' + JSON.stringify(excludeUrls) + ';',
717
+ ' if (excludeUrls.some(prefix => url.startsWith(prefix))) return fetch(url, options);',
718
+ ' if (!cfg.enabled || !options.body) return fetch(url, options);',
719
+ ' try {',
720
+ ' const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;',
721
+ ' const encrypted = await encryptionService.encrypt(body);',
722
+ hasHmac
723
+ ? [
724
+ " const timestamp = Date.now().toString();",
725
+ " const nonce = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2);",
726
+ " const hmacSecret = (typeof process !== 'undefined' && process.env['ENCRYPTION_HMAC_SECRET']) || '';",
727
+ " const sigPayload = timestamp + '.' + nonce + '.' + JSON.stringify(encrypted);",
728
+ " const sigKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(hmacSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);",
729
+ " const sigBuf = await crypto.subtle.sign('HMAC', sigKey, new TextEncoder().encode(sigPayload));",
730
+ " const signature = btoa(String.fromCharCode(...new Uint8Array(sigBuf)));",
731
+ " options = { ...options, body: JSON.stringify(encrypted), headers: { ...options.headers, 'Content-Type': 'application/json', 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Signature': signature } };",
732
+ ].join('\n')
733
+ : ' options = { ...options, body: JSON.stringify(encrypted), headers: { ...options.headers, "Content-Type": "application/json" } };',
734
+ ' } catch (err) {',
735
+ " if (cfg.debug) console.debug('[encryptRequest] error:', err);",
736
+ ' }',
737
+ ' return fetch(url, options);',
738
+ '}',
739
+ ].join('\n');
740
+ }
741
+
742
+ function generateAxiosInstance() {
743
+ return [
744
+ "import axios from 'axios';",
745
+ "import { applyEncryptionInterceptor } from '../encryption/encryption.interceptor';",
746
+ '',
747
+ 'const axiosInstance = axios.create({',
748
+ " // baseURL: 'https://your-api-base-url.com',",
749
+ '});',
750
+ '',
751
+ 'applyEncryptionInterceptor(axiosInstance);',
752
+ '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER',
753
+ '',
754
+ 'export default axiosInstance;',
755
+ ].join('\n');
756
+ }
757
+
758
+ function patchIndexPolyfill(indexPath, dryRun) {
759
+ const polyfillLine = "import 'react-native-get-random-values'; // crypto.getRandomValues polyfill \u2014 must be first import";
760
+ if (dryRun) {
761
+ console.log('DRY-RUN PATCH ' + indexPath + ' (react-native-get-random-values polyfill)');
762
+ return;
763
+ }
764
+ if (!fs.existsSync(indexPath)) {
765
+ console.log('SKIP PATCH (file not found): ' + indexPath);
766
+ return;
767
+ }
768
+ let content;
769
+ try { content = fs.readFileSync(indexPath, 'utf8'); } catch (e) {
770
+ console.error('Error: could not read ' + indexPath + ': ' + e.message);
771
+ return;
772
+ }
773
+ if (content.includes('react-native-get-random-values')) {
774
+ console.log('ALREADY PATCHED ' + indexPath + ' (polyfill already present)');
775
+ return;
776
+ }
777
+ try {
778
+ fs.writeFileSync(indexPath, polyfillLine + '\n' + content, 'utf8');
779
+ console.log('PATCHED ' + indexPath);
780
+ } catch (e) {
781
+ console.error('Error: could not patch ' + indexPath + ': ' + e.message);
782
+ }
783
+ }
784
+
785
+ // ── Production React Native generators (approaches 4 & 5) ────────────────────
786
+
787
+ function generateRnProductionTypes() {
788
+ return [
789
+ "// src/services/security/types.ts",
790
+ "export interface EncryptedEnvelope {",
791
+ " data: string; // base64(ciphertext + 16-byte GCM auth_tag)",
792
+ " key: string; // base64(RSA-OAEP encrypted AES key bytes)",
793
+ " iv: string; // base64(12-byte IV)",
794
+ " nonce: string; // appId^unix_timestamp_ms.random_hex_16",
795
+ " key_id: string; // RSA key version identifier from key endpoint",
796
+ "}",
797
+ "",
798
+ "export interface EncryptedResponse {",
799
+ " data: string; // base64(ciphertext + 16-byte GCM auth_tag)",
800
+ " iv: string; // base64(new server-generated 12-byte IV)",
801
+ " nonce?: string;",
802
+ "}",
803
+ ].join('\n');
804
+ }
805
+
806
+ function generateRnProductionEncryptionService() {
807
+ return [
808
+ "import { randomBytes, createCipheriv } from 'react-native-quick-crypto';",
809
+ "",
810
+ "export const encryptPayload = async (",
811
+ " plaintext: string,",
812
+ " _rsaPublicKeyPem: string",
813
+ "): Promise<{ ciphertext: string; aesKey: Buffer; iv: Buffer }> => {",
814
+ " const aesKey = randomBytes(32) as Buffer;",
815
+ " const iv = randomBytes(12) as Buffer;",
816
+ " const cipher = createCipheriv('aes-256-gcm', aesKey, iv) as any;",
817
+ " const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);",
818
+ " const tag = cipher.getAuthTag() as Buffer;",
819
+ " const ciphertext = Buffer.concat([encrypted, tag]).toString('base64');",
820
+ " return { ciphertext, aesKey, iv };",
821
+ "};",
822
+ ].join('\n');
823
+ }
824
+
825
+ function generateRnProductionDecryptionService() {
826
+ return [
827
+ "import { createDecipheriv } from 'react-native-quick-crypto';",
828
+ "",
829
+ "export const decryptResponse = (",
830
+ " ciphertextBase64: string,",
831
+ " ivBase64: string,",
832
+ " aesKey: Buffer",
833
+ "): string => {",
834
+ " const combined = Buffer.from(ciphertextBase64, 'base64');",
835
+ " const tag = combined.slice(combined.length - 16);",
836
+ " const ciphertext = combined.slice(0, combined.length - 16);",
837
+ " const iv = Buffer.from(ivBase64, 'base64');",
838
+ " const decipher = createDecipheriv('aes-256-gcm', aesKey, iv) as any;",
839
+ " decipher.setAuthTag(tag);",
840
+ " return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');",
841
+ "};",
842
+ ].join('\n');
843
+ }
844
+
845
+ function generateRnProductionRsaKeyService(config) {
846
+ const endpoint = config.rsaKeyEndpoint || '<API_BASE_URL>/api/security/public-key';
847
+ const retries = config.rsaKeyRetries || 3;
848
+ return [
849
+ "import axios from 'axios';",
850
+ "",
851
+ "let cachedPublicKey: string | null = null;",
852
+ "let cachedKeyId: string | null = null;",
853
+ "const MAX_RETRIES = " + retries + ";",
854
+ "",
855
+ "export const getRsaPublicKey = async (",
856
+ " endpoint: string = '" + endpoint + "'",
857
+ "): Promise<{ publicKey: string; keyId: string }> => {",
858
+ " if (cachedPublicKey && cachedKeyId) return { publicKey: cachedPublicKey, keyId: cachedKeyId };",
859
+ " let lastError: Error | null = null;",
860
+ " for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {",
861
+ " try {",
862
+ " const { data } = await axios.get(endpoint);",
863
+ " cachedPublicKey = data.public_key;",
864
+ " cachedKeyId = data.key_id;",
865
+ " return { publicKey: data.public_key, keyId: data.key_id };",
866
+ " } catch (e: any) { lastError = e; }",
867
+ " }",
868
+ " throw new Error(`RSA key fetch failed after ${MAX_RETRIES} attempts: ${lastError?.message}`);",
869
+ "};",
870
+ "",
871
+ "export const invalidateRsaKey = () => { cachedPublicKey = null; cachedKeyId = null; };",
872
+ ].join('\n');
873
+ }
874
+
875
+ function generateRnProductionKeychainService() {
876
+ return [
877
+ "import * as Keychain from 'react-native-keychain';",
878
+ "",
879
+ "export const storeToken = async (key: string, value: string): Promise<void> => {",
880
+ " await Keychain.setGenericPassword(key, value, { service: key });",
881
+ "};",
882
+ "",
883
+ "export const getToken = async (key: string): Promise<string | null> => {",
884
+ " const result = await Keychain.getGenericPassword({ service: key });",
885
+ " return result ? result.password : null;",
886
+ "};",
887
+ "",
888
+ "export const deleteToken = async (key: string): Promise<void> => {",
889
+ " await Keychain.resetGenericPassword({ service: key });",
890
+ "};",
891
+ ].join('\n');
892
+ }
893
+
894
+ function generateRnProductionHmacService() {
895
+ return [
896
+ "import { createHmac } from 'react-native-quick-crypto';",
897
+ "",
898
+ "export const buildCanonicalString = (",
899
+ " method: string, url: string, nonce: string, encryptedData: string",
900
+ "): string => `${method.toUpperCase()}|${url}|${nonce}|${encryptedData}`;",
901
+ "",
902
+ "export const signRequest = (canonical: string, hmacSecret: string): string => {",
903
+ " if (!hmacSecret) throw new Error('HMAC_SECRET is not configured');",
904
+ " const hmac = createHmac('sha256', hmacSecret) as any;",
905
+ " hmac.update(canonical);",
906
+ " return hmac.digest('base64');",
907
+ "};",
908
+ ].join('\n');
909
+ }
910
+
911
+ function generateRnProductionEncryptionConfig(config) {
912
+ const excludeLines = (config.excludeUrls || []).map(u => " '" + u + "',");
913
+ const endpoint = config.rsaKeyEndpoint || '<API_BASE_URL>/api/security/public-key';
914
+ const alwaysOn = config.encryptionToggle === 'always-on';
915
+ const encryptionEnabledLine = alwaysOn
916
+ ? "export const ENCRYPTION_ENABLED = true; // always-on mode"
917
+ : "export const ENCRYPTION_ENABLED = process.env.ENCRYPTION_ENABLED === 'true';";
918
+ return [
919
+ "// Paths that bypass encryption (public endpoints, health checks, etc.)",
920
+ "export const EXCLUDED_PATHS: string[] = [",
921
+ ...excludeLines,
922
+ "];",
923
+ "",
924
+ encryptionEnabledLine,
925
+ "export const ENCRYPTION_LAYER = process.env.ENCRYPTION_LAYER ?? 'rn'; // 'rn' | 'native'",
926
+ "export const RSA_KEY_ENDPOINT = process.env.RSA_KEY_ENDPOINT ?? '" + endpoint + "';",
927
+ "export const HMAC_ENABLED = process.env.HMAC_ENABLED === 'true';",
928
+ "export const HMAC_SECRET = process.env.ENCRYPTION_HMAC_SECRET ?? '';",
929
+ ].join('\n');
930
+ }
931
+
932
+ function generateRnProductionInterceptor(approach, config) {
933
+ const hasHmac = config.hmac && config.hmac.enabled;
934
+ const isNative = approach === 5;
935
+ const cryptoImport = isNative
936
+ ? "import { nativeEncryptAES, nativeEncryptRSA } from './native-crypto.module';"
937
+ : "import { encryptPayload } from './encryption.service';";
938
+ const decryptImport = isNative
939
+ ? "import { nativeDecryptAES } from './native-crypto.module';"
940
+ : "import { decryptResponse } from './decryption.service';";
941
+ const hmacImport = hasHmac ? "import { buildCanonicalString, signRequest } from './hmac.service';" : null;
942
+ const encryptBlock = isNative ? [
943
+ " const aesKeyBuf = randomBytes(32) as Buffer;",
944
+ " const aesKeyBase64 = aesKeyBuf.toString('base64');",
945
+ " const { ciphertext, iv } = await nativeEncryptAES(plaintext, aesKeyBase64);",
946
+ " const encryptedKey = await nativeEncryptRSA(publicKey, aesKeyBase64);",
947
+ " requestMap.set(requestId, { aesKeyBase64, iv });",
948
+ ].join('\n') : [
949
+ " const { ciphertext, aesKey, iv } = await encryptPayload(plaintext, publicKey);",
950
+ " const encryptedKey = publicEncrypt({ key: publicKey, padding: 4 /* RSA_PKCS1_OAEP_PADDING */ }, aesKey).toString('base64');",
951
+ " requestMap.set(requestId, { aesKey, iv: iv.toString('base64') });",
952
+ ].join('\n');
953
+ const decryptBlock = isNative ? [
954
+ " const { aesKeyBase64 } = requestMap.get(requestId)!;",
955
+ " requestMap.delete(requestId);",
956
+ " if (encResp?.data && encResp?.iv) {",
957
+ " response.data = JSON.parse(await nativeDecryptAES(encResp.data, encResp.iv, aesKeyBase64));",
958
+ " }",
959
+ ].join('\n') : [
960
+ " const { aesKey, iv: _iv } = requestMap.get(requestId)!;",
961
+ " requestMap.delete(requestId);",
962
+ " if (encResp?.data && encResp?.iv) {",
963
+ " response.data = JSON.parse(decryptResponse(encResp.data, encResp.iv, aesKey));",
964
+ " }",
965
+ ].join('\n');
966
+ return [
967
+ "import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';",
968
+ isNative
969
+ ? "import { randomBytes } from 'react-native-quick-crypto';"
970
+ : "import { randomBytes, publicEncrypt } from 'react-native-quick-crypto';",
971
+ cryptoImport,
972
+ decryptImport,
973
+ "import { getRsaPublicKey, invalidateRsaKey } from './rsa-key.service';",
974
+ hmacImport,
975
+ "import { ENCRYPTION_ENABLED, EXCLUDED_PATHS, RSA_KEY_ENDPOINT, HMAC_ENABLED, HMAC_SECRET } from './encryption-config';",
976
+ "",
977
+ "const requestMap = new Map<string, any>();",
978
+ "",
979
+ "export function applyProductionEncryptionInterceptor(instance: AxiosInstance): void {",
980
+ " instance.interceptors.request.use(",
981
+ " async (config: InternalAxiosRequestConfig) => {",
982
+ " if (!ENCRYPTION_ENABLED) return config;",
983
+ " if (!config.data) return config;",
984
+ " if (config.data instanceof FormData) return config;",
985
+ " if (EXCLUDED_PATHS.some((p: string) => config.url?.includes(p))) return config;",
986
+ " const method = (config.method ?? '').toUpperCase();",
987
+ " if (!['POST', 'PUT', 'PATCH'].includes(method)) return config;",
988
+ " try {",
989
+ " const { publicKey, keyId } = await getRsaPublicKey(RSA_KEY_ENDPOINT);",
990
+ " const plaintext = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);",
991
+ " const requestId = (randomBytes(16) as Buffer).toString('hex');",
992
+ " const nonce = `app^${Date.now()}.${(randomBytes(8) as Buffer).toString('hex')}`;",
993
+ encryptBlock,
994
+ " const envelope = { data: ciphertext, key: encryptedKey, iv: iv.toString ? iv.toString('base64') : iv, nonce, key_id: keyId };",
995
+ " config.data = envelope;",
996
+ " config.headers['Content-Type'] = 'application/json';",
997
+ " config.headers['X-Request-Id'] = requestId;",
998
+ " (config as any).__requestId = requestId;",
999
+ hasHmac ? [
1000
+ " if (HMAC_ENABLED) {",
1001
+ " const canonical = buildCanonicalString(method, config.url ?? '', nonce, ciphertext);",
1002
+ " const signature = signRequest(canonical, HMAC_SECRET);",
1003
+ " config.headers['X-Request-Signature'] = signature;",
1004
+ " config.headers['X-Nonce'] = nonce;",
1005
+ " }",
1006
+ ].join('\n') : null,
1007
+ " } catch (err: any) {",
1008
+ " if (__DEV__) console.debug('[EncryptionInterceptor] encrypt error:', err?.message);",
1009
+ " }",
1010
+ " return config;",
1011
+ " },",
1012
+ " (error: unknown) => Promise.reject(error)",
1013
+ " );",
1014
+ "",
1015
+ " instance.interceptors.response.use(",
1016
+ " (response: AxiosResponse) => {",
1017
+ " const requestId = (response.config as any).__requestId;",
1018
+ " if (!requestId || !requestMap.has(requestId)) return response;",
1019
+ " const encResp = response.data;",
1020
+ " try {",
1021
+ decryptBlock,
1022
+ " } catch (err: any) {",
1023
+ " requestMap.delete(requestId);",
1024
+ " if (__DEV__) console.debug('[EncryptionInterceptor] decrypt error:', err?.message);",
1025
+ " throw Object.assign(new Error('DECRYPTION_AUTH_TAG_FAILURE'), { isAuthTagFailure: true });",
1026
+ " }",
1027
+ " return response;",
1028
+ " },",
1029
+ " async (error: any) => {",
1030
+ " const requestId = error?.config?.__requestId;",
1031
+ " if (requestId) requestMap.delete(requestId);",
1032
+ " if (error?.response?.status === 401 && error?.response?.data?.code === 'KEY_ID_NOT_FOUND') {",
1033
+ " invalidateRsaKey();",
1034
+ " return instance.request(error.config);",
1035
+ " }",
1036
+ " return Promise.reject(error);",
1037
+ " }",
1038
+ " );",
1039
+ "}",
1040
+ ].filter(l => l !== null).join('\n');
1041
+ }
1042
+
1043
+ function generateNativeAndroidModule(config) {
1044
+ const pkg = config.androidPackage || 'com.example.security';
1045
+ return [
1046
+ 'package ' + pkg,
1047
+ '',
1048
+ 'import android.util.Base64',
1049
+ 'import com.facebook.react.bridge.*',
1050
+ 'import javax.crypto.Cipher',
1051
+ 'import javax.crypto.spec.GCMParameterSpec',
1052
+ 'import javax.crypto.spec.SecretKeySpec',
1053
+ 'import java.security.KeyFactory',
1054
+ 'import java.security.spec.X509EncodedKeySpec',
1055
+ '',
1056
+ 'class NativeCryptoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {',
1057
+ ' override fun getName() = "NativeCryptoModule"',
1058
+ '',
1059
+ ' @ReactMethod',
1060
+ ' fun encryptAES(plaintext: String, keyBase64: String, promise: Promise) {',
1061
+ ' try {',
1062
+ ' val keyBytes = Base64.decode(keyBase64, Base64.NO_WRAP)',
1063
+ ' require(keyBytes.size == 32) { "AES key must be 32 bytes" }',
1064
+ ' val cipher = Cipher.getInstance("AES/GCM/NoPadding")',
1065
+ ' cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyBytes, "AES"))',
1066
+ ' val cipherWithTag = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))',
1067
+ ' val result = WritableNativeMap().apply {',
1068
+ ' putString("ciphertext", Base64.encodeToString(cipherWithTag, Base64.NO_WRAP))',
1069
+ ' putString("iv", Base64.encodeToString(cipher.iv, Base64.NO_WRAP))',
1070
+ ' }',
1071
+ ' promise.resolve(result)',
1072
+ ' } catch (e: Exception) { promise.reject("ERR_ENCRYPT", e.message, e) }',
1073
+ ' }',
1074
+ '',
1075
+ ' @ReactMethod',
1076
+ ' fun decryptAES(ciphertextBase64: String, ivBase64: String, keyBase64: String, promise: Promise) {',
1077
+ ' try {',
1078
+ ' val keyBytes = Base64.decode(keyBase64, Base64.NO_WRAP)',
1079
+ ' require(keyBytes.size == 32) { "AES key must be 32 bytes" }',
1080
+ ' val cipherWithTag = Base64.decode(ciphertextBase64, Base64.NO_WRAP)',
1081
+ ' val iv = Base64.decode(ivBase64, Base64.NO_WRAP)',
1082
+ ' val cipher = Cipher.getInstance("AES/GCM/NoPadding")',
1083
+ ' cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(keyBytes, "AES"), GCMParameterSpec(128, iv))',
1084
+ ' promise.resolve(String(cipher.doFinal(cipherWithTag), Charsets.UTF_8))',
1085
+ ' } catch (e: Exception) { promise.reject("ERR_DECRYPT", e.message, e) }',
1086
+ ' }',
1087
+ '',
1088
+ ' @ReactMethod',
1089
+ ' fun encryptRSA(publicKeyPem: String, dataBase64: String, promise: Promise) {',
1090
+ ' try {',
1091
+ ' val pem = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace("\\n", "").trim()',
1092
+ ' val pubKey = KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(Base64.decode(pem, Base64.NO_WRAP)))',
1093
+ ' val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")',
1094
+ ' cipher.init(Cipher.ENCRYPT_MODE, pubKey)',
1095
+ ' promise.resolve(Base64.encodeToString(cipher.doFinal(Base64.decode(dataBase64, Base64.NO_WRAP)), Base64.NO_WRAP))',
1096
+ ' } catch (e: Exception) { promise.reject("ERR_RSA_ENCRYPT", e.message, e) }',
1097
+ ' }',
1098
+ '}',
1099
+ ].join('\n');
1100
+ }
1101
+
1102
+ function generateNativeAndroidPackage(config) {
1103
+ const pkg = config.androidPackage || 'com.example.security';
1104
+ return [
1105
+ 'package ' + pkg,
1106
+ '',
1107
+ 'import com.facebook.react.ReactPackage',
1108
+ 'import com.facebook.react.bridge.NativeModule',
1109
+ 'import com.facebook.react.bridge.ReactApplicationContext',
1110
+ 'import com.facebook.react.uimanager.ViewManager',
1111
+ '',
1112
+ 'class NativeCryptoPackage : ReactPackage {',
1113
+ ' override fun createNativeModules(ctx: ReactApplicationContext): List<NativeModule> =',
1114
+ ' listOf(NativeCryptoModule(ctx))',
1115
+ ' override fun createViewManagers(ctx: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()',
1116
+ '}',
1117
+ '// Register NativeCryptoPackage in MainApplication.kt getPackages()',
1118
+ ].join('\n');
1119
+ }
1120
+
1121
+ function generateNativeIosModule() {
1122
+ return [
1123
+ '// NativeCryptoModule.swift',
1124
+ 'import CryptoKit',
1125
+ 'import Foundation',
1126
+ '',
1127
+ '@objc(NativeCryptoModule)',
1128
+ 'class NativeCryptoModule: NSObject {',
1129
+ '',
1130
+ ' @objc func encryptAES(_ plaintext: String, keyBase64: String,',
1131
+ ' resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {',
1132
+ ' do {',
1133
+ ' guard let data = plaintext.data(using: .utf8),',
1134
+ ' let keyData = Data(base64Encoded: keyBase64), keyData.count == 32 else {',
1135
+ ' return reject("ERR_INVALID_INPUT", "Invalid plaintext or key", nil)',
1136
+ ' }',
1137
+ ' let key = SymmetricKey(data: keyData)',
1138
+ ' let nonce = try AES.GCM.Nonce()',
1139
+ ' let sealed = try AES.GCM.seal(data, using: key, nonce: nonce)',
1140
+ ' resolve(["ciphertext": (sealed.ciphertext + sealed.tag).base64EncodedString(),',
1141
+ ' "iv": Data(nonce).base64EncodedString()])',
1142
+ ' } catch { reject("ERR_ENCRYPT", error.localizedDescription, error) }',
1143
+ ' }',
1144
+ '',
1145
+ ' @objc func decryptAES(_ ciphertextBase64: String, ivBase64: String, keyBase64: String,',
1146
+ ' resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {',
1147
+ ' do {',
1148
+ ' guard let combined = Data(base64Encoded: ciphertextBase64),',
1149
+ ' let ivData = Data(base64Encoded: ivBase64),',
1150
+ ' let keyData = Data(base64Encoded: keyBase64),',
1151
+ ' keyData.count == 32, combined.count > 16 else {',
1152
+ ' return reject("ERR_INVALID_INPUT", "Invalid inputs", nil)',
1153
+ ' }',
1154
+ ' let key = SymmetricKey(data: keyData)',
1155
+ ' let nonce = try AES.GCM.Nonce(data: ivData)',
1156
+ ' let box = try AES.GCM.SealedBox(nonce: nonce, ciphertext: combined.dropLast(16), tag: combined.suffix(16))',
1157
+ ' guard let plain = String(data: try AES.GCM.open(box, using: key), encoding: .utf8) else {',
1158
+ ' return reject("ERR_DECODE", "UTF-8 decode failed", nil)',
1159
+ ' }',
1160
+ ' resolve(plain)',
1161
+ ' } catch { reject("ERR_DECRYPT", error.localizedDescription, error) }',
1162
+ ' }',
1163
+ '',
1164
+ ' @objc func encryptRSA(_ publicKeyPEM: String, dataBase64: String,',
1165
+ ' resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {',
1166
+ ' guard let data = Data(base64Encoded: dataBase64) else {',
1167
+ ' return reject("ERR_INVALID_INPUT", "Invalid data", nil)',
1168
+ ' }',
1169
+ ' let pem = publicKeyPEM',
1170
+ ' .replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")',
1171
+ ' .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")',
1172
+ ' .replacingOccurrences(of: "\\n", with: "")',
1173
+ ' guard let keyData = Data(base64Encoded: pem),',
1174
+ ' let secKey = SecKeyCreateWithData(keyData as CFData,',
1175
+ ' [kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary, nil) else {',
1176
+ ' return reject("ERR_KEY_LOAD", "Failed to load RSA key", nil)',
1177
+ ' }',
1178
+ ' var error: Unmanaged<CFError>?',
1179
+ ' guard let encrypted = SecKeyCreateEncryptedData(secKey, .rsaEncryptionOAEPSHA256, data as CFData, &error) else {',
1180
+ ' return reject("ERR_RSA_ENCRYPT", error?.takeRetainedValue().localizedDescription ?? "RSA encrypt failed", nil)',
1181
+ ' }',
1182
+ ' resolve((encrypted as Data).base64EncodedString())',
1183
+ ' }',
1184
+ '}',
1185
+ ].join('\n');
1186
+ }
1187
+
1188
+ function generateNativeIosBridge() {
1189
+ return [
1190
+ '// NativeCryptoModule.m',
1191
+ '#import <React/RCTBridgeModule.h>',
1192
+ '@interface RCT_EXTERN_MODULE(NativeCryptoModule, NSObject)',
1193
+ 'RCT_EXTERN_METHOD(encryptAES:(NSString *)plaintext keyBase64:(NSString *)keyBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)',
1194
+ 'RCT_EXTERN_METHOD(decryptAES:(NSString *)ciphertextBase64 ivBase64:(NSString *)ivBase64 keyBase64:(NSString *)keyBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)',
1195
+ 'RCT_EXTERN_METHOD(encryptRSA:(NSString *)publicKeyPEM dataBase64:(NSString *)dataBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)',
1196
+ '@end',
1197
+ ].join('\n');
1198
+ }
1199
+
1200
+ function generateNativeJsBridge() {
1201
+ return [
1202
+ "import { NativeModules } from 'react-native';",
1203
+ "const { NativeCryptoModule } = NativeModules;",
1204
+ "",
1205
+ "if (!NativeCryptoModule) {",
1206
+ " throw new Error('[NativeCryptoModule] Native module not registered. Verify MainApplication and Xcode wiring.');",
1207
+ "}",
1208
+ "",
1209
+ "export interface NativeEncryptResult { ciphertext: string; iv: string; }",
1210
+ "",
1211
+ "export const nativeEncryptAES = (plaintext: string, keyBase64: string): Promise<NativeEncryptResult> =>",
1212
+ " NativeCryptoModule.encryptAES(plaintext, keyBase64);",
1213
+ "",
1214
+ "export const nativeDecryptAES = (ciphertextBase64: string, ivBase64: string, keyBase64: string): Promise<string> =>",
1215
+ " NativeCryptoModule.decryptAES(ciphertextBase64, ivBase64, keyBase64);",
1216
+ "",
1217
+ "export const nativeEncryptRSA = (publicKeyPEM: string, dataBase64: string): Promise<string> =>",
1218
+ " NativeCryptoModule.encryptRSA(publicKeyPEM, dataBase64);",
1219
+ ].join('\n');
1220
+ }
1221
+
1222
+ // ── CLI ───────────────────────────────────────────────────────────────────────
1223
+
1224
+ if (args.includes('--help') || args.includes('-h')) {
1225
+ console.log([
1226
+ 'Usage: node install.js --config-json <path> [--dry-run] [--detection-mode <mode>]',
1227
+ '',
1228
+ 'Options:',
1229
+ ' --config-json Path to the resolved config JSON file (required)',
1230
+ ' --dry-run Print file paths and content without writing to disk',
1231
+ ' --detection-mode One of: replace (default), reuse, refactor',
1232
+ ' replace Overwrite all generated files (default)',
1233
+ ' reuse Skip files that already exist; generate only missing ones',
1234
+ ' refactor Write scaffold files (*.scaffold.*) alongside existing; do not overwrite',
1235
+ ' --help Print this help and exit',
1236
+ '',
1237
+ 'Exit codes:',
1238
+ ' 0 Success',
1239
+ ' 1 Config JSON missing or invalid',
1240
+ ' 2 File write error',
1241
+ ].join('\n'));
1242
+ process.exit(0);
1243
+ }
1244
+
1245
+ const configIndex = args.indexOf('--config-json');
1246
+ if (configIndex === -1 || !args[configIndex + 1]) {
1247
+ console.error('Error: --config-json is required.');
1248
+ console.error('Run with --help for usage.');
1249
+ process.exit(1);
1250
+ }
1251
+
1252
+ const dryRun = args.includes('--dry-run');
1253
+ const configPath = path.resolve(args[configIndex + 1]);
1254
+
1255
+ const detectionModeIndex = args.indexOf('--detection-mode');
1256
+ const detectionMode = (detectionModeIndex !== -1 && args[detectionModeIndex + 1])
1257
+ ? args[detectionModeIndex + 1]
1258
+ : 'replace';
1259
+ if (!['replace', 'reuse', 'refactor'].includes(detectionMode)) {
1260
+ console.error('Error: --detection-mode must be one of: replace, reuse, refactor');
1261
+ process.exit(1);
1262
+ }
1263
+
1264
+ const config = loadJson(configPath, 'config JSON');
1265
+ const matrix = loadJson(MATRIX_PATH, 'platform-matrix.json');
1266
+
1267
+ const platform = config.platform;
1268
+ if (!platform || !matrix.platforms[platform]) {
1269
+ console.error('Error: config.platform "' + platform + '" is not a supported platform.');
1270
+ console.error('Supported: ' + Object.keys(matrix.platforms).join(', '));
1271
+ process.exit(1);
1272
+ }
1273
+
1274
+ const platformDef = matrix.platforms[platform];
1275
+ const projectPath = path.resolve(config.projectPath || process.cwd());
1276
+ const approach = config.approach || 1;
1277
+
1278
+ // ── Production React Native (approaches 4 & 5) ────────────────────────────────
1279
+ if (platform === 'react-native' && (approach === 4 || approach === 5 || approach === 6)) {
1280
+
1281
+ // ── Approach 6 (EC / ECDH+AES-256-GCM) ─────────────────────────────────────
1282
+ if (approach === 6) {
1283
+ const secDir = 'src/services/security';
1284
+ const isSessionInit = config.ecContractType === 'session-init';
1285
+ const isNativeLayer = config.encryptionLayer === 'native';
1286
+
1287
+ const ecTypes = [
1288
+ "// src/services/security/types.ts",
1289
+ "// Per-request cryptographic material (used by both RSA and EC modes)",
1290
+ "export type RequestCryptoMaterial = {",
1291
+ " aesKey: Uint8Array;",
1292
+ " requestIv: Uint8Array;",
1293
+ " keyId?: string;",
1294
+ "};",
1295
+ "",
1296
+ "export type EcPublicKeyResponse = {",
1297
+ " public_key: string; // base64(65-byte uncompressed P-256 point)",
1298
+ " key_id: string;",
1299
+ " expires_at?: string;",
1300
+ "};",
1301
+ "",
1302
+ "export type EcEncryptedEnvelope = {",
1303
+ " data: string;",
1304
+ " ephemeral_public_key: string;",
1305
+ " iv: string;",
1306
+ " nonce: string;",
1307
+ " key_id?: string;",
1308
+ "};",
1309
+ ].join('\n');
1310
+
1311
+ const endpoint = config.ecKeyEndpoint || '<API_BASE_URL>/api/security/ec-public-key';
1312
+ const retries = config.ecKeyRetries || 3;
1313
+ const ecKeyService = [
1314
+ "import axios from 'axios';",
1315
+ "import type { EcPublicKeyResponse } from './types';",
1316
+ "",
1317
+ "const DEFAULT_EC_KEY_PATH = '/api/security/ec-public-key';",
1318
+ "const MAX_RETRIES = " + retries + ";",
1319
+ "",
1320
+ "export class EcKeyService {",
1321
+ " private cachedKey: EcPublicKeyResponse | null = null;",
1322
+ " constructor(private readonly apiBaseUrl: string) {}",
1323
+ " getCached(): EcPublicKeyResponse | null { return this.cachedKey; }",
1324
+ " clearCache(): void { this.cachedKey = null; }",
1325
+ " async getPublicKey(accessToken?: string): Promise<EcPublicKeyResponse> {",
1326
+ " if (this.cachedKey) return this.cachedKey;",
1327
+ " return this.refreshPublicKey(accessToken);",
1328
+ " }",
1329
+ " async refreshPublicKey(accessToken?: string): Promise<EcPublicKeyResponse> {",
1330
+ " const url = '" + endpoint + "'.startsWith('http') ? '" + endpoint + "' : this.apiBaseUrl.replace(/\\/$/, '') + DEFAULT_EC_KEY_PATH;",
1331
+ " let lastError: unknown;",
1332
+ " for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {",
1333
+ " try {",
1334
+ " const response = await axios.get<EcPublicKeyResponse>(url, {",
1335
+ " timeout: 10000,",
1336
+ " headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,",
1337
+ " });",
1338
+ " if (!response.data?.public_key) throw new Error('EC public key response missing public_key');",
1339
+ " const decoded = Buffer.from(response.data.public_key, 'base64');",
1340
+ " if (decoded.length !== 65 || decoded[0] !== 0x04) {",
1341
+ " throw new Error('Invalid EC public key: must be 65-byte uncompressed P-256 point');",
1342
+ " }",
1343
+ " this.cachedKey = response.data;",
1344
+ " return response.data;",
1345
+ " } catch (error) { lastError = error; }",
1346
+ " }",
1347
+ " throw lastError instanceof Error ? lastError : new Error('Failed to fetch EC public key');",
1348
+ " }",
1349
+ "}",
1350
+ ].join('\n');
1351
+
1352
+ const ecCryptoUtil = isSessionInit ? [
1353
+ "import {",
1354
+ " createCipheriv, createDecipheriv, createECDH, createHmac,",
1355
+ " randomBytes, Buffer as QCBuffer,",
1356
+ "} from 'react-native-quick-crypto';",
1357
+ "import { Buffer } from 'buffer';",
1358
+ "import type { EcEncryptedEnvelope, RequestCryptoMaterial } from './types';",
1359
+ "",
1360
+ "declare function btoa(input: string): string;",
1361
+ "",
1362
+ "export function toBase64(buf: Uint8Array): string {",
1363
+ " const bytes = new Uint8Array(buf);",
1364
+ " let binary = '';",
1365
+ " for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);",
1366
+ " return btoa(binary);",
1367
+ "}",
1368
+ "",
1369
+ "function toBuffer(input: unknown): Buffer {",
1370
+ " if (Buffer.isBuffer(input)) return input;",
1371
+ " return Buffer.from(input as Uint8Array);",
1372
+ "}",
1373
+ "",
1374
+ "function byteCopy(buf: Buffer | Uint8Array, start?: number, end?: number): Uint8Array {",
1375
+ " const view = start !== undefined ? buf.slice(start, end) : buf;",
1376
+ " return new Uint8Array(view);",
1377
+ "}",
1378
+ "",
1379
+ "function urlSafeBase64ToStd(input: string): string {",
1380
+ " const normalized = input.replace(/-/g, '+').replace(/_/g, '/');",
1381
+ " return normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');",
1382
+ "}",
1383
+ "",
1384
+ "function hkdfSha256(ikm: Buffer, salt: Buffer, info: Buffer): Buffer {",
1385
+ " const prk = toBuffer(createHmac('sha256', byteCopy(salt)).update(byteCopy(ikm)).digest());",
1386
+ " const t1 = toBuffer(createHmac('sha256', byteCopy(prk)).update(byteCopy(Buffer.concat([info, Buffer.from([0x01])]))).digest());",
1387
+ " return t1.slice(0, 32);",
1388
+ "}",
1389
+ "",
1390
+ "export function buildSessionAad(keyId: string, deviceId: string, apiVersion = 'v1'): Buffer {",
1391
+ " return Buffer.from(`{\"api_ver\":\"${apiVersion}\",\"device_id\":\"${deviceId}\",\"key_id\":\"${keyId}\"}`, 'utf8');",
1392
+ "}",
1393
+ "",
1394
+ "export function deriveSessionKey(",
1395
+ " clientEcdh: ReturnType<typeof createECDH>,",
1396
+ " serverEcdhPubBase64: string,",
1397
+ " saltBase64: string,",
1398
+ " keyId: string,",
1399
+ " deviceId: string,",
1400
+ " apiVersion = 'v1',",
1401
+ "): Buffer {",
1402
+ " const serverPubBytes = Buffer.from(urlSafeBase64ToStd(serverEcdhPubBase64), 'base64');",
1403
+ " const sharedSecret = toBuffer(clientEcdh.computeSecret(serverPubBytes as any));",
1404
+ " const saltBytes = Buffer.from(urlSafeBase64ToStd(saltBase64), 'base64');",
1405
+ " // IMPORTANT: confirm the exact info string with your backend team",
1406
+ " const contextInfo = `" + (config.ecHkdfInfo || 'REPLACE_WITH_BACKEND_HKDF_INFO_STRING') + "`;",
1407
+ " return hkdfSha256(sharedSecret, saltBytes, Buffer.from(contextInfo, 'utf8'));",
1408
+ "}",
1409
+ "",
1410
+ "export function sessionEncrypt(payload: unknown, aesKey: Buffer, nonce: string, aad?: Buffer): string {",
1411
+ " const iv = Buffer.from(nonce, 'utf8');",
1412
+ " const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');",
1413
+ " const cipher = createCipheriv('aes-256-gcm', byteCopy(aesKey), byteCopy(iv));",
1414
+ " if (aad && aad.length > 0) cipher.setAAD(QCBuffer.from(byteCopy(aad)));",
1415
+ " const encrypted = toBuffer(cipher.update(byteCopy(plaintext)));",
1416
+ " const finalBlock = toBuffer(cipher.final());",
1417
+ " const authTag = toBuffer(cipher.getAuthTag());",
1418
+ " return toBase64(Buffer.concat([encrypted, finalBlock, authTag]));",
1419
+ "}",
1420
+ "",
1421
+ "export function sessionDecrypt(encryptedBase64: string, aesKey: Buffer, nonce: string, aad?: Buffer): string {",
1422
+ " const decoded = Buffer.from(encryptedBase64, 'base64');",
1423
+ " if (decoded.length < 16) throw new Error(`Response too short: ${decoded.length} bytes`);",
1424
+ " const authTag = byteCopy(decoded, decoded.length - 16);",
1425
+ " const ciphertext = byteCopy(decoded, 0, decoded.length - 16);",
1426
+ " const iv = Buffer.from(nonce, 'utf8');",
1427
+ " const decipher = createDecipheriv('aes-256-gcm', byteCopy(aesKey), byteCopy(iv));",
1428
+ " if (aad && aad.length > 0) decipher.setAAD(QCBuffer.from(byteCopy(aad)));",
1429
+ " decipher.setAuthTag(QCBuffer.from(authTag));",
1430
+ " const decrypted = toBuffer(decipher.update(ciphertext));",
1431
+ " const finalBlock = toBuffer(decipher.final());",
1432
+ " return Buffer.concat([decrypted, finalBlock]).toString('utf8');",
1433
+ "}",
1434
+ ].join('\n') : [
1435
+ "import {",
1436
+ " createCipheriv, createECDH, createHmac, randomBytes, Buffer as QCBuffer,",
1437
+ "} from 'react-native-quick-crypto';",
1438
+ "import { Buffer } from 'buffer';",
1439
+ "import type { EcEncryptedEnvelope, RequestCryptoMaterial } from './types';",
1440
+ "",
1441
+ "declare function btoa(input: string): string;",
1442
+ "",
1443
+ "export function toBase64(buf: Uint8Array): string {",
1444
+ " const bytes = new Uint8Array(buf);",
1445
+ " let binary = '';",
1446
+ " for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);",
1447
+ " return btoa(binary);",
1448
+ "}",
1449
+ "",
1450
+ "function toBuffer(input: unknown): Buffer {",
1451
+ " if (Buffer.isBuffer(input)) return input;",
1452
+ " return Buffer.from(input as Uint8Array);",
1453
+ "}",
1454
+ "",
1455
+ "function byteCopy(buf: Buffer | Uint8Array, start?: number, end?: number): Uint8Array {",
1456
+ " const view = start !== undefined ? buf.slice(start, end) : buf;",
1457
+ " return new Uint8Array(view);",
1458
+ "}",
1459
+ "",
1460
+ "function hkdfSha256(ikm: Buffer, salt: Buffer, info: Buffer): Buffer {",
1461
+ " const prk = toBuffer(createHmac('sha256', byteCopy(salt)).update(byteCopy(ikm)).digest());",
1462
+ " const t1 = toBuffer(createHmac('sha256', byteCopy(prk)).update(byteCopy(Buffer.concat([info, Buffer.from([0x01])]))).digest());",
1463
+ " return t1.slice(0, 32);",
1464
+ "}",
1465
+ "",
1466
+ "export async function ecEncryptPayload(",
1467
+ " payload: unknown,",
1468
+ " serverEcPublicKeyBase64: string,",
1469
+ " nonce: string,",
1470
+ " keyId?: string,",
1471
+ "): Promise<{ envelope: EcEncryptedEnvelope; material: RequestCryptoMaterial }> {",
1472
+ " const ecdh = createECDH('prime256v1');",
1473
+ " ecdh.generateKeys();",
1474
+ " const ephemeralPublicKeyBytes = toBuffer(ecdh.getPublicKey());",
1475
+ " const serverPublicKeyBytes = Buffer.from(serverEcPublicKeyBase64, 'base64');",
1476
+ " const sharedSecret = toBuffer(ecdh.computeSecret(serverPublicKeyBytes as any));",
1477
+ " const aesKey = hkdfSha256(sharedSecret, Buffer.from(nonce, 'utf8'), Buffer.from('enc', 'utf8'));",
1478
+ " const iv = toBuffer(randomBytes(12));",
1479
+ " const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');",
1480
+ " const cipher = createCipheriv('aes-256-gcm', byteCopy(aesKey), byteCopy(iv));",
1481
+ " const encrypted = toBuffer(cipher.update(byteCopy(plaintext)));",
1482
+ " const finalBlock = toBuffer(cipher.final());",
1483
+ " const authTag = toBuffer(cipher.getAuthTag());",
1484
+ " const data = Buffer.concat([encrypted, finalBlock, authTag]);",
1485
+ " return {",
1486
+ " envelope: {",
1487
+ " data: toBase64(data),",
1488
+ " ephemeral_public_key: toBase64(ephemeralPublicKeyBytes),",
1489
+ " iv: toBase64(iv),",
1490
+ " nonce,",
1491
+ " key_id: keyId,",
1492
+ " },",
1493
+ " material: { aesKey, requestIv: iv, keyId },",
1494
+ " };",
1495
+ "}",
1496
+ "",
1497
+ "export async function ecDeriveKey(",
1498
+ " serverEcPublicKeyBase64: string,",
1499
+ " nonce: string,",
1500
+ "): Promise<{ derivedAesKeyBase64: string; ephemeralPublicKeyBase64: string }> {",
1501
+ " const ecdh = createECDH('prime256v1');",
1502
+ " ecdh.generateKeys();",
1503
+ " const ephemeralPublicKeyBytes = toBuffer(ecdh.getPublicKey());",
1504
+ " const serverPublicKeyBytes = Buffer.from(serverEcPublicKeyBase64, 'base64');",
1505
+ " const sharedSecret = toBuffer(ecdh.computeSecret(serverPublicKeyBytes as any));",
1506
+ " const aesKey = hkdfSha256(sharedSecret, Buffer.from(nonce, 'utf8'), Buffer.from('enc', 'utf8'));",
1507
+ " return {",
1508
+ " derivedAesKeyBase64: toBase64(aesKey),",
1509
+ " ephemeralPublicKeyBase64: toBase64(ephemeralPublicKeyBytes),",
1510
+ " };",
1511
+ "}",
1512
+ ].join('\n');
1513
+
1514
+ const ecConfig = [
1515
+ "export const ENCRYPTION_MODEL = 'ec';",
1516
+ "export const ENCRYPTION_LAYER = process.env.ENCRYPTION_LAYER ?? '" + (isNativeLayer ? 'native' : 'rn') + "';",
1517
+ "export const ENCRYPTION_ENABLED = process.env.ENCRYPTION_ENABLED === 'true';",
1518
+ "export const EC_KEY_ENDPOINT = process.env.EC_PUBLIC_KEY_ENDPOINT ?? '" + endpoint + "';",
1519
+ "export const HMAC_ENABLED = process.env.HMAC_ENABLED === 'true';",
1520
+ "export const HMAC_SECRET = process.env.ENCRYPTION_HMAC_SECRET ?? '';",
1521
+ "export const EXCLUDED_PATHS: string[] = [",
1522
+ ...(config.excludeUrls || []).map(u => " '" + u + "',"),
1523
+ "];",
1524
+ ].join('\n');
1525
+
1526
+ const ecFiles = {
1527
+ [secDir + '/types.ts']: ecTypes,
1528
+ [secDir + '/ec-key.service.ts']: ecKeyService,
1529
+ [secDir + '/ec-crypto.util.ts']: ecCryptoUtil,
1530
+ [secDir + '/keychain.service.ts']: generateRnProductionKeychainService(),
1531
+ [secDir + '/hmac.service.ts']: generateRnProductionHmacService(),
1532
+ [secDir + '/encryption-config.ts']: ecConfig,
1533
+ };
1534
+
1535
+ for (const [relPath, content] of Object.entries(ecFiles)) {
1536
+ const absPath = path.join(projectPath, relPath);
1537
+ if (detectionMode === 'reuse' && fs.existsSync(absPath)) {
1538
+ console.log('SKIP (reuse mode, file exists) ' + relPath);
1539
+ continue;
1540
+ }
1541
+ const targetRelPath = detectionMode === 'refactor' ? scaffoldPath(relPath) : relPath;
1542
+ writeFile(path.join(projectPath, targetRelPath), content, dryRun);
1543
+ }
1544
+
1545
+ const axiosWiringPathEc = path.join(projectPath, 'src/api/axiosInstance.ts');
1546
+ if (!fs.existsSync(axiosWiringPathEc)) {
1547
+ writeFile(axiosWiringPathEc, generateAxiosInstance(), dryRun);
1548
+ }
1549
+
1550
+ const entryPolyfillFileEc = platformDef.entryPolyfillFile;
1551
+ if (entryPolyfillFileEc) {
1552
+ patchIndexPolyfill(path.join(projectPath, entryPolyfillFileEc), dryRun);
1553
+ }
1554
+
1555
+ console.log('INSTALL_RESULT : SUCCESS');
1556
+ process.exit(0);
1557
+ }
1558
+
1559
+ const secDir = 'src/services/security';
1560
+ const prodFiles = {
1561
+ [secDir + '/types.ts']: generateRnProductionTypes(),
1562
+ [secDir + '/encryption.service.ts']: generateRnProductionEncryptionService(),
1563
+ [secDir + '/decryption.service.ts']: generateRnProductionDecryptionService(),
1564
+ [secDir + '/rsa-key.service.ts']: generateRnProductionRsaKeyService(config),
1565
+ [secDir + '/keychain.service.ts']: generateRnProductionKeychainService(),
1566
+ [secDir + '/hmac.service.ts']: generateRnProductionHmacService(),
1567
+ [secDir + '/encryption-config.ts']: generateRnProductionEncryptionConfig(config),
1568
+ [secDir + '/encryption.interceptor.ts']: generateRnProductionInterceptor(approach, config),
1569
+ };
1570
+
1571
+ if (approach === 5) {
1572
+ const androidPkg = (config.androidPackage || 'com.example.security').replace(/\./g, '/');
1573
+ const androidDir = 'android/app/src/main/java/' + androidPkg;
1574
+ Object.assign(prodFiles, {
1575
+ [androidDir + '/NativeCryptoModule.kt']: generateNativeAndroidModule(config),
1576
+ [androidDir + '/NativeCryptoPackage.kt']: generateNativeAndroidPackage(config),
1577
+ 'ios/NativeCryptoModule.swift': generateNativeIosModule(),
1578
+ 'ios/NativeCryptoModule.m': generateNativeIosBridge(),
1579
+ [secDir + '/native-crypto.module.ts']: generateNativeJsBridge(),
1580
+ });
1581
+ }
1582
+
1583
+ for (const [relPath, content] of Object.entries(prodFiles)) {
1584
+ const absPath = path.join(projectPath, relPath);
1585
+ if (detectionMode === 'reuse' && fs.existsSync(absPath)) {
1586
+ console.log('SKIP (reuse mode, file exists) ' + relPath);
1587
+ continue;
1588
+ }
1589
+ const targetRelPath = detectionMode === 'refactor' ? scaffoldPath(relPath) : relPath;
1590
+ writeFile(path.join(projectPath, targetRelPath), content, dryRun);
1591
+ }
1592
+
1593
+ // Module wiring for production approaches
1594
+ const axiosWiringPath = path.join(projectPath, 'src/api/axiosInstance.ts');
1595
+ const axiosContent = [
1596
+ "import axios from 'axios';",
1597
+ "import { applyProductionEncryptionInterceptor } from '../services/security/encryption.interceptor';",
1598
+ '',
1599
+ 'const axiosInstance = axios.create({});',
1600
+ 'applyProductionEncryptionInterceptor(axiosInstance);',
1601
+ '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER',
1602
+ '',
1603
+ 'export default axiosInstance;',
1604
+ ].join('\n');
1605
+
1606
+ if (!fs.existsSync(axiosWiringPath)) {
1607
+ writeFile(axiosWiringPath, axiosContent, dryRun);
1608
+ } else {
1609
+ patchFile(axiosWiringPath, '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER',
1610
+ '// Production encryption interceptor registered by encryption-decryption-utility skill', dryRun);
1611
+ }
1612
+
1613
+ // Polyfill for quick-crypto
1614
+ const entryPolyfillFile = platformDef.entryPolyfillFile;
1615
+ if (entryPolyfillFile) {
1616
+ patchIndexPolyfill(path.join(projectPath, entryPolyfillFile), dryRun);
1617
+ }
1618
+
1619
+ if (detectionMode === 'refactor') {
1620
+ console.log('');
1621
+ console.log('REFACTOR INSTRUCTIONS:');
1622
+ console.log(' Scaffold files written alongside your existing files (*.scaffold.* suffix).');
1623
+ console.log(' Review each scaffold file and manually migrate logic into src/services/security/.');
1624
+ console.log(' The wiring file was NOT modified. Register your centralized module manually.');
1625
+ }
1626
+
1627
+ console.log('INSTALL_RESULT : SUCCESS');
1628
+ process.exit(0);
1629
+ }
1630
+
1631
+ // ── Approaches 1-3 (and all other platforms) ──────────────────────────────────
1632
+ const filePaths = platformDef.filePaths;
1633
+
1634
+ const files = {
1635
+ [filePaths.config]: generateConfig(platform, config),
1636
+ [filePaths.service]: generateService(platform, config),
1637
+ [filePaths.interceptor]: generateInterceptor(platform, config),
1638
+ };
1639
+
1640
+ for (const [relPath, content] of Object.entries(files)) {
1641
+ const absPath = path.join(projectPath, relPath);
1642
+ if (detectionMode === 'reuse' && fs.existsSync(absPath)) {
1643
+ console.log('SKIP (reuse mode, file exists) ' + relPath);
1644
+ continue;
1645
+ }
1646
+ const targetRelPath = detectionMode === 'refactor' ? scaffoldPath(relPath) : relPath;
1647
+ writeFile(path.join(projectPath, targetRelPath), content, dryRun);
1648
+ }
1649
+
1650
+ const moduleWiringPath = path.join(projectPath, filePaths.moduleWiring);
1651
+
1652
+ // Wiring patch is skipped in refactor mode — the scaffold files must be wired manually.
1653
+ if (detectionMode !== 'refactor') {
1654
+ // Angular 17+ standalone: app.module.ts does not exist — patch app.config.ts instead
1655
+ if (platform === 'angular' && !fs.existsSync(moduleWiringPath)) {
1656
+ const standaloneWiringFile = platformDef.standaloneWiringFile;
1657
+ if (standaloneWiringFile) {
1658
+ const standaloneWiringPath = path.join(projectPath, standaloneWiringFile);
1659
+ const standaloneInsert = [
1660
+ "import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';",
1661
+ "import { HTTP_INTERCEPTORS } from '@angular/common/http';",
1662
+ "import { EncryptionInterceptor } from './core/encryption/encryption.interceptor';",
1663
+ '// Encryption interceptor registered by encryption-decryption-utility skill',
1664
+ ].join('\n');
1665
+ patchFile(standaloneWiringPath, '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER', standaloneInsert, dryRun);
1666
+ console.log('NOTE Angular standalone project detected. Patched app.config.ts instead of app.module.ts.');
1667
+ console.log('NOTE Add provideHttpClient(withInterceptorsFromDi()) and the HTTP_INTERCEPTORS provider to your appConfig.providers array manually if not already present.');
1668
+ } else {
1669
+ console.log('SKIP PATCH (standaloneWiringFile not defined for platform): ' + platform);
1670
+ }
1671
+ } else if (platform === 'react' || platform === 'react-native') {
1672
+ // React / React Native: create axiosInstance.ts if missing, otherwise patch it
1673
+ if (!fs.existsSync(moduleWiringPath)) {
1674
+ writeFile(moduleWiringPath, generateAxiosInstance(), dryRun);
1675
+ } else {
1676
+ patchFile(
1677
+ moduleWiringPath,
1678
+ '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER',
1679
+ '// Encryption interceptor registered by encryption-decryption-utility skill',
1680
+ dryRun
1681
+ );
1682
+ }
1683
+ // React Native only: prepend react-native-get-random-values polyfill to index.js
1684
+ if (platform === 'react-native') {
1685
+ const entryPolyfillFile = platformDef.entryPolyfillFile;
1686
+ if (entryPolyfillFile) {
1687
+ patchIndexPolyfill(path.join(projectPath, entryPolyfillFile), dryRun);
1688
+ }
1689
+ }
1690
+ } else {
1691
+ patchFile(
1692
+ moduleWiringPath,
1693
+ '// ENCRYPTION_INTERCEPTOR_PLACEHOLDER',
1694
+ '// Encryption interceptor registered by encryption-decryption-utility skill',
1695
+ dryRun
1696
+ );
1697
+ }
1698
+ }
1699
+
1700
+ if (detectionMode === 'refactor') {
1701
+ console.log('');
1702
+ console.log('REFACTOR INSTRUCTIONS:');
1703
+ console.log(' Scaffold files written alongside your existing files (*.scaffold.* suffix).');
1704
+ console.log(' Review each scaffold file and manually migrate logic into your centralized encryption module.');
1705
+ console.log(' Files written:');
1706
+ for (const relPath of Object.keys(files)) {
1707
+ console.log(' ' + scaffoldPath(relPath));
1708
+ }
1709
+ console.log(' The wiring file was NOT modified. Register your centralized module manually.');
1710
+ }
1711
+
1712
+ console.log('INSTALL_RESULT : SUCCESS');
1713
+ process.exit(0);