mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-09-16 23:24:37 +00:00
ePassport: render the MRZ personal number without its fillers
9303 Part 4, positions 29 to 42: "Any special characters, including spaces, in the personal identification number ... shall be replaced by the filler character (<). The number shall be followed by the filler character (<) repeated up to position 42." So the field holds two different kinds of filler. The trailing run is padding, which was already stripped, but a filler inside the number stands for a space or special character substituted on the way in, and those were printed as they came - a US passport showed its personal number with a '<' in the middle of it. Strip the padding, then put a space back for what remains. It is the closest recovery available: the original could have been a hyphen, and the MRZ does not keep which. optional_data itself stays a faithful copy of the field. Nothing else reads it, and it is the personal number that is being rendered here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
aadcda0cb8
commit
8bb7d1191a
+1
-1
@@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
|
||||
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
|
||||
|
||||
## [unreleased][unreleased]
|
||||
- Fixed `tools/ePassport` - a document number longer than nine characters left its overflow in the MRZ optional-data field, where it was reported as the holder's personal number (@pkilar)
|
||||
- Fixed `tools/ePassport` - the personal number read from the MRZ optional-data field showed a filler character where a space belonged, and a document number longer than nine characters left its overflow there (@pkilar)
|
||||
- Added `tools/ePassport` - EF_DG13 is decoded, and a Polish document's PESEL is shown as the personal number when it carries no EF_DG11 (@pkilar)
|
||||
- Fixed `tools/ePassport` - a disabled button drew Kivy's banded disabled texture, which left its label unreadable (@pkilar)
|
||||
- Fixed `hf 14b` - a card asking for a waiting time extension of 4 or more left the timeout at zero, so any ISO14443-B card needing time to answer looked like it had stopped responding (@pkilar)
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .mrz import Mrz
|
||||
from .mrz import FILLER, Mrz
|
||||
|
||||
|
||||
class FileState:
|
||||
@@ -81,6 +81,17 @@ class DocumentDetails:
|
||||
return not any(v for k, v in vars(self).items() if not k.startswith("image_"))
|
||||
|
||||
|
||||
def _mrz_personal_number(field: str) -> str:
|
||||
"""The MRZ optional-data field read as a personal number.
|
||||
|
||||
9303 replaces every space and special character in the number with ``<``
|
||||
and then pads the field with the same character, so the trailing run is
|
||||
padding while a filler inside the number stood for something we cannot
|
||||
recover. A space is the closest we can put back.
|
||||
"""
|
||||
return field.rstrip(FILLER).replace(FILLER, " ").strip()
|
||||
|
||||
|
||||
#: Poland carries the PESEL, its national identity number, in this DG13 tag.
|
||||
PESEL_TAG = "5F70"
|
||||
|
||||
@@ -232,7 +243,7 @@ class PassportRecord:
|
||||
if self.personal.personal_number:
|
||||
return self.personal.personal_number
|
||||
if self.mrz is not None:
|
||||
optional = self.mrz.optional_data.value.strip()
|
||||
optional = _mrz_personal_number(self.mrz.optional_data.value)
|
||||
if optional:
|
||||
return optional
|
||||
if self.dg13_personal_number:
|
||||
@@ -258,7 +269,7 @@ class PassportRecord:
|
||||
"""Where :attr:`personal_number` came from, for an honest caption."""
|
||||
if self.personal.personal_number:
|
||||
return "DG11"
|
||||
if self.mrz is not None and self.mrz.optional_data.value.strip():
|
||||
if self.mrz is not None and _mrz_personal_number(self.mrz.optional_data.value):
|
||||
return "MRZ"
|
||||
if self.dg13_personal_number:
|
||||
return "DG13"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""The personal number taken from the MRZ optional-data field.
|
||||
|
||||
ICAO 9303 Part 4, positions 29 to 42: "Any special characters, including
|
||||
spaces, in the personal identification number ... shall be replaced by the
|
||||
filler character (<). The number shall be followed by the filler character
|
||||
(<) repeated up to position 42."
|
||||
|
||||
So the trailing run is padding, and a filler inside the number stands for a
|
||||
space or a special character that was substituted on the way in. Printing it
|
||||
raw put a '<' in the middle of the number.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from epassport.emrtd.model import PassportRecord
|
||||
from epassport.emrtd.mrz import Checked, Mrz
|
||||
|
||||
|
||||
def _record(optional: str) -> PassportRecord:
|
||||
record = PassportRecord()
|
||||
blank = Checked("")
|
||||
record.mrz = Mrz(
|
||||
kind="TD3",
|
||||
lines=[],
|
||||
document_number=blank,
|
||||
date_of_birth=blank,
|
||||
date_of_expiry=blank,
|
||||
composite=blank,
|
||||
nationality="USA",
|
||||
optional_data=Checked(optional),
|
||||
)
|
||||
return record
|
||||
|
||||
|
||||
def test_a_filler_inside_the_number_stands_for_a_space() -> None:
|
||||
assert _record("123456789<0123").personal_number == "123456789 0123"
|
||||
|
||||
|
||||
def test_trailing_padding_never_reaches_the_value() -> None:
|
||||
assert _record("ZE184226B").personal_number == "ZE184226B"
|
||||
assert _record("ZE184226B<<<<<").personal_number == "ZE184226B"
|
||||
|
||||
|
||||
def test_a_plain_number_is_untouched() -> None:
|
||||
assert _record("6512168").personal_number == "6512168"
|
||||
|
||||
|
||||
def test_an_empty_field_yields_no_number() -> None:
|
||||
assert _record("").personal_number_source == ""
|
||||
|
||||
|
||||
def test_the_source_is_still_reported_as_the_mrz() -> None:
|
||||
assert _record("123456789<0123").personal_number_source == "MRZ"
|
||||
Reference in New Issue
Block a user