@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { nativeModule } from './native';
|
|
4
|
+
import { readChip, type EmrtdReadResult, EmrtdSessionError } from './session';
|
|
5
|
+
import {
|
|
6
|
+
MAX_CHIP_ATTEMPTS,
|
|
7
|
+
chipRetryDelayMs,
|
|
8
|
+
isRetryableChipError,
|
|
9
|
+
isRetryableStartError,
|
|
10
|
+
} from './retry';
|
|
11
|
+
import { nfcSheetMessage, type NfcReadStage } from './stages';
|
|
12
|
+
import type { MrzKeyFields } from './crypto';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// The read loop: open the session, run BAC with the MRZ key, read the files,
|
|
16
|
+
// close the session — and when the chip drops, do the whole thing again.
|
|
17
|
+
//
|
|
18
|
+
// A dropped session is retried by opening a NEW one, because that is the only
|
|
19
|
+
// recovery there is: iOS tears the session down and secure messaging dies with
|
|
20
|
+
// it — the BAC-derived keys and the send-sequence counter cannot outlive the
|
|
21
|
+
// session, so there is nothing to resume into.
|
|
22
|
+
//
|
|
23
|
+
// The session is ALWAYS closed, including on failure — a session left open
|
|
24
|
+
// holds iOS's NFC sheet on screen over a flow that has already moved on, and
|
|
25
|
+
// on Android keeps reader mode attached to a dead activity.
|
|
26
|
+
//
|
|
27
|
+
// Nothing here concludes that a chip is genuine. The files go to the server,
|
|
28
|
+
// which hashes DG1 against the signed security object and checks the document
|
|
29
|
+
// signer's signature. A client-side "authentic" flag would be a claim an
|
|
30
|
+
// attacker controls.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface ChipReadOptions {
|
|
34
|
+
/** Shown in iOS's system NFC sheet. Android has none and ignores it. */
|
|
35
|
+
alertMessage?: string;
|
|
36
|
+
/** Shown when the read finishes, on iOS. */
|
|
37
|
+
successMessage?: string;
|
|
38
|
+
/** Read progress, so the UI can narrate an otherwise invisible operation. */
|
|
39
|
+
onStage?: (stage: NfcReadStage) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read generation. Each read claims a fresh value; `cancelChipRead` (and any
|
|
44
|
+
* newer read) bumps it, and a loop that wakes from its between-attempt sleep
|
|
45
|
+
* to find the world has moved on stops INSTEAD of reopening a session over
|
|
46
|
+
* whatever screen the user is on now. Closing the current session alone cannot
|
|
47
|
+
* do that — the loop's whole job is to open another one.
|
|
48
|
+
*/
|
|
49
|
+
let generation = 0;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Abandon a read that is still in progress.
|
|
53
|
+
*
|
|
54
|
+
* Necessary because opening the session BLOCKS until a document is presented —
|
|
55
|
+
* on Android for up to a minute. A user who leaves the step during that wait
|
|
56
|
+
* cannot be rescued by `readPassportChip`'s own cleanup: that runs in the catch
|
|
57
|
+
* around `readChip`, which is never reached while the session is still waiting
|
|
58
|
+
* for a tap. Without this, reader mode stays attached to an activity the flow
|
|
59
|
+
* has already left, and on iOS the system sheet sits over the next screen.
|
|
60
|
+
*
|
|
61
|
+
* Safe to call when nothing is running — closing an already-closed session is a
|
|
62
|
+
* no-op, so callers do not have to track whether a read is live.
|
|
63
|
+
*/
|
|
64
|
+
export async function cancelChipRead(): Promise<void> {
|
|
65
|
+
generation += 1;
|
|
66
|
+
const native = nativeModule();
|
|
67
|
+
if (!native) return;
|
|
68
|
+
try {
|
|
69
|
+
await native.stopSession('Cancelled');
|
|
70
|
+
} catch {
|
|
71
|
+
// Nothing to cancel, or the session had already gone. Either way there is
|
|
72
|
+
// nothing left to clean up and nobody to report it to.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readPassportChip(
|
|
77
|
+
mrz: MrzKeyFields,
|
|
78
|
+
options: ChipReadOptions = {},
|
|
79
|
+
): Promise<EmrtdReadResult> {
|
|
80
|
+
const native = nativeModule();
|
|
81
|
+
if (!native) {
|
|
82
|
+
throw new EmrtdSessionError('This build cannot read NFC chips.', 'transport');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const run = ++generation;
|
|
86
|
+
const abandoned = (): boolean => generation !== run;
|
|
87
|
+
const settle = (): Promise<void> =>
|
|
88
|
+
new Promise((resolve) => setTimeout(resolve, chipRetryDelayMs(Platform.OS)));
|
|
89
|
+
|
|
90
|
+
// The stage the read died at is the single most diagnostic fact a failure
|
|
91
|
+
// carries — 'waiting' is a session/detection problem, 'authenticating' is
|
|
92
|
+
// SELECT/BAC (first contact), the read stages are a mid-transfer drop — so
|
|
93
|
+
// it is tracked here and logged with the RAW platform error per attempt.
|
|
94
|
+
// The same wrapper narrates the iOS system sheet, which COVERS the app
|
|
95
|
+
// during the read: without it the sheet sits on its opening line while
|
|
96
|
+
// three files transfer, and the silence reads as a hang (Flutter updates
|
|
97
|
+
// its sheet per stage; this is that, for RN).
|
|
98
|
+
let lastStage: NfcReadStage = 'waiting';
|
|
99
|
+
const onStage = (stage: NfcReadStage): void => {
|
|
100
|
+
lastStage = stage;
|
|
101
|
+
if (stage !== 'waiting' && stage !== 'done') {
|
|
102
|
+
try {
|
|
103
|
+
// Older installed builds predate this native method — narration is a
|
|
104
|
+
// courtesy, never a reason to fail a read.
|
|
105
|
+
void native.updateSessionMessage(nfcSheetMessage(stage)).catch(() => undefined);
|
|
106
|
+
} catch {
|
|
107
|
+
// Method absent on this build.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
options.onStage?.(stage);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// A read that got DG1 but not the SOD is usable yet WORTH RETRYING: without
|
|
114
|
+
// the security object the server cannot verify the chip against the issuer,
|
|
115
|
+
// and the portrait is never fetched. Bank the partial and spend remaining
|
|
116
|
+
// attempts going for the complete read — settle for the partial only when
|
|
117
|
+
// the budget is exhausted.
|
|
118
|
+
let partial: EmrtdReadResult | null = null;
|
|
119
|
+
|
|
120
|
+
let lastError: unknown;
|
|
121
|
+
for (let attempt = 1; attempt <= MAX_CHIP_ATTEMPTS; attempt += 1) {
|
|
122
|
+
try {
|
|
123
|
+
// A completeness retry re-presents the sheet moments after it closed on
|
|
124
|
+
// what looked like a finished read — without an explanation, the natural
|
|
125
|
+
// move is to take the document away, which forfeits the retry.
|
|
126
|
+
await native.startSession(
|
|
127
|
+
partial
|
|
128
|
+
? 'Almost done — keep the document against your phone to finish'
|
|
129
|
+
: options.alertMessage ?? 'Hold your phone near the passport',
|
|
130
|
+
);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Opening can be refused TRANSIENTLY: iOS reports an immediate
|
|
133
|
+
// `Code=202` for a session begun while the previous sheet is still
|
|
134
|
+
// dismissing — the very failure this loop exists to recover from, and
|
|
135
|
+
// exactly what a reopen straight after a drop runs into. Those get the
|
|
136
|
+
// settle-and-retry treatment like any other drop; a refusal another
|
|
137
|
+
// attempt cannot change (cancelled sheet, no document presented, no
|
|
138
|
+
// radio) is reported as-is.
|
|
139
|
+
lastError = err;
|
|
140
|
+
devLog(attempt, 'session-open', err);
|
|
141
|
+
// startSession can fail AFTER arming the radio (Android enables reader
|
|
142
|
+
// mode before discovery/connect can still throw), so the session must be
|
|
143
|
+
// torn down on this path too — or reader mode outlives the attempt,
|
|
144
|
+
// attached to the activity with a dead callback.
|
|
145
|
+
await native.stopSession('').catch(() => undefined);
|
|
146
|
+
if (attempt >= MAX_CHIP_ATTEMPTS || !isRetryableStartError(err)) throw err;
|
|
147
|
+
await settle();
|
|
148
|
+
if (abandoned()) throw cancelledError();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
onStage('waiting');
|
|
154
|
+
const result = await readChip(native, mrz, onStage);
|
|
155
|
+
if (!result.sod && attempt < MAX_CHIP_ATTEMPTS && !abandoned()) {
|
|
156
|
+
partial = result;
|
|
157
|
+
devLog(attempt, 'security-file', new Error('EF.SOD missing — retrying for a complete read'));
|
|
158
|
+
await native.stopSession('').catch(() => undefined);
|
|
159
|
+
onStage('waiting');
|
|
160
|
+
await settle();
|
|
161
|
+
if (abandoned()) throw cancelledError();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
await native.stopSession(options.successMessage ?? 'Passport read');
|
|
165
|
+
return result;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
lastError = err;
|
|
168
|
+
devLog(attempt, lastStage, err);
|
|
169
|
+
const willRetry = attempt < MAX_CHIP_ATTEMPTS && isRetryableChipError(err) && !abandoned();
|
|
170
|
+
// Between attempts the sheet is dismissed QUIETLY — painting it with an
|
|
171
|
+
// error the next attempt may erase reads as a crash mid-recovery. The
|
|
172
|
+
// message is reserved for the terminal failure, where it is the user's
|
|
173
|
+
// only feedback on iOS (the system sheet covers the app) — unless a
|
|
174
|
+
// banked partial makes this a success after all.
|
|
175
|
+
await native
|
|
176
|
+
.stopSession(
|
|
177
|
+
willRetry || partial
|
|
178
|
+
? ''
|
|
179
|
+
: err instanceof EmrtdSessionError
|
|
180
|
+
? err.message
|
|
181
|
+
: 'Could not read the chip',
|
|
182
|
+
)
|
|
183
|
+
.catch(() => undefined);
|
|
184
|
+
|
|
185
|
+
if (!willRetry) {
|
|
186
|
+
// A banked DG1-only read beats surfacing an error: the user already
|
|
187
|
+
// saw their data come off the chip once this run.
|
|
188
|
+
if (partial) return partial;
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Back to the start: the next attempt re-runs BAC from scratch, which is
|
|
193
|
+
// what a new session requires. The pause lets iOS finish dismissing the
|
|
194
|
+
// sheet and the user reseat the document.
|
|
195
|
+
onStage('waiting');
|
|
196
|
+
await settle();
|
|
197
|
+
if (abandoned()) throw cancelledError();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The attempt budget ran out — a banked partial is still a read.
|
|
202
|
+
if (partial) return partial;
|
|
203
|
+
// Unreachable — the loop either returns or throws — but it keeps the function
|
|
204
|
+
// honest about always producing a result.
|
|
205
|
+
throw lastError ?? new EmrtdSessionError('Could not read the chip.', 'transport');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The read was abandoned mid-loop (step left, or a newer read superseded it). */
|
|
209
|
+
function cancelledError(): EmrtdSessionError {
|
|
210
|
+
return new EmrtdSessionError('The chip read was cancelled.', 'transport');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Per-attempt failure diagnostics, dev builds only.
|
|
215
|
+
*
|
|
216
|
+
* The friendly on-screen message deliberately hides the platform error, which
|
|
217
|
+
* makes field debugging impossible without this line: the STAGE names where in
|
|
218
|
+
* the protocol the read died, and the raw string names why.
|
|
219
|
+
*/
|
|
220
|
+
function devLog(attempt: number, stage: string, err: unknown): void {
|
|
221
|
+
if (typeof __DEV__ === 'undefined' || !__DEV__) return;
|
|
222
|
+
const raw = err instanceof Error ? err.message : String(err ?? '');
|
|
223
|
+
console.log(`[myaza] nfc attempt ${attempt} failed at '${stage}': ${raw}`);
|
|
224
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { EmrtdSessionError } from './session';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Which chip-read failures are worth another session.
|
|
5
|
+
//
|
|
6
|
+
// A dropped session is NOT recoverable in place. iOS tears the whole thing down
|
|
7
|
+
// (`NFCError 202`), so there is no session left to retry an APDU into — and
|
|
8
|
+
// secure messaging cannot survive it either, because the session keys and the
|
|
9
|
+
// send-sequence counter are established by BAC and die with it. The only
|
|
10
|
+
// recovery is a NEW session from the top, which is why this is a decision about
|
|
11
|
+
// restarting rather than about resending.
|
|
12
|
+
//
|
|
13
|
+
// Restarting is the right default because the usual cause is physical: the chip
|
|
14
|
+
// sits in the cover or centre spread of a booklet, and a few millimetres of
|
|
15
|
+
// movement breaks the link. The user has not done anything wrong and the
|
|
16
|
+
// document is fine — asking them to press a button to try what we could simply
|
|
17
|
+
// try again is friction for its own sake.
|
|
18
|
+
//
|
|
19
|
+
// What must NOT be retried is anything a second attempt cannot change:
|
|
20
|
+
// • the user cancelling — they meant it, and reopening the sheet fights them;
|
|
21
|
+
// • BAC failing — the MRZ is check-digit validated before it is used as a
|
|
22
|
+
// key, so a mismatch means this chip belongs to a different document. A
|
|
23
|
+
// retry produces the same failure and wastes the user's time twice.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Total session attempts, including the first. */
|
|
27
|
+
export const MAX_CHIP_ATTEMPTS = 3;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pause before reopening the session, per platform.
|
|
31
|
+
*
|
|
32
|
+
* iOS: the system sheet takes well over a second to animate away after
|
|
33
|
+
* `invalidate()`, and a new `NFCTagReaderSession` begun while it is still
|
|
34
|
+
* dismissing is refused with an immediate `Code=202 "Session invalidated
|
|
35
|
+
* unexpectedly"` — the very error the retry exists to recover from. The
|
|
36
|
+
* previous 700ms sat inside that window, so the recovery attempt died before
|
|
37
|
+
* its sheet ever appeared and the user was shown the 202 message for a blip
|
|
38
|
+
* that was entirely recoverable. The pause also gives them time to reseat the
|
|
39
|
+
* document — the thing most likely to make the next attempt succeed.
|
|
40
|
+
*
|
|
41
|
+
* Android has no sheet to dismiss; it only needs the reseat beat.
|
|
42
|
+
*/
|
|
43
|
+
export function chipRetryDelayMs(os: string): number {
|
|
44
|
+
return os === 'ios' ? 2200 : 700;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* iOS session-level `NFCReaderError` codes that justify another session.
|
|
49
|
+
* 200 (user cancelled) is deliberately absent.
|
|
50
|
+
*/
|
|
51
|
+
const RETRYABLE_IOS_SESSION = /Code=(201|202|203)\b/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* iOS transceive-level codes — how a physical drop surfaces while a command is
|
|
55
|
+
* IN FLIGHT: 100 tag connection lost, 101 retry exceeded, 102 tag response
|
|
56
|
+
* error, 103 session invalidated, 104 tag not connected. During SELECT and BAC
|
|
57
|
+
* — the first seconds of contact, where most drops happen — these arrive RAW,
|
|
58
|
+
* because nothing has wrapped them in an EmrtdSessionError yet. 105 (packet too
|
|
59
|
+
* long) is deliberately absent: that is a protocol bug, and a new session
|
|
60
|
+
* reproduces it exactly.
|
|
61
|
+
*/
|
|
62
|
+
const RETRYABLE_IOS_TRANSCEIVE = /Code=(10[0-4])\b/;
|
|
63
|
+
|
|
64
|
+
/** Android's IsoDep raises `TagLostException("Tag was lost.")` on a drop. */
|
|
65
|
+
const RETRYABLE_ANDROID = /tag was lost/i;
|
|
66
|
+
|
|
67
|
+
/** Whether a failed chip read is worth reopening the session for. */
|
|
68
|
+
export function isRetryableChipError(err: unknown): boolean {
|
|
69
|
+
if (err instanceof EmrtdSessionError) {
|
|
70
|
+
// A chip that stopped answering mid-read is the physical case: the document
|
|
71
|
+
// moved. `bac_failed` and `select_failed` are about WHICH document this is,
|
|
72
|
+
// and a new session cannot change that.
|
|
73
|
+
return err.code === 'read_failed' || err.code === 'transport';
|
|
74
|
+
}
|
|
75
|
+
const raw = err instanceof Error ? err.message : String(err ?? '');
|
|
76
|
+
return (
|
|
77
|
+
RETRYABLE_IOS_SESSION.test(raw) ||
|
|
78
|
+
RETRYABLE_IOS_TRANSCEIVE.test(raw) ||
|
|
79
|
+
RETRYABLE_ANDROID.test(raw)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a failure to OPEN a session is worth another try.
|
|
85
|
+
*
|
|
86
|
+
* Narrower than the mid-read set, because here the codes mean different
|
|
87
|
+
* things: 202 is the radio refusing a session while the previous sheet is
|
|
88
|
+
* still tearing down, and 203 is the radio busy elsewhere — both are states
|
|
89
|
+
* that pass on their own. 201 means the user never presented a document in the
|
|
90
|
+
* whole wait, and 200 that they dismissed the sheet; reopening it fights them.
|
|
91
|
+
*/
|
|
92
|
+
export function isRetryableStartError(err: unknown): boolean {
|
|
93
|
+
const raw = err instanceof Error ? err.message : String(err ?? '');
|
|
94
|
+
// Android: a drop between discovery and connect() surfaces here as
|
|
95
|
+
// TagLostException. Without this arm every Android start failure was
|
|
96
|
+
// terminal — the 3-attempt budget only ever applied to iOS.
|
|
97
|
+
return /Code=(202|203)\b/.test(raw) || RETRYABLE_ANDROID.test(raw);
|
|
98
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { concat, padToBlock, timingSafeEqual, unpadFromBlock } from './bytes';
|
|
2
|
+
import { encodeTlv, readTlvSequence } from './der';
|
|
3
|
+
import {
|
|
4
|
+
BLOCK_SIZE,
|
|
5
|
+
decrypt3Des,
|
|
6
|
+
encrypt3Des,
|
|
7
|
+
macWithPadding,
|
|
8
|
+
type EmrtdPrimitives,
|
|
9
|
+
type SessionKeys,
|
|
10
|
+
} from './crypto';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Secure messaging (ICAO 9303 Part 11 §9.8).
|
|
14
|
+
//
|
|
15
|
+
// After BAC every command and response is wrapped: the data encrypted under
|
|
16
|
+
// KSenc, the whole thing MACed under KSmac, and a send-sequence counter mixed
|
|
17
|
+
// in so neither side can replay a message from earlier in the session.
|
|
18
|
+
//
|
|
19
|
+
// The SSC is the part that is easy to get subtly wrong. It is incremented
|
|
20
|
+
// BEFORE each command and again BEFORE checking the response, so one command
|
|
21
|
+
// consumes two values. Getting that out of step produces a MAC failure on the
|
|
22
|
+
// second command of a session — which looks like a wrong key rather than an
|
|
23
|
+
// off-by-one, and is why it is centralised here in one small object.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const DO87 = 0x87; // Encrypted data, with a padding-content indicator
|
|
27
|
+
const DO85 = 0x85; // Encrypted data, no indicator (some chips use this)
|
|
28
|
+
const DO97 = 0x97; // Expected response length
|
|
29
|
+
const DO99 = 0x99; // Processing status (SW1 SW2)
|
|
30
|
+
const DO8E = 0x8e; // Cryptographic checksum
|
|
31
|
+
|
|
32
|
+
export class SecureMessagingError extends Error {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'SecureMessagingError';
|
|
36
|
+
Object.setPrototypeOf(this, SecureMessagingError.prototype);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** An APDU, before wrapping. */
|
|
41
|
+
export interface CommandApdu {
|
|
42
|
+
cla: number;
|
|
43
|
+
ins: number;
|
|
44
|
+
p1: number;
|
|
45
|
+
p2: number;
|
|
46
|
+
data?: Uint8Array;
|
|
47
|
+
/** Expected response length. Undefined means the command expects none. */
|
|
48
|
+
le?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A secure-messaging session: the keys plus the counter.
|
|
53
|
+
*
|
|
54
|
+
* Stateful ON PURPOSE — the SSC has to advance in lockstep with the exchange,
|
|
55
|
+
* and threading it through every call site is how it drifts.
|
|
56
|
+
*/
|
|
57
|
+
export class SecureMessagingSession {
|
|
58
|
+
private ssc: Uint8Array;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly p: EmrtdPrimitives,
|
|
62
|
+
private readonly keys: SessionKeys,
|
|
63
|
+
initialSsc: Uint8Array,
|
|
64
|
+
) {
|
|
65
|
+
this.ssc = new Uint8Array(initialSsc);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Increment the counter as a big-endian integer, with wraparound. */
|
|
69
|
+
private incrementSsc(): void {
|
|
70
|
+
for (let i = this.ssc.length - 1; i >= 0; i--) {
|
|
71
|
+
this.ssc[i] = (this.ssc[i]! + 1) & 0xff;
|
|
72
|
+
if (this.ssc[i] !== 0) break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Wrap a command for transmission. Consumes one SSC value. */
|
|
77
|
+
protect(apdu: CommandApdu): Uint8Array {
|
|
78
|
+
this.incrementSsc();
|
|
79
|
+
|
|
80
|
+
// The class byte gains the secure-messaging bits, and the MAC is computed
|
|
81
|
+
// over the PADDED header — a detail the standard is easy to misread.
|
|
82
|
+
const cla = apdu.cla | 0x0c;
|
|
83
|
+
const header = padToBlock(
|
|
84
|
+
new Uint8Array([cla, apdu.ins, apdu.p1, apdu.p2]),
|
|
85
|
+
BLOCK_SIZE,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const parts: Uint8Array[] = [];
|
|
89
|
+
|
|
90
|
+
if (apdu.data && apdu.data.length > 0) {
|
|
91
|
+
const encrypted = encrypt3Des(this.p, this.keys.ksEnc, padToBlock(apdu.data, BLOCK_SIZE));
|
|
92
|
+
// The leading 0x01 is the padding-content indicator: "ISO 9797-1 method
|
|
93
|
+
// 2 was used". Omitting it is a common source of chips rejecting DO'87'.
|
|
94
|
+
parts.push(encodeTlv(DO87, concat(new Uint8Array([0x01]), encrypted)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (apdu.le !== undefined) {
|
|
98
|
+
const le =
|
|
99
|
+
apdu.le > 0xff
|
|
100
|
+
? new Uint8Array([(apdu.le >> 8) & 0xff, apdu.le & 0xff])
|
|
101
|
+
: new Uint8Array([apdu.le & 0xff]);
|
|
102
|
+
parts.push(encodeTlv(DO97, le));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = concat(...parts);
|
|
106
|
+
const mac = macWithPadding(this.p, this.keys.ksMac, concat(this.ssc, header, body));
|
|
107
|
+
const checksum = encodeTlv(DO8E, mac);
|
|
108
|
+
|
|
109
|
+
const payload = concat(body, checksum);
|
|
110
|
+
// Le of 0x00 asks for the maximum: the wrapped response is longer than the
|
|
111
|
+
// wrapped command, and a short Le silently truncates it.
|
|
112
|
+
return concat(
|
|
113
|
+
new Uint8Array([cla, apdu.ins, apdu.p1, apdu.p2, payload.length]),
|
|
114
|
+
payload,
|
|
115
|
+
new Uint8Array([0x00]),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Unwrap a response. Consumes one SSC value.
|
|
121
|
+
*
|
|
122
|
+
* Throws when the checksum is absent or does not verify — an unauthenticated
|
|
123
|
+
* response is not data, it is whatever the channel decided to hand back.
|
|
124
|
+
*/
|
|
125
|
+
unprotect(response: Uint8Array): { data: Uint8Array; statusWord: number } {
|
|
126
|
+
this.incrementSsc();
|
|
127
|
+
|
|
128
|
+
const parsed = readTlvSequence(response);
|
|
129
|
+
const checksumIndex = parsed.findIndex((o) => o.tag === DO8E);
|
|
130
|
+
if (checksumIndex < 0) throw new SecureMessagingError('The response carried no checksum.');
|
|
131
|
+
const checksum = parsed[checksumIndex]!;
|
|
132
|
+
|
|
133
|
+
// The MAC covers the objects BEFORE DO'8E' — everything after it is outside
|
|
134
|
+
// the protected data. Taking a prefix rather than filtering matters: a
|
|
135
|
+
// response that still carries its outer status word parses `9000` as a
|
|
136
|
+
// perfectly valid empty TLV, and folding that into the MAC fails every
|
|
137
|
+
// verification for a reason nothing about the message reveals.
|
|
138
|
+
const objects = parsed.slice(0, checksumIndex);
|
|
139
|
+
const macInput = concat(
|
|
140
|
+
this.ssc,
|
|
141
|
+
...objects.map((o) => encodeTlv(o.tag, o.value)),
|
|
142
|
+
);
|
|
143
|
+
const expected = macWithPadding(this.p, this.keys.ksMac, macInput);
|
|
144
|
+
if (!timingSafeEqual(expected, checksum.value)) {
|
|
145
|
+
throw new SecureMessagingError('The response failed its integrity check.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const statusObject = objects.find((o) => o.tag === DO99);
|
|
149
|
+
// The status comes from DO'99', NOT from the outer status word. A chip can
|
|
150
|
+
// return 0x9000 on the wrapper while reporting a failure inside — reading
|
|
151
|
+
// the outer word treats that as success.
|
|
152
|
+
const statusWord =
|
|
153
|
+
statusObject && statusObject.value.length >= 2
|
|
154
|
+
? (statusObject.value[0]! << 8) | statusObject.value[1]!
|
|
155
|
+
: 0x0000;
|
|
156
|
+
|
|
157
|
+
const encrypted = objects.find((o) => o.tag === DO87 || o.tag === DO85);
|
|
158
|
+
if (!encrypted) return { data: new Uint8Array(0), statusWord };
|
|
159
|
+
|
|
160
|
+
// DO'87' carries the padding-content indicator as its first byte; DO'85'
|
|
161
|
+
// does not.
|
|
162
|
+
const body =
|
|
163
|
+
encrypted.tag === DO87 ? encrypted.value.subarray(1) : encrypted.value;
|
|
164
|
+
if (body.length === 0 || body.length % BLOCK_SIZE !== 0) {
|
|
165
|
+
throw new SecureMessagingError('The response ciphertext was misaligned.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
data: unpadFromBlock(decrypt3Des(this.p, this.keys.ksEnc, body), BLOCK_SIZE),
|
|
170
|
+
statusWord,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The current counter — for diagnostics and tests only. */
|
|
175
|
+
get sendSequenceCounter(): Uint8Array {
|
|
176
|
+
return new Uint8Array(this.ssc);
|
|
177
|
+
}
|
|
178
|
+
}
|