On this page
Short answer. A plain hash of an 11-digit number is not safe, because there are only 100 billion possibilities. Use a keyed hash (HMAC-SHA-256) with a secret stored outside the database, and version the key so you can rotate it.
Why a plain hash fails
A BVN or NIN has 11 digits, so there are 1011 possible values. Fast hashes like SHA-256 are designed to be quick, and a modern GPU can compute billions of them per second. An attacker with a copy of your database can hash every possible number and match the results: seconds to minutes of work, not years.
So "we only store a hash" is not an answer unless you say which hash.
The fix: a keyed hash
HMAC mixes a secret key into the hash. Without the key, an attacker cannot recompute a single value, so brute force over the number space gets them nowhere. The key must live outside the database: a secrets manager or a KMS, not a column next to the data.
import { createHmac, timingSafeEqual } from "node:crypto";
// Normalise first so "222 2333 3444" and "22223333444" hash the same.
const normalise = (id) => String(id).replace(/\D/g, "");
export function hashId(id, key, keyId = "k1") {
const digest = createHmac("sha256", key).update(normalise(id)).digest("hex");
return keyId + ":" + digest; // keep the key version with the hash
}
export function sameHash(a, b) {
const x = Buffer.from(a), y = Buffer.from(b);
return x.length === y.length && timingSafeEqual(x, y);
}
Why not a random salt per user?
A unique salt per row defeats lookup tables, but it also means the same BVN hashes differently every time, so you can no longer tell that someone is verifying the same number twice. For identity numbers you want a stable hash for duplicate detection and audit. A keyed hash gives you both stability and resistance to brute force.
Do not confuse this with password hashing
Passwords need a deliberately slow function such as PBKDF2, scrypt or Argon2, because users pick weak secrets. Identity numbers need a keyed hash because the space is small and you must be able to recompute the value on demand. Different problem, different tool.
Rotate the key without losing history
- Prefix every stored hash with a key ID (
k1:...). - When you introduce
k2, compute both for new lookups and write the new one. - Match against either until the old records age out of your retention window.
- Retire
k1once nothing depends on it.
The key is now your crown jewel
Even a keyed hash over a small space can be enumerated by anyone who holds the key. Restrict who and what can read it, never log it, keep it out of source control and rotate it on a schedule and after any suspected exposure.
What IDNexus stores
Raw identity numbers are never persisted. We retain only hashes, together with the audit metadata for each lookup, and the privacy policy lists the retention periods. In your own systems, we recommend the keyed pattern above for any identity number you need to remember.
Store less, leak less
IDNexus keeps identity numbers only as hashes, never in raw form.
Create account