@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.
- package/README.md +111 -8
- package/android/build.gradle +6 -0
- package/android/src/main/AndroidManifest.xml +14 -4
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
- package/app.plugin.js +102 -13
- package/ios/HybridMyazaEmrtd.swift +447 -0
- package/ios/HybridMyazaFaceDetector.swift +104 -2
- package/ios/HybridMyazaRectDetector.swift +103 -0
- package/ios/HybridMyazaTextRecognizer.swift +111 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
- package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
- package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
- package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
- package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
- package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
- package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
- package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
- package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
- package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
- package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
- package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
- package/package.json +16 -4
- package/src/MyazaKYC.tsx +115 -22
- package/src/capture/documentIdentity.ts +153 -0
- package/src/capture/documentSignals.ts +177 -0
- package/src/capture/documentTextGate.ts +171 -0
- package/src/capture/hints.ts +57 -0
- package/src/capture/index.ts +24 -0
- package/src/capture/rectDetector.ts +84 -0
- package/src/capture/rectGate.ts +91 -0
- package/src/capture/useAutoCapture.ts +198 -0
- package/src/components/CameraViewfinder.tsx +226 -70
- package/src/components/ContactCodeEntry.tsx +139 -0
- package/src/components/CountryField.tsx +87 -0
- package/src/components/CountryFlag.tsx +15 -15
- package/src/components/DialCodePicker.tsx +186 -0
- package/src/components/DocumentCropper.tsx +1 -1
- package/src/components/DocumentReview.tsx +142 -0
- package/src/components/DocumentReviewSide.tsx +135 -0
- package/src/components/DocumentReviewZoom.tsx +119 -0
- package/src/components/ExpiryCountdown.tsx +52 -0
- package/src/components/FlashOverlay.tsx +156 -0
- package/src/components/Icon.tsx +67 -1
- package/src/components/KycFlow.tsx +66 -94
- package/src/components/KycSheet.tsx +148 -18
- package/src/components/MediaSourceSheet.tsx +163 -0
- package/src/components/MyazaButton.tsx +14 -2
- package/src/components/MyazaDateField.tsx +240 -0
- package/src/components/MyazaInput.tsx +73 -26
- package/src/components/MyazaSelect.tsx +283 -0
- package/src/components/MyazaWordmark.tsx +33 -0
- package/src/components/OptionRow.tsx +132 -0
- package/src/components/PhoneNumberInput.tsx +149 -0
- package/src/components/PoweredBy.tsx +89 -0
- package/src/components/ReadyPrimer.tsx +149 -0
- package/src/components/StepView.tsx +91 -0
- package/src/components/Typography.tsx +18 -3
- package/src/components/VerifiedNotice.tsx +38 -0
- package/src/components/WhatsAppIcon.tsx +29 -0
- package/src/components/WorkflowGate.tsx +128 -0
- package/src/components/brand-font.ts +113 -0
- package/src/components/flashHoleGeometry.ts +35 -0
- package/src/components/flow/FatalConfigError.tsx +56 -0
- package/src/components/fonts.ts +2 -19
- package/src/components/readyPrimerContent.ts +52 -0
- package/src/components/runtime.tsx +38 -43
- package/src/components/stepHeaderMeta.tsx +87 -0
- package/src/components/theme-provider.tsx +89 -0
- package/src/components/useWorkflowMount.ts +116 -0
- package/src/components/viewfinder/CardGuide.tsx +60 -0
- package/src/components/viewfinder/DocumentGhost.tsx +149 -0
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
- package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
- package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
- package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
- package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
- package/src/config/brand.ts +97 -0
- package/src/config/business.ts +179 -0
- package/src/config/businessSteps.ts +105 -0
- package/src/config/contact.ts +159 -0
- package/src/config/countryNames.g.ts +254 -0
- package/src/config/currencyFlags.ts +144 -0
- package/src/config/font-resolve.ts +78 -0
- package/src/config/idTypes.ts +96 -1
- package/src/config/keyPeople.ts +152 -0
- package/src/config/phone.ts +39 -0
- package/src/config/proofOfAddress.ts +75 -0
- package/src/config/questionnaire.ts +153 -0
- package/src/config/regions.ts +118 -0
- package/src/config/registrationHint.ts +87 -0
- package/src/config/stepOrder.ts +118 -0
- package/src/config/theme.ts +38 -2
- package/src/config/workflowMerge.ts +124 -0
- package/src/emrtd/bac.ts +128 -0
- package/src/emrtd/bytes.ts +127 -0
- package/src/emrtd/crypto.ts +163 -0
- package/src/emrtd/der.ts +192 -0
- package/src/emrtd/dg2.ts +111 -0
- package/src/emrtd/files.ts +215 -0
- package/src/emrtd/index.ts +30 -0
- package/src/emrtd/mrzKey.ts +54 -0
- package/src/emrtd/native.ts +79 -0
- package/src/emrtd/optionalRead.ts +42 -0
- package/src/emrtd/read.ts +224 -0
- package/src/emrtd/retry.ts +98 -0
- package/src/emrtd/secureMessaging.ts +178 -0
- package/src/emrtd/session.ts +200 -0
- package/src/emrtd/stages.ts +122 -0
- package/src/index.ts +110 -0
- package/src/lib/calendar.ts +71 -0
- package/src/lib/use-keyboard-inset.ts +34 -0
- package/src/liveness/challengeManager.ts +27 -1
- package/src/liveness/faceContinuity.ts +153 -0
- package/src/liveness/flashDetector.ts +145 -0
- package/src/liveness/flashReadyGate.ts +106 -0
- package/src/liveness/flashRunner.ts +118 -0
- package/src/liveness/integritySignals.ts +60 -0
- package/src/liveness/livenessState.ts +139 -0
- package/src/liveness/speech.ts +19 -1
- package/src/liveness/types.ts +48 -1
- package/src/liveness/useFlashPhase.ts +78 -0
- package/src/liveness/useLiveness.ts +260 -75
- package/src/liveness/visionCameraFaceDetector.ts +14 -0
- package/src/mrz/extract.ts +101 -0
- package/src/mrz/parse.ts +178 -0
- package/src/mrz/textRecognizer.ts +149 -0
- package/src/screens/ApplicantRoleStep.tsx +331 -0
- package/src/screens/BusinessDetailsStep.tsx +212 -0
- package/src/screens/BusinessDocumentSlot.tsx +167 -0
- package/src/screens/BusinessDocumentsStep.tsx +213 -0
- package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
- package/src/screens/BusinessKeyPersonRow.tsx +170 -0
- package/src/screens/CompanyInfoFields.tsx +83 -0
- package/src/screens/ConsentStep.tsx +68 -68
- package/src/screens/ContactChannelChoice.tsx +133 -0
- package/src/screens/ContactCodeStep.tsx +130 -0
- package/src/screens/ContactDestinationField.tsx +70 -0
- package/src/screens/ContactFooterNote.tsx +34 -0
- package/src/screens/ContactVerificationStep.tsx +199 -0
- package/src/screens/ContactVerifiedPanel.tsx +36 -0
- package/src/screens/CountrySelectStep.tsx +210 -0
- package/src/screens/DocumentCaptureStep.tsx +156 -132
- package/src/screens/IdInputStep.tsx +13 -5
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/LivenessStep.tsx +155 -247
- package/src/screens/MrzScanView.tsx +173 -0
- package/src/screens/NfcStep.tsx +290 -0
- package/src/screens/ProofOfAddressStep.tsx +238 -0
- package/src/screens/QuestionnaireField.tsx +219 -0
- package/src/screens/QuestionnaireMoneyField.tsx +134 -0
- package/src/screens/QuestionnaireStep.tsx +76 -0
- package/src/screens/SubmittedStep.tsx +5 -0
- package/src/screens/consent/model.ts +109 -0
- package/src/screens/document/CameraPhase.tsx +150 -0
- package/src/screens/document/RequiredPill.tsx +60 -0
- package/src/screens/liveness/LightingBanner.tsx +50 -0
- package/src/screens/liveness/LivenessOutcome.tsx +109 -0
- package/src/screens/liveness/ProgressDots.tsx +67 -0
- package/src/screens/liveness/SelfiePreview.tsx +39 -0
- package/src/screens/liveness/constants.ts +31 -0
- package/src/screens/liveness/index.ts +8 -0
- package/src/screens/liveness/resolveGuidance.ts +27 -0
- package/src/screens/liveness/useSelfieUpload.ts +107 -0
- package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
- package/src/screens/nfc/NfcReadBody.tsx +63 -0
- package/src/screens/nfc/NfcReadProgress.tsx +96 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
- package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
- package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
- package/src/screens/nfc/availability.ts +73 -0
- package/src/screens/nfc/readErrorMessage.ts +93 -0
- package/src/screens/nfc/useChipPortrait.ts +58 -0
- package/src/screens/nfc/useNfcAvailability.ts +82 -0
- package/src/screens/useFlashHole.ts +105 -0
- package/src/screens/useFlashSequence.ts +96 -0
- package/src/screens/usePoaAttach.ts +121 -0
- package/src/services/api-types.ts +164 -0
- package/src/services/api-verify-types.ts +99 -0
- package/src/services/api.ts +72 -102
- package/src/services/cardCrop.ts +25 -10
- package/src/services/contactErrors.ts +43 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/documentPicker.ts +20 -0
- package/src/services/fingerprint-sources.ts +129 -0
- package/src/services/fingerprint.ts +101 -0
- package/src/services/haptics.ts +38 -0
- package/src/services/mediaCompress.ts +7 -2
- package/src/services/resolveUrl.ts +2 -2
- package/src/services/workflowGate.ts +138 -0
- package/src/specs/MyazaEmrtd.nitro.ts +114 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
- package/src/specs/MyazaRectDetector.nitro.ts +50 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
- package/src/store/derive.ts +225 -0
- package/src/store/kycStore.ts +250 -149
- package/src/store/state.ts +253 -0
- package/src/store/submit.ts +101 -0
- package/src/store/submitApplicant.ts +81 -0
- package/src/types/appearance.ts +113 -0
- package/src/types/business.ts +131 -0
- package/src/types/config.ts +150 -124
- package/src/types/id-types.ts +57 -0
- package/src/types/verification.ts +5 -0
- package/src/types/workflow.ts +141 -0
- package/src/utils/amountFormat.ts +73 -0
- package/src/utils/tokens.ts +4 -2
- package/src/__tests__/cardCrop.test.ts +0 -39
- package/src/__tests__/deviceMetadata.test.ts +0 -34
- package/src/__tests__/errors.test.ts +0 -34
- package/src/__tests__/flow.test.ts +0 -61
- package/src/__tests__/gestureDetector.test.ts +0 -37
- package/src/__tests__/liveness.test.ts +0 -112
- package/src/__tests__/resolveUrl.test.ts +0 -64
- package/src/__tests__/validators.test.ts +0 -38
package/src/emrtd/dg2.ts
ADDED
|
@@ -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
|
+
}
|