Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 151 additions & 70 deletions app/ios/LiveMRZScannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct LiveMRZScannerView: View {
@State private var lastMRZDetection: Date = Date()
@State private var parsedMRZ: QKMRZResult? = nil
@State private var scanComplete: Bool = false
@State private var overrideDocumentNumber: String? = nil // for TD1 overflow format (ID cards)
var onScanComplete: ((QKMRZResult) -> Void)? = nil
var onScanResultAsDict: (([String: Any]) -> Void)? = nil

Expand Down Expand Up @@ -60,12 +61,17 @@ struct LiveMRZScannerView: View {
}

private func mapVisionResultToDictionary(_ result: QKMRZResult) -> [String: Any] {

// using manually validated document number for TD1 documents with overflow format
// this is necessary for NFC chip authentication which requires the full document number
let documentNumber = overrideDocumentNumber ?? result.documentNumber

return [
"documentType": result.documentType,
"countryCode": result.countryCode,
"surnames": result.surnames,
"givenNames": result.givenNames,
"documentNumber": result.documentNumber,
"documentNumber": documentNumber, // using the overriden if available
"nationalityCountryCode": result.nationalityCountryCode,
"dateOfBirth": result.birthdate?.description ?? "",
"sex": result.sex ?? "",
Expand All @@ -80,44 +86,106 @@ struct LiveMRZScannerView: View {
]
}

private func correctBelgiumDocumentNumber(result: String) -> String? {
// Belgium TD1 format: IDBEL000001115<7027
let line1RegexPattern = "IDBEL(?<doc9>[A-Z0-9]{9})<(?<doc3>[A-Z0-9<]{3})(?<checkDigit>\\d)"
guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil }
let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count))
/// Calculates the MRZ check digit using the ICAO 9303 standard
private func calculateMRZCheckDigit(_ input: String) -> Int {
let weights = [7, 3, 1]
var sum = 0

for (index, char) in input.enumerated() {
let value: Int
if char.isNumber {
value = Int(String(char)) ?? 0
} else if char.isLetter {
// mapping letters to values: A=10, B=11, ..., Z=35
value = Int(char.asciiValue ?? 0) - Int(Character("A").asciiValue ?? 0) + 10
} else if char == "<" {
value = 0
} else {
value = 0
}

if let line1Matcher = line1Matcher {
let doc9Range = line1Matcher.range(withName: "doc9")
let doc3Range = line1Matcher.range(withName: "doc3")
let checkDigitRange = line1Matcher.range(withName: "checkDigit")
let weight = weights[index % 3]
sum += value * weight
}

let doc9 = (result as NSString).substring(with: doc9Range)
let doc3 = (result as NSString).substring(with: doc3Range)
let checkDigit = (result as NSString).substring(with: checkDigitRange)
return sum % 10
}

if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) {
let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)"
return correctedMRZLine
}
/// Extracts and validates the document number from TD1 MRZ line 1, handling both standard and overflow formats.
/// TD1 format uses an overflow mechanism when document numbers exceed 9 digits.
/// Example overflow format: IDBEL595392450<8039<<<<<<<<<< where positions 6-14 contain the principal part (595392450),
/// position 15 contains the overflow indicator (<), positions 16-18 contain overflow digits (803), and position 19 contains the check digit (9).
/// The full document number becomes: 595392450803.
/// This overflow format can occur for any country using TD1 MRZ (ID cards).
private func extractAndValidateTD1DocumentNumber(line1: String) -> (documentNumber: String, isValid: Bool)? {
guard line1.count == 30 else { return nil }

// extracting positions 6-14 (9 characters - principal part)
let startIndex6 = line1.index(line1.startIndex, offsetBy: 5)
let endIndex14 = line1.index(line1.startIndex, offsetBy: 14)
let principalPart = String(line1[startIndex6..<endIndex14])

// checking position 15 for overflow indicator
let pos15Index = line1.index(line1.startIndex, offsetBy: 14)
let pos15 = line1[pos15Index]

if pos15 != "<" {
// handling standard format where position 15 is the check digit
let checkDigit = Int(String(pos15)) ?? -1
let calculatedCheck = calculateMRZCheckDigit(principalPart)
let isValid = (checkDigit == calculatedCheck)
print("[extractAndValidateTD1DocumentNumber] Standard format: \(principalPart), check=\(checkDigit), calculated=\(calculatedCheck), valid=\(isValid)")
return (principalPart, isValid)
}
Comment on lines +132 to 139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Strip PII from standard-format logging.

This print dumps the raw document number and check digit into logs, which violates our “never log PII” rule for MRZ data and creates GDPR/CCPA exposure in release builds. Please remove or properly gate/redact the output.

[suggested fix]

-            print("[extractAndValidateTD1DocumentNumber] Standard format: \(principalPart), check=\(checkDigit), calculated=\(calculatedCheck), valid=\(isValid)")
+            #if DEBUG
+            let maskedDoc = String(principalPart.prefix(3)) + String(repeating: "•", count: Swift.max(principalPart.count - 6, 0)) + String(principalPart.suffix(3))
+            print("[extractAndValidateTD1DocumentNumber] Standard format valid=\(isValid) (doc=\(maskedDoc))")
+            #endif

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if pos15 != "<" {
// handling standard format where position 15 is the check digit
let checkDigit = Int(String(pos15)) ?? -1
let calculatedCheck = calculateMRZCheckDigit(principalPart)
let isValid = (checkDigit == calculatedCheck)
print("[extractAndValidateTD1DocumentNumber] Standard format: \(principalPart), check=\(checkDigit), calculated=\(calculatedCheck), valid=\(isValid)")
return (principalPart, isValid)
}
if pos15 != "<" {
// handling standard format where position 15 is the check digit
let checkDigit = Int(String(pos15)) ?? -1
let calculatedCheck = calculateMRZCheckDigit(principalPart)
let isValid = (checkDigit == calculatedCheck)
#if DEBUG
let maskedDoc = String(principalPart.prefix(3)) + String(repeating: "", count: Swift.max(principalPart.count - 6, 0)) + String(principalPart.suffix(3))
print("[extractAndValidateTD1DocumentNumber] Standard format valid=\(isValid) (doc=\(maskedDoc))")
#endif
return (principalPart, isValid)
}
🤖 Prompt for AI Agents
In app/ios/LiveMRZScannerView.swift around lines 132 to 139, the current print
call outputs raw MRZ PII (document number and check digits); remove that raw
output and replace it with a non-PII-safe logging strategy: either completely
remove the print in release builds or gate it behind a debug-only flag (e.g.,
#if DEBUG) and, if you must log, redact the document number (mask all but last
1–2 characters) and only log a boolean validity result and non-sensitive
context. Ensure no raw principalPart or digits appear in any release logs.

return nil
}

private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? {
// For Belgium TD1 format: IDBEL000001115<7027
// doc9 = "000001115" (9 digits)
// doc3 = "702" (3 digits after <)
// checkDigit = "7" (single check digit)
// handling overflow format: scanning positions 16+ until we hit <
let pos16Index = line1.index(line1.startIndex, offsetBy: 15)
let remainingPart = String(line1[pos16Index...])

// finding the overflow digits and the check digit
var overflowDigits = ""
var checkDigitChar: Character?

var cleanDoc9 = doc9
// Strip first 3 characters
let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3)
cleanDoc9 = String(cleanDoc9[startIndex...])
for char in remainingPart {
if char == "<" {
break
}
overflowDigits.append(char)
}

guard overflowDigits.count > 0 else {
print("[extractAndValidateTD1DocumentNumber] ERROR: No overflow digits found")
return nil
}

let fullDocumentNumber = cleanDoc9 + doc3
// extracting check digit (last character of overflow)
checkDigitChar = overflowDigits.last
let overflowWithoutCheck = String(overflowDigits.dropLast())

// constructing full document number: principal + overflow (without check digit)
let fullDocumentNumber = principalPart + overflowWithoutCheck

return fullDocumentNumber
// validating check digit against full document number
guard let checkDigitChar = checkDigitChar,
let checkDigit = Int(String(checkDigitChar)) else {
print("[extractAndValidateTD1DocumentNumber] ERROR: Invalid check digit")
return nil
}
let calculatedCheck = calculateMRZCheckDigit(fullDocumentNumber)
let isValid = (checkDigit == calculatedCheck)

#if DEBUG
print("[extractAndValidateTD1DocumentNumber] Overflow format:")
print(" Principal part (6-14): \(principalPart)")
print(" Overflow with check: \(overflowDigits)")
print(" Overflow without check: \(overflowWithoutCheck)")
print(" Full document number: \(fullDocumentNumber)")
print(" Check digit: \(checkDigit)")
print(" Calculated check: \(calculatedCheck)")
print(" Valid: \(isValid)")
#endif

return (fullDocumentNumber, isValid)
}

private func isValidMRZResult(_ result: QKMRZResult) -> Bool {
Expand All @@ -131,59 +199,69 @@ struct LiveMRZScannerView: View {
onScanResultAsDict?(mapVisionResultToDictionary(result))
}

private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? {
print("[LiveMRZScannerView] Processing Belgium document")
/// Processes TD1 documents (ID cards) by manually extracting and validating the document number using the overflow format handler,
/// then parses the remaining MRZ fields (name, dates, etc.) using QKMRZParser. This bypasses QKMRZParser's validation for the
/// document number field since it doesn't handle TD1 overflow format correctly.
private func processTD1DocumentWithOverflow(result: String, parser: QKMRZParser) -> QKMRZResult? {
print("[LiveMRZScannerView] Processing TD1 document with manual overflow validation")

guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else {
print("[LiveMRZScannerView] Failed to correct Belgium document number")
return nil
}

// print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)")

// Split MRZ into lines and replace the first line
let lines = result.components(separatedBy: "\n")
guard lines.count >= 3 else {
print("[LiveMRZScannerView] Invalid MRZ format - not enough lines")
return nil
}

let originalFirstLine = lines[0]
// print("[LiveMRZScannerView] Original first line: \(originalFirstLine)")

// Pad the corrected line to 30 characters (TD1 format)
let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0)
// print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)")
let line1 = lines[0]
print("[LiveMRZScannerView] Line 1: \(line1)")

// Reconstruct the MRZ with the corrected first line
var correctedLines = lines
correctedLines[0] = paddedCorrectedLine
let correctedMRZString = correctedLines.joined(separator: "\n")
// print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)")
// extracting and validating document number manually using overflow format handler
guard let (documentNumber, isDocNumberValid) = extractAndValidateTD1DocumentNumber(line1: line1) else {
print("[LiveMRZScannerView] Failed to extract TD1 document number")
return nil
}

guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else {
print("[LiveMRZScannerView] Belgium MRZ result is not valid")
if !isDocNumberValid {
print("[LiveMRZScannerView] TD1 document number check digit is INVALID")
return nil
}

// print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)")
print("[LiveMRZScannerView] TD1 document number validated: \(documentNumber) ✓")

// Try the corrected MRZ first
if isValidMRZResult(belgiumMRZResult) {
return belgiumMRZResult
// parsing the original MRZ to get all other fields (name, birthdate, etc.)
Comment on lines +214 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop logging full MRZ line/document number.

print is emitting the entire MRZ line and the validated document number in plaintext. That’s sensitive PII and must not reach production logs. Wrap these diagnostics in #if DEBUG and mask the values before logging.

[suggested fix]

-        let line1 = lines[0]
-        print("[LiveMRZScannerView] Line 1: \(line1)")
+        let line1 = lines[0]
+        #if DEBUG
+        let maskedLine1 = "\(line1.prefix(4))…\(line1.suffix(4))"
+        print("[LiveMRZScannerView] Line 1: \(maskedLine1)")
+        #endif
@@
-        print("[LiveMRZScannerView] TD1 document number validated: \(documentNumber) ✓")
+        #if DEBUG
+        let maskedDoc = String(documentNumber.prefix(3)) + String(repeating: "•", count: Swift.max(documentNumber.count - 6, 0)) + String(documentNumber.suffix(3))
+        print("[LiveMRZScannerView] TD1 document number validated ✓ (doc=\(maskedDoc))")
+        #endif

As per coding guidelines.

🤖 Prompt for AI Agents
In app/ios/LiveMRZScannerView.swift around lines 214 to 230, the code prints
full MRZ line and document number (PII) to logs; wrap these diagnostic prints in
a compile-time conditional (#if DEBUG ... #endif) so they are only emitted in
debug builds, and mask sensitive values before logging (e.g., replace all but
last 2–4 chars of the document number with asterisks and redact the MRZ line to
show only safe metadata like length or masked segments). Ensure the
guard/failure logs do not expose raw PII and keep only non-sensitive status text
in production.

// using QKMRZParser for non-documentNumber fields
guard let mrzResult = parser.parse(mrzString: result) else {
print("[LiveMRZScannerView] Failed to parse MRZ with QKMRZParser")
return nil
}

// If document number is still invalid, try single character correction
if !belgiumMRZResult.isDocumentNumberValid {
if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) {
// print("[LiveMRZScannerView] Single correction successful: \(correctedResult)")
if isValidMRZResult(correctedResult) {
return correctedResult
}
}
// validating that other fields are also correct
if !mrzResult.isBirthdateValid || !mrzResult.isExpiryDateValid {
print("[LiveMRZScannerView] TD1 document has invalid birthdate or expiry date")
return nil
}

return nil
#if DEBUG
print("[LiveMRZScannerView] QKMRZParser extracted fields:")
print(" countryCode: \(mrzResult.countryCode)")
print(" surnames: \(mrzResult.surnames)")
print(" givenNames: \(mrzResult.givenNames)")
print(" birthdate: \(mrzResult.birthdate?.description ?? "nil")")
print(" sex: \(mrzResult.sex ?? "nil")")
print(" expiryDate: \(mrzResult.expiryDate?.description ?? "nil")")
print(" personalNumber: \(mrzResult.personalNumber)")
print(" Parser's documentNumber: \(mrzResult.documentNumber)")
print(" Our validated documentNumber: \(documentNumber)")
#endif

// storing the manually validated full document number
// this will be used for NFC chip authentication (BAC keys)
overrideDocumentNumber = documentNumber
#if DEBUG
print("[LiveMRZScannerView] Set overrideDocumentNumber to: \(documentNumber)")
#endif

// returning MRZ result, the document number will be overridden in mapVisionResultToDictionary
return mrzResult
}

var body: some View {
Expand All @@ -208,10 +286,13 @@ struct LiveMRZScannerView: View {
return
}

// Handle Belgium documents (only if not already valid)
if doc.countryCode == "BEL" {
if let belgiumResult = processBelgiumDocument(result: result, parser: parser) {
handleValidMRZResult(belgiumResult)
// handling TD1 documents with potential overflow format (only if not already valid)
// TD1 format has 3 lines of 30 characters each
let lines = result.components(separatedBy: "\n")
if lines.count >= 3 && lines[0].count == 30 {
// trying overflow validation
if let td1Result = processTD1DocumentWithOverflow(result: result, parser: parser) {
handleValidMRZResult(td1Result)
}
return
}
Expand Down
9 changes: 6 additions & 3 deletions packages/mobile-sdk-alpha/src/processing/mrz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,12 @@ function validateTD3CheckDigits(lines: string[]): Omit<MRZValidation, 'format' |
}

export function checkScannedInfo(passportNumber: string, dateOfBirth: string, dateOfExpiry: string): boolean {
if (passportNumber.length > 9) {
return false;
}
// TD1 overflow format allows document numbers > 9 characters per ICAO 9303:
// When document numbers exceed 9 characters, the overflow digits are stored in the optional data
// field and the full document number is reconstructed during MRZ parsing.
// if (passportNumber.length > 9) {
// return false;
// }
Comment on lines +189 to +194
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incomplete TD1 overflow implementation.

Removing the length check without updating the TD1 extraction and validation logic creates an incomplete implementation. The iOS code (per PR objectives) added extractAndValidateTD1DocumentNumber to detect overflow (position 15 == '<'), extract overflow characters from optional data (positions 16+), and validate using the correct check digit. This TypeScript implementation allows long document numbers to pass checkScannedInfo but lacks the corresponding overflow extraction and validation logic in extractTD1Info and validateTD1CheckDigits.

This could lead to:

  • Incorrect document numbers being extracted (missing overflow characters)
  • Failed check digit validation (validating against wrong data)
  • NFC/BAC authentication failures

Apply these fixes:

1. Update extractTD1Info to handle overflow:

 function extractTD1Info(lines: string[]): Omit<MRZInfo, 'validation'> {
   const line1 = lines[0];
   const line2 = lines[1];
 
   const concatenatedLines = line1 + line2;
+
+  // Handle TD1 overflow format per ICAO 9303
+  // When document number exceeds 9 chars, overflow is in optional data
+  let documentNumber = concatenatedLines.slice(5, 14).replace(/</g, '').trim();
+  const documentNumberCheckDigitPos = concatenatedLines.slice(14, 15);
+  
+  // Detect overflow: if position 15 is '<', overflow exists in optional data
+  if (documentNumberCheckDigitPos === '<') {
+    // Extract overflow from optional data (positions 15-29, which is indices 15-29)
+    const optionalData = concatenatedLines.slice(15, 30);
+    // Find the check digit (last non-< character in overflow)
+    let overflowChars = '';
+    for (let i = 0; i < optionalData.length; i++) {
+      if (optionalData[i] !== '<') {
+        overflowChars += optionalData[i];
+      } else {
+        break;
+      }
+    }
+    // Append overflow to document number (excluding last char which is check digit)
+    if (overflowChars.length > 0) {
+      documentNumber += overflowChars.slice(0, -1);
+    }
+  }
 
   return {
     documentType: concatenatedLines.slice(0, 2),
     issuingCountry: concatenatedLines.slice(2, 5),
-    documentNumber: concatenatedLines.slice(5, 14).replace(/</g, '').trim(),
+    documentNumber,
     dateOfBirth: concatenatedLines.slice(30, 36),
     dateOfExpiry: concatenatedLines.slice(38, 44),
   };
 }

2. Update validateTD1CheckDigits to validate overflow:

 function validateTD1CheckDigits(lines: string[]): Omit<MRZValidation, 'format' | 'overall'> {
   const line1 = lines[0];
   const line2 = lines[1];
   const concatenatedLines = line1 + line2;
 
-  const documentNumber = concatenatedLines.slice(5, 14);
-  const documentNumberCheckDigit = concatenatedLines.slice(14, 15);
+  // Handle TD1 overflow format
+  let documentNumber = concatenatedLines.slice(5, 14);
+  let documentNumberCheckDigit = concatenatedLines.slice(14, 15);
+  
+  // Detect overflow: if position 15 is '<', check digit is in optional data
+  if (documentNumberCheckDigit === '<') {
+    const optionalData = concatenatedLines.slice(15, 30);
+    // Find overflow characters and check digit
+    let overflowChars = '';
+    for (let i = 0; i < optionalData.length; i++) {
+      if (optionalData[i] !== '<') {
+        overflowChars += optionalData[i];
+      } else {
+        break;
+      }
+    }
+    if (overflowChars.length > 0) {
+      // Last char is check digit, rest is overflow
+      documentNumberCheckDigit = overflowChars.slice(-1);
+      documentNumber += overflowChars.slice(0, -1);
+    }
+  }
+  
   const dateOfBirth = concatenatedLines.slice(30, 36);
   const dobCheckDigit = concatenatedLines.slice(36, 37);
   const dateOfExpiry = concatenatedLines.slice(38, 44);
   const expiryCheckDigit = concatenatedLines.slice(44, 45);
 
   return {
     passportNumberChecksum: verifyCheckDigit(documentNumber, documentNumberCheckDigit),
     dateOfBirthChecksum: verifyCheckDigit(dateOfBirth, dobCheckDigit),
     dateOfExpiryChecksum: verifyCheckDigit(dateOfExpiry, expiryCheckDigit),
     compositeChecksum: true, // TD1 doesn't have a composite check digit like TD3
   };
 }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This specific code path is only reached on Android. This PR addresses only the iOS platform.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seshanthS Thank you for the clarification! I understand now—this TypeScript code path is Android-only, and since this PR addresses only iOS, the Android TD1 overflow implementation is appropriately deferred.

The change to comment out the length check makes sense as an interim step: it prevents Android from prematurely rejecting document numbers > 9 characters, while the full TD1 overflow extraction and validation logic (similar to what was implemented in the iOS Swift code) would be added when Android support is implemented later—as noted in the PR TODOs.

For the iOS flow, the Swift implementation in LiveMRZScannerView.swift with extractAndValidateTD1DocumentNumber and processTD1DocumentWithOverflow provides the complete overflow handling before the data reaches this TypeScript layer.


✏️ Learnings added
Learnt from: seshanthS
Repo: selfxyz/self PR: 1337
File: packages/mobile-sdk-alpha/src/processing/mrz.ts:189-194
Timestamp: 2025-11-18T12:17:14.797Z
Learning: In packages/mobile-sdk-alpha/src/processing/mrz.ts, the checkScannedInfo function and related TD1 extraction/validation logic are only reached on Android. iOS uses native Swift parsing (LiveMRZScannerView.swift) that bypasses this TypeScript layer.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: shazarre
Repo: selfxyz/self PR: 1236
File: packages/mobile-sdk-alpha/src/flows/onboarding/document-nfc-screen.tsx:356-378
Timestamp: 2025-10-23T12:08:55.529Z
Learning: In packages/mobile-sdk-alpha/src/flows/onboarding/document-nfc-screen.tsx, the NFC native events emitted via NativeEventEmitter are generic status strings (e.g., "PACE succeeded", "BAC failed", "Reading DG1 succeeded") and do not contain sensitive MRZ data or passport numbers, so they do not require sanitization before logging.

Learnt from: aaronmgdr
Repo: selfxyz/self PR: 889
File: app/src/utils/utils.ts:20-29
Timestamp: 2025-08-25T14:07:52.997Z
Learning: In the SELF passport/identity verification app, the primary PII concerns are MRZ data from passports and document numbers. SSNs and credit cards are not relevant to this domain, so PII redaction should focus on passport-specific data patterns rather than general financial/personal identifiers.

if (dateOfBirth.length !== 6) {
return false;
}
Expand Down
Loading