National Health AuthorityNHA Docs

Command Palette

Search for a command to run...

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

FieldWhere it appearsEncrypt?
Aadhaar numberABHA enrolment, HPR ID creation (Aadhaar route)✅ Always
OTP (Aadhaar / mobile)Every verifyOTP / verifyMobileOTP call✅ Always
Mobile numberSome demographic / mobile-auth flows⚠️ When the field is documented as encrypted
FHIR bundleHIP 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:

GEThttps://abhasbx.abdm.gov.in/abha/api/v3/profile/public/certificate

ABHA (ABDM) public key — used for ABHA enrolment / login Aadhaar & OTP fields.

cURL
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)"
Response
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
...base64 DER of the 2048-bit RSA public key...
-----END PUBLIC KEY-----
GEThttps://apihspsbx.abdm.gov.in/v4/int/api/v1/auth/cert

HPR / NHPR public key — used for the Milestone 4 HPR ID Aadhaar & OTP fields.

cURL
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.

encrypt.js
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:

POST/v2/registration/aadhaar/verifyOTP

The 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
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"
}'
Response
{
"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.

TermMeaning
ECDHElliptic-curve Diffie-Hellman key exchange.
Curve25519The elliptic curve used for the ECDH operation.
AES-GCMThe symmetric cipher for health data encryption.
DHPKThe ECDH public key.
DHSKThe ECDH private key.
RANDA 32-byte random value, also called a nonce.
DHK(U,P)The ECDH shared secret for the HIU and HIP session.
HKDFThe 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.

HIP keyMaterial in a data push
{
  "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)