@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/bac.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { concat, timingSafeEqual } from './bytes';
|
|
2
|
+
import {
|
|
3
|
+
decrypt3Des,
|
|
4
|
+
deriveSessionKeys,
|
|
5
|
+
encrypt3Des,
|
|
6
|
+
keySeed,
|
|
7
|
+
macWithPadding,
|
|
8
|
+
type EmrtdPrimitives,
|
|
9
|
+
type MrzKeyFields,
|
|
10
|
+
type SessionKeys,
|
|
11
|
+
} from './crypto';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Basic Access Control (ICAO 9303 Part 11 §4.3).
|
|
15
|
+
//
|
|
16
|
+
// A mutual authentication that proves both sides know a key derived from the
|
|
17
|
+
// printed MRZ — which is the point: the chip only talks to someone who has
|
|
18
|
+
// physically seen the document.
|
|
19
|
+
//
|
|
20
|
+
// 1. Ask the chip for a challenge (RND.IC).
|
|
21
|
+
// 2. Send back RND.IFD | RND.IC | K.IFD, encrypted and MACed under the MRZ
|
|
22
|
+
// keys.
|
|
23
|
+
// 3. The chip returns RND.IC | RND.IFD | K.IC the same way.
|
|
24
|
+
// 4. Both sides XOR K.IFD with K.IC to get the session seed.
|
|
25
|
+
//
|
|
26
|
+
// Step 3 is where the chip is AUTHENTICATED: a chip that cannot produce a valid
|
|
27
|
+
// MAC, or that echoes back the wrong nonce, does not hold the key. Both checks
|
|
28
|
+
// below are mandatory — skipping the nonce check in particular would accept a
|
|
29
|
+
// replayed response from an earlier session.
|
|
30
|
+
//
|
|
31
|
+
// Pure: no I/O. The caller does the APDU exchange, which is what makes this
|
|
32
|
+
// testable against the standard's worked example.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** What to send the chip, and what is needed to check its reply. */
|
|
36
|
+
export interface BacChallenge {
|
|
37
|
+
/** The EXTERNAL AUTHENTICATE command data: E.IFD | M.IFD (40 bytes). */
|
|
38
|
+
command: Uint8Array;
|
|
39
|
+
/** Kept for the reply check — never leaves this process. */
|
|
40
|
+
rndIfd: Uint8Array;
|
|
41
|
+
rndIc: Uint8Array;
|
|
42
|
+
kIfd: Uint8Array;
|
|
43
|
+
kEnc: Uint8Array;
|
|
44
|
+
kMac: Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the mutual-authenticate command from the chip's challenge.
|
|
49
|
+
*
|
|
50
|
+
* `rndIfd` and `kIfd` are injectable so the ICAO worked example can be
|
|
51
|
+
* reproduced exactly; production leaves them out and takes real randomness.
|
|
52
|
+
* They MUST be unpredictable in production — a fixed RND.IFD makes the whole
|
|
53
|
+
* exchange replayable.
|
|
54
|
+
*/
|
|
55
|
+
export function buildBacChallenge(
|
|
56
|
+
p: EmrtdPrimitives,
|
|
57
|
+
mrz: MrzKeyFields,
|
|
58
|
+
rndIc: Uint8Array,
|
|
59
|
+
fixed?: { rndIfd?: Uint8Array; kIfd?: Uint8Array },
|
|
60
|
+
): BacChallenge {
|
|
61
|
+
const seed = keySeed(p, mrz);
|
|
62
|
+
const { ksEnc: kEnc, ksMac: kMac } = deriveSessionKeys(p, seed);
|
|
63
|
+
|
|
64
|
+
const rndIfd = fixed?.rndIfd ?? p.randomBytes(8);
|
|
65
|
+
const kIfd = fixed?.kIfd ?? p.randomBytes(16);
|
|
66
|
+
|
|
67
|
+
const s = concat(rndIfd, rndIc, kIfd);
|
|
68
|
+
const eIfd = encrypt3Des(p, kEnc, s);
|
|
69
|
+
const mIfd = macWithPadding(p, kMac, eIfd);
|
|
70
|
+
|
|
71
|
+
return { command: concat(eIfd, mIfd), rndIfd, rndIc, kIfd, kEnc, kMac };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class BacError extends Error {
|
|
75
|
+
constructor(message: string) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = 'BacError';
|
|
78
|
+
Object.setPrototypeOf(this, BacError.prototype);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Verify the chip's reply and derive the session.
|
|
84
|
+
*
|
|
85
|
+
* Throws {@link BacError} when the chip fails to prove it holds the key. That
|
|
86
|
+
* is not a "retry" condition — it means the MRZ was read wrong, or the thing
|
|
87
|
+
* answering is not the document.
|
|
88
|
+
*/
|
|
89
|
+
export function completeBac(
|
|
90
|
+
p: EmrtdPrimitives,
|
|
91
|
+
challenge: BacChallenge,
|
|
92
|
+
response: Uint8Array,
|
|
93
|
+
): { keys: SessionKeys; ssc: Uint8Array } {
|
|
94
|
+
if (response.length !== 40) {
|
|
95
|
+
throw new BacError(`Unexpected response length ${response.length}; expected 40.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const eIc = response.subarray(0, 32);
|
|
99
|
+
const mIc = response.subarray(32, 40);
|
|
100
|
+
|
|
101
|
+
// The MAC first: without it, everything decrypted below is attacker-chosen.
|
|
102
|
+
if (!timingSafeEqual(macWithPadding(p, challenge.kMac, eIc), mIc)) {
|
|
103
|
+
throw new BacError('The chip’s response failed its integrity check.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const r = decrypt3Des(p, challenge.kEnc, eIc);
|
|
107
|
+
const rndIcEcho = r.subarray(0, 8);
|
|
108
|
+
const rndIfdEcho = r.subarray(8, 16);
|
|
109
|
+
const kIc = r.subarray(16, 32);
|
|
110
|
+
|
|
111
|
+
// Both nonces must come back. Checking only the MAC would accept a replayed
|
|
112
|
+
// response from an earlier session with the same document.
|
|
113
|
+
if (!timingSafeEqual(rndIcEcho, challenge.rndIc)) {
|
|
114
|
+
throw new BacError('The chip echoed the wrong challenge.');
|
|
115
|
+
}
|
|
116
|
+
if (!timingSafeEqual(rndIfdEcho, challenge.rndIfd)) {
|
|
117
|
+
throw new BacError('The chip echoed the wrong terminal nonce.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const seed = new Uint8Array(16);
|
|
121
|
+
for (let i = 0; i < 16; i++) seed[i] = challenge.kIfd[i]! ^ kIc[i]!;
|
|
122
|
+
|
|
123
|
+
// The send-sequence counter starts as the low four bytes of each nonce, in
|
|
124
|
+
// that order — it is incremented before every secure-messaging command.
|
|
125
|
+
const ssc = concat(challenge.rndIc.subarray(4, 8), challenge.rndIfd.subarray(4, 8));
|
|
126
|
+
|
|
127
|
+
return { keys: deriveSessionKeys(p, seed), ssc };
|
|
128
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Byte helpers.
|
|
3
|
+
//
|
|
4
|
+
// React Native has no Buffer and no Node crypto, and the native bridge speaks
|
|
5
|
+
// base64, so these are the conversions everything else in the eMRTD code sits
|
|
6
|
+
// on. Kept dependency-free and exact: a subtle bug here would surface as a
|
|
7
|
+
// failed MAC three layers up, where it looks like a wrong key.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
11
|
+
|
|
12
|
+
/** Bytes → base64. */
|
|
13
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
14
|
+
let out = '';
|
|
15
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
16
|
+
const b0 = bytes[i]!;
|
|
17
|
+
const b1 = bytes[i + 1];
|
|
18
|
+
const b2 = bytes[i + 2];
|
|
19
|
+
out += B64_CHARS[b0 >> 2];
|
|
20
|
+
out += B64_CHARS[((b0 & 0x03) << 4) | ((b1 ?? 0) >> 4)];
|
|
21
|
+
out += b1 === undefined ? '=' : B64_CHARS[((b1 & 0x0f) << 2) | ((b2 ?? 0) >> 6)];
|
|
22
|
+
out += b2 === undefined ? '=' : B64_CHARS[b2 & 0x3f];
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** base64 → bytes. Ignores whitespace and padding. */
|
|
28
|
+
export function fromBase64(b64: string): Uint8Array {
|
|
29
|
+
const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');
|
|
30
|
+
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
|
|
31
|
+
let o = 0;
|
|
32
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
33
|
+
const c0 = B64_CHARS.indexOf(clean[i]!);
|
|
34
|
+
const c1 = B64_CHARS.indexOf(clean[i + 1] ?? 'A');
|
|
35
|
+
const c2 = clean[i + 2] === undefined ? -1 : B64_CHARS.indexOf(clean[i + 2]!);
|
|
36
|
+
const c3 = clean[i + 3] === undefined ? -1 : B64_CHARS.indexOf(clean[i + 3]!);
|
|
37
|
+
out[o++] = (c0 << 2) | (c1 >> 4);
|
|
38
|
+
if (c2 >= 0) out[o++] = ((c1 & 0x0f) << 4) | (c2 >> 2);
|
|
39
|
+
if (c3 >= 0) out[o++] = ((c2 & 0x03) << 6) | c3;
|
|
40
|
+
}
|
|
41
|
+
return out.subarray(0, o);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Bytes → lowercase hex. For logs and test vectors. */
|
|
45
|
+
export function toHex(bytes: Uint8Array): string {
|
|
46
|
+
let out = '';
|
|
47
|
+
for (const b of bytes) out += b.toString(16).padStart(2, '0');
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Hex → bytes. Whitespace is ignored so spec vectors can be pasted as-is. */
|
|
52
|
+
export function fromHex(hex: string): Uint8Array {
|
|
53
|
+
const clean = hex.replace(/\s+/g, '');
|
|
54
|
+
const out = new Uint8Array(clean.length / 2);
|
|
55
|
+
for (let i = 0; i < out.length; i++) {
|
|
56
|
+
out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** ASCII → bytes. The MRZ alphabet is ASCII by definition. */
|
|
62
|
+
export function fromAscii(text: string): Uint8Array {
|
|
63
|
+
const out = new Uint8Array(text.length);
|
|
64
|
+
for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 0xff;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function concat(...parts: Uint8Array[]): Uint8Array {
|
|
69
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
70
|
+
const out = new Uint8Array(total);
|
|
71
|
+
let o = 0;
|
|
72
|
+
for (const p of parts) {
|
|
73
|
+
out.set(p, o);
|
|
74
|
+
o += p.length;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function xor(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
80
|
+
const out = new Uint8Array(Math.min(a.length, b.length));
|
|
81
|
+
for (let i = 0; i < out.length; i++) out[i] = a[i]! ^ b[i]!;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Constant-time equality.
|
|
87
|
+
*
|
|
88
|
+
* Used for MAC comparison. A short-circuiting `===` over a MAC leaks, through
|
|
89
|
+
* timing, how many leading bytes were right — which is enough to forge one byte
|
|
90
|
+
* at a time. The cost of doing it properly is nothing.
|
|
91
|
+
*/
|
|
92
|
+
export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
93
|
+
if (a.length !== b.length) return false;
|
|
94
|
+
let diff = 0;
|
|
95
|
+
for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;
|
|
96
|
+
return diff === 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ISO/IEC 9797-1 padding method 2: append 0x80, then zeros to a block edge.
|
|
101
|
+
* Applied to plaintext before encryption AND to data before MACing.
|
|
102
|
+
*/
|
|
103
|
+
export function padToBlock(data: Uint8Array, blockSize = 8): Uint8Array {
|
|
104
|
+
// The 0x80 is ALWAYS appended, so already-aligned input grows by a whole
|
|
105
|
+
// block. That is the standard's rule and it is what makes the padding
|
|
106
|
+
// unambiguously strippable — without it, data ending in 0x80 would be
|
|
107
|
+
// indistinguishable from padding.
|
|
108
|
+
const size = Math.ceil((data.length + 1) / blockSize) * blockSize;
|
|
109
|
+
const out = new Uint8Array(size);
|
|
110
|
+
out.set(data);
|
|
111
|
+
out[data.length] = 0x80;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Strip ISO 9797-1 method 2 padding.
|
|
117
|
+
*
|
|
118
|
+
* Returns the input unchanged when it does not end in a valid padding run — a
|
|
119
|
+
* chip that pads differently should cost us nothing.
|
|
120
|
+
*/
|
|
121
|
+
export function unpadFromBlock(data: Uint8Array, blockSize = 8): Uint8Array {
|
|
122
|
+
for (let i = data.length - 1; i >= 0 && data.length - i <= blockSize; i--) {
|
|
123
|
+
if (data[i] === 0x80) return data.subarray(0, i);
|
|
124
|
+
if (data[i] !== 0x00) break;
|
|
125
|
+
}
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { concat, fromBase64, padToBlock, toBase64, xor } from './bytes';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// eMRTD crypto (ICAO 9303 Part 11).
|
|
5
|
+
//
|
|
6
|
+
// The small set of operations BAC is built from: two-key 3DES in CBC mode, the
|
|
7
|
+
// ISO 9797-1 Algorithm 3 "retail MAC", and the key-derivation function.
|
|
8
|
+
//
|
|
9
|
+
// The BLOCK CIPHERS come from the platform (see MyazaEmrtd.nitro.ts); the
|
|
10
|
+
// CONSTRUCTIONS on top of them are here, in TypeScript, because that is the
|
|
11
|
+
// part the standard specifies and the part worth testing. Every function below
|
|
12
|
+
// is checked against the worked example in ICAO 9303 Part 11 Appendix D — which
|
|
13
|
+
// is what makes this trustworthy without a passport in hand.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** The block-cipher and digest operations the protocol needs. */
|
|
17
|
+
export interface EmrtdPrimitives {
|
|
18
|
+
sha1(data: Uint8Array): Uint8Array;
|
|
19
|
+
sha256(data: Uint8Array): Uint8Array;
|
|
20
|
+
desEde2Cbc(key16: Uint8Array, data: Uint8Array, iv: Uint8Array, encrypt: boolean): Uint8Array;
|
|
21
|
+
desBlock(key8: Uint8Array, block: Uint8Array, encrypt: boolean): Uint8Array;
|
|
22
|
+
aesCbc(key: Uint8Array, data: Uint8Array, iv: Uint8Array, encrypt: boolean): Uint8Array;
|
|
23
|
+
aesCmac(key: Uint8Array, data: Uint8Array): Uint8Array;
|
|
24
|
+
randomBytes(length: number): Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Cipher-block size for DES/3DES, and the unit padding aligns to. */
|
|
28
|
+
export const BLOCK_SIZE = 8;
|
|
29
|
+
|
|
30
|
+
const ZERO_IV = new Uint8Array(BLOCK_SIZE);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ISO 9797-1 Algorithm 3 MAC ("retail MAC"): single-DES-CBC the whole message
|
|
34
|
+
* under the first half of the key, then decrypt the final block with the second
|
|
35
|
+
* half and re-encrypt with the first.
|
|
36
|
+
*
|
|
37
|
+
* `data` must already be padded — the caller pads, because the same padded
|
|
38
|
+
* bytes are needed for the MAC and (in secure messaging) for the length field.
|
|
39
|
+
*/
|
|
40
|
+
export function retailMac(
|
|
41
|
+
p: EmrtdPrimitives,
|
|
42
|
+
key16: Uint8Array,
|
|
43
|
+
data: Uint8Array,
|
|
44
|
+
): Uint8Array {
|
|
45
|
+
const k1 = key16.subarray(0, 8);
|
|
46
|
+
const k2 = key16.subarray(8, 16);
|
|
47
|
+
|
|
48
|
+
// CBC by hand: the primitive is a raw block cipher, and chaining is one XOR.
|
|
49
|
+
let chain: Uint8Array = new Uint8Array(BLOCK_SIZE);
|
|
50
|
+
for (let off = 0; off < data.length; off += BLOCK_SIZE) {
|
|
51
|
+
const input = xor(data.subarray(off, off + BLOCK_SIZE), chain);
|
|
52
|
+
chain = p.desBlock(k1, input, true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return p.desBlock(k1, p.desBlock(k2, chain, false), true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set the (cryptographically ignored) DES parity bits.
|
|
60
|
+
*
|
|
61
|
+
* DES discards the low bit of every key byte, so this changes nothing about the
|
|
62
|
+
* cipher — but the standard's worked examples carry adjusted keys, and matching
|
|
63
|
+
* them is how the derivation is verified.
|
|
64
|
+
*/
|
|
65
|
+
export function adjustParity(key: Uint8Array): Uint8Array {
|
|
66
|
+
const out = new Uint8Array(key);
|
|
67
|
+
for (let i = 0; i < out.length; i++) {
|
|
68
|
+
let ones = 0;
|
|
69
|
+
for (let bit = 1; bit < 8; bit++) if ((out[i]! & (1 << bit)) !== 0) ones++;
|
|
70
|
+
out[i] = ones % 2 === 0 ? out[i]! | 0x01 : out[i]! & 0xfe;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* SHA-1 over a seed plus a 4-byte counter — the raw digest, before any
|
|
77
|
+
* key-specific treatment. The counter is 1 for the encryption key, 2 for the
|
|
78
|
+
* MAC key, 3 for the PACE password key.
|
|
79
|
+
*/
|
|
80
|
+
export function deriveDigestSha1(
|
|
81
|
+
p: EmrtdPrimitives,
|
|
82
|
+
seed: Uint8Array,
|
|
83
|
+
counter: number,
|
|
84
|
+
): Uint8Array {
|
|
85
|
+
return p.sha1(concat(seed, new Uint8Array([0, 0, 0, counter])));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* ICAO 9303 key derivation for 3DES: the leading 16 digest bytes, parity
|
|
90
|
+
* adjusted. AES keys skip the parity step — it is a DES-only convention, and
|
|
91
|
+
* applying it to an AES key silently produces the wrong key.
|
|
92
|
+
*/
|
|
93
|
+
export function deriveKey(
|
|
94
|
+
p: EmrtdPrimitives,
|
|
95
|
+
seed: Uint8Array,
|
|
96
|
+
counter: number,
|
|
97
|
+
): Uint8Array {
|
|
98
|
+
return adjustParity(deriveDigestSha1(p, seed, counter).subarray(0, 16));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The MRZ-derived key lives in ./mrzKey; re-exported so callers keep one import.
|
|
102
|
+
export { keySeed, mrzKeySeedInput, type MrzKeyFields } from './mrzKey';
|
|
103
|
+
|
|
104
|
+
/** The pair of session keys derived from a seed. */
|
|
105
|
+
export interface SessionKeys {
|
|
106
|
+
/** Encryption key (counter 1). */
|
|
107
|
+
ksEnc: Uint8Array;
|
|
108
|
+
/** MAC key (counter 2). */
|
|
109
|
+
ksMac: Uint8Array;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function deriveSessionKeys(p: EmrtdPrimitives, seed: Uint8Array): SessionKeys {
|
|
113
|
+
return { ksEnc: deriveKey(p, seed, 1), ksMac: deriveKey(p, seed, 2) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 3DES-CBC with the zero IV that BAC specifies. */
|
|
117
|
+
export function encrypt3Des(
|
|
118
|
+
p: EmrtdPrimitives,
|
|
119
|
+
key16: Uint8Array,
|
|
120
|
+
data: Uint8Array,
|
|
121
|
+
): Uint8Array {
|
|
122
|
+
return p.desEde2Cbc(key16, data, ZERO_IV, true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function decrypt3Des(
|
|
126
|
+
p: EmrtdPrimitives,
|
|
127
|
+
key16: Uint8Array,
|
|
128
|
+
data: Uint8Array,
|
|
129
|
+
): Uint8Array {
|
|
130
|
+
return p.desEde2Cbc(key16, data, ZERO_IV, false);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** MAC over already-unpadded data — pads, then runs the retail MAC. */
|
|
134
|
+
export function macWithPadding(
|
|
135
|
+
p: EmrtdPrimitives,
|
|
136
|
+
key16: Uint8Array,
|
|
137
|
+
data: Uint8Array,
|
|
138
|
+
): Uint8Array {
|
|
139
|
+
return retailMac(p, key16, padToBlock(data, BLOCK_SIZE));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Adapts the native HybridObject (base64 in and out) to {@link EmrtdPrimitives}. */
|
|
143
|
+
export function primitivesFromNative(native: {
|
|
144
|
+
sha1(d: string): string;
|
|
145
|
+
sha256(d: string): string;
|
|
146
|
+
desEde2Cbc(k: string, d: string, iv: string, e: boolean): string;
|
|
147
|
+
desBlock(k: string, b: string, e: boolean): string;
|
|
148
|
+
aesCbc(k: string, d: string, iv: string, e: boolean): string;
|
|
149
|
+
aesCmac(k: string, d: string): string;
|
|
150
|
+
randomBytes(n: number): string;
|
|
151
|
+
}): EmrtdPrimitives {
|
|
152
|
+
const b = fromBase64;
|
|
153
|
+
const s = toBase64;
|
|
154
|
+
return {
|
|
155
|
+
sha1: (d) => b(native.sha1(s(d))),
|
|
156
|
+
sha256: (d) => b(native.sha256(s(d))),
|
|
157
|
+
desEde2Cbc: (k, d, iv, e) => b(native.desEde2Cbc(s(k), s(d), s(iv), e)),
|
|
158
|
+
desBlock: (k, blk, e) => b(native.desBlock(s(k), s(blk), e)),
|
|
159
|
+
aesCbc: (k, d, iv, e) => b(native.aesCbc(s(k), s(d), s(iv), e)),
|
|
160
|
+
aesCmac: (k, d) => b(native.aesCmac(s(k), s(d))),
|
|
161
|
+
randomBytes: (n) => b(native.randomBytes(n)),
|
|
162
|
+
};
|
|
163
|
+
}
|
package/src/emrtd/der.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// A minimal BER-TLV / DER reader.
|
|
3
|
+
//
|
|
4
|
+
// Every structure the chip returns is BER-TLV: the elementary files, the
|
|
5
|
+
// security object, the secure-messaging response objects. This reads exactly
|
|
6
|
+
// what those need and nothing more — a general ASN.1 library would be a large
|
|
7
|
+
// dependency for a handful of shapes, and the parsing surface here is exposed
|
|
8
|
+
// to whatever a chip (or something impersonating one) sends.
|
|
9
|
+
//
|
|
10
|
+
// Which is why it is DEFENSIVE throughout: every length is checked against the
|
|
11
|
+
// remaining buffer before it is used. A malformed tag returns null rather than
|
|
12
|
+
// reading past the end.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface Tlv {
|
|
16
|
+
/** The tag, as an integer — 0x5F1F for a two-byte tag, and so on. */
|
|
17
|
+
tag: number;
|
|
18
|
+
/** The value, as a view into the source buffer (no copy). */
|
|
19
|
+
value: Uint8Array;
|
|
20
|
+
/** Offset just past this element, for iterating siblings. */
|
|
21
|
+
end: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read one TLV at `offset`, or null when the buffer does not hold a complete,
|
|
26
|
+
* well-formed element there.
|
|
27
|
+
*/
|
|
28
|
+
export function readTlv(data: Uint8Array, offset = 0): Tlv | null {
|
|
29
|
+
if (offset >= data.length) return null;
|
|
30
|
+
|
|
31
|
+
// Tag: low five bits all set means the tag number continues into further
|
|
32
|
+
// bytes, each of which carries a continuation flag in its top bit.
|
|
33
|
+
let tag = data[offset]!;
|
|
34
|
+
let i = offset + 1;
|
|
35
|
+
if ((tag & 0x1f) === 0x1f) {
|
|
36
|
+
// Multi-byte tag: each subsequent byte continues the number while its top
|
|
37
|
+
// bit is set. Bounded at four bytes — nothing a passport emits is longer,
|
|
38
|
+
// and an unbounded loop here is exactly what a crafted file would exploit.
|
|
39
|
+
for (let n = 0; ; n++) {
|
|
40
|
+
if (i >= data.length || n >= 3) return null;
|
|
41
|
+
const byte = data[i]!;
|
|
42
|
+
tag = tag * 256 + byte;
|
|
43
|
+
i++;
|
|
44
|
+
if ((byte & 0x80) === 0) break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (i >= data.length) return null;
|
|
49
|
+
|
|
50
|
+
// Length: short form is one byte under 0x80; long form's low bits give the
|
|
51
|
+
// number of length bytes that follow.
|
|
52
|
+
let length = data[i]!;
|
|
53
|
+
i++;
|
|
54
|
+
if ((length & 0x80) !== 0) {
|
|
55
|
+
const count = length & 0x7f;
|
|
56
|
+
// 0x80 alone is the indefinite form, which DER forbids and a chip will not
|
|
57
|
+
// send; more than four length bytes exceeds anything addressable here.
|
|
58
|
+
if (count === 0 || count > 4 || i + count > data.length) return null;
|
|
59
|
+
length = 0;
|
|
60
|
+
for (let n = 0; n < count; n++) {
|
|
61
|
+
length = length * 256 + data[i]!;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (i + length > data.length) return null;
|
|
67
|
+
return { tag, value: data.subarray(i, i + length), end: i + length };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Just the tag and length, without needing the value to be present. */
|
|
71
|
+
export interface TlvHeader {
|
|
72
|
+
tag: number;
|
|
73
|
+
/** Bytes the tag and length occupy. */
|
|
74
|
+
headerLength: number;
|
|
75
|
+
/** Bytes of value that FOLLOW the header. */
|
|
76
|
+
valueLength: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read a TLV header from a prefix of the element.
|
|
81
|
+
*
|
|
82
|
+
* `readTlv` requires the whole value to be present, which is exactly what the
|
|
83
|
+
* caller does not have when probing a chip file's size — the point of the probe
|
|
84
|
+
* is to LEARN that size. This reads only as far as the length field.
|
|
85
|
+
*/
|
|
86
|
+
export function readTlvHeader(data: Uint8Array, offset = 0): TlvHeader | null {
|
|
87
|
+
if (offset >= data.length) return null;
|
|
88
|
+
|
|
89
|
+
let tag = data[offset]!;
|
|
90
|
+
let i = offset + 1;
|
|
91
|
+
if ((tag & 0x1f) === 0x1f) {
|
|
92
|
+
for (let n = 0; ; n++) {
|
|
93
|
+
if (i >= data.length || n >= 3) return null;
|
|
94
|
+
const byte = data[i]!;
|
|
95
|
+
tag = tag * 256 + byte;
|
|
96
|
+
i++;
|
|
97
|
+
if ((byte & 0x80) === 0) break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (i >= data.length) return null;
|
|
102
|
+
let valueLength = data[i]!;
|
|
103
|
+
i++;
|
|
104
|
+
if ((valueLength & 0x80) !== 0) {
|
|
105
|
+
const count = valueLength & 0x7f;
|
|
106
|
+
if (count === 0 || count > 4 || i + count > data.length) return null;
|
|
107
|
+
valueLength = 0;
|
|
108
|
+
for (let n = 0; n < count; n++) {
|
|
109
|
+
valueLength = valueLength * 256 + data[i]!;
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { tag, headerLength: i - offset, valueLength };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Every TLV at this level, in order. Stops at the first malformed element. */
|
|
118
|
+
export function readTlvSequence(data: Uint8Array, offset = 0): Tlv[] {
|
|
119
|
+
const out: Tlv[] = [];
|
|
120
|
+
let i = offset;
|
|
121
|
+
while (i < data.length) {
|
|
122
|
+
const tlv = readTlv(data, i);
|
|
123
|
+
if (!tlv) break;
|
|
124
|
+
out.push(tlv);
|
|
125
|
+
// A zero-length element at the same offset would loop forever.
|
|
126
|
+
if (tlv.end <= i) break;
|
|
127
|
+
i = tlv.end;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The first TLV with this tag at the top level, or null. */
|
|
133
|
+
export function findTlv(data: Uint8Array, tag: number): Tlv | null {
|
|
134
|
+
return readTlvSequence(data).find((t) => t.tag === tag) ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Depth-first search for a tag at any nesting level.
|
|
139
|
+
*
|
|
140
|
+
* Constructed elements (bit 6 of the first tag byte) are descended into. The
|
|
141
|
+
* depth is bounded because a crafted file could otherwise nest deeply enough to
|
|
142
|
+
* exhaust the stack.
|
|
143
|
+
*/
|
|
144
|
+
export function findTlvDeep(data: Uint8Array, tag: number, maxDepth = 12): Tlv | null {
|
|
145
|
+
if (maxDepth <= 0) return null;
|
|
146
|
+
for (const tlv of readTlvSequence(data)) {
|
|
147
|
+
if (tlv.tag === tag) return tlv;
|
|
148
|
+
const firstTagByte = tagFirstByte(tlv.tag);
|
|
149
|
+
if ((firstTagByte & 0x20) !== 0) {
|
|
150
|
+
const nested = findTlvDeep(tlv.value, tag, maxDepth - 1);
|
|
151
|
+
if (nested) return nested;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The leading byte of a (possibly multi-byte) tag. */
|
|
158
|
+
function tagFirstByte(tag: number): number {
|
|
159
|
+
let t = tag;
|
|
160
|
+
while (t > 0xff) t >>= 8;
|
|
161
|
+
return t;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Encode a TLV.
|
|
166
|
+
*
|
|
167
|
+
* Only the short and two-byte long forms are emitted, which covers every
|
|
168
|
+
* command this SDK builds — the longest is a DG2 read, and command data never
|
|
169
|
+
* approaches 64KB.
|
|
170
|
+
*/
|
|
171
|
+
export function encodeTlv(tag: number, value: Uint8Array): Uint8Array {
|
|
172
|
+
const tagBytes: number[] = [];
|
|
173
|
+
let t = tag;
|
|
174
|
+
while (t > 0) {
|
|
175
|
+
tagBytes.unshift(t & 0xff);
|
|
176
|
+
t >>= 8;
|
|
177
|
+
}
|
|
178
|
+
if (tagBytes.length === 0) tagBytes.push(0);
|
|
179
|
+
|
|
180
|
+
const lengthBytes: number[] =
|
|
181
|
+
value.length < 0x80
|
|
182
|
+
? [value.length]
|
|
183
|
+
: value.length <= 0xff
|
|
184
|
+
? [0x81, value.length]
|
|
185
|
+
: [0x82, (value.length >> 8) & 0xff, value.length & 0xff];
|
|
186
|
+
|
|
187
|
+
const out = new Uint8Array(tagBytes.length + lengthBytes.length + value.length);
|
|
188
|
+
out.set(tagBytes, 0);
|
|
189
|
+
out.set(lengthBytes, tagBytes.length);
|
|
190
|
+
out.set(value, tagBytes.length + lengthBytes.length);
|
|
191
|
+
return out;
|
|
192
|
+
}
|