Skip to main content

Encrypt Data with KEM

This guide walks you through encrypting data using Qpher hybrid KEM-DEM encryption, which combines Kyber768 post-quantum key encapsulation with AES-256-GCM symmetric encryption.

key_version is optional — pinning it requires the active version

key_version is now optional on encrypt — omit it to use your tenant's active key (the response returns the version that was used). If you pin a specific key_version, it must reference an active key — a new tenant's active key is usually higher than 1, so copying 1 verbatim returns ERR_NOT_FOUND_001. Find your active version with GET /api/v1/kms/keys/active?algorithm=Kyber768 (use algorithm=Dilithium3 for signing). Decrypt still requires the exact key_version that was used to encrypt.

Prerequisites

  • A Qpher account with an active API key
  • At least one active Kyber768 key pair (see Key Management), or an active X-Wing key pair for hybrid encryption
  • The key_version of the active key you want to use

How Qpher KEM Encryption Works

Qpher uses a hybrid KEM-DEM (Key Encapsulation Mechanism - Data Encapsulation Mechanism) scheme:

  1. KEM step: Kyber768 encapsulates a shared secret using your public key
  2. KDF step: HKDF-SHA256 derives a 256-bit symmetric key from the shared secret
  3. DEM step: AES-256-GCM encrypts your plaintext with the derived key

The shared secret is never stored or returned. Your private key never leaves the Qpher secure enclave.

1088B
KEM Ciphertext (1088B)IV (Nonce) (12B)AES Ciphertext (21B)Auth Tag (16B)
Total: 1137 bytes (overhead + plaintext)

Step 1: Prepare Your Plaintext

Your plaintext must be base64-encoded before sending it to the API.

Base64 Encoding Required

All plaintext data must be base64-encoded in the request body. The API will reject raw binary or unencoded strings. Maximum plaintext size is 1 MB (1,048,576 bytes) before encoding.

Request size

plaintext is capped at ~1 MB (1,400,000 base64 characters). For larger data, don't send it through /kem/encrypt — use key-wrap (/kem/key/wrap) or the client-side encryption pattern (encapsulate + local AES-256-GCM) so only a small wrapped key / shared secret transits the API, not your data.

Step 2: Send the Encrypt Request

POST/api/v1/kem/encryptEncrypt plaintext using Kyber768 hybrid KEM-DEM
Encrypt Data
curl -X POST https://api.qpher.ai/api/v1/kem/encrypt \
  -H "Content-Type: application/json" \
  -H "x-api-key: qph_your_key_here" \
  -d '{
    "plaintext": "SGVsbG8sIFdvcmxkIQ=="
  }'

Using X-Wing Hybrid KEM (Pro/Enterprise)

X-Wing Hybrid KEM (Pro/Enterprise)

For defense-in-depth, add "algorithm": "X-Wing" to combine X25519 classical key exchange with ML-KEM-768. This ensures security even if a breakthrough in lattice cryptanalysis is discovered. Requires an active X-Wing key pair — see Hybrid Cryptography.

Encrypt with X-Wing Hybrid KEM
curl -X POST https://api.qpher.ai/api/v1/kem/encrypt \
  -H "Content-Type: application/json" \
  -H "x-api-key: qph_your_key_here" \
  -d '{
    "plaintext": "SGVsbG8sIFdvcmxkIQ==",
    "key_version": 1,
    "algorithm": "X-Wing"
  }'

Step 3: Understand the Response

RequestPOST/api/v1/kem/encrypt
Content-Type: application/json
x-api-key: qph_your_key_here
{
  "plaintext": "SGVsbG8sIFdvcmxkIQ==",
  "key_version": 1
}
Response200
{
  "data": {
    "ciphertext": "base64-encoded-ciphertext...",
    "key_version": 1,
    "algorithm": "Kyber768"
  },
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-15T10:30:00Z"
}
FieldDescription
ciphertextBase64-encoded ciphertext containing the KEM ciphertext, IV, AES ciphertext, and auth tag
key_versionThe key version used for encryption — store this alongside the ciphertext
algorithmThe KEM algorithm used (Kyber768 or X-Wing)
request_idUnique request identifier for tracing and support
Store the key_version returned by encrypt

When you encrypt, the response includes the key_version that was used (resolved from your tenant's active key if you omitted it). Always store that returned key_version alongside the ciphertext — you will need the exact same version to decrypt later. You can omit key_version on encrypt to use the active key, or pass it explicitly to pin a version. Decrypt always requires the exact key_version that was used to encrypt.

Error Handling

HTTP StatusError CodeCauseResolution
400ERR_INVALID_001Missing or invalid plaintext or key_versionEnsure plaintext is valid base64 and key_version is a positive integer
400ERR_KEM_001Plaintext exceeds 1 MB limitSplit data into smaller chunks
401ERR_AUTH_001Invalid or missing API keyCheck your x-api-key header
404ERR_KEM_003Key version not foundVerify the key_version exists for your tenant
409ERR_KEM_004Key is not in active statusOnly active keys can encrypt — generate or rotate to get a new active key
429ERR_RATE_001Rate limit exceededReduce request frequency or upgrade your plan