# Exporting the client state to Authenticate the enrolled user

### Exporting the Client State (RSA-wrapped AES key)

Once a user has completed enrollment, you can export their client *state*.\
This produces an encrypted backup that can be restored or transferred securely to another device.

#### Overview

The export process uses a **hybrid encryption flow** combining RSA and AES:

1. Generate a random **AES-256 symmetric key** (`client_state_key`).
2. Encrypt this key using the **Keyless RSA public key** with `RSAES-OAEP-SHA-256`.
3. Send the RSA-encrypted AES key (hex-encoded) in the `Kl-Client-State-Key` header.
4. Keyless decrypts it internally and uses the AES key to encrypt the client state with **AES-GCM-SIV**.
5. You receive a binary blob which can be decrypted locally using the same AES key.

> **Note:**\
> In sandbox environments, the returned ciphertext may also be compatible with AES-GCM, so standard AES-GCM decryption can be used if AES-GCM-SIV support is unavailable.

#### Endpoint

**Method:** `POST`\
**Path:**

```
/v1/users/{customer}/{username}/export-client-state
```

**Required Headers:**

| Header                      | Description                                                                                         |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `Kl-Key-Id`                 | Registered RSA key alias (e.g. `alias/kl-core-production-authentication-service-image-key-sandbox`) |
| `Kl-Key-Algorithm`          | Must be `RSAES-OAEP-SHA-256`                                                                        |
| `Kl-Client-State-Key`       | RSA-encrypted AES key (hex-encoded)                                                                 |
| `Kl-Client-State-Algorithm` | `AES-GCM-SIV`                                                                                       |
| `Kl-Client-State-Type`      | `BACKUP`                                                                                            |
| `Kl-Api-Key`                | Your Keyless API key                                                                                |
| `Accept`                    | `application/octet-stream`                                                                          |

#### Example Implementation (Python)

Below is a full working example using `requests` and `cryptography`.

> ⚠️ Replace placeholders (`<your-api-key>`, `<your-customer>`, `<your-username>`, `<your-public-key>`) with real configuration values.

```python
import os
import json
import requests
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend

# ----------------------------
# CONFIGURATION
# ----------------------------
URL = "https://<your-keyless-endpoint>/v1/users/<customer>/<username>/export-client-state"

HEADERS_TEMPLATE = {
    "Kl-Key-Id": "alias/<your-key-alias>",
    "Kl-Key-Algorithm": "RSAES-OAEP-SHA-256",
    "Kl-Client-State-Algorithm": "AES-GCM-SIV",  # informational
    "Kl-Client-State-Type": "BACKUP",
    "Kl-Api-Key": "<your-api-key>",
    "Accept": "application/octet-stream",
}

PUBLIC_KEY_PEM = '''-----BEGIN PUBLIC KEY-----
<your-public-key>
-----END PUBLIC KEY-----'''

# ----------------------------
# STEP 1. Generate AES-256 symmetric key
# ----------------------------
sym_key = os.urandom(32)
print(f"[+] Generated AES key: {sym_key.hex()}")

# ----------------------------
# STEP 2. Encrypt AES key with RSA public key (OAEP-SHA256)
# ----------------------------
public_key = serialization.load_pem_public_key(
    PUBLIC_KEY_PEM.encode(), backend=default_backend()
)

encrypted_sym_key = public_key.encrypt(
    sym_key,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# ----------------------------
# STEP 3. Send hex-encoded RSA-encrypted AES key
# ----------------------------
headers = HEADERS_TEMPLATE.copy()
headers["Kl-Client-State-Key"] = encrypted_sym_key.hex()

print("[+] Sending request to Keyless Export Client State endpoint...")
response = requests.post(URL, headers=headers)
print(f"[+] Response status: {response.status_code}")

if response.status_code != 200:
    print(f"[!] Error {response.status_code}: {response.text}")
    exit(1)

# ----------------------------
# STEP 4. Decrypt returned client state with AES-GCM
# ----------------------------
encrypted_blob = response.content

if len(encrypted_blob) < 12:
    raise ValueError("Invalid response: cannot extract nonce and ciphertext")

nonce, ciphertext = encrypted_blob[:12], encrypted_blob[12:]
aesgcm = AESGCM(sym_key)

try:
    plaintext = aesgcm.decrypt(nonce, ciphertext, None)
    print("\n[+] ✅ Decrypted client state:")
    try:
        print(json.dumps(json.loads(plaintext.decode()), indent=2))
    except json.JSONDecodeError:
        print(plaintext.decode(errors='ignore'))
except Exception as e:
    print(f"[!] Decryption failed: {e}")
```

#### Example Response

**Status:** `200 OK`\
**Content-Type:** `application/octet-stream`\
Binary blob: `nonce || ciphertext`

After successful decryption, the plaintext contains the user’s client state in JSON:

```json
{
  "user_id": "12345678",
  "enrolled_factors": ["face", "device"],
  "created_at": "2025-10-10T14:32:00Z"
}
```

#### Common Errors

| Code          | Message                              | Explanation                                                                                                 |
| ------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **422**       | `bytes_invalid_encoding`             | `Kl-Client-State-Key` must be hex-encoded (not Base64).                                                     |
| **409**       | `IMAGE_ENCRYPTION_ERROR`             | The server couldn’t decrypt the key. Ensure your RSA public key matches the alias and OAEP-SHA-256 is used. |
| **400 / 401** | `Unauthorized`                       | Invalid or missing API key.                                                                                 |
| —             | `cryptography.exceptions.InvalidTag` | AES key or nonce mismatch — check byte order and header setup.                                              |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keyless.io/idv-bridge/readme/idv-bridge-saas/exporting-the-client-state-to-authenticate-the-enrolled-user.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
