@myazahq/kyc-sdk-react-native 2.0.2 → 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 (281) 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/CameraViewfinder.tsx +226 -70
  99. package/src/components/ContactCodeEntry.tsx +139 -0
  100. package/src/components/CountryField.tsx +87 -0
  101. package/src/components/CountryFlag.tsx +15 -15
  102. package/src/components/DialCodePicker.tsx +186 -0
  103. package/src/components/DocumentCropper.tsx +1 -1
  104. package/src/components/DocumentReview.tsx +142 -0
  105. package/src/components/DocumentReviewSide.tsx +135 -0
  106. package/src/components/DocumentReviewZoom.tsx +119 -0
  107. package/src/components/ExpiryCountdown.tsx +52 -0
  108. package/src/components/FlashOverlay.tsx +156 -0
  109. package/src/components/Icon.tsx +67 -1
  110. package/src/components/KycFlow.tsx +66 -94
  111. package/src/components/KycSheet.tsx +148 -18
  112. package/src/components/MediaSourceSheet.tsx +163 -0
  113. package/src/components/MyazaButton.tsx +14 -2
  114. package/src/components/MyazaDateField.tsx +240 -0
  115. package/src/components/MyazaInput.tsx +73 -26
  116. package/src/components/MyazaSelect.tsx +283 -0
  117. package/src/components/MyazaWordmark.tsx +33 -0
  118. package/src/components/OptionRow.tsx +132 -0
  119. package/src/components/PhoneNumberInput.tsx +149 -0
  120. package/src/components/PoweredBy.tsx +89 -0
  121. package/src/components/ReadyPrimer.tsx +149 -0
  122. package/src/components/StepView.tsx +91 -0
  123. package/src/components/Typography.tsx +18 -3
  124. package/src/components/VerifiedNotice.tsx +38 -0
  125. package/src/components/WhatsAppIcon.tsx +29 -0
  126. package/src/components/WorkflowGate.tsx +128 -0
  127. package/src/components/brand-font.ts +113 -0
  128. package/src/components/flashHoleGeometry.ts +35 -0
  129. package/src/components/flow/FatalConfigError.tsx +56 -0
  130. package/src/components/fonts.ts +2 -19
  131. package/src/components/readyPrimerContent.ts +52 -0
  132. package/src/components/runtime.tsx +38 -43
  133. package/src/components/stepHeaderMeta.tsx +87 -0
  134. package/src/components/theme-provider.tsx +89 -0
  135. package/src/components/useWorkflowMount.ts +116 -0
  136. package/src/components/viewfinder/CardGuide.tsx +60 -0
  137. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  138. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  139. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  140. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  141. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  142. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  143. package/src/config/brand.ts +97 -0
  144. package/src/config/business.ts +179 -0
  145. package/src/config/businessSteps.ts +105 -0
  146. package/src/config/contact.ts +159 -0
  147. package/src/config/countryNames.g.ts +254 -0
  148. package/src/config/currencyFlags.ts +144 -0
  149. package/src/config/font-resolve.ts +78 -0
  150. package/src/config/idTypes.ts +96 -1
  151. package/src/config/keyPeople.ts +152 -0
  152. package/src/config/phone.ts +39 -0
  153. package/src/config/proofOfAddress.ts +75 -0
  154. package/src/config/questionnaire.ts +153 -0
  155. package/src/config/regions.ts +118 -0
  156. package/src/config/registrationHint.ts +87 -0
  157. package/src/config/stepOrder.ts +118 -0
  158. package/src/config/theme.ts +38 -2
  159. package/src/config/workflowMerge.ts +124 -0
  160. package/src/emrtd/bac.ts +128 -0
  161. package/src/emrtd/bytes.ts +127 -0
  162. package/src/emrtd/crypto.ts +163 -0
  163. package/src/emrtd/der.ts +192 -0
  164. package/src/emrtd/dg2.ts +111 -0
  165. package/src/emrtd/files.ts +215 -0
  166. package/src/emrtd/index.ts +30 -0
  167. package/src/emrtd/mrzKey.ts +54 -0
  168. package/src/emrtd/native.ts +79 -0
  169. package/src/emrtd/optionalRead.ts +42 -0
  170. package/src/emrtd/read.ts +224 -0
  171. package/src/emrtd/retry.ts +98 -0
  172. package/src/emrtd/secureMessaging.ts +178 -0
  173. package/src/emrtd/session.ts +200 -0
  174. package/src/emrtd/stages.ts +122 -0
  175. package/src/index.ts +110 -0
  176. package/src/lib/calendar.ts +71 -0
  177. package/src/lib/use-keyboard-inset.ts +34 -0
  178. package/src/liveness/challengeManager.ts +27 -1
  179. package/src/liveness/faceContinuity.ts +153 -0
  180. package/src/liveness/flashDetector.ts +145 -0
  181. package/src/liveness/flashReadyGate.ts +106 -0
  182. package/src/liveness/flashRunner.ts +118 -0
  183. package/src/liveness/integritySignals.ts +60 -0
  184. package/src/liveness/livenessState.ts +139 -0
  185. package/src/liveness/speech.ts +19 -1
  186. package/src/liveness/types.ts +48 -1
  187. package/src/liveness/useFlashPhase.ts +78 -0
  188. package/src/liveness/useLiveness.ts +260 -75
  189. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  190. package/src/mrz/extract.ts +101 -0
  191. package/src/mrz/parse.ts +178 -0
  192. package/src/mrz/textRecognizer.ts +149 -0
  193. package/src/screens/ApplicantRoleStep.tsx +331 -0
  194. package/src/screens/BusinessDetailsStep.tsx +212 -0
  195. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  196. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  197. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  198. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  199. package/src/screens/CompanyInfoFields.tsx +83 -0
  200. package/src/screens/ConsentStep.tsx +68 -68
  201. package/src/screens/ContactChannelChoice.tsx +133 -0
  202. package/src/screens/ContactCodeStep.tsx +130 -0
  203. package/src/screens/ContactDestinationField.tsx +70 -0
  204. package/src/screens/ContactFooterNote.tsx +34 -0
  205. package/src/screens/ContactVerificationStep.tsx +199 -0
  206. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  207. package/src/screens/CountrySelectStep.tsx +210 -0
  208. package/src/screens/DocumentCaptureStep.tsx +156 -132
  209. package/src/screens/IdInputStep.tsx +13 -5
  210. package/src/screens/IdTypeStep.tsx +74 -33
  211. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  212. package/src/screens/LivenessStep.tsx +155 -247
  213. package/src/screens/MrzScanView.tsx +173 -0
  214. package/src/screens/NfcStep.tsx +290 -0
  215. package/src/screens/ProofOfAddressStep.tsx +238 -0
  216. package/src/screens/QuestionnaireField.tsx +219 -0
  217. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  218. package/src/screens/QuestionnaireStep.tsx +76 -0
  219. package/src/screens/SubmittedStep.tsx +5 -0
  220. package/src/screens/consent/model.ts +109 -0
  221. package/src/screens/document/CameraPhase.tsx +150 -0
  222. package/src/screens/document/RequiredPill.tsx +60 -0
  223. package/src/screens/liveness/LightingBanner.tsx +50 -0
  224. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  225. package/src/screens/liveness/ProgressDots.tsx +67 -0
  226. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  227. package/src/screens/liveness/constants.ts +31 -0
  228. package/src/screens/liveness/index.ts +8 -0
  229. package/src/screens/liveness/resolveGuidance.ts +27 -0
  230. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  231. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  232. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  233. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  234. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  235. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  236. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  237. package/src/screens/nfc/availability.ts +73 -0
  238. package/src/screens/nfc/readErrorMessage.ts +93 -0
  239. package/src/screens/nfc/useChipPortrait.ts +58 -0
  240. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  241. package/src/screens/useFlashHole.ts +105 -0
  242. package/src/screens/useFlashSequence.ts +96 -0
  243. package/src/screens/usePoaAttach.ts +121 -0
  244. package/src/services/api-types.ts +164 -0
  245. package/src/services/api-verify-types.ts +99 -0
  246. package/src/services/api.ts +72 -102
  247. package/src/services/cardCrop.ts +25 -10
  248. package/src/services/contactErrors.ts +43 -0
  249. package/src/services/deviceMetadata.ts +1 -1
  250. package/src/services/documentPicker.ts +20 -0
  251. package/src/services/fingerprint-sources.ts +129 -0
  252. package/src/services/fingerprint.ts +101 -0
  253. package/src/services/haptics.ts +38 -0
  254. package/src/services/mediaCompress.ts +7 -2
  255. package/src/services/resolveUrl.ts +2 -2
  256. package/src/services/workflowGate.ts +138 -0
  257. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  258. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  259. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  260. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  261. package/src/store/derive.ts +225 -0
  262. package/src/store/kycStore.ts +250 -149
  263. package/src/store/state.ts +253 -0
  264. package/src/store/submit.ts +101 -0
  265. package/src/store/submitApplicant.ts +81 -0
  266. package/src/types/appearance.ts +113 -0
  267. package/src/types/business.ts +131 -0
  268. package/src/types/config.ts +150 -124
  269. package/src/types/id-types.ts +57 -0
  270. package/src/types/verification.ts +5 -0
  271. package/src/types/workflow.ts +141 -0
  272. package/src/utils/amountFormat.ts +73 -0
  273. package/src/utils/tokens.ts +4 -2
  274. package/src/__tests__/cardCrop.test.ts +0 -39
  275. package/src/__tests__/deviceMetadata.test.ts +0 -34
  276. package/src/__tests__/errors.test.ts +0 -34
  277. package/src/__tests__/flow.test.ts +0 -61
  278. package/src/__tests__/gestureDetector.test.ts +0 -37
  279. package/src/__tests__/liveness.test.ts +0 -112
  280. package/src/__tests__/resolveUrl.test.ts +0 -64
  281. package/src/__tests__/validators.test.ts +0 -38
@@ -0,0 +1,111 @@
1
+ import { fromBase64, toBase64 } from './bytes';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // The chip's portrait (DG2).
5
+ //
6
+ // DG2 is a CBEFF container, not an image file: the picture sits somewhere
7
+ // inside a biometric header, and the only dependable way to find it is to scan
8
+ // for the image's own magic bytes. That is what this does.
9
+ //
10
+ // Two encodings occur in practice, and the difference decides whether anything
11
+ // can be shown at all:
12
+ //
13
+ // • JPEG (FF D8 FF) — renders anywhere, so it becomes a data URI.
14
+ // • JPEG 2000 (JP2 / raw J2K codestream) — the COMMON case on passports, and
15
+ // React Native cannot decode it. There is no shame in that: the read still
16
+ // stands, the bytes still go to the server, and the panel simply shows the
17
+ // generic success mark instead of a face.
18
+ //
19
+ // So a missing preview is NEVER an error. The portrait is a confirmation for
20
+ // the user, not evidence — the server is what verifies DG2 against the signed
21
+ // security object.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type PortraitFormat = 'jpeg' | 'jp2' | 'unknown';
25
+
26
+ export interface ChipPortrait {
27
+ format: PortraitFormat;
28
+ /** A `data:` URI, only when the format is one RN can actually render. */
29
+ dataUri: string | null;
30
+ /**
31
+ * The IMAGE bytes, base64, sliced out of the CBEFF container — for handing to
32
+ * a platform decoder when RN cannot render the format itself.
33
+ *
34
+ * This is not the same as the DG2 blob. DG2 is a biometric container with a
35
+ * header in front of the picture; passing the whole thing to an image decoder
36
+ * asks it to recognise a file format that is not there, and it will decline.
37
+ * The offset is already known here, so the slice is free.
38
+ */
39
+ imageBase64: string | null;
40
+ }
41
+
42
+ const JPEG_MAGIC = [0xff, 0xd8, 0xff];
43
+ // JP2 container: the 12-byte signature box.
44
+ const JP2_MAGIC = [0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20];
45
+ // Raw JPEG 2000 codestream (SOC + SIZ), which some chips store unwrapped.
46
+ const J2K_MAGIC = [0xff, 0x4f, 0xff, 0x51];
47
+
48
+ function indexOfBytes(haystack: Uint8Array, needle: number[], from = 0): number {
49
+ outer: for (let i = from; i <= haystack.length - needle.length; i += 1) {
50
+ for (let j = 0; j < needle.length; j += 1) {
51
+ if (haystack[i + j] !== needle[j]) continue outer;
52
+ }
53
+ return i;
54
+ }
55
+ return -1;
56
+ }
57
+
58
+ /**
59
+ * Find the portrait inside a DG2 elementary file.
60
+ *
61
+ * Scanning for magic bytes rather than walking the CBEFF/BIT structure is
62
+ * deliberate: the header layout varies between issuers, and a parser that is
63
+ * wrong about one of them returns nothing, while the magic bytes are fixed by
64
+ * the image formats themselves.
65
+ */
66
+ export function extractChipPortrait(dg2Base64: string | undefined): ChipPortrait {
67
+ if (!dg2Base64) return { format: 'unknown', dataUri: null, imageBase64: null };
68
+
69
+ let bytes: Uint8Array;
70
+ try {
71
+ bytes = fromBase64(dg2Base64);
72
+ } catch {
73
+ return { format: 'unknown', dataUri: null, imageBase64: null };
74
+ }
75
+
76
+ // EARLIEST signature wins, exactly like the Flutter SDK. Checking formats in
77
+ // preference order instead was subtly wrong: 20KB of compressed JPEG 2000
78
+ // wavelet data can contain a spurious `FF D8 FF` by chance, and preferring
79
+ // JPEG meant slicing at that garbage offset — producing a "jpeg" data URI
80
+ // that renders as nothing, while the real, decodable JP2 header sat earlier
81
+ // in the file, never consulted. The image's true start is the FIRST magic in
82
+ // the container.
83
+ const candidates: Array<[PortraitFormat, number[]]> = [
84
+ ['jpeg', JPEG_MAGIC],
85
+ ['jp2', JP2_MAGIC],
86
+ ['jp2', J2K_MAGIC],
87
+ ];
88
+ let format: PortraitFormat = 'unknown';
89
+ let at = -1;
90
+ for (const [candidate, magic] of candidates) {
91
+ const index = indexOfBytes(bytes, magic);
92
+ if (index >= 0 && (at < 0 || index < at)) {
93
+ at = index;
94
+ format = candidate;
95
+ }
96
+ }
97
+
98
+ if (at < 0) return { format: 'unknown', dataUri: null, imageBase64: null };
99
+
100
+ // Sliced from the magic bytes onward: a renderer or platform decoder needs
101
+ // the image FILE, not the biometric container it is wrapped in.
102
+ const imageBase64 = toBase64(bytes.subarray(at));
103
+ return {
104
+ format,
105
+ // Only baseline JPEG renders directly in RN; JP2 is reported honestly so a
106
+ // caller can hand it to the platform decoder — and so "no preview" is
107
+ // distinguishable from a DG2 that failed to parse at all.
108
+ dataUri: format === 'jpeg' ? `data:image/jpeg;base64,${imageBase64}` : null,
109
+ imageBase64,
110
+ };
111
+ }
@@ -0,0 +1,215 @@
1
+ import { concat } from './bytes';
2
+ import { readTlvHeader } from './der';
3
+ import type { SecureMessagingSession } from './secureMessaging';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Reading elementary files off the chip.
7
+ //
8
+ // SELECT the file, read its TLV header to learn the length, then READ BINARY in
9
+ // chunks. Three details make the difference between this working on one
10
+ // passport and working on all of them:
11
+ //
12
+ // • The READ LENGTH IS ADAPTIVE. Chips disagree about how much they will
13
+ // return in one go, and one that is asked for too much answers 0x6CXX with
14
+ // the length it WILL accept. Starting optimistic and stepping down on
15
+ // refusal reads a 30KB DG2 in a fraction of the round trips a conservative
16
+ // fixed size would need.
17
+ // • Offsets past 0x7FFF do not fit the short READ BINARY form, so DG2 —
18
+ // which routinely exceeds 32KB — needs the extended form with the offset in
19
+ // the command data. Masking it into the short form instead reads the WRONG
20
+ // BYTES and silently returns a corrupt portrait.
21
+ // • The status comes from inside secure messaging, not the outer word.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Elementary file identifiers. */
25
+ export const EF = {
26
+ COM: 0x011e,
27
+ DG1: 0x0101,
28
+ DG2: 0x0102,
29
+ SOD: 0x011d,
30
+ } as const;
31
+
32
+ const SW_OK = 0x9000;
33
+ /** "Wrong length — use this one instead", with the acceptable length in SW2. */
34
+ const SW_WRONG_LE = 0x6c00;
35
+ /** Some chips report end-of-file this way rather than returning fewer bytes. */
36
+ const SW_EOF = 0x6282;
37
+ /** "Part of the returned data may be corrupted" — a warning that carries data. */
38
+ const SW_CORRUPT_WARNING = 0x6281;
39
+
40
+ /**
41
+ * Status words whose response still carries usable data. `61XX` ("more bytes
42
+ * available") and the corruption warning were being ladder-stepped like hard
43
+ * errors, discarding data the chip HAD returned — the Flutter reader accepts
44
+ * both, and chips that answer this way exist.
45
+ */
46
+ function carriesData(statusWord: number): boolean {
47
+ return (
48
+ statusWord === SW_OK ||
49
+ statusWord === SW_EOF ||
50
+ statusWord === SW_CORRUPT_WARNING ||
51
+ (statusWord & 0xff00) === 0x6100
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Chunk sizes to try, largest first.
57
+ *
58
+ * The ladder exists because chips do not agree and do not advertise: the only
59
+ * way to find the limit is to ask and be told no.
60
+ */
61
+ const READ_LADDER = [224, 160, 128, 96, 64, 32, 16, 8, 1];
62
+ const DEFAULT_READ = 112;
63
+
64
+ export class EmrtdFileError extends Error {
65
+ constructor(message: string) {
66
+ super(message);
67
+ this.name = 'EmrtdFileError';
68
+ Object.setPrototypeOf(this, EmrtdFileError.prototype);
69
+ }
70
+ }
71
+
72
+ /** Sends one wrapped APDU and returns the unwrapped answer. */
73
+ export type Transceive = (command: Uint8Array) => Promise<Uint8Array>;
74
+
75
+ async function send(
76
+ sm: SecureMessagingSession,
77
+ transceive: Transceive,
78
+ apdu: Parameters<SecureMessagingSession['protect']>[0],
79
+ ): Promise<{ data: Uint8Array; statusWord: number }> {
80
+ return sm.unprotect(await transceive(sm.protect(apdu)));
81
+ }
82
+
83
+ /** SELECT an elementary file by identifier. */
84
+ export async function selectFile(
85
+ sm: SecureMessagingSession,
86
+ transceive: Transceive,
87
+ fileId: number,
88
+ ): Promise<void> {
89
+ const { statusWord } = await send(sm, transceive, {
90
+ cla: 0x00,
91
+ ins: 0xa4,
92
+ p1: 0x02,
93
+ p2: 0x0c,
94
+ data: new Uint8Array([(fileId >> 8) & 0xff, fileId & 0xff]),
95
+ });
96
+ if (statusWord !== SW_OK) {
97
+ throw new EmrtdFileError(
98
+ `The chip refused to select file ${fileId.toString(16)} (0x${statusWord.toString(16)}).`,
99
+ );
100
+ }
101
+ }
102
+
103
+ /** One READ BINARY at an offset, using whichever command form the offset needs. */
104
+ async function readAt(
105
+ sm: SecureMessagingSession,
106
+ transceive: Transceive,
107
+ offset: number,
108
+ length: number,
109
+ ): Promise<{ data: Uint8Array; statusWord: number }> {
110
+ if (offset < 0x8000) {
111
+ return send(sm, transceive, {
112
+ cla: 0x00,
113
+ ins: 0xb0,
114
+ p1: (offset >> 8) & 0x7f,
115
+ p2: offset & 0xff,
116
+ le: length,
117
+ });
118
+ }
119
+
120
+ // Beyond 32KB the offset no longer fits the header, so it moves into the
121
+ // command data as a DO'54' — the extended form. Masking it into the short
122
+ // form would read from the wrong place and return a corrupt file.
123
+ return send(sm, transceive, {
124
+ cla: 0x00,
125
+ ins: 0xb1,
126
+ p1: 0x00,
127
+ p2: 0x00,
128
+ data: new Uint8Array([0x54, 0x02, (offset >> 8) & 0xff, offset & 0xff]),
129
+ le: length,
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Read the currently-selected file in full.
135
+ *
136
+ * `expectedLength` comes from the file's own TLV header, read first.
137
+ */
138
+ async function readSelected(
139
+ sm: SecureMessagingSession,
140
+ transceive: Transceive,
141
+ expectedLength: number,
142
+ alreadyRead: Uint8Array,
143
+ ): Promise<Uint8Array> {
144
+ const chunks: Uint8Array[] = [alreadyRead];
145
+ let offset = alreadyRead.length;
146
+ let chunkSize = DEFAULT_READ;
147
+
148
+ while (offset < expectedLength) {
149
+ const want = Math.min(chunkSize, expectedLength - offset);
150
+ const { data, statusWord } = await readAt(sm, transceive, offset, want);
151
+
152
+ if (statusWord === SW_WRONG_LE || (statusWord & 0xff00) === SW_WRONG_LE) {
153
+ // The chip named the length it will accept. Honour it exactly rather
154
+ // than guessing again.
155
+ const allowed = statusWord & 0x00ff;
156
+ if (allowed > 0) {
157
+ chunkSize = allowed;
158
+ continue;
159
+ }
160
+ }
161
+
162
+ if (!carriesData(statusWord)) {
163
+ // Step down the ladder and retry; a chip that will not answer at ANY
164
+ // size is a real failure, which the exhausted ladder reports below.
165
+ const next = READ_LADDER.find((n) => n < chunkSize);
166
+ if (next === undefined) {
167
+ throw new EmrtdFileError(
168
+ `The chip stopped responding at offset ${offset} (0x${statusWord.toString(16)}).`,
169
+ );
170
+ }
171
+ chunkSize = next;
172
+ continue;
173
+ }
174
+
175
+ if (data.length === 0) break; // End of file.
176
+ chunks.push(data);
177
+ offset += data.length;
178
+ }
179
+
180
+ return concat(...chunks);
181
+ }
182
+
183
+ /**
184
+ * Select and read an elementary file.
185
+ *
186
+ * The first read is small and deliberate: it fetches the TLV header, which
187
+ * carries the file's real length. Reading blind until the chip stops is slower
188
+ * and, on chips that pad, returns trailing rubbish.
189
+ */
190
+ export async function readFile(
191
+ sm: SecureMessagingSession,
192
+ transceive: Transceive,
193
+ fileId: number,
194
+ ): Promise<Uint8Array> {
195
+ await selectFile(sm, transceive, fileId);
196
+
197
+ // Four bytes is enough for a tag plus a long-form length of up to 0xFFFF.
198
+ const head = await readAt(sm, transceive, 0, 4);
199
+ if (!carriesData(head.statusWord)) {
200
+ throw new EmrtdFileError(
201
+ `The chip refused to read file ${fileId.toString(16)} (0x${head.statusWord.toString(16)}).`,
202
+ );
203
+ }
204
+ if (head.data.length === 0) return new Uint8Array(0);
205
+
206
+ // The header names the file's true size. A header that does not parse from
207
+ // four bytes is not a failure — the length field can extend past them — so
208
+ // the fallback reads until the chip stops, which is slower but never wrong.
209
+ const header = readTlvHeader(head.data);
210
+ const expected = header
211
+ ? header.headerLength + header.valueLength
212
+ : Number.MAX_SAFE_INTEGER;
213
+
214
+ return readSelected(sm, transceive, expected, head.data);
215
+ }
@@ -0,0 +1,30 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The eMRTD engine's public surface.
3
+ //
4
+ // Barrel-only by design (the docbio rule): nothing outside this directory
5
+ // reaches into the protocol internals. What escapes is a single call that takes
6
+ // the MRZ fields and returns the chip's files, plus the availability check the
7
+ // step order needs.
8
+ //
9
+ // A build without the native module leaves everything unavailable and the chip
10
+ // step skips — the same null-degrade posture the rest of the platform uses for
11
+ // optional native capabilities.
12
+ //
13
+ // Layout: native.ts resolves the Nitro module and answers capability
14
+ // questions; read.ts owns the session/retry loop; session.ts downward is the
15
+ // protocol itself (BAC, secure messaging, file reads), tested against ICAO
16
+ // 9303's worked examples in CI.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type { EmrtdReadResult } from './session';
20
+ export { EmrtdSessionError } from './session';
21
+ export type { MrzKeyFields } from './crypto';
22
+ export type { NfcReadStage } from './stages';
23
+ export {
24
+ NFC_STAGE_ORDER,
25
+ nfcStageDetail,
26
+ nfcStageLabel,
27
+ nfcStageProgress,
28
+ } from './stages';
29
+ export { decodeChipImage, isNfcAvailable, nfcUnavailableReason } from './native';
30
+ export { cancelChipRead, readPassportChip, type ChipReadOptions } from './read';
@@ -0,0 +1,54 @@
1
+ import { fromAscii } from './bytes';
2
+ import type { EmrtdPrimitives } from './crypto';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // The BAC key, derived from the printed MRZ.
6
+ //
7
+ // This is what makes Basic Access Control work at all: the chip will only talk
8
+ // to someone who has physically seen the document, because the key comes from
9
+ // the strip printed on it.
10
+ //
11
+ // Split from crypto.ts (200-line rule) because it is the one part that is about
12
+ // the DOCUMENT rather than the ciphers.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** ICAO 7-3-1 weighted check digit over the MRZ alphabet. */
16
+ function checkDigit(value: string): number {
17
+ const weights = [7, 3, 1];
18
+ let sum = 0;
19
+ for (let i = 0; i < value.length; i++) {
20
+ const ch = value.charCodeAt(i);
21
+ const v = ch >= 0x30 && ch <= 0x39 ? ch - 0x30 : ch >= 0x41 && ch <= 0x5a ? ch - 0x41 + 10 : 0;
22
+ sum += v * weights[i % 3]!;
23
+ }
24
+ return sum % 10;
25
+ }
26
+
27
+ export interface MrzKeyFields {
28
+ documentNumber: string;
29
+ /** YYMMDD, as it appears in the MRZ. */
30
+ dateOfBirth: string;
31
+ dateOfExpiry: string;
32
+ }
33
+
34
+ /**
35
+ * The MRZ information BAC hashes: document number, date of birth and expiry,
36
+ * each followed by its own check digit.
37
+ *
38
+ * The document number is filler-padded to nine characters FIRST, and the check
39
+ * digit is computed over the padded form — getting that order wrong yields a
40
+ * key the chip rejects with no explanation.
41
+ */
42
+ export function mrzKeySeedInput(fields: MrzKeyFields): string {
43
+ const field = (v: string): string => `${v}${checkDigit(v)}`;
44
+ return (
45
+ field(fields.documentNumber.padEnd(9, '<')) +
46
+ field(fields.dateOfBirth) +
47
+ field(fields.dateOfExpiry)
48
+ );
49
+ }
50
+
51
+ /** Kseed: the first 16 bytes of SHA-1 over the MRZ information. */
52
+ export function keySeed(p: EmrtdPrimitives, fields: MrzKeyFields): Uint8Array {
53
+ return p.sha1(fromAscii(mrzKeySeedInput(fields))).subarray(0, 16);
54
+ }
@@ -0,0 +1,79 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+
3
+ import type { MyazaEmrtd } from '../specs/MyazaEmrtd.nitro';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Resolving the native eMRTD module, and the capability questions that only it
7
+ // can answer. The read loop lives in read.ts; this file owns the registry
8
+ // lookup so both have one source of truth for "is there a native side at all".
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * The native module, resolved LAZILY and re-tried until it appears.
13
+ *
14
+ * It used to be created at module load. That is fragile in a way that fails
15
+ * SILENTLY and permanently: if this module happens to evaluate before Nitro's
16
+ * registry is populated — which Fast Refresh causes routinely — the reference
17
+ * is null for the rest of the session, `isNfcAvailable()` reports false, and
18
+ * the chip step quietly skips itself. Nothing logs, and the flow looks like a
19
+ * device without an NFC radio.
20
+ *
21
+ * Resolving on demand costs one registry lookup and cannot get stuck.
22
+ */
23
+ let cached: MyazaEmrtd | null = null;
24
+ export function nativeModule(): MyazaEmrtd | null {
25
+ if (cached) return cached;
26
+ try {
27
+ if (!NitroModules.hasHybridObject('MyazaEmrtd')) return null;
28
+ cached = NitroModules.createHybridObject<MyazaEmrtd>('MyazaEmrtd');
29
+ return cached;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Why NFC is unavailable, when it is.
37
+ *
38
+ * Two very different failures both surface as "no NFC": the Nitro registry not
39
+ * having the object yet (a JS-side race, recoverable — a retry moments later
40
+ * succeeds), and the device/entitlement genuinely not supporting reads (final).
41
+ * Collapsing them into one boolean sent debugging to the wrong layer once
42
+ * already, so the distinction is reported.
43
+ */
44
+ export function nfcUnavailableReason(): 'module_not_registered' | 'device_unsupported' | null {
45
+ try {
46
+ const mod = nativeModule();
47
+ if (!mod) return 'module_not_registered';
48
+ return mod.isAvailable() ? null : 'device_unsupported';
49
+ } catch {
50
+ return 'module_not_registered';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Decode an image the JS side cannot render, returning base64 JPEG.
56
+ *
57
+ * Used for JPEG 2000 chip portraits (DG2), which React Native has no decoder
58
+ * for. Null when the platform declines the format — the preview is a courtesy
59
+ * and the chip read stands without it.
60
+ */
61
+ export async function decodeChipImage(dataBase64: string): Promise<string | null> {
62
+ try {
63
+ // The native side reports "could not decode" as an empty string; normalise
64
+ // it to null so callers have one absent value to check.
65
+ const jpeg = await nativeModule()?.decodeImage(dataBase64);
66
+ return jpeg ? jpeg : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** Whether this build and device can read a chip at all. */
73
+ export function isNfcAvailable(): boolean {
74
+ try {
75
+ return nativeModule()?.isAvailable() ?? false;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
@@ -0,0 +1,42 @@
1
+ import { readFile, type Transceive } from './files';
2
+ import type { SecureMessagingSession } from './secureMessaging';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Best-effort file reads (EF.SOD, DG2).
6
+ //
7
+ // These files are optional by CONTRACT — the read stands without them — but
8
+ // they are not optional in VALUE: without the SOD the server cannot verify
9
+ // DG1 against the issuer, and without DG2 there is no portrait. A transient
10
+ // wobble a few chunks into a kilobyte transfer was costing both, silently:
11
+ // one bare `.catch(() => null)` threw away the file AND the reason.
12
+ //
13
+ // So an optional read gets a second in-session attempt (the file is
14
+ // re-selected from scratch, so a clean retry is protocol-safe — the
15
+ // send-sequence counter advances per exchange either way), and every failure
16
+ // is named in dev builds instead of vanishing.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** In-session tries per optional file, including the first. */
20
+ const OPTIONAL_READ_TRIES = 2;
21
+
22
+ export async function readOptionalFile(
23
+ sm: SecureMessagingSession,
24
+ transceive: Transceive,
25
+ fileId: number,
26
+ name: string,
27
+ ): Promise<Uint8Array | null> {
28
+ for (let attempt = 1; attempt <= OPTIONAL_READ_TRIES; attempt += 1) {
29
+ try {
30
+ return await readFile(sm, transceive, fileId);
31
+ } catch (err) {
32
+ // A dead session fails the retry instantly, so trying again costs
33
+ // nothing when it cannot help — and saves the file when the failure was
34
+ // a single misread chunk.
35
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
36
+ const raw = err instanceof Error ? err.message : String(err ?? '');
37
+ console.log(`[myaza] nfc ${name} read failed (try ${attempt}/${OPTIONAL_READ_TRIES}): ${raw}`);
38
+ }
39
+ }
40
+ }
41
+ return null;
42
+ }