Products · Security
Decrypting & Responding
Server code to decrypt a request from Monime and encrypt your reply, step by step and as a full handler.
Each request holds two base64 fields: encryptedAesKey, the AES key encrypted with your RSA public key, and encryptedExchangeData, the request encrypted with that AES key. Your server decrypts the AES key, decrypts the request, and encrypts its reply with the same AES key. Encryption Model describes the format.
Step 1: Load your private key at startup
Read the private key once when your server starts. The samples expect a PKCS#8 key, which starts with -----BEGIN PRIVATE KEY-----. Key Setup shows how to make one.
const crypto = require("crypto");
// Load at startup. MONIME_RSA_PRIVATE_KEY is the full PEM string.
const PRIVATE_KEY = process.env.MONIME_RSA_PRIVATE_KEY;Step 2: Decode the two fields
Decode both fields from base64 to bytes.
{
"encryptedAesKey": "gqRhH8xT...==",
"encryptedExchangeData": "aBx92Kz...=="
}function decodeRequestFields(body) {
return {
encryptedKeyBuf: Buffer.from(body.encryptedAesKey, "base64"),
encryptedDataBuf: Buffer.from(body.encryptedExchangeData, "base64"),
};
}Step 3: Decrypt the AES key
Decrypt encryptedAesKey with your RSA private key, using OAEP with SHA-256. The result is the 16-byte AES key for this request. Keep it: you need it again in step 5.
function unwrapAesKey(encryptedKeyBuf) {
return crypto.privateDecrypt(
{
key: PRIVATE_KEY,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedKeyBuf,
);
// Returns the 16-byte AES key as a Buffer.
}Step 4: Decrypt the request
Byte 0 of the decoded encryptedExchangeData is the IV length (12). The next 12 bytes are the IV. The rest is the ciphertext, followed by the 16-byte GCM tag.
function decryptPayload(encryptedDataBuf, aesKey) {
// Blob layout: [1 byte IV length][IV][ciphertext][16 byte auth tag]
const ivLength = encryptedDataBuf[0]; // 12
const iv = encryptedDataBuf.subarray(1, 1 + ivLength);
const tag = encryptedDataBuf.subarray(encryptedDataBuf.length - 16); // last 16 bytes
const ciphertext = encryptedDataBuf.subarray(1 + ivLength, encryptedDataBuf.length - 16);
const decipher = crypto.createDecipheriv("aes-128-gcm", aesKey, iv, {
authTagLength: 16,
});
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
return JSON.parse(decrypted.toString("utf8"));
// Returns { global, currentPage, flowData, exportedData? }
}Step 5: Encrypt and return your reply
Build your navigate or stop reply and encrypt it with the same AES key as the request, using a new random 12-byte IV. Return the base64 text as the body with HTTP status 200. Don't wrap it in JSON or add encryptedAesKey.
function encryptResponse(responseObject, aesKey) {
const iv = crypto.randomBytes(12); // new IV for each reply
const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, iv, {
authTagLength: 16,
});
const ciphertext = Buffer.concat([
cipher.update(JSON.stringify(responseObject), "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag(); // 16 bytes
// Blob layout: [1 byte IV length][12 byte IV][ciphertext][16 byte auth tag]
const blob = Buffer.alloc(1 + iv.length + ciphertext.length + tag.length);
blob.writeUInt8(iv.length, 0);
iv.copy(blob, 1);
ciphertext.copy(blob, 1 + iv.length);
tag.copy(blob, 1 + iv.length + ciphertext.length);
return blob.toString("base64");
}
// Example usage:
const response = { action: "navigate", pageId: "confirm", pageData: { fee: "50" } };
// const response = { action: "stop", message: "Transfer complete. Ref: TXN-0042." };
const encrypted = encryptResponse(response, aesKey);
res.setHeader("Content-Type", "text/plain");
return res.send(encrypted);Full handler
All five steps in one HTTP handler.
The same handler works for exchange actions and dynamic pages. The request and reply formats are the same for both.