Exporting the client state to authenticate the enrolled user
Exporting the client state (RSA-wrapped AES key)
After a user successfully completes 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:
-
Generate a random AES-256 symmetric key (
client_state_key). -
Encrypt this key using the PingOne Recognize RSA public key with
RSAES-OAEP-SHA-256. -
Send the RSA-encrypted AES key (hex-encoded) in the
Kl-Client-State-Keyheader. -
PingOne Recognize decrypts it internally and uses the AES key to encrypt the client state with AES-GCM-SIV.
-
You receive a binary blob which can be decrypted locally using the same AES key.
|
In sandbox environments, the returned ciphertext might 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 |
|---|---|
|
Registered RSA key alias (for example, |
|
Must be |
|
RSA-encrypted AES key (hex-encoded) |
|
|
|
|
|
Your PingOne Recognize API key |
|
|
Example implementation (Python)
The following is a full working example using requests and cryptography.
|
Replace placeholders ( |
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 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:
{
"user_id": "12345678",
"enrolled_factors": ["face", "device"],
"created_at": "2025-10-10T14:32:00Z"
}
Common errors
| Code | Message | Explanation |
|---|---|---|
|
|
|
|
|
The server couldn’t decrypt the key. Ensure your RSA public key matches the alias and OAEP-SHA-256 is used. |
|
|
Invalid or missing API key. |
— |
|
AES key or nonce mismatch. Check byte order and header setup. |