OpenPGP Encryption Configuration
1. What is payload encryption?
Payload encryption is the process of converting sensitive data (the payload) into an unreadable format before sending it over a network, so that only the intended recipient can read it. The payload refers to the actual data transmitted in an API request or response (e.g., personal details, bank account info, documents).
The main goal is to ensure confidentiality and integrity of data during transmission.
2. Why do we need payload encryption?
- Data security — protect sensitive information (like PAN, Aadhaar, bank details) from being exposed.
- Prevent man-in-the-middle attacks — even if traffic is intercepted, encrypted data remains unreadable.
- Compliance requirements — many regulatory bodies mandate encryption for sensitive data.
- Additional layer of security — goes beyond HTTPS encryption for zero-trust architecture.
3. Types of payload encryption
Symmetric encryption
- Uses one single key for both encryption and decryption.
- Faster but less secure because the key needs to be shared.
- Example: AES (Advanced Encryption Standard).
Screenshot: Symmetric encryption diagram. (Pending migration from Notion.)
Asymmetric encryption
- Uses two keys:
- Public key — used for encryption.
- Private key — used for decryption.
- More secure because the private key is never shared.
Screenshot: Asymmetric encryption diagram. (Pending migration from Notion.)
Hybrid encryption
Hybrid encryption combines asymmetric encryption (public-key cryptography) and symmetric encryption (secret-key cryptography) to leverage the strengths of both methods.
Why hybrid?
- Symmetric encryption is fast for large data but key exchange is hard.
- Asymmetric encryption is secure for key exchange but slow for large data.
- Combining them gives speed (symmetric for payload) and security (asymmetric for key exchange).
Screenshot: Hybrid encryption flow diagram. (Pending migration from Notion.)
4. Our approach
At HyperVerge, we use OpenPGP with hybrid encryption for payload encryption.
Encryption flow:
- A random symmetric session key is generated (e.g., AES key).
- The payload is encrypted using that symmetric key (fast and efficient).
- That symmetric key is then encrypted with the recipient's public key (asymmetric).
- Both the encrypted payload and the encrypted session key are sent together.
Decryption flow:
- The private key decrypts the encrypted symmetric session key.
- The symmetric key then decrypts the payload.
- HV's public key is used for encryption on the client side.
- Client's public key is used to map to us in our DB.
- Always check with Engineering before proceeding. Some APIs do not support encryption — e.g.,
readPanandreadAadhaar. For Thomas APIs, pass additional parameter"encryptedPayload": "yes"in the body. - Sample PR for passing this additional parameter.
- We are transitioning to using gKYC APIs and do not recommend implementing payload encryption for older APIs like
readAadhaarandreadPan, as these are primarily internal APIs.
5. When should we suggest OpenPGP payload encryption?
Payload encryption using OpenPGP is optional, because we already ensure data security through multiple industry-standard measures:
- Data at rest — encrypted with AES-256.
- Data in transit — secured using TLS 1.2 or higher.
- Access control:
- MFA for all access.
- All access is logged, monitored, and follows the principle of least privilege.
- Access is granted only after approvals.
- Compliance & security testing:
- ISO 27001 & ISO 27018 certified.
- SOC 2 Type II compliance.
- Annual VAPT by CERT-In empaneled auditors.
- Regular privileged system access reviews and firewall audits.
We recommend OpenPGP only if the customer mandates encryption at the payload level for additional security beyond HTTPS/TLS and AES-256.
Typical cases:
- Customer's internal security policy requires application-level encryption.
- Regulatory or compliance requirements in their domain.
- They handle extremely sensitive data and want zero trust even beyond network encryption.
6. OpenPGP code snippets (for clients to implement)
JavaScript (package: openpgp)
const openpgp = require("openpgp");
const fs = require("fs");
const readFile = (path) => fs.readFileSync(path).toString();
const encrypt = async (message, publicKey) => {
const armoredPublicKey = await openpgp.readKey({ armoredKey: publicKey });
return openpgp.encrypt({
message: await openpgp.createMessage({ text: message }),
encryptionKeys: armoredPublicKey
});
};
const decrypt = async (encryptedMessage, privateKey, passphrase) => {
const armoredPrivateKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privateKey }),
passphrase,
config: { preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256 }
});
const message = await openpgp.readMessage({ armoredMessage: encryptedMessage });
const { data } = await openpgp.decrypt({ message, decryptionKeys: armoredPrivateKey });
return data;
};
(async () => {
const publicKey = readFile("../pubkey.asc");
const privateKey = readFile("../privkey.asc");
const encryptedText = await encrypt("This is coming from javascript", publicKey);
console.log(encryptedText);
const passphrase = readFile("../passphrase.txt");
const decryptedText = await decrypt(readFile("./pgpMessage.asc"), privateKey, passphrase);
console.log(decryptedText);
})();
Python (package: gnupg)
import gnupg
def read_file(path):
with open(path, 'r') as f:
return f.read()
def encrypt(message, public_key):
gpg = gnupg.GPG()
import_result = gpg.import_keys(public_key)
fingerprint = import_result.fingerprints[0]
encrypted = gpg.encrypt(message, fingerprint, passphrase='<passphrase>', always_trust=True)
return str(encrypted)
def decrypt(encrypted_message, private_key, passphrase):
gpg = gnupg.GPG()
gpg.import_keys(private_key)
decrypted = gpg.decrypt(encrypted_message, passphrase=passphrase, always_trust=True)
return str(decrypted) if decrypted.ok else None
Go (package: go-crypto/openpgp)
// Full example in the Notion source; uses github.com/ProtonMail/go-crypto/openpgp
// Reads armored keys, generates armored PGP messages on Encrypt and decodes with passphrase on Decrypt.
See Notion source for the full Go implementation.
C# (package: BouncyCastle.OpenPGP)
See Notion source for the full C# implementation using PgpEncryptedDataGenerator, PgpPublicKey, and PgpPrivateKey.
Java (package: Bouncy Castle PGP)
See Notion source for the full Java implementation using PGPEncryptedDataGenerator, PGPSignatureGenerator, AES-256, SHA-256, and armored output.
Other languages:
- PHP: singpolyma/openpgp-php
- Ruby: gpgme / openpgp
- C++: rnp
- Rust: rpgp
7. Steps to implement payload encryption
1. Generate and exchange keys
- Obtain the client's public key.
- Generate an RSA key pair.
Key details:
- Algorithm: RSA
- Key size: 2048 bits
- Expiry: Never
2. Map keys in the HyperVerge database (handled by Engineering)
Internal engineering steps
Share the client's public key with the gKYC team to map the configs.
If the request contains images (previously sent via multipart/form-data), convert them into base64 strings and form a JSON payload before encryption.
Add a client:
curl --location --request POST 'https://self-serve-staging.dev.hyperverge.org/api/company/add/existingCredential' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"appId": "client_appid",
"appKey": "client_appkey",
"clientId": "hyperverge.co",
"type": "STAGING",
"useCase": "default"
}'
Update metadata with keys:
curl --location --request PUT 'https://self-serve-staging.dev.hyperverge.org/api/internal/metadata/update' \
--header 'appid: appid' \
--header 'appkey: appkey' \
--header 'Content-Type: application/json' \
--data-raw '{
"key": "identifier",
"appid": "client_appid",
"data": {
"clientPublicKey": "---------KEY--------",
"hvPrivateKey": "---------KEY--------",
"hvPassphrase": "hvPassPhrase"
},
"keysToEncrypt": ["identifier.clientPublicKey", "identifier.hvPrivateKey", "identifier.hvPassphrase"]
}'
Fetch metadata for an App ID:
curl --location --request GET 'https://self-serve-staging.dev.hyperverge.org/api/internal/metadata' \
--header 'appid: appid' \
--header 'appkey: appkey' \
--header 'key: identifier' \
--header 'clientappid: client_appid'
Production URL: https://self-serve-portal.hyperverge.co/api/internal/metadata
3. Share keys with the client
- Share HyperVerge's public key and the identifier with the client.
- Naming convention for identifiers:
client_name_testing→ UATclient_name_production→ Production
- Always generate different public keys for UAT and Production (both client and HyperVerge).
4. Client-side implementation
- Encryption — client uses HyperVerge's public key + identifier to encrypt the payload.
- Decryption — client uses their private key + passphrase to decrypt HyperVerge's API response.
- Both operations are handled on the client side.
Steps to encrypt the request
- Form the request JSON (J).
- Stringify the JSON (S).
- Pass (S) as input to PGP encryption. The encryption function returns an encrypted string (E). It uses HV's public key to encrypt the payload.
- Wrap (E) in a JSON with the
messagekeyword:
{ "message": "<(E)>" }
Steps to decrypt the response
- The response sent back after processing is also in encrypted format (R). Same structure as the request:
{ "message": "<(R)>" }
- Decrypt (R) using the client's private key to reveal the original API response JSON.
- The JSON will be in stringified format and must be deserialised before being processed.
Example — PANDetailedFetchWithoutPhoneNumber
API cURL with encrypted data
curl --location 'https://ind-thomas.hyperverge.co/v1/PANDetailedFetchWithoutPhoneNumber' \
--header 'Content-Type: application/json' \
--header 'appId: {{appId}}' \
--header 'appKey: {{appKey}}' \
--header 'transactionId: {{transactionId}}' \
--data '{"message": "-----BEGIN PGP MESSAGE-----\n\n<encrypted body>\n-----END PGP MESSAGE-----"}'
API response after decryption
{
"status": "success",
"statusCode": 200,
"metaData": {
"requestId": "",
"transactionId": ""
},
"result": {
"data": {
"message": "Valid PAN.",
"panData": {
"name": "",
"gender": "",
"pan": "",
"firstName": "",
"middleName": "",
"lastName": "",
"dateOfBirth": "",
"maskedAadhaarNumber": "",
"address": {
"street": "",
"city": "",
"state": "",
"pincode": "",
"line1": "",
"line2": ""
},
"aadhaarLinked": true
}
}
}
}