@myazahq/kyc-sdk-react-native 2.0.1 → 2.1.0

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 (283) hide show
  1. package/README.md +111 -8
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/AndroidManifest.xml +14 -4
  4. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
  8. package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
  9. package/app.plugin.js +102 -13
  10. package/ios/HybridMyazaEmrtd.swift +447 -0
  11. package/ios/HybridMyazaFaceDetector.swift +104 -2
  12. package/ios/HybridMyazaRectDetector.swift +103 -0
  13. package/ios/HybridMyazaTextRecognizer.swift +111 -0
  14. package/nitrogen/generated/.gitattributes +1 -0
  15. package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
  16. package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
  17. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
  18. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
  19. package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
  20. package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
  21. package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
  22. package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
  23. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
  24. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
  25. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
  26. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
  27. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
  28. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
  30. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
  31. package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
  42. package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
  44. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
  45. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
  46. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
  47. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
  48. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
  49. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
  50. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
  52. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
  54. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
  56. package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
  57. package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
  58. package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
  59. package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  61. package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
  62. package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
  63. package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
  64. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  66. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
  67. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
  68. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
  70. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
  71. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
  72. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
  74. package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
  75. package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
  76. package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
  77. package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
  78. package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
  79. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
  80. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
  81. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
  82. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
  83. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
  84. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
  85. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
  86. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
  87. package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
  88. package/package.json +16 -4
  89. package/src/MyazaKYC.tsx +115 -22
  90. package/src/capture/documentIdentity.ts +153 -0
  91. package/src/capture/documentSignals.ts +177 -0
  92. package/src/capture/documentTextGate.ts +171 -0
  93. package/src/capture/hints.ts +57 -0
  94. package/src/capture/index.ts +24 -0
  95. package/src/capture/rectDetector.ts +84 -0
  96. package/src/capture/rectGate.ts +91 -0
  97. package/src/capture/useAutoCapture.ts +198 -0
  98. package/src/components/CameraPermissionView.tsx +39 -3
  99. package/src/components/CameraViewfinder.tsx +226 -70
  100. package/src/components/ContactCodeEntry.tsx +139 -0
  101. package/src/components/CountryField.tsx +87 -0
  102. package/src/components/CountryFlag.tsx +15 -15
  103. package/src/components/DialCodePicker.tsx +186 -0
  104. package/src/components/DocumentCropper.tsx +1 -1
  105. package/src/components/DocumentReview.tsx +142 -0
  106. package/src/components/DocumentReviewSide.tsx +135 -0
  107. package/src/components/DocumentReviewZoom.tsx +119 -0
  108. package/src/components/ExpiryCountdown.tsx +52 -0
  109. package/src/components/FlashOverlay.tsx +156 -0
  110. package/src/components/Icon.tsx +67 -1
  111. package/src/components/KycFlow.tsx +66 -94
  112. package/src/components/KycSheet.tsx +148 -18
  113. package/src/components/MediaSourceSheet.tsx +163 -0
  114. package/src/components/MyazaButton.tsx +14 -2
  115. package/src/components/MyazaDateField.tsx +240 -0
  116. package/src/components/MyazaInput.tsx +73 -26
  117. package/src/components/MyazaSelect.tsx +283 -0
  118. package/src/components/MyazaWordmark.tsx +33 -0
  119. package/src/components/OptionRow.tsx +132 -0
  120. package/src/components/PhoneNumberInput.tsx +149 -0
  121. package/src/components/PoweredBy.tsx +89 -0
  122. package/src/components/ReadyPrimer.tsx +149 -0
  123. package/src/components/StepView.tsx +91 -0
  124. package/src/components/Typography.tsx +18 -3
  125. package/src/components/VerifiedNotice.tsx +38 -0
  126. package/src/components/WhatsAppIcon.tsx +29 -0
  127. package/src/components/WorkflowGate.tsx +128 -0
  128. package/src/components/brand-font.ts +113 -0
  129. package/src/components/flashHoleGeometry.ts +35 -0
  130. package/src/components/flow/FatalConfigError.tsx +56 -0
  131. package/src/components/fonts.ts +2 -19
  132. package/src/components/readyPrimerContent.ts +52 -0
  133. package/src/components/runtime.tsx +38 -43
  134. package/src/components/stepHeaderMeta.tsx +87 -0
  135. package/src/components/theme-provider.tsx +89 -0
  136. package/src/components/useWorkflowMount.ts +116 -0
  137. package/src/components/viewfinder/CardGuide.tsx +60 -0
  138. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  139. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  140. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  141. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  142. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  143. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  144. package/src/config/brand.ts +97 -0
  145. package/src/config/business.ts +179 -0
  146. package/src/config/businessSteps.ts +105 -0
  147. package/src/config/contact.ts +159 -0
  148. package/src/config/countryNames.g.ts +254 -0
  149. package/src/config/currencyFlags.ts +144 -0
  150. package/src/config/font-resolve.ts +78 -0
  151. package/src/config/idTypes.ts +98 -1
  152. package/src/config/keyPeople.ts +152 -0
  153. package/src/config/phone.ts +39 -0
  154. package/src/config/proofOfAddress.ts +75 -0
  155. package/src/config/questionnaire.ts +153 -0
  156. package/src/config/regions.ts +118 -0
  157. package/src/config/registrationHint.ts +87 -0
  158. package/src/config/stepOrder.ts +118 -0
  159. package/src/config/theme.ts +38 -2
  160. package/src/config/workflowMerge.ts +124 -0
  161. package/src/emrtd/bac.ts +128 -0
  162. package/src/emrtd/bytes.ts +127 -0
  163. package/src/emrtd/crypto.ts +163 -0
  164. package/src/emrtd/der.ts +192 -0
  165. package/src/emrtd/dg2.ts +111 -0
  166. package/src/emrtd/files.ts +215 -0
  167. package/src/emrtd/index.ts +30 -0
  168. package/src/emrtd/mrzKey.ts +54 -0
  169. package/src/emrtd/native.ts +79 -0
  170. package/src/emrtd/optionalRead.ts +42 -0
  171. package/src/emrtd/read.ts +224 -0
  172. package/src/emrtd/retry.ts +98 -0
  173. package/src/emrtd/secureMessaging.ts +178 -0
  174. package/src/emrtd/session.ts +200 -0
  175. package/src/emrtd/stages.ts +122 -0
  176. package/src/index.ts +110 -0
  177. package/src/lib/calendar.ts +71 -0
  178. package/src/lib/use-keyboard-inset.ts +34 -0
  179. package/src/liveness/challengeManager.ts +27 -1
  180. package/src/liveness/faceContinuity.ts +153 -0
  181. package/src/liveness/flashDetector.ts +145 -0
  182. package/src/liveness/flashReadyGate.ts +106 -0
  183. package/src/liveness/flashRunner.ts +118 -0
  184. package/src/liveness/integritySignals.ts +60 -0
  185. package/src/liveness/livenessState.ts +139 -0
  186. package/src/liveness/speech.ts +19 -1
  187. package/src/liveness/types.ts +48 -1
  188. package/src/liveness/useFlashPhase.ts +78 -0
  189. package/src/liveness/useLiveness.ts +260 -75
  190. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  191. package/src/mrz/extract.ts +101 -0
  192. package/src/mrz/parse.ts +178 -0
  193. package/src/mrz/textRecognizer.ts +149 -0
  194. package/src/screens/ApplicantRoleStep.tsx +331 -0
  195. package/src/screens/BusinessDetailsStep.tsx +212 -0
  196. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  197. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  198. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  199. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  200. package/src/screens/CompanyInfoFields.tsx +83 -0
  201. package/src/screens/ConsentStep.tsx +68 -68
  202. package/src/screens/ContactChannelChoice.tsx +133 -0
  203. package/src/screens/ContactCodeStep.tsx +130 -0
  204. package/src/screens/ContactDestinationField.tsx +70 -0
  205. package/src/screens/ContactFooterNote.tsx +34 -0
  206. package/src/screens/ContactVerificationStep.tsx +199 -0
  207. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  208. package/src/screens/CountrySelectStep.tsx +210 -0
  209. package/src/screens/DocumentCaptureStep.tsx +190 -145
  210. package/src/screens/IdInputStep.tsx +19 -9
  211. package/src/screens/IdTypeStep.tsx +74 -33
  212. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  213. package/src/screens/LivenessStep.tsx +181 -267
  214. package/src/screens/MrzScanView.tsx +173 -0
  215. package/src/screens/NfcStep.tsx +290 -0
  216. package/src/screens/ProofOfAddressStep.tsx +238 -0
  217. package/src/screens/QuestionnaireField.tsx +219 -0
  218. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  219. package/src/screens/QuestionnaireStep.tsx +76 -0
  220. package/src/screens/SubmittedStep.tsx +5 -0
  221. package/src/screens/consent/model.ts +109 -0
  222. package/src/screens/document/CameraPhase.tsx +150 -0
  223. package/src/screens/document/RequiredPill.tsx +60 -0
  224. package/src/screens/liveness/LightingBanner.tsx +50 -0
  225. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  226. package/src/screens/liveness/ProgressDots.tsx +67 -0
  227. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  228. package/src/screens/liveness/constants.ts +31 -0
  229. package/src/screens/liveness/index.ts +8 -0
  230. package/src/screens/liveness/resolveGuidance.ts +27 -0
  231. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  232. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  233. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  234. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  235. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  236. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  237. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  238. package/src/screens/nfc/availability.ts +73 -0
  239. package/src/screens/nfc/readErrorMessage.ts +93 -0
  240. package/src/screens/nfc/useChipPortrait.ts +58 -0
  241. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  242. package/src/screens/useFlashHole.ts +105 -0
  243. package/src/screens/useFlashSequence.ts +96 -0
  244. package/src/screens/usePoaAttach.ts +121 -0
  245. package/src/services/api-types.ts +164 -0
  246. package/src/services/api-verify-types.ts +99 -0
  247. package/src/services/api.ts +72 -102
  248. package/src/services/cardCrop.ts +25 -10
  249. package/src/services/contactErrors.ts +43 -0
  250. package/src/services/deviceMetadata.ts +1 -1
  251. package/src/services/documentPicker.ts +20 -0
  252. package/src/services/fingerprint-sources.ts +129 -0
  253. package/src/services/fingerprint.ts +101 -0
  254. package/src/services/haptics.ts +38 -0
  255. package/src/services/mediaCompress.ts +7 -2
  256. package/src/services/resolveUrl.ts +3 -2
  257. package/src/services/validators.ts +3 -0
  258. package/src/services/workflowGate.ts +138 -0
  259. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  260. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  261. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  262. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  263. package/src/store/derive.ts +225 -0
  264. package/src/store/kycStore.ts +250 -149
  265. package/src/store/state.ts +253 -0
  266. package/src/store/submit.ts +101 -0
  267. package/src/store/submitApplicant.ts +81 -0
  268. package/src/types/appearance.ts +113 -0
  269. package/src/types/business.ts +131 -0
  270. package/src/types/config.ts +150 -119
  271. package/src/types/id-types.ts +57 -0
  272. package/src/types/verification.ts +5 -0
  273. package/src/types/workflow.ts +141 -0
  274. package/src/utils/amountFormat.ts +73 -0
  275. package/src/utils/tokens.ts +4 -2
  276. package/src/__tests__/cardCrop.test.ts +0 -39
  277. package/src/__tests__/deviceMetadata.test.ts +0 -34
  278. package/src/__tests__/errors.test.ts +0 -34
  279. package/src/__tests__/flow.test.ts +0 -61
  280. package/src/__tests__/gestureDetector.test.ts +0 -37
  281. package/src/__tests__/liveness.test.ts +0 -112
  282. package/src/__tests__/resolveUrl.test.ts +0 -64
  283. package/src/__tests__/validators.test.ts +0 -38
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # @myazahq/kyc-sdk-react-native
2
2
 
3
- Myaza KYC SDK for **React Native (Expo)** — ID verification, document capture, and
4
- active **on-device** liveness. Mirrors the [web](https://www.npmjs.com/package/@myazahq/kyc-sdk-react)
5
- and Flutter SDKs feature-for-feature and calls the same Myaza KYC API server.
3
+ Myaza KYC SDK for **React Native (Expo)** — ID verification with document
4
+ auto-capture, **eMRTD chip reading (NFC)**, active **on-device** liveness,
5
+ email/phone OTP verification, questionnaires, proof of address, and full
6
+ business (**KYB**) verification, for any supported country. Mirrors the
7
+ [web](https://www.npmjs.com/package/@myazahq/kyc-sdk-react) and Flutter SDKs
8
+ feature-for-feature — including `workflowId` embeds — and calls the same Myaza
9
+ KYC API server.
6
10
 
7
11
  The SDK is a **thin UI layer**: it captures the user's data (ID number, document
8
12
  photos, a live selfie), uploads the media, and submits a verification request.
@@ -157,7 +161,8 @@ export default function VerifyScreen() {
157
161
  title: "You're all set, {firstName}!",
158
162
  description: "We'll email you once your verification is reviewed.",
159
163
  }}
160
- metadata={{ userId: 'test_user_123' }}
164
+ userId='usr_123'
165
+ metadata={{ orderId: 'ord_456' }}
161
166
  onStart={() => console.log('KYC started')}
162
167
  onStepChange={(step) => console.log('Step:', step)}
163
168
  onSubmit={(submission) => {
@@ -180,14 +185,28 @@ export default function VerifyScreen() {
180
185
  | Prop | Type | Default | Description |
181
186
  | ----------------------- | ----------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
182
187
  | `apiKey` | `string` | — | **Required.** Sent as `Authorization: Bearer`. The **environment is derived from the key prefix** (`pk_test_…` → sandbox, `pk_live_…` → production); an unrecognized prefix throws. |
183
- | `country` | `'NG' \| 'GH' \| 'KE' \| 'ZA' \| 'CI'` | — | **Required.** Country whose ID types are offered. |
188
+ | `country` | `string` (ISO-2) | — | Country whose ID types are offered. **Required unless `workflowId` is set** (the workflow carries its own country). Any ISO-2 code works — `'NG' \| 'GH' \| 'KE' \| 'ZA' \| 'CI'` keep autocomplete and client-side ID-number validation; other countries render their ID types from the server. |
189
+ | `workflowId` | `string` (`wf_…`) | — | Run a **published Workflow** built in the dashboard. Workflow config wins over overlapping props. See [Workflows](#workflows). |
190
+ | `countries` | `WorkflowCountry[]` | — | Multi-region — more than one entry inserts a country-select step between consent and ID type. Usually from a workflow. |
184
191
  | `idTypes` | `IdType[]` | all allowed for org | Subset of ID types to offer; must be valid for `country`. |
192
+ | `userId` | `string` | — | **Your** reference for the person being verified. Not matched during verification — it correlates repeat checks of the same user onto one identity so results map back to your record. Prefer this over putting a user id in `metadata`. |
185
193
  | `userData` | `{ firstName?, lastName?, dateOfBirth? }` | — | Pre-fills the user's details. |
186
194
  | `enableSelfie` | `boolean` | `true` | Capture a selfie during liveness. |
187
195
  | `enableDocumentCapture` | `boolean` | `true` | Enable the document-scan step for document IDs. |
188
196
  | `allowDocumentUpload` | `boolean` | `true` | Allow picking a document photo from the gallery as an alternative to the camera. `false` hides every "upload instead" affordance (it's still offered on the camera-permission-denied screen as an escape hatch). |
189
197
  | `enableLiveness` | `boolean` | `true` | Run the liveness challenge step. The server can still disable it per ID type. |
198
+ | `livenessMode` | `'gestures' \| 'flash' \| 'both'` | `'gestures'` | How liveness is proven. See [Liveness](#liveness). |
199
+ | `flashSequenceLength` | `number` (2–5) | `4` | Colours in the flash sequence, for `'flash'` / `'both'`. |
190
200
  | `voiceGuidance` | `boolean \| { enabled?, language? }` | `true` | Spoken liveness instructions (accessibility, TTS **output** — no microphone). `false` mutes it; pass `{ language: 'fr-FR' }` to set the voice. See [Robustness & error handling](#robustness--error-handling). |
201
+ | `emailVerification` | `EmailVerificationConfig` | off | Email OTP step after consent. See [Optional steps](#optional-steps). |
202
+ | `phoneVerification` | `PhoneVerificationConfig` | off | Phone OTP step (SMS or WhatsApp). See [Optional steps](#optional-steps). |
203
+ | `proofOfAddress` | `ProofOfAddressConfig` | off | Proof-of-address upload after capture. See [Optional steps](#optional-steps). |
204
+ | `questionnaire` | `QuestionnaireConfig` | off | Compliance declarations before submission. See [Optional steps](#optional-steps). |
205
+ | `nfc` | `NfcConfig` | off | **eMRTD chip read** — native on this SDK, and the strongest assurance level available. See [Optional steps](#optional-steps). |
206
+ | `subjectType` | `'individual' \| 'business'` | `'individual'` | Business (KYB) flows require a published KYB workflow. See [Business (KYB) flows](#business-kyb-flows). |
207
+ | `business` | `WorkflowBusinessConfig` | — | KYB registry configuration. Normally supplied by a resolved workflow. |
208
+ | `deviceIntelligence` | `boolean` | `true` | Device + IP fraud signals (multi-accounting, emulator, velocity). **Billed per verification**; `false` disables the analysis, its charge, and the SDK's fingerprint collection. |
209
+ | `requireMobileDevice` | `boolean` | `false` | Refuse to run on a desktop/laptop — relevant because React Native also targets desktop runtimes and emulators. The server re-checks and rejects with `mobile_device_required`. |
191
210
  | `showThemeToggle` | `boolean` | `true` | Show a light/dark toggle inside the modal header. When `false`, the flow stays on `appearance.theme`. |
192
211
  | `disableClose` | `boolean` | `false` | Hide the close (X) and block **all** user dismissal (X, Android back, iOS swipe-down). The flow can then only be closed programmatically via `useMyazaKYC().close()`. |
193
212
  | `appearance` | `KYCAppearance` | brand defaults | Brand & theme the modal — colors, logo, light/dark. See [Appearance & theming](#appearance--theming). |
@@ -209,11 +228,95 @@ base URL) from the API key prefix, the single source of truth:
209
228
 
210
229
  | Prefix | Environment | Base URL |
211
230
  | ---------- | ----------- | --------------------------------- |
212
- | `pk_test_` | sandbox | `https://sandbox.identity.myaza.app` |
213
- | `pk_live_` | production | `https://identity.myaza.app` |
231
+ | `pk_test_` | sandbox | `https://trust.myaza.app` |
232
+ | `pk_live_` | production | `https://trust.myaza.app` |
214
233
 
215
234
  An unrecognized or malformed key throws at setup (it never silently defaults).
216
235
 
236
+ ## Workflows
237
+
238
+ Instead of configuring the flow in code, build it in the Myaza dashboard and
239
+ reference it by id:
240
+
241
+ ```tsx
242
+ <MyazaKYC apiKey="pk_live_xxx" workflowId="wf_abc123" userId="usr_123" />
243
+ ```
244
+
245
+ - **Workflow config wins** over any overlapping prop — country, ID types, step
246
+ toggles, appearance, copy. Set them in the builder, not in code.
247
+ - **Runtime data always comes from your code**: `userId`, `userData`,
248
+ `metadata`, and every callback.
249
+ - `country` becomes optional, because the workflow carries it.
250
+
251
+ This is the recommended way to drive the optional steps below: compliance teams
252
+ change the flow in the dashboard without shipping a new app build — which
253
+ matters far more on mobile than on web, where a redeploy is instant and an app
254
+ store review is not.
255
+
256
+ ## Optional steps
257
+
258
+ Steps that are off unless configured. Each is normally switched on in the
259
+ dashboard workflow builder (so it rides `workflowId`), but every one can also be
260
+ passed directly as a prop.
261
+
262
+ The flow runs them in this order:
263
+
264
+ ```
265
+ consent → email-verification → phone-verification → country-select → id-type
266
+ → id-input / document-capture → nfc → liveness → proof-of-address
267
+ → questionnaire → submitted
268
+ ```
269
+
270
+ | Prop | Shape | What it adds |
271
+ |---|---|---|
272
+ | `emailVerification` | `{ enabled?, required?, codeLength?, maxAttempts?, inputStyle? }` | Email OTP right after consent. `required: false` adds a "skip for now". `codeLength` 4–8 (default 6), `maxAttempts` 1–5 (default 3). |
273
+ | `phoneVerification` | same, plus `{ channels?, defaultCountry? }` | Phone OTP. `channels` defaults to `['sms']`; add `'whatsapp'` to offer it. |
274
+ | `proofOfAddress` | `{ enabled?, documentTypes?, otherLabel?, maxAgeDays? }` | Upload a utility bill, bank statement, tenancy agreement, or other document. `maxAgeDays` is the recency window (default 90). |
275
+ | `questionnaire` | `{ enabled?, title?, description?, fields }` | Compliance declarations before submission. Field `type` is one of `text`, `number`, `money`, `select`, `multiselect`, `boolean`, `date`. |
276
+ | `nfc` | `{ enabled?, idTypes?, allowSkip? }` | Reads the passport/ID **chip** (eMRTD). |
277
+
278
+ **The chip read is a real, native capability on this SDK** — unlike the web SDK,
279
+ which cannot do ISO-DEP from a browser. It gives the strongest assurance level
280
+ available. A device with no NFC radio skips the step automatically; `allowSkip`
281
+ adds a manual escape hatch for a chip that will not read, revealed after a failed
282
+ attempt rather than offered on arrival. Skipping never fails the verification —
283
+ the chip result is a soft sub-result.
284
+
285
+ Answers and outcomes arrive in the verification webhook
286
+ (`data.questionnaire`, `data.emailVerification`, `data.phoneVerification`,
287
+ `data.proofOfAddress`).
288
+
289
+ ```tsx
290
+ <MyazaKYC
291
+ apiKey="pk_live_xxx"
292
+ country="NG"
293
+ nfc={{ enabled: true, allowSkip: true }}
294
+ phoneVerification={{ enabled: true, channels: ["sms", "whatsapp"] }}
295
+ questionnaire={{
296
+ title: "A few final questions",
297
+ fields: [
298
+ { key: "source_of_funds", label: "Source of funds", type: "select", required: true,
299
+ options: [{ value: "salary", label: "Salary" }, { value: "business", label: "Business income" }] },
300
+ ],
301
+ }}
302
+ />
303
+ ```
304
+
305
+ ## Business (KYB) flows
306
+
307
+ When a resolved workflow's config carries `subjectType: 'business'`, the SDK runs
308
+ a company-verification flow instead of the individual one: a registry lookup
309
+ (country, product, registration number/name), and — when the workflow configures
310
+ them — a company profile, directors & owners, and supporting-document uploads.
311
+
312
+ KYB is **workflow-required**: there is no prop-only business flow, because the
313
+ server rejects a business submission that does not reference a published KYB
314
+ workflow.
315
+
316
+ If the workflow asks the submitter to verify their own identity, the ordinary
317
+ individual capture leg runs afterwards for them, and the success screen can hand
318
+ back invite links for any directors or owners who need their own check.
319
+
217
320
  ## Trigger component & hook
218
321
 
219
322
  `<MyazaKYC />` renders a styled trigger that opens the modal. Beyond the config
@@ -411,7 +514,7 @@ and a short liveness video is recorded and uploaded best-effort.
411
514
 
412
515
  ## Documentation
413
516
 
414
- Full documentation, configuration options, and webhook setup: **[identity.myaza.co/documentation/sdks](https://identity.myaza.co/documentation/sdks)**.
517
+ Full documentation, configuration options, and webhook setup: **[trust.myaza.co/documentation/sdks](https://trust.myaza.co/documentation/sdks)**.
415
518
 
416
519
  ## License
417
520
 
@@ -91,6 +91,10 @@ repositories {
91
91
 
92
92
  dependencies {
93
93
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
94
+
95
+ // JPEG 2000 for the DG2 chip portrait — BitmapFactory cannot decode it and
96
+ // never will; JJ2000 is the pure-Java reference implementation. ~500KB.
97
+ implementation 'edu.ucar:jj2000:5.2'
94
98
  implementation 'com.facebook.react:react-android'
95
99
 
96
100
  // Nitro core — provides the HybridObject base + the NitroModules prefab the
@@ -107,4 +111,6 @@ dependencies {
107
111
 
108
112
  // Google ML Kit face detection — Android only. Same version as the Flutter SDK.
109
113
  implementation 'com.google.mlkit:face-detection:16.1.6'
114
+ // On-device text recognition, for reading the MRZ off a passport or ID card.
115
+ implementation 'com.google.mlkit:text-recognition:16.0.1'
110
116
  }
@@ -1,12 +1,22 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <!--
3
- The ML Kit face-detection model is downloaded on demand by Play Services.
4
- This meta-data tells Play Services to fetch it at install time so the first
5
- liveness session doesn't pay the download cost. Mirrors the Flutter SDK.
3
+ NFC, for reading a passport's eMRTD chip. `required="false"` on the feature
4
+ is deliberate: the rest of the SDK works without NFC, and marking it
5
+ required would hide the host app from every device that has no NFC radio.
6
+ The chip step checks availability at runtime and skips instead.
6
7
  -->
8
+ <uses-permission android:name="android.permission.NFC" />
9
+ <uses-feature android:name="android.hardware.nfc" android:required="false" />
10
+
7
11
  <application>
12
+ <!--
13
+ ML Kit models are downloaded on demand by Play Services. Naming them here
14
+ makes Play Services fetch them at INSTALL time, so the first liveness or
15
+ MRZ scan does not pay the download cost — which on a slow connection is
16
+ the difference between a step that works and one the user abandons.
17
+ -->
8
18
  <meta-data
9
19
  android:name="com.google.mlkit.vision.DEPENDENCIES"
10
- android:value="face" />
20
+ android:value="face,ocr" />
11
21
  </application>
12
22
  </manifest>
@@ -0,0 +1,358 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import android.app.Activity
4
+ import android.nfc.NfcAdapter
5
+ import android.os.Bundle
6
+ import android.nfc.Tag
7
+ import android.nfc.tech.IsoDep
8
+ import android.graphics.Bitmap
9
+ import android.graphics.BitmapFactory
10
+ import android.util.Base64
11
+ import java.io.ByteArrayOutputStream
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.margelo.nitro.NitroModules
14
+ import com.margelo.nitro.core.Promise
15
+ import java.security.SecureRandom
16
+ import javax.crypto.Cipher
17
+ import javax.crypto.spec.IvParameterSpec
18
+ import javax.crypto.spec.SecretKeySpec
19
+ import java.security.MessageDigest
20
+ import java.util.concurrent.CompletableFuture
21
+ import java.util.concurrent.TimeUnit
22
+ import java.util.concurrent.TimeoutException
23
+
24
+ /** How long to wait for the user to present a document before giving up. */
25
+ private const val NFC_WAIT_SECONDS = 60L
26
+
27
+ /**
28
+ * How long Android waits between "is the tag still there?" probes.
29
+ *
30
+ * Generous on purpose: the probes interleave with ISO-DEP traffic, and the
31
+ * default (~125ms) is a known cause of chips dropping partway through a large
32
+ * DG2 read. The read itself has its own timeout, so a tag genuinely removed is
33
+ * still noticed.
34
+ */
35
+ private const val PRESENCE_CHECK_DELAY_MS = 5_000
36
+
37
+ /**
38
+ * Android half of eMRTD chip reading, implementing the nitrogen-generated
39
+ * `HybridMyazaEmrtdSpec` from src/specs/MyazaEmrtd.nitro.ts. The iOS half is
40
+ * HybridMyazaEmrtd.swift.
41
+ *
42
+ * Two responsibilities, deliberately no more:
43
+ *
44
+ * - TRANSPORT. `IsoDep` reader-mode, wrapped so TypeScript can send an APDU and
45
+ * get bytes back. Everything above — BAC, secure messaging, file reads — is
46
+ * TypeScript, tested against ICAO 9303's worked examples in CI.
47
+ * - PRIMITIVES. javax.crypto's DES, AES and SHA. A hand-written DES in
48
+ * JavaScript would be slower and a novel implementation on a path that
49
+ * authenticates a passport.
50
+ *
51
+ * This file decides NOTHING about authenticity. It moves bytes.
52
+ */
53
+ class HybridMyazaEmrtd : HybridMyazaEmrtdSpec() {
54
+
55
+ // @Volatile because every entry point runs on an ARBITRARY pool thread
56
+ // (Nitro's Promise.async dispatches to Dispatchers.Default) and the reader
57
+ // callback lands on a binder thread. Without it the `isoDep = dep` write in
58
+ // startSession has no happens-before edge to the read in transceive, and a
59
+ // fresh connection can be invisible to the very next call — the iOS side
60
+ // avoids this whole class by serializing on the main queue.
61
+ @Volatile private var isoDep: IsoDep? = null
62
+ @Volatile private var pending: CompletableFuture<Tag>? = null
63
+ @Volatile private var adapter: NfcAdapter? = null
64
+
65
+ private val context: ReactApplicationContext?
66
+ get() = NitroModules.applicationContext
67
+
68
+ // ── Transport ─────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Whether this device can read chips.
72
+ *
73
+ * DELIBERATELY not the "sticky true" the iOS side uses. There, a false is
74
+ * always the platform lying about fixed hardware, so the first true is kept
75
+ * forever. Here `isEnabled` reports a SETTING the user can turn off mid-flow,
76
+ * so a false is real, actionable information and must be allowed to retract a
77
+ * previous true — telling someone their chip can be read when they have just
78
+ * disabled NFC would be worse than useless.
79
+ *
80
+ * Do not "harmonise" the two platforms: they are answering different
81
+ * questions.
82
+ */
83
+ override fun isAvailable(): Boolean {
84
+ val ctx = context ?: return false
85
+ return NfcAdapter.getDefaultAdapter(ctx)?.isEnabled == true
86
+ }
87
+
88
+ /**
89
+ * Decode an image format JS cannot render, returning base64 JPEG.
90
+ *
91
+ * Passport chips overwhelmingly store DG2 as JPEG 2000, which React Native
92
+ * cannot display. BitmapFactory handles the formats the platform knows —
93
+ * which does NOT include JPEG 2000 (an earlier comment here claimed it did;
94
+ * it never has, which is why the portrait showed on iOS and never on
95
+ * Android) — so JP2/J2K falls through to the bundled JJ2000 decoder.
96
+ *
97
+ * Empty string on failure rather than throwing: the preview is a courtesy and
98
+ * the chip read's verdict does not depend on it.
99
+ */
100
+ override fun decodeImage(dataBase64: String): Promise<String> {
101
+ return Promise.async {
102
+ try {
103
+ val bytes = Base64.decode(dataBase64, Base64.NO_WRAP)
104
+ val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
105
+ ?: Jp2Decoder.decode(bytes)
106
+ ?: return@async ""
107
+ val out = ByteArrayOutputStream()
108
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
109
+ Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
110
+ } catch (_: Throwable) {
111
+ ""
112
+ }
113
+ }
114
+ }
115
+
116
+ override fun startSession(alertMessage: String): Promise<EmrtdTagInfo> {
117
+ // `alertMessage` is iOS's system NFC sheet; Android shows nothing of its
118
+ // own, so the SDK's own UI carries the instruction there.
119
+ return Promise.async {
120
+ val activity = context?.currentActivity
121
+ ?: throw IllegalStateException("No foreground activity to attach the NFC reader to.")
122
+ val nfc = NfcAdapter.getDefaultAdapter(activity)
123
+ ?: throw IllegalStateException("This device has no NFC hardware.")
124
+
125
+ val future = CompletableFuture<Tag>()
126
+ pending = future
127
+ adapter = nfc
128
+
129
+ // Reader mode with NFC-A/B and the platform's own NDEF check SKIPPED: a
130
+ // passport is not an NDEF tag, and letting the system probe for one adds
131
+ // a round trip and can knock the chip off the field.
132
+ //
133
+ // The presence-check delay is the setting that decides whether a large
134
+ // file survives. Android otherwise probes the tag every ~125ms to see if
135
+ // it is still there, and those probes interleave with an ISO-DEP exchange
136
+ // — long enough into a 30KB DG2 read, some chips drop the connection. The
137
+ // library the Flutter SDK uses sets this for the same reason.
138
+ val options = Bundle().apply {
139
+ putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, PRESENCE_CHECK_DELAY_MS)
140
+ }
141
+ nfc.enableReaderMode(
142
+ activity,
143
+ { tag -> future.complete(tag) },
144
+ NfcAdapter.FLAG_READER_NFC_A or
145
+ NfcAdapter.FLAG_READER_NFC_B or
146
+ NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
147
+ options,
148
+ )
149
+
150
+ // BOUNDED. `get()` with no timeout waits forever for a tap that may never
151
+ // come, and the caller would have no way out — the same unbounded-wait
152
+ // trap the SDK avoids everywhere else. A user who does not present a
153
+ // document gets a clean timeout instead of a frozen screen.
154
+ val tag = try {
155
+ future.get(NFC_WAIT_SECONDS, TimeUnit.SECONDS)
156
+ } catch (e: TimeoutException) {
157
+ throw IllegalStateException("No chip was detected. Try again and hold the phone still.")
158
+ }
159
+ val dep = IsoDep.get(tag)
160
+ ?: throw IllegalStateException("That tag does not speak ISO-DEP.")
161
+ // Passports are slow and DG2 is large; the default timeout aborts
162
+ // mid-file on a chip that is working perfectly well.
163
+ dep.timeout = 20_000
164
+ dep.connect()
165
+ isoDep = dep
166
+
167
+ EmrtdTagInfo(connected = true, uid = Base64.encodeToString(tag.id, Base64.NO_WRAP))
168
+ }
169
+ }
170
+
171
+ override fun stopSession(message: String): Promise<Unit> {
172
+ return Promise.async {
173
+ // Cancel a wait that is still in flight, so closing the sheet releases
174
+ // the caller rather than leaving it blocked until the timeout.
175
+ pending?.completeExceptionally(IllegalStateException("The scan was cancelled."))
176
+ runCatching { isoDep?.close() }
177
+ isoDep = null
178
+ pending = null
179
+ val activity = context?.currentActivity
180
+ if (activity != null) runCatching { adapter?.disableReaderMode(activity) }
181
+ adapter = null
182
+ }
183
+ }
184
+
185
+ override fun updateSessionMessage(message: String): Promise<Unit> {
186
+ // Android has no system NFC sheet to write to — the SDK's own screen
187
+ // narrates the read there. Accepted and ignored so the JS side can call
188
+ // one API on both platforms.
189
+ return Promise.async { }
190
+ }
191
+
192
+ override fun transceive(apduBase64: String): Promise<EmrtdApduResponse> {
193
+ return Promise.async {
194
+ val dep = isoDep ?: throw IllegalStateException("No chip is connected.")
195
+ val response = dep.transceive(Base64.decode(apduBase64, Base64.NO_WRAP))
196
+ if (response.size < 2) throw IllegalStateException("The chip returned a truncated response.")
197
+
198
+ // A non-success status word RESOLVES rather than throwing: 0x6CXX and
199
+ // 0x63CF are answers the protocol layer must interpret, not failures.
200
+ val sw1 = response[response.size - 2].toInt() and 0xFF
201
+ val sw2 = response[response.size - 1].toInt() and 0xFF
202
+ val data = response.copyOfRange(0, response.size - 2)
203
+
204
+ EmrtdApduResponse(
205
+ data = Base64.encodeToString(data, Base64.NO_WRAP),
206
+ statusWord = ((sw1 shl 8) or sw2).toDouble(),
207
+ )
208
+ }
209
+ }
210
+
211
+ // ── Primitives ────────────────────────────────────────────────────────────
212
+
213
+ override fun sha1(dataBase64: String): String = digest("SHA-1", dataBase64)
214
+
215
+ override fun sha256(dataBase64: String): String = digest("SHA-256", dataBase64)
216
+
217
+ private fun digest(algorithm: String, dataBase64: String): String =
218
+ Base64.encodeToString(
219
+ MessageDigest.getInstance(algorithm).digest(decode(dataBase64)),
220
+ Base64.NO_WRAP,
221
+ )
222
+
223
+ override fun desEde2Cbc(
224
+ keyBase64: String,
225
+ dataBase64: String,
226
+ ivBase64: String,
227
+ encrypt: Boolean,
228
+ ): String {
229
+ // Two-key 3DES is K1|K2|K1. javax.crypto's DESede wants all 24 bytes, so
230
+ // the expansion happens here rather than being assumed.
231
+ val key16 = decode(keyBase64)
232
+ require(key16.size == 16) { "3DES needs a 16-byte two-key value." }
233
+ val key24 = ByteArray(24)
234
+ System.arraycopy(key16, 0, key24, 0, 16)
235
+ System.arraycopy(key16, 0, key24, 16, 8)
236
+
237
+ val iv = decode(ivBase64)
238
+ // NoPadding: the protocol applies ISO 9797-1 method 2 itself, and letting
239
+ // the provider add PKCS#5 on top would corrupt every message.
240
+ return crypt(
241
+ "DESede/CBC/NoPadding",
242
+ SecretKeySpec(key24, "DESede"),
243
+ IvParameterSpec(if (iv.isEmpty()) ByteArray(8) else iv),
244
+ decode(dataBase64),
245
+ encrypt,
246
+ )
247
+ }
248
+
249
+ override fun desBlock(keyBase64: String, blockBase64: String, encrypt: Boolean): String =
250
+ // ECB on a single block — the retail MAC's inner primitive. The chaining is
251
+ // done in TypeScript, where the standard's construction is testable.
252
+ crypt(
253
+ "DES/ECB/NoPadding",
254
+ SecretKeySpec(decode(keyBase64), "DES"),
255
+ null,
256
+ decode(blockBase64),
257
+ encrypt,
258
+ )
259
+
260
+ override fun aesCbc(
261
+ keyBase64: String,
262
+ dataBase64: String,
263
+ ivBase64: String,
264
+ encrypt: Boolean,
265
+ ): String {
266
+ val iv = decode(ivBase64)
267
+ return crypt(
268
+ "AES/CBC/NoPadding",
269
+ SecretKeySpec(decode(keyBase64), "AES"),
270
+ IvParameterSpec(if (iv.isEmpty()) ByteArray(16) else iv),
271
+ decode(dataBase64),
272
+ encrypt,
273
+ )
274
+ }
275
+
276
+ override fun aesCmac(keyBase64: String, dataBase64: String): String {
277
+ // No CMAC in the platform providers, so RFC 4493 is built on AES-ECB here.
278
+ // A short, exactly-specified construction; the alternative is a
279
+ // third-party dependency inside the passport path.
280
+ val key = SecretKeySpec(decode(keyBase64), "AES")
281
+ val message = decode(dataBase64)
282
+ val block = 16
283
+
284
+ val l = decode(crypt("AES/ECB/NoPadding", key, null, ByteArray(block), true))
285
+ val k1 = shiftLeftXor(l)
286
+ val k2 = shiftLeftXor(k1)
287
+
288
+ val blocks = mutableListOf<ByteArray>()
289
+ var i = 0
290
+ while (i < message.size) {
291
+ blocks.add(message.copyOfRange(i, minOf(i + block, message.size)))
292
+ i += block
293
+ }
294
+ if (blocks.isEmpty()) blocks.add(ByteArray(0))
295
+
296
+ // The final block is XORed with K1 when the message divides evenly, and
297
+ // padded then XORed with K2 when it does not.
298
+ var last = blocks.removeAt(blocks.size - 1)
299
+ last = if (last.size == block) {
300
+ xor(last, k1)
301
+ } else {
302
+ val padded = ByteArray(block)
303
+ System.arraycopy(last, 0, padded, 0, last.size)
304
+ padded[last.size] = 0x80.toByte()
305
+ xor(padded, k2)
306
+ }
307
+
308
+ var chain = ByteArray(block)
309
+ for (b in blocks + listOf(last)) {
310
+ chain = decode(crypt("AES/ECB/NoPadding", key, null, xor(chain, b), true))
311
+ }
312
+ return Base64.encodeToString(chain, Base64.NO_WRAP)
313
+ }
314
+
315
+ override fun randomBytes(length: Double): String {
316
+ val bytes = ByteArray(length.toInt())
317
+ // SecureRandom, not Random: the BAC nonce's unpredictability is what stops
318
+ // the exchange being replayable.
319
+ SecureRandom().nextBytes(bytes)
320
+ return Base64.encodeToString(bytes, Base64.NO_WRAP)
321
+ }
322
+
323
+ // ── Helpers ───────────────────────────────────────────────────────────────
324
+
325
+ private fun decode(b64: String): ByteArray =
326
+ if (b64.isEmpty()) ByteArray(0) else Base64.decode(b64, Base64.NO_WRAP)
327
+
328
+ private fun xor(a: ByteArray, b: ByteArray): ByteArray =
329
+ ByteArray(minOf(a.size, b.size)) { (a[it].toInt() xor b[it].toInt()).toByte() }
330
+
331
+ /** One-bit left shift, XORed with the RFC 4493 constant on overflow. */
332
+ private fun shiftLeftXor(input: ByteArray): ByteArray {
333
+ val out = ByteArray(input.size)
334
+ var carry = 0
335
+ for (i in input.indices.reversed()) {
336
+ val byte = input[i].toInt() and 0xFF
337
+ out[i] = ((byte shl 1) or carry).toByte()
338
+ carry = if (byte and 0x80 != 0) 1 else 0
339
+ }
340
+ if (input[0].toInt() and 0x80 != 0) {
341
+ out[out.size - 1] = (out[out.size - 1].toInt() xor 0x87).toByte()
342
+ }
343
+ return out
344
+ }
345
+
346
+ private fun crypt(
347
+ transformation: String,
348
+ key: SecretKeySpec,
349
+ iv: IvParameterSpec?,
350
+ data: ByteArray,
351
+ encrypt: Boolean,
352
+ ): String {
353
+ val cipher = Cipher.getInstance(transformation)
354
+ val mode = if (encrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE
355
+ if (iv == null) cipher.init(mode, key) else cipher.init(mode, key, iv)
356
+ return Base64.encodeToString(cipher.doFinal(data), Base64.NO_WRAP)
357
+ }
358
+ }