Hospital visitor logging under PDPA: hashing, partial IC, and what we'd do differently.
By Timothy Mo ·
Designing visitor logging for a Singapore hospital under PDPA — SHA-256-hashed IC numbers plus last-4-digit verification — and what we'd build today, given Singpass, MGF, and PDPC's evolved enforcement.
We built a visitor-logging system for a Singapore hospital during the strict COVID-era visitor caps. The constraint: hospitals had to enforce per-day per-patient visitor limits, and PDPA prohibits the storage of full NRIC/FIN numbers. Our design used a SHA-256 hash of the IC for uniqueness, plus the last 4 digits for human verification by security staff.
The system is still in production. PDPC enforcement has tightened. Singpass NRIC verification is now usable for this kind of workflow.
What the original design got right
- No full IC stored. PDPA-compliant by construction, not by promise.
- SHA-256 for the hashed IC. Fast, well-understood, no key management for the hash itself.
- Partial IC for human verification. Security guards could match the last 4 digits to the physical IC without ever typing the full number into a system.
This is still the right architecture for the constraint the hospital had. The constraint has now moved.
What we got wrong (and changed)
1. Plain SHA-256 without a salt
Salting was originally listed as a “future improvement.” That was wrong — it should have been there from day one. An IC is a small input space (the format is well-known and the digit ranges are bounded). A plain SHA-256 hash of an IC is trivially reversible by an attacker with a list of all possible ICs and a lookup table. We added a per-hospital salt, with the salt held in Azure Key Vault.
If you took the original article as a template — go and add a salt now. Today.
2. Hashing is the wrong primitive for “is this the same person”
Hashing answers “is this byte-string identical to that byte-string.” For human uniqueness, you also want to handle:
- A trailing whitespace difference between scans.
- An OCR’d
0vs a typedO. - A guard who fat-fingered a digit.
Our updated design normalises the IC string aggressively (uppercase, strip non-alphanumeric, validate format) before hashing. The original showed the hash function but not the normalisation; that omission caused at least one duplicate-allow incident in the field.
3. The retention window was unclear
PDPA expects data retention to be the minimum necessary for the purpose. A visit log for visitor-cap enforcement does not need to live for years. The current build auto-purges visit records 14 days after the visit (the cap window is one day; 14 days gives a buffer for dispute resolution). The hashed IC is purged with the visit record, since on its own it is no longer useful.
What we’d build today
If we were starting from a blank page, we’d use:
Singpass NRIC verification, where the workflow allows it
Singpass NRIC verification (via Myinfo / Singpass Login) is now mature enough that for non-emergency visitor-flow scenarios, we’d skip the IC-scan path entirely:
- Visitor authenticates with Singpass on a kiosk or their phone.
- The hospital receives a per-visit pseudonymous identifier (not the NRIC) plus the limited demographic fields it actually needs.
- The hospital never holds the NRIC, even hashed.
This is structurally simpler and harder to mis-implement.
A salted hash + normalisation path for fall-back
For unaccompanied / non-Singpass-friendly visitors (foreign visitors with FINs, anyone without a smartphone), we keep the salted-hash + partial-IC path:
private static string HashIC(string raw, byte[] salt)
{
var normalised = (raw ?? "")
.Trim()
.ToUpperInvariant()
.Where(char.IsLetterOrDigit)
.ToArray();
var input = new string(normalised);
using var sha = SHA256.Create();
var bytes = Encoding.ASCII.GetBytes(input);
var combined = salt.Concat(bytes).ToArray();
var hash = sha.ComputeHash(combined);
return Convert.ToHexString(hash);
}
Salt is held in Key Vault; rotation is a six-monthly hospital-led exercise.
Schema
Visitors
visitor_id int identity, PK
hashed_ic char(64) NOT NULL
partial_ic char(4) NOT NULL
ic_country_code char(2)
visit_date date NOT NULL
patient_visited_id int FK
recorded_by_guard_id int FK
recorded_at datetime2 NOT NULL
purge_after date NOT NULL -- visit_date + 14
INDEX ix_hashed_ic_visit_date (hashed_ic, visit_date)
Plus a nightly job that DELETEs rows where purge_after < today(), audited.
On the PDPA reading today
PDPC’s enforcement decisions have made it clear that:
- “We hashed it” does not, on its own, take the data out of personal-data scope if the hash is reversible (small input space without salt).
- Retention beyond purpose is a finable category, not just a polite-letter category, for healthcare workloads.
- Auditability of access to the table that holds the hashes is an expected control — not a “future improvement.”
If you operated a hospital visitor-logging system between 2021 and 2024 on a similar pattern: please review your salt and retention posture this quarter.
The PDPA discipline doesn’t move: don’t store full ICs, don’t store more than you need, don’t keep it longer than you need, audit who reads what. Boring controls compound.
— Timothy Mo, wGrow