Data Encryption
Encrypt sensitive identity fields and HIP health data payloads for ABDM v3 APIs.
TLS protects data in transit. ABDM also requires application-layer encryption for sensitive fields and health data. Aadhaar numbers and OTPs use RSA public-key encryption. HIP health data transfer uses ECDH on Curve25519 and AES-GCM.
Never log these values
Aadhaar numbers, OTPs, and the plaintext you feed into the cipher must never be written to logs, traces, analytics, or crash reports. Encrypt them as early as possible and discard the plaintext immediately.
What must be encrypted
| Field | Where it appears | Encrypt? |
|---|---|---|
| Aadhaar number | ABHA enrolment, HPR ID creation (Aadhaar route) | ✅ Always |
| OTP (Aadhaar / mobile) | Every verifyOTP / verifyMobileOTP call | ✅ Always |
| Mobile number | Some demographic / mobile-auth flows | ⚠️ When the field is documented as encrypted |
| FHIR bundle | HIP to HIU health information transfer | ✅ Always |
| Everything else (names, codes, tokens) | — | ❌ Sent as plaintext JSON over TLS |
If a request field is named otp, aadhaar, loginId, or similar in an
identity flow, assume it must carry the Base64 of the RSA-encrypted value,
not the raw digits.
Identity encryption scheme
ABDM uses RSA with OAEP padding. In JCE/Java notation:
RSA/ECB/OAEPWithSHA-1AndMGF1Padding- Key: the RSA public key from ABDM's certificate (2048-bit).
- Padding: OAEP with SHA-1 digest and MGF1 mask generation.
- Output: raw ciphertext bytes, then Base64-encoded into the JSON field.
Confirm the OAEP hash for your track
ABDM's long-standing default is OAEP with SHA-1 (RSA/ECB/OAEPWith
SHA-1AndMGF1Padding). A few newer endpoints have moved to SHA-256. If
the service rejects your ciphertext with a decryption/padding error, retry
with SHA-256 (and MGF1 over SHA-256). Verify against the sandbox for the
specific API version you target.
Step 1 — Fetch the public certificate
The certificate is fetched once (then cached) using your gateway bearer token. Different services publish their own certificate:
https://abhasbx.abdm.gov.in/abha/api/v3/profile/public/certificateABHA (ABDM) public key — used for ABHA enrolment / login Aadhaar & OTP fields.
curl "https://abhasbx.abdm.gov.in/abha/api/v3/profile/public/certificate" \
-H "Authorization: Bearer $TOKEN" \
-H "REQUEST-ID: $(uuidgen | tr 'A-Z' 'a-z')" \
-H "TIMESTAMP: $(date -u +%Y-%m-%dT%H:%M:%S.000Z)"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
...base64 DER of the 2048-bit RSA public key...
-----END PUBLIC KEY-----https://apihspsbx.abdm.gov.in/v4/int/api/v1/auth/certHPR / NHPR public key — used for the Milestone 4 HPR ID Aadhaar & OTP fields.
curl "https://apihspsbx.abdm.gov.in/v4/int/api/v1/auth/cert" \
-H "Authorization: Bearer $TOKEN"The response body is a PEM-encoded public key (a -----BEGIN PUBLIC KEY
----- block). Cache it — it changes rarely — but refresh on a decryption
error in case it rotated.
Step 2 — Encrypt the value
Load the PEM key, RSA-encrypt the UTF-8 bytes of the value with OAEP/SHA-1, and Base64-encode the result.
import crypto from "node:crypto";
/** publicKeyPem: the PEM string returned by the certificate endpoint. */
export function encryptForAbdm(value, publicKeyPem) {
return crypto
.publicEncrypt(
{
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha1", // switch to "sha256" if the API requires it
},
Buffer.from(String(value), "utf8"),
)
.toString("base64");
}
// Usage:
// const encAadhaar = encryptForAbdm("999999990019", pem);
// const encOtp = encryptForAbdm("123456", pem);Step 3 — Send the ciphertext
Put the Base64 string into the request field. For example, verifying an Aadhaar OTP during HPR ID creation:
/v2/registration/aadhaar/verifyOTPThe otp field carries the Base64 of the encrypted OTP — never
the raw six digits. The txnId ties this call to the earlier
generateLink / OTP-request step.
curl -X POST "https://apihspsbx.abdm.gov.in/v4/int/v2/registration/aadhaar/verifyOTP" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"otp": "BASE64_RSA_ENCRYPTED_OTP",
"txnId": "TXN_ID"
}'{
"txnId": "TXN_ID",
"authResult": "success",
"message": "OTP verified"
}Health data encryption scheme
Use this scheme when the HIP sends health data to an HIU. See the full flow in Milestone 2 data transfer.
| Term | Meaning |
|---|---|
| ECDH | Elliptic-curve Diffie-Hellman key exchange. |
| Curve25519 | The elliptic curve used for the ECDH operation. |
| AES-GCM | The symmetric cipher for health data encryption. |
| DHPK | The ECDH public key. |
| DHSK | The ECDH private key. |
| RAND | A 32-byte random value, also called a nonce. |
| DHK(U,P) | The ECDH shared secret for the HIU and HIP session. |
| HKDF | The key derivation function. |
| SK(U,P) | The AES-GCM key for one data transfer session. |
The HIU sends its public key material in the health information request. The HIP then creates sender key material and encrypts the FHIR bundle.
Read the HIU key material
Read the HIU public key, nonce, curve, and algorithm from the request. Reject unsupported algorithms or expired public keys.
Generate HIP key material
Generate a short-term ECDH key pair on Curve25519. Generate a new 32-byte random nonce for the HIP.
Derive the shared secret
Compute the ECDH shared secret from the HIU public key and HIP private key. Keep the HIP private key only for this transfer session.
Derive SALT and IV
XOR the HIP nonce and HIU nonce. Use the first 20 bytes as the HKDF salt. Use the last 12 bytes as the AES-GCM IV.
Derive the AES-GCM key
Use HKDF with the ECDH shared secret and the salt. Generate a 256-bit AES-GCM key for the session.
Encrypt and send
Encrypt the FHIR bundle with AES-GCM and the IV. Send the encrypted content, HIP public key, and HIP nonce to the HIU.
{
"cryptoAlg": "ECDH",
"curve": "Curve25519",
"dhPublicKey": {
"expiry": "2026-08-11T15:06:33.000Z",
"parameters": "Curve25519/32byte random key",
"keyValue": "<base64-hip-public-key>"
},
"nonce": "<base64-hip-32-byte-nonce>"
}Use Fidelius or a vetted port
NHA published Fidelius as a reference implementation for this exchange. Prefer Fidelius or a tested port over hand-written cryptography.
Operational rules
- Cache the certificate, but key it by service and refresh on any decryption/padding failure — the key can rotate.
- One value per encryption — encrypt the Aadhaar and the OTP separately; don't concatenate them.
- RSA size limit — a 2048-bit key with OAEP/SHA-1 encrypts at most ~214 bytes. Aadhaar numbers and OTPs are tiny, so this is never a constraint, but don't try to RSA-encrypt large blobs.
- UTF-8 in, Base64 out — mismatched encodings are the most common cause of "invalid encrypted value" errors.
- No plaintext at rest — don't persist Aadhaar/OTP even briefly; encrypt in the same function that receives them.
Sources
- ABDM Sandbox V3 Documentation (sandbox.abdm.gov.in/sandbox/v3)
- HSP Swagger — getPublicCertificate (apihspsbx.abdm.gov.in/v4/int)
- Verified live: GET /abha/api/v3/profile/public/certificate and GET /api/v1/auth/cert (both 401 without auth)
- ABDM Proposed Simplified Milestone 2 (DOCX→MD, 2026-08)