# Verification & Troubleshooting (/docs/products/ussd-flows/developers/security/troubleshooting)

## Requests aren't signed

Encryption keeps the request private, but it doesn't prove who sent it. Your public key is public, so anyone who knows your URL can send a request your server will decrypt. To stay safe:

- Treat `flowData` and `exportedData` as untrusted input.
- Before you move money or change an account, check the facts on your side, such as a payment's status in your own records.
- Keep exchange URLs out of public code and logs.

## RSA settings by language

Monime uses RSA-OAEP with SHA-256 for both the OAEP hash and MGF1. Getting this wrong is the most common cause of failed decryption.

| Language | Works with the defaults? | What to do                                                 |
| -------- | ------------------------ | ---------------------------------------------------------- |
| Node.js  | Yes                      | `oaepHash: "sha256"` sets both hashes                      |
| Python   | Yes                      | Pass SHA-256 for both in `OAEP()`                          |
| Go       | Yes                      | `rsa.DecryptOAEP(sha256.New(), ...)` uses SHA-256 for both |
| Java     | No                       | Set `MGF1ParameterSpec.SHA256` through `OAEPParameterSpec` |
| PHP      | No                       | Use phpseclib v3 instead of `openssl_private_decrypt`      |

## Fixes

**PHP: openssl\_private\_decrypt fails**

`openssl_private_decrypt()` with `OPENSSL_PKCS1_OAEP_PADDING` always uses SHA-1 for OAEP, and you can't change it. Use [phpseclib v3](https://phpseclib.com/) for the RSA step:

```bash
composer require phpseclib/phpseclib
```

```php
use phpseclib3\Crypt\PublicKeyLoader;

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

$aesKey = $privateKey->decrypt($encryptedKey);
```

PHP's own `openssl_decrypt` and `openssl_encrypt` work for the AES step.

**Java: RSA decryption fails with OAEPWithSHA-256AndMGF1Padding**

`Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")` uses SHA-256 for the OAEP hash but SHA-1 for MGF1. Set both with `OAEPParameterSpec`:

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

Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
    "SHA-256",
    "MGF1",
    MGF1ParameterSpec.SHA256,
    PSource.PSpecified.DEFAULT
);
cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
byte[] aesKey = cipher.doFinal(encryptedKey);
```

**RSA decryption fails straight away**

Check these in order:

- **The keys don't match.** The private key on your server must come from the same pair as the public key in the dashboard. If you're not sure, make a new pair and replace both.
- **The field names are wrong.** Monime sends `encryptedAesKey` and `encryptedExchangeData`. Reading any other name gives an empty value and a confusing RSA error.
- **The private key is in the wrong format.** The Java and Go samples need a key that starts with `-----BEGIN PRIVATE KEY-----`. [Key Setup](/docs/products/ussd-flows/developers/security/key-setup#make-a-key-pair) shows how to convert one.
- **The public key is in the wrong format.** The key in the dashboard must start with `-----BEGIN PUBLIC KEY-----`, not `-----BEGIN RSA PUBLIC KEY-----`.

**The AES key decrypts, but the request doesn't**

The RSA step works, so the problem is in the AES step:

- Use AES-128-GCM, not CBC or another mode.
- Read the IV from inside the data: byte 0 is its length (12) and the next 12 bytes are the IV. The request has no separate `iv` field.
- In Node.js and PHP, split off the last 16 bytes and pass them as the tag. Python, Java and Go read the tag from the end of the ciphertext.

[Encryption Model](/docs/products/ussd-flows/developers/security/overview#wire-format) shows the byte layout.

**Decryption fails after you change the public key**

After you save a new public key, live sessions can keep using the old one for up to 30 minutes. Keep the old private key working during that time. [Key Setup](/docs/products/ussd-flows/developers/security/key-setup#rotate-a-key) has the steps.

**The subscriber sees an error after your server replies**

Monime couldn't use your reply. Check that:

- The status is HTTP 200.
- The body is the base64 text only: no JSON wrapper, no `encryptedAesKey`, no line breaks.
- You encrypted with the same AES key as the request, not a new one.
- You used a new 12-byte IV, laid out as in [Encryption Model](/docs/products/ussd-flows/developers/security/overview#wire-format).
- The decrypted reply is valid `navigate` or `stop` JSON. [Response Format](/docs/products/ussd-flows/developers/exchange-api/response#when-a-call-fails) lists every case that ends the session.

**The reference server works, but my server doesn't**

Send the same request to both and compare the results step by step. [Testing](/docs/products/ussd-flows/developers/security/testing) shows how to deploy a reference server.

If you're still stuck, email [support@monime.io](mailto:support@monime.io) with your flow's name and the error your server logs. Don't send your private key.
