Nirmitee.io
Implementing ABDM Fidelius Encryption in Node.js: The Complete Guide

Implementing ABDM Fidelius Encryption in Node.js: The Complete Guide

March 20, 2026
14 min read
ABDM

Every ABDM integrator hits the same wall: ABDM-9999: Could not read encrypted content. Here's how we solved it in Node.js — and what every blog and GitHub README gets wrong about the BouncyCastle curve parameters.

The Problem Every ABDM Developer Faces

You've built your HIP integration. Discovery works. Linking works. Consent flows through. Then you try to push health records to the patient's PHR app, and ABDM rejects your data:

ABDM-9999: Could not read encrypted content from input encoded key spec not recognized

ABDM requires all health data to be encrypted using a protocol called Fidelius — even in the sandbox environment. There's no bypass. No debug mode. No plain-text option.

What is the Fidelius Protocol?

Fidelius is ABDM's encryption protocol for health data transfer between HIPs (Health Information Providers) and HIUs (Health Information Users). It ensures perfect forward secrecy — every data transfer uses fresh ephemeral keys.

The Protocol in 6 Steps

  1. Key Generation — ECDH keypairs on Curve25519 (Weierstrass form, NOT Montgomery X25519)
  2. Nonce Exchange — Each party generates a 32-byte random nonce
  3. Nonce Processing — XOR both nonces: first 20 bytes = HKDF salt, last 12 bytes = AES-GCM IV
  4. Shared Secret — ECDH: sender_private x requester_public = x-coordinate
  5. Key Derivation — HKDF-SHA256(salt, shared_secret) = 32-byte AES key
  6. Encryption — AES-256-GCM(key, IV, plaintext) = ciphertext + 16-byte auth tag

Output: base64(ciphertext + tag)

The BouncyCastle Curve Parameter Trap

This is where every non-Java implementation fails. ABDM uses BouncyCastle's Curve25519 in Short Weierstrass form. When you search for the parameters, every source points to CustomNamedCurves.java. The comments there list wrong values.

Wrong (from comments and blogs everywhere)

a = "2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451"
b = "20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9"

Correct (from Curve25519.java — the actual implementation class)

a = "2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144"
b = "7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864"

How We Proved It

We verified mathematically: with the wrong parameters, the generator point does NOT satisfy the curve equation y² = x³ + ax + b (mod p). With the correct parameters from Curve25519.java, it validates perfectly.

Source: bcgit/bc-java Curve25519.java

The Complete Data Transfer Flow

The HIU generates a keypair and sends only the public key with the health info request. The HIP generates its own keypair, encrypts data, and sends its public key back. Both sides derive the same shared secret independently.

Node.js Implementation

We built the first native TypeScript Fidelius implementation — no Java, no subprocess, no JVM. About 200 lines using elliptic + Node.js crypto.

Curve Setup

import elliptic from 'elliptic'
import BN from 'bn.js'

const bc25519 = new (elliptic.curve.short as any)({
  p: '7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed',
  a: '2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144',
  b: '7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864',
  n: '1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed',
  g: [
    '2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a',
    '20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9',
  ],
  h: '08',
})

Encryption Function

function encrypt(opts) {
  // XOR nonces -> salt (20 bytes) + IV (12 bytes)
  const xorNonces = Buffer.alloc(32)
  for (let i = 0; i < 32; i++)
    xorNonces[i] = senderNonce[i] ^ requesterNonce[i]
  const salt = xorNonces.subarray(0, 20)
  const iv = xorNonces.subarray(20, 32)

  // ECDH shared secret
  const sharedSecret = pubPoint.mul(privBN).getX().toArray('be', 32)

  // HKDF-SHA256 -> 32-byte AES key
  const aesKey = crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.alloc(0), 32)

  // AES-256-GCM
  const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv)
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
  return Buffer.concat([encrypted, cipher.getAuthTag()]).toString('base64')
}

Test Vector Validation

Private Key:  DMxHPri8d7IT23KgLk281zZenMfVHSdeamq0RhwlIBk=
Plaintext:    "Wormtail should never have been Potter cottage's secret keeper."
Expected:     pzMvVZNNVtJzqPkkxcCbBUWgDEBy/mBXIeT2dJWI16ZA...
Result:       MATCH (all 10 test vectors pass)

Data Push Format: 7 Common Mistakes

  1. Sending requester's keyMaterial instead of sender's (HIP's)
  2. Using database UUID as careContextReference instead of actual reference number
  3. Sending plain base64 FHIR instead of Fidelius encrypted content
  4. Using raw uncompressed EC point instead of X.509 DER format for keyValue
  5. Using SHA-256 checksum when ABDM expects MD5
  6. Adding extra headers (REQUEST-ID, X-CM-ID) to dataPushUrl — only Authorization + Content-Type needed
  7. Reading transactionId from hiRequest object when it's at the body top level

Implementations by Language

The curve parameter trap affects all languages. If you're implementing Fidelius in Go, PHP, Ruby, or any other language — use the correct values documented above.

We Can Help

At Nirmitee, we've implemented ABDM M1, M2, and M3 end-to-end against the live sandbox. We built the first native TypeScript SDK with Fidelius encryption, NRCES-compliant FHIR generation, and callback handling.

Resources

Struggling with healthcare data exchange? Our Healthcare Interoperability Solutions practice helps organizations connect clinical systems at scale. Talk to our team to get started.

Frequently Asked Questions

Can I skip Fidelius encryption in the ABDM sandbox?

No. ABDM validates encryption even in sandbox mode. All health data must be encrypted using the Fidelius protocol.

Why do most non-Java ABDM encryption implementations fail?

Because they use wrong Curve25519 parameters from BouncyCastle's CustomNamedCurves.java comments. The correct values are in Curve25519.java.

Is there a native Node.js Fidelius implementation?

Yes. Nirmitee built the first native TypeScript implementation using the elliptic npm package. Zero Java dependency. Available as @nirmitee/abdm-sdk.

What is the difference between X25519 and BouncyCastle Curve25519?

X25519 uses Montgomery form (TLS/SSH). BouncyCastle Curve25519 uses Short Weierstrass form. Standard Node.js crypto X25519 is NOT compatible with ABDM Fidelius.

How do I validate my Fidelius implementation?

Use test vectors from mgrmtech/fidelius-cli. If your implementation produces the exact expected ciphertext for the known plaintext and keys, it's compatible with ABDM.