Skip to content

Repository files navigation

epfo-passbook-redactor

Redact and merge EPFO member passbook PDFs — one file per employer, pages in financial-year order, with the sensitive parts genuinely removed rather than covered.

Background-check vendors, landlords and loan officers routinely ask for your EPF passbook. Handing one over discloses your exact monthly salary for every month you have ever worked, plus your UAN and full member ID. Usually the asker only needs to see that the employment happened and that the account is real. This tool draws that line for you.

It removes the text, it does not draw a box over it. Covering a value with a black rectangle in a PDF viewer leaves the original string in the file, where copy-paste, pdftotext or a text search will find it. That is how "redacted" documents leak. Everything here goes through MuPDF redaction annotations, so the glyphs are gone before the file is written — then three separate checks confirm it.


⚠️ Never commit a real passbook

This repo contains no real passbook data, and it is set up to keep it that way: .gitignore blocks every PDF except samples/, every generated page is stamped SYNTHETIC SAMPLE - NOT A REAL EPFO DOCUMENT, and a pre-commit hook rejects any PDF missing that stamp on any page — so a real passbook redacted into samples/output/ by mistake is still caught. Every sample and test fixture is generated by epfo-synth.

If you want something to try the tool on, generate it — don't borrow yours.


What gets redacted

The defaults were chosen for the common case: proving employment history without disclosing pay.

Removed by default

Before After
Establishment ID BACGH9703905715 BACGH██████████
Employer name CHAHAL PRIVATE LIMITED ██████ PRIVATE LIMITED
Member ID (header and the footer on every page) BACGH97039057153335943 BACGH█████████████████
UAN 115631219101 1156████████
EPF wages, EPF employee/employer contributions and balances 94,500 ▒▒▒▒▒▒

Kept readable by default

  • Every Employee Pension Scheme figure — EPS wages, pension contribution, pension balance and closing pension corpus.
  • Your name and date of birth, so a verifier can tie the document to you.

EPS is kept deliberately. EPS wages are capped at the statutory ceiling and the pension contribution derives from that ceiling, so neither reveals an actual salary — while EPF wages and EPF contributions reveal it exactly. Full field list: docs/masking-fields.md.

Getting your passbook

Download it from the official EPFO member passbook portal: https://passbook.epfindia.gov.in/MemberPassBook/login — sign in with your UAN and password, pick a member ID, then View Passbook and download.

You will get one of two shapes, and both work here:

  • A whole account as one PDF, a financial year per page. Point the tool straight at the file.
  • One PDF per financial year, which is what you get pulling a year at a time. Point the tool at the folder.

samples/input/ has an example of each — see samples/README.md. This tool never touches the portal or the network; it only reads PDFs you already have.

Install

git clone https://github.com/asadani/epfo-passbook-redactor
cd epfo-passbook-redactor
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e .

Python 3.10+. epfo-redact and epfo-synth land on your PATH; python -m epfo_redactor works too.

For the synthetic generator and the test suite:

pip install -e ".[dev]"

Usage

# One PDF per employer, from folders of yearly downloads
epfo-redact AZJHH/ BACGH/ -o out/

# Point at the whole download folder; subdirectories are searched
epfo-redact ~/Downloads/passbooks -o out/

# See what would change, write nothing
epfo-redact AZJHH/ --dry-run

# Redact everything, including your name and the EPS columns
epfo-redact AZJHH/ --profile strict

# Keep the wage column, and leave the zero-withdrawal rows readable
epfo-redact AZJHH/ --no-mask epf-wages --keep-rows withdrawals

Output is named EPFO_Passbook_<PREFIX>_FY<first>-<last>_redacted.pdf. Only the 5-letter regional prefix goes in the filename — never the full establishment code, so the filename cannot undo the redaction inside.

Options

Option Effect
-o, --out DIR Output directory (default ./redacted)
--profile NAME default, strict, identity, financial, none
--mask FIELD Add a field. Repeatable
--no-mask FIELD Remove a field. Repeatable
--keep-chars FIELD=N Characters left readable at the start of a field
--keep-rows KINDS Rows left readable inside redacted columns
--style STYLE black, grey, white, hatch
--field-style FIELD=STYLE Style for one field, or for identity / financial
--group-by MODE establishment (default), dir, none
--merge / --no-merge Merge each group into one PDF (default: merge)
--config PATH YAML config (auto-discovers ./epfo-redact.yaml)
--dry-run Report what would change; write nothing
--no-fail-on-leak Report leaks without a non-zero exit
--list-fields Print every field, profile and style

Defaults are black boxes on the identity fields and grey on the table, so the statement still reads as a table.

Profiles

Profile Redacts
default Establishment ID, employer name, member ID, UAN, and the three EPF money columns
strict Everything, including your name, DOB and all EPS figures
identity Header identifiers only — the full financial history stays readable
financial Table only — the header stays readable
none Nothing; build a set up with --mask

Config file

Drop an epfo-redact.yaml next to where you run the command:

profile: default
mask: [dob]
no-mask: [epf-wages]
keep-rows: withdrawals, transfers
keep-chars:
  uan: 2
style: grey
field-style:
  member-id: black
group-by: establishment
out: redacted

Precedence, lowest to highest: field defaults → profile → config file → command-line flags. See examples/epfo-redact.yaml.

Generating test data

epfo-synth --seed 42                              # 2 employers, FY2015-2021
epfo-synth --employers 3 --years 2009:2026 -o /tmp/demo
epfo-synth --no-combined -o /tmp/per-year         # per-year files only

Names come from Faker, establishment codes are random letter groups, and every rupee figure is simulated from the statutory rules — 12% EPF, 8.33% EPS capped at the wage ceiling, interest credited at year end, balances carried forward. The pages are drawn at the real template's geometry so the redactor sees the structure it would see on a genuine passbook — down to the EPFO letterhead, the bilingual Hindi/English column headings, the red Total Withdrawals row and the disclaimer block. The Hindi is real Unicode Devanagari and needs a Devanagari font installed (Nirmala UI on Windows, Noto Sans Devanagari or Lohit on Linux); without one the English labels are written alone. The real document sets its Hindi in a legacy Kruti Dev encoding, which extracts as Latin gibberish — reproducing that would have looked right and read wrong.

Two shapes come out, because both turn up in practice:

samples/input/
  EPFO_Passbook_AZJHH_FY2015-2017.pdf    a whole account, one financial year per page
  AZJHH/AZJHH33423314449906821_2015.pdf  the same account, one file per year

Asking the member portal for a whole account gives you the first; pulling a year at a time gives you the second. The redactor takes either — see samples/README.md. samples/input/ and samples/output/ hold a committed set generated with --seed 42 and the result of running the tool over it.

How it finds the columns

Column positions are detected from the header labels, not hardcoded. The tool finds the EPF / EPS / Employee / Employer / Pension labels, clusters the numeric cells by their right edge (the table is right-aligned), and matches each cluster to the label above it. Balance and contribution columns share a right edge, so one detected band covers both. A fixed fallback geometry exists but is only used if label detection fails outright.

One trap worth knowing about: the Total Contributions for the year [ 2021 ] rows put a bare year inside the EPS-wages column band. It is a row label, not a cell, and is excluded explicitly — otherwise enabling eps-wages would silently redact the year.

Verification

Every run verifies its own output, and exits non-zero if anything fails:

  1. Text — no redacted literal survives text extraction.
  2. Content stream — no redacted literal survives in the decompressed page streams, searched as both ASCII ((ABC123), how iText writes it) and hex (<414243…>, how MuPDF writes it). Streams are deflated, so searching the raw file finds nothing; this decompresses first.
  3. Columns — no numeric cell remains anywhere in a column that was cleared, which catches a row the planner missed rather than a value it knew about.
pip install -e ".[dev]"
pytest -q
black --check . && flake8

Development

  • CLAUDE.md — architecture, invariants and the gotchas that already cost time once. Read before changing anything.
  • docs/how-it-was-built.md — why the design is what it is, how correctness was established, and the bugs found on the way.
  • docs/masking-fields.md — every field, profile and style.
pip install -e ".[dev]"
pytest -q            # 88 tests, ~30s, all on synthetic data
black . && flake8
pre-commit install   # includes a hook refusing PDFs that are not synthetic samples

Project layout

epfo_redactor/
├── cli.py         command line interface
├── fields.py      what can be redacted; profiles
├── layout.py      label-driven column and row detection
├── redact.py      rect planning and redaction
├── merge.py       grouping, ordering, merging, metadata scrub
├── verify.py      the three leak checks
├── config.py      YAML config and precedence
└── synth/         synthetic passbook generator
    ├── generate.py  the page, drawn at the real template's geometry
    ├── fonts.py     which faces this machine can offer, and the fallbacks
    └── assets/      the EPFO letterhead
samples/
├── input/         what you would have downloaded, in both shapes
└── output/        what `epfo-redact samples/input -o samples/output` produces

Limitations

  • Built for the EPFO member passbook PDF, the one you download from the member portal. Other EPFO documents are not handled.
  • Column detection relies on the English header labels. A future template that renames or drops them falls back to fixed geometry, and the run reports [fallback geometry] — check that output by eye.
  • Scanned or image-only passbooks are refused: no text layer means nothing to detect and nothing to remove. Any PDF producing zero redactions is refused too, rather than reported as successfully processed.
  • The verifier's content-stream check is a substring search. A string broken across kerned pieces would evade it; check 1 is what covers that case.
  • Look at the output before you send it. The checks are good, but they are checks on a machine's idea of what mattered.

License

MIT. See LICENSE.

About

Redact EPFO member passbook PDFs - removes salary, UAN and member ID, keeps EPS pension details readable. Merges one file per employer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages