wGrow
menu
Field note · Migrated 2026-04-22 · 7 min

Hospital visitor logging under PDPA: hashing, partial IC, and what we'd do differently in 2026.

Our 2023 design — SHA-256-hashed IC numbers plus last-4-digit verification — was sound for COVID-era visitor caps. Three years on, here is what we'd build today, given Singpass, MGF, and PDPC's evolved enforcement.

We built a visitor-logging system for a Singapore hospital in 2021–2022, 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. We wrote it up in 2023 as Team-Notes #57.

The system is still in production. PDPC enforcement has tightened. Singpass NRIC verification is now usable for this kind of workflow. Here is the rewrite.

What the original 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 in 2024)

1. Plain SHA-256 without a salt

The 2023 article mentioned salting as a “future improvement.” In retrospect, 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 in 2024, with the salt held in Azure Key Vault.

If you took our 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 0 vs a typed O.
  • A guard who fat-fingered a digit.

Our 2024 update normalises the IC string aggressively (uppercase, strip non-alphanumeric, validate format) before hashing. The 2023 article 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. Our 2024 update 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 (2026)

If we were starting from a blank page in 2026, 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. Same shape as 2024:

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 (2026)

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 in 2026

PDPC’s enforcement decisions over 2024–2026 have made it clear that:

  1. “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).
  2. Retention beyond purpose is a finable category, not just a polite-letter category, for healthcare workloads.
  3. 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.

What we cut from the original

  • The framing of “future improvements.” Salting was not a future improvement; it should have been the floor.
  • The SingPass section — overtaken by current Singpass capabilities, now properly wired into the recommendation.

What carries over unchanged

The PDPA discipline. 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; this is the third article in a row where I’ve written that sentence and meant it.

— wGrow studio · migrated from Team-Notes #57