# Decrypting & Responding (/docs/products/ussd-flows/developers/security/implement)

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](/docs/products/ussd-flows/developers/security/overview) describes the format.

> **Warning:** In PHP and Java, the RSA step needs extra settings. PHP's `openssl_private_decrypt` can't use SHA-256 for OAEP, so use [phpseclib v3](https://phpseclib.com/). In Java, set `MGF1ParameterSpec.SHA256` through `OAEPParameterSpec`. See [Troubleshooting](/docs/products/ussd-flows/developers/security/troubleshooting).

## 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](/docs/products/ussd-flows/developers/security/key-setup) shows how to make one.

```javascript Node.js
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;
```

```python Python
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import os

# Load and parse the PEM key once at startup.
private_key = load_pem_private_key(
    os.environ["MONIME_RSA_PRIVATE_KEY"].encode(),
    password=None,
)
```

```php PHP
<?php
// composer require phpseclib/phpseclib
// openssl_private_decrypt only supports SHA-1 for OAEP, so use phpseclib.
use phpseclib3\Crypt\PublicKeyLoader;

$privateKey = PublicKeyLoader::load(getenv("MONIME_RSA_PRIVATE_KEY"))
    ->withHash('sha256')
    ->withMGFHash('sha256');
```

```java Java
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

// Needs a PKCS#8 key ("BEGIN PRIVATE KEY").
String pem = System.getenv("MONIME_RSA_PRIVATE_KEY")
    .replace("-----BEGIN PRIVATE KEY-----", "")
    .replace("-----END PRIVATE KEY-----", "")
    .replaceAll("\\s", "");

PrivateKey privateKey = KeyFactory.getInstance("RSA")
    .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem)));
```

```go Go
import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "os"
)

var privateKey *rsa.PrivateKey

func init() {
    block, _ := pem.Decode([]byte(os.Getenv("MONIME_RSA_PRIVATE_KEY")))
    if block == nil {
        panic("MONIME_RSA_PRIVATE_KEY is empty or not a PEM key")
    }
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        panic(err)
    }
    rsaKey, ok := key.(*rsa.PrivateKey)
    if !ok {
        panic("MONIME_RSA_PRIVATE_KEY is not an RSA key")
    }
    privateKey = rsaKey
}
```

## Step 2: Decode the two fields

Decode both fields from base64 to bytes.

```json
{
  "encryptedAesKey": "gqRhH8xT...==",
  "encryptedExchangeData": "aBx92Kz...=="
}
```

```javascript Node.js
function decodeRequestFields(body) {
  return {
    encryptedKeyBuf:  Buffer.from(body.encryptedAesKey, "base64"),
    encryptedDataBuf: Buffer.from(body.encryptedExchangeData, "base64"),
  };
}
```

```python Python
import base64

def decode_request_fields(body: dict) -> tuple[bytes, bytes]:
    encrypted_key  = base64.b64decode(body["encryptedAesKey"])
    encrypted_data = base64.b64decode(body["encryptedExchangeData"])
    return encrypted_key, encrypted_data
```

```php PHP
function decodeRequestFields(array $body): array {
    return [
        'encryptedKey'  => base64_decode($body['encryptedAesKey']),
        'encryptedData' => base64_decode($body['encryptedExchangeData']),
    ];
}
```

```java Java
byte[] encryptedKey  = Base64.getDecoder().decode(body.get("encryptedAesKey"));
byte[] encryptedData = Base64.getDecoder().decode(body.get("encryptedExchangeData"));
```

```go Go
import "encoding/base64"

func decodeRequestFields(body map[string]string) ([]byte, []byte, error) {
    encryptedKey, err := base64.StdEncoding.DecodeString(body["encryptedAesKey"])
    if err != nil {
        return nil, nil, err
    }
    encryptedData, err := base64.StdEncoding.DecodeString(body["encryptedExchangeData"])
    if err != nil {
        return nil, nil, err
    }
    return encryptedKey, encryptedData, nil
}
```

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

```javascript Node.js
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.
}
```

```python Python
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives import hashes

def unwrap_aes_key(encrypted_key: bytes) -> bytes:
    return private_key.decrypt(
        encrypted_key,
        asym_padding.OAEP(
            mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )
    # Returns the raw 16-byte AES key.
```

```php PHP
function unwrapAesKey(string $encryptedKey): string {
    global $privateKey;
    // phpseclib, set up for SHA-256 in step 1.
    return $privateKey->decrypt($encryptedKey);
    // Returns the 16-byte AES key.
}
```

```java Java
import javax.crypto.Cipher;
import java.security.spec.MGF1ParameterSpec;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;

// Without OAEPParameterSpec, Java uses SHA-1 for MGF1 and decryption fails.
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
    "SHA-256",
    "MGF1",
    MGF1ParameterSpec.SHA256,
    PSource.PSpecified.DEFAULT
);
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
byte[] aesKey = rsaCipher.doFinal(encryptedKey);
// aesKey is the 16-byte AES key.
```

```go Go
import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
)

func unwrapAesKey(encryptedKey []byte) ([]byte, error) {
    // Go uses the same hash for OAEP and MGF1.
    return rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil)
}
```

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

> **Note:** Python, Java and Go expect the tag at the end of the ciphertext, so pass everything after the IV. Node.js and PHP need the last 16 bytes split off and passed as the tag.

```javascript Node.js
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? }
}
```

```python Python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import json

def decrypt_payload(encrypted_data: bytes, aes_key: bytes) -> dict:
    iv_length  = encrypted_data[0]            # 12
    iv         = encrypted_data[1 : 1 + iv_length]
    ciphertext = encrypted_data[1 + iv_length :]

    # AESGCM handles the 16-byte GCM auth tag automatically.
    decrypted = AESGCM(aes_key).decrypt(iv, ciphertext, None)
    return json.loads(decrypted.decode("utf-8"))
    # Returns { global, currentPage, flowData, exportedData? }
```

```php PHP
function decryptPayload(string $encryptedData, string $aesKey): array {
    $ivLength  = ord($encryptedData[0]);      // 12
    $iv        = substr($encryptedData, 1, $ivLength);
    $remainder = substr($encryptedData, 1 + $ivLength);

    // PHP requires the 16-byte GCM auth tag to be split off manually.
    $ciphertext = substr($remainder, 0, -16);
    $tag        = substr($remainder, -16);

    $decrypted = openssl_decrypt(
        $ciphertext,
        "aes-128-gcm",
        $aesKey,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
    );

    if ($decrypted === false) {
        throw new RuntimeException("Couldn't decrypt the request. Check the AES key and the tag.");
    }

    return json_decode($decrypted, true);
    // Returns ['global' => ..., 'currentPage' => ..., 'flowData' => ..., 'exportedData' => ...?]
}
```

```java Java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

int    ivLength   = encryptedData[0] & 0xFF;   // 12
byte[] iv         = Arrays.copyOfRange(encryptedData, 1, 1 + ivLength);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, 1 + ivLength, encryptedData.length);

// AES/GCM/NoPadding reads the tag from the end of the ciphertext.
Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding");
aesCipher.init(Cipher.DECRYPT_MODE,
    new SecretKeySpec(aesKey, "AES"),
    new GCMParameterSpec(128, iv));

String json = new String(aesCipher.doFinal(ciphertext), StandardCharsets.UTF_8);
// Parse json to read global, currentPage, flowData and exportedData.
```

```go Go
import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/json"
    "fmt"
)

func decryptPayload(encryptedData []byte, aesKey []byte) (map[string]interface{}, error) {
    if len(encryptedData) < 1+12+16 {
        return nil, fmt.Errorf("encrypted data is too short")
    }
    ivLength := int(encryptedData[0]) // 12
    iv := encryptedData[1 : 1+ivLength]
    ciphertext := encryptedData[1+ivLength:]

    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return nil, err
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    // gcm.Open reads the tag from the end of the ciphertext.
    decrypted, err := gcm.Open(nil, iv, ciphertext, nil)
    if err != nil {
        return nil, fmt.Errorf("AES-GCM decrypt: %w", err)
    }

    var payload map[string]interface{}
    if err := json.Unmarshal(decrypted, &payload); err != nil {
        return nil, err
    }
    return payload, nil
}
```

## 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`.

```javascript Node.js
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);
```

```python Python
import os
import json
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt_response(response_object: dict, aes_key: bytes) -> str:
    iv         = os.urandom(12)  # new IV for each reply
    ciphertext = AESGCM(aes_key).encrypt(
        iv,
        json.dumps(response_object).encode("utf-8"),
        None,
    )

    # Embed IV: [1 byte length][12 byte IV][ciphertext + 16 byte auth tag]
    blob = bytes([len(iv)]) + iv + ciphertext
    return base64.b64encode(blob).decode()

# Example usage, inside a Flask view:
response = {"action": "navigate", "pageId": "confirm", "pageData": {"fee": "50"}}
# response = {"action": "stop", "message": "Transfer complete. Ref: TXN-0042."}
encrypted = encrypt_response(response, aes_key)
return Response(encrypted, content_type="text/plain")
```

```php PHP
function encryptResponse(array $response, string $aesKey): string {
    $iv  = random_bytes(12);  // new IV for each reply
    $tag = "";

    $ciphertext = openssl_encrypt(
        json_encode($response),
        "aes-128-gcm",
        $aesKey,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
        "",
        16,
    );

    // Embed IV: [1 byte length][12 byte IV][ciphertext][16 byte auth tag]
    $blob = chr(strlen($iv)) . $iv . $ciphertext . $tag;
    return base64_encode($blob);
}

// Example usage:
$response = ["action" => "navigate", "pageId" => "confirm", "pageData" => ["fee" => "50"]];
header("Content-Type: text/plain");
echo encryptResponse($response, $aesKey);
```

```java Java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Map;

byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);  // new IV for each reply

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,
    new SecretKeySpec(aesKey, "AES"),
    new GCMParameterSpec(128, iv));

byte[] encrypted = cipher.doFinal(responseJson.getBytes(StandardCharsets.UTF_8));

// Embed IV: [1 byte length][12 byte IV][ciphertext + 16 byte auth tag]
byte[] blob = new byte[1 + iv.length + encrypted.length];
blob[0] = (byte) iv.length;
System.arraycopy(iv, 0, blob, 1, iv.length);
System.arraycopy(encrypted, 0, blob, 1 + iv.length, encrypted.length);

// Return as text/plain
String base64Blob = Base64.getEncoder().encodeToString(blob);
```

```go Go
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "io"
)

func encryptResponse(responseData map[string]interface{}, aesKey []byte) (string, error) {
    responseJson, err := json.Marshal(responseData)
    if err != nil {
        return "", err
    }

    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return "", err
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }

    iv := make([]byte, 12) // new IV for each reply
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        return "", err
    }

    encrypted := gcm.Seal(nil, iv, responseJson, nil)

    // Embed IV: [1 byte length][12 byte IV][ciphertext + 16 byte auth tag]
    blob := make([]byte, 1+len(iv)+len(encrypted))
    blob[0] = byte(len(iv))
    copy(blob[1:], iv)
    copy(blob[1+len(iv):], encrypted)

    // Return as text/plain
    return base64.StdEncoding.EncodeToString(blob), nil
}
```

## Full handler

All five steps in one HTTP handler.

**Show complete handler**

```javascript Node.js (Express)
const express = require("express");
const crypto  = require("crypto");

const app        = express();
const PRIVATE_KEY = process.env.MONIME_RSA_PRIVATE_KEY;

app.use(express.json());

app.post("/ussd/exchange", (req, res) => {
  try {
    // Steps 2 to 4: decode, decrypt the AES key, decrypt the request
    const { encryptedKeyBuf, encryptedDataBuf } = decodeRequestFields(req.body);
    const aesKey  = unwrapAesKey(encryptedKeyBuf);
    const payload = decryptPayload(encryptedDataBuf, aesKey);

    const { global: g, flowData, currentPage } = payload;

    // Your business logic
    const responseObject = processFlow(g, flowData, currentPage);

    // Step 5: encrypt and return
    res.type("text/plain").send(encryptResponse(responseObject, aesKey));
  } catch (err) {
    // Monime ends the session with an error for any status other than 200.
    console.error("Exchange error:", err);
    res.status(400).json({ error: "Bad request" });
  }
});

function processFlow(g, flowData, currentPage) {
  if (currentPage === "enter_amount") {
    return {
      action: "navigate",
      pageId: "confirm_transfer",
      pageData: { fee: calculateFee(flowData.enter_amount) },
    };
  }
  if (currentPage === "confirm_transfer") {
    // process the transfer ...
    return { action: "stop", message: "Transfer complete. Ref: TXN-0042." };
  }
  return { action: "stop", message: "Thank you." };
}

function calculateFee(amount) {
  return Math.round(Number(amount) * 0.01);
}

// Helper functions from the steps above
function decodeRequestFields(body) {
  return {
    encryptedKeyBuf:  Buffer.from(body.encryptedAesKey, "base64"),
    encryptedDataBuf: Buffer.from(body.encryptedExchangeData, "base64"),
  };
}

function unwrapAesKey(encryptedKeyBuf) {
  return crypto.privateDecrypt(
    { key: PRIVATE_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
    encryptedKeyBuf,
  );
}

function decryptPayload(encryptedDataBuf, aesKey) {
  const ivLength   = encryptedDataBuf[0];
  const iv         = encryptedDataBuf.subarray(1, 1 + ivLength);
  const tag        = encryptedDataBuf.subarray(encryptedDataBuf.length - 16);
  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"));
}

function encryptResponse(responseObject, aesKey) {
  const iv         = crypto.randomBytes(12);
  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();
  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");
}

app.listen(3000);
```

```python Python (Flask)
import os, json, base64
from flask import Flask, request, Response
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.serialization import load_pem_private_key

app         = Flask(__name__)
private_key = load_pem_private_key(os.environ["MONIME_RSA_PRIVATE_KEY"].encode(), password=None)

@app.route("/ussd/exchange", methods=["POST"])
def ussd_exchange():
    body = request.get_json()

    # Steps 2 to 4: decode, decrypt the AES key, decrypt the request
    encrypted_key  = base64.b64decode(body["encryptedAesKey"])
    encrypted_data = base64.b64decode(body["encryptedExchangeData"])
    aes_key        = private_key.decrypt(
        encrypted_key,
        asym_padding.OAEP(mgf=asym_padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
    )
    iv_length  = encrypted_data[0]
    iv         = encrypted_data[1 : 1 + iv_length]
    ciphertext = encrypted_data[1 + iv_length :]
    payload    = json.loads(AESGCM(aes_key).decrypt(iv, ciphertext, None).decode("utf-8"))

    g, flow_data, current_page = payload["global"], payload["flowData"], payload["currentPage"]

    # Your business logic
    response = process_flow(g, flow_data, current_page)

    # Step 5: encrypt and return
    iv2        = os.urandom(12)
    ciphertext = AESGCM(aes_key).encrypt(iv2, json.dumps(response).encode("utf-8"), None)
    blob       = bytes([len(iv2)]) + iv2 + ciphertext
    return Response(base64.b64encode(blob).decode(), content_type="text/plain")

def process_flow(g, flow_data, current_page):
    if current_page == "enter_amount":
        return {"action": "navigate", "pageId": "confirm_transfer",
                "pageData": {"fee": int(float(flow_data.get("enter_amount", 0)) * 0.01)}}
    if current_page == "confirm_transfer":
        return {"action": "stop", "message": "Transfer complete. Ref: TXN-0042."}
    return {"action": "stop", "message": "Thank you."}
```

```php PHP
<?php
// Requires: composer require phpseclib/phpseclib
use phpseclib3\Crypt\PublicKeyLoader;

// Load RSA key with SHA-256 for both OAEP hash and MGF1
$privateKey = PublicKeyLoader::load(getenv("MONIME_RSA_PRIVATE_KEY"))
    ->withHash('sha256')
    ->withMGFHash('sha256');

$body    = json_decode(file_get_contents("php://input"), true);

// Steps 2 to 4
$encKey  = base64_decode($body["encryptedAesKey"]);
$encData = base64_decode($body["encryptedExchangeData"]);

$aesKey  = $privateKey->decrypt($encKey);

$ivLength   = ord($encData[0]);
$iv         = substr($encData, 1, $ivLength);
$remainder  = substr($encData, 1 + $ivLength);
$ciphertext = substr($remainder, 0, -16);
$tag        = substr($remainder, -16);
$payload    = json_decode(
    openssl_decrypt($ciphertext, "aes-128-gcm", $aesKey, OPENSSL_RAW_DATA, $iv, $tag),
    true,
);

$currentPage = $payload["currentPage"];
$flowData    = $payload["flowData"];

// Your business logic
if ($currentPage === "enter_amount") {
    $response = ["action" => "navigate", "pageId" => "confirm_transfer",
                 "pageData" => ["fee" => round($flowData["enter_amount"] * 0.01)]];
} elseif ($currentPage === "confirm_transfer") {
    $response = ["action" => "stop", "message" => "Transfer complete. Ref: TXN-0042."];
} else {
    $response = ["action" => "stop", "message" => "Thank you."];
}

// Step 5: encrypt and return
$iv2 = random_bytes(12);
$tag2 = "";
$enc  = openssl_encrypt(json_encode($response), "aes-128-gcm", $aesKey, OPENSSL_RAW_DATA, $iv2, $tag2, "", 16);
$blob = chr(strlen($iv2)) . $iv2 . $enc . $tag2;

header("Content-Type: text/plain");
echo base64_encode($blob);
```

```java Java (Spring)
import org.springframework.web.bind.annotation.*;
import javax.crypto.Cipher;
import javax.crypto.spec.*;
import java.security.*;
import java.security.spec.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

@RestController
public class UssdExchangeController {

    private static final int GCM_TAG_BITS = 128;
    private final PrivateKey privateKey;

    public UssdExchangeController() throws Exception {
        String pem = System.getenv("MONIME_RSA_PRIVATE_KEY")
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");
        this.privateKey = KeyFactory.getInstance("RSA")
            .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem)));
    }

    @PostMapping(value = "/ussd/exchange", produces = "text/plain")
    public String handleExchange(@RequestBody Map<String, String> body) throws Exception {
        // Steps 2 to 4: decode, decrypt the AES key, decrypt the request
        byte[] encKey  = Base64.getDecoder().decode(body.get("encryptedAesKey"));
        byte[] encData = Base64.getDecoder().decode(body.get("encryptedExchangeData"));

        // Without OAEPParameterSpec, Java uses SHA-1 for MGF1 and decryption fails.
        Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPPadding");
        OAEPParameterSpec oaepParams = new OAEPParameterSpec(
            "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
        rsa.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
        byte[] aesKey = rsa.doFinal(encKey);

        int    ivLen      = encData[0] & 0xFF;
        byte[] iv         = Arrays.copyOfRange(encData, 1, 1 + ivLen);
        byte[] ciphertext = Arrays.copyOfRange(encData, 1 + ivLen, encData.length);

        Cipher aes = Cipher.getInstance("AES/GCM/NoPadding");
        aes.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
        Map<?, ?> payload = new com.fasterxml.jackson.databind.ObjectMapper()
            .readValue(aes.doFinal(ciphertext), Map.class);

        // Your business logic
        String currentPage = (String) payload.get("currentPage");
        String responseJson = processFlow(currentPage, payload);

        // Step 5: encrypt and return
        byte[] iv2 = new byte[12];
        new SecureRandom().nextBytes(iv2);
        aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv2));
        byte[] encrypted = aes.doFinal(responseJson.getBytes(StandardCharsets.UTF_8));

        byte[] blob = new byte[1 + iv2.length + encrypted.length];
        blob[0] = (byte) iv2.length;
        System.arraycopy(iv2, 0, blob, 1, iv2.length);
        System.arraycopy(encrypted, 0, blob, 1 + iv2.length, encrypted.length);

        return Base64.getEncoder().encodeToString(blob);
    }

    private String processFlow(String currentPage, Map<?, ?> payload) throws Exception {
        Map<String, Object> response = switch (currentPage) {
            case "enter_amount" -> Map.of("action", "navigate", "pageId", "confirm_transfer",
                                          "pageData", Map.of("fee", "50"));
            case "confirm_transfer" -> Map.of("action", "stop",
                                              "message", "Transfer complete. Ref: TXN-0042.");
            default -> Map.of("action", "stop", "message", "Thank you.");
        };
        return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(response);
    }
}
```

```go Go (net/http)
package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "encoding/pem"
    "io"
    "net/http"
    "os"
)

var privateKey *rsa.PrivateKey

func init() {
    block, _ := pem.Decode([]byte(os.Getenv("MONIME_RSA_PRIVATE_KEY")))
    if block == nil {
        panic("MONIME_RSA_PRIVATE_KEY is empty or not a PEM key")
    }
    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        panic(err)
    }
    rsaKey, ok := key.(*rsa.PrivateKey)
    if !ok {
        panic("MONIME_RSA_PRIVATE_KEY is not an RSA key")
    }
    privateKey = rsaKey
}

func handleExchange(w http.ResponseWriter, r *http.Request) {
    // Monime ends the session with an error for any status other than 200.
    var body map[string]string
    if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    // Steps 2 to 4: decode, decrypt the AES key, decrypt the request
    encKey, err := base64.StdEncoding.DecodeString(body["encryptedAesKey"])
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    encData, err := base64.StdEncoding.DecodeString(body["encryptedExchangeData"])
    if err != nil || len(encData) < 1+12+16 {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encKey, nil)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    ivLen := int(encData[0])
    iv := encData[1 : 1+ivLen]
    ciphertext := encData[1+ivLen:]

    block, err := aes.NewCipher(aesKey)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }
    decrypted, err := gcm.Open(nil, iv, ciphertext, nil)
    if err != nil {
        http.Error(w, "Decryption failed", http.StatusBadRequest)
        return
    }

    var payload map[string]interface{}
    if err := json.Unmarshal(decrypted, &payload); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    // Your business logic
    currentPage, _ := payload["currentPage"].(string)
    var response map[string]interface{}
    switch currentPage {
    case "enter_amount":
        response = map[string]interface{}{
            "action":   "navigate",
            "pageId":   "confirm_transfer",
            "pageData": map[string]string{"fee": "50"},
        }
    case "confirm_transfer":
        response = map[string]interface{}{"action": "stop", "message": "Transfer complete. Ref: TXN-0042."}
    default:
        response = map[string]interface{}{"action": "stop", "message": "Thank you."}
    }

    // Step 5: encrypt and return
    responseJson, err := json.Marshal(response)
    if err != nil {
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }
    iv2 := make([]byte, 12)
    if _, err := io.ReadFull(rand.Reader, iv2); err != nil {
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }
    encrypted := gcm.Seal(nil, iv2, responseJson, nil)

    blob := make([]byte, 1+len(iv2)+len(encrypted))
    blob[0] = byte(len(iv2))
    copy(blob[1:], iv2)
    copy(blob[1+len(iv2):], encrypted)

    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte(base64.StdEncoding.EncodeToString(blob)))
}

func main() {
    http.HandleFunc("/ussd/exchange", handleExchange)
    http.ListenAndServe(":3000", nil)
}
```

The same handler works for exchange actions and dynamic pages. The request and reply formats are the same for both.
