Skip to main content

Key Versioning

This guide explains how key versions and the active key work in Qpher, how the key lifecycle works, and best practices for managing multiple key versions in production.

Prerequisites​

  • Familiarity with Key Management operations
  • Understanding of basic encryption and signing concepts

Key versions and the active key​

Encrypt and sign may omit key_version: Qpher uses the active key and returns its version. Decrypt, verify, encapsulate and key wrap always need it.

Store the version each response returns alongside the ciphertext or signature: decrypt and verify need it.

Why versions matter​

BenefitExplanation
Predictable decryptionYou always know which key will be used to decrypt data
Safe key rotationOld data continues to decrypt correctly after rotation
Audit trailEvery operation is tied to a specific key version for compliance
No silent failuresIf a key is archived, the operation fails loudly instead of trying another key

Key Lifecycle Diagram​

State Transitions​

TransitionTriggerWhat Happens
GeneratePOST /api/v1/kms/keys/generateCreates the next version (1 for a new algorithm) with active status
RotatePOST /api/v1/kms/keys/rotateCreates new active version (N+1), old version becomes retired
RetirePOST /api/v1/kms/keys/retireManually sets a version to retired
ArchiveQpher Portal onlyArchiving is irreversible: it deletes the private key file (encrypted copies of the file can remain in Qpher's backups), and Qpher no longer uses an archived key.

Allowed Operations Per Status​

Encrypt Decrypt Sign Verify
active Yes Yes Yes Yes
retired No Yes No Yes
archived No No No No

How Rotation Works​

When you rotate a key, two things happen atomically:

  1. A new key pair is generated with version N+1, set to active
  2. The previous active key (version N) moves to retired
Before rotation:
v1 [active]

After first rotation:
v1 [retired] v2 [active]

After second rotation:
v1 [retired] v2 [retired] v3 [active]
Rotation is atomic

The new version becomes active and the previous one retired in one step, so there is always exactly one active version. Old data stays decryptable with the retired key.

Decrypting Old Data with Retired Keys​

This is the most common key versioning scenario. You have data encrypted with key version 1, but you have since rotated to version 2.

Decrypt with a Retired Key
import requests

# Data encrypted with key version 1 (now retired)
old_record = {
    "ciphertext": "base64-ciphertext-from-v1...",
    "key_version": 1,  # This key is now retired
}

# Decryption still works with retired keys
response = requests.post(
    "https://api.qpher.ai/api/v1/kem/decrypt",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "qph_your_key_here",
    },
    json={
        "ciphertext": old_record["ciphertext"],
        "key_version": old_record["key_version"],
    },
)

result = response.json()
print(f"Decrypted successfully with retired key v{old_record['key_version']}")

Re-Encryption Strategy​

If you want to re-encrypt old data under a new key version (for example, before archiving an old key), follow this pattern:

Re-Encrypt Data Under New Key
import requests

API_BASE = "https://api.qpher.ai"
HEADERS = {
    "Content-Type": "application/json",
    "x-api-key": "qph_your_key_here",
}

def re_encrypt(ciphertext: str, old_version: int, new_version: int) -> dict:
    """Decrypt with old key, re-encrypt with new key."""
    # Step 1: Decrypt with the old (retired) key
    decrypt_resp = requests.post(
        f"{API_BASE}/api/v1/kem/decrypt",
        headers=HEADERS,
        json={"ciphertext": ciphertext, "key_version": old_version},
    )
    plaintext = decrypt_resp.json()["data"]["plaintext"]

    # Step 2: Encrypt with the new (active) key
    encrypt_resp = requests.post(
        f"{API_BASE}/api/v1/kem/encrypt",
        headers=HEADERS,
        json={"plaintext": plaintext, "key_version": new_version},
    )
    new_data = encrypt_resp.json()["data"]

    return {
        "ciphertext": new_data["ciphertext"],
        "key_version": new_data["key_version"],
    }

# Re-encrypt a record from v1 to v2
updated = re_encrypt(
    ciphertext="old-ciphertext...",
    old_version=1,
    new_version=2,
)
print(f"Re-encrypted under key v{updated['key_version']}")
Re-Encryption Best Practices

Re-encrypt data in batches with error handling and progress tracking. Always verify the re-encryption succeeded before archiving the old key. Consider running re-encryption during off-peak hours to minimize API usage impact.

Best Practices​

  1. Always store key_version alongside ciphertext or signatures — you cannot decrypt or verify without it.

  2. Let encrypt and sign use the active key — omit key_version and store the version the response returns; pin a version only when you need to.

  3. Rotate keys on a regular schedule — quarterly rotation is a common baseline; adjust based on your compliance requirements.

  4. Re-encrypt and re-sign before archiving — archiving is irreversible, so re-encrypt data under the new key, and re-sign anything whose signature must stay verifiable, before archiving the old version.

  5. Monitor key usage — track which key versions are still being used for decryption to know when it is safe to archive old versions.

Error Handling​

HTTP StatusError CodeCauseResolution
404ERR_NOT_FOUND_001The key version does not exist for this algorithm, or key_version was omitted and the algorithm has no active keyList your keys with GET /api/v1/kms/keys
404ERR_CRYPTO_004Encrypt, sign, sign-hash, encapsulate or key wrap with a key that is not activeOmit key_version on encrypt, sign and sign-hash to use the active key, or pin the active version
404ERR_CRYPTO_004Decapsulate or key unwrap with an archived keyArchiving cannot be undone; Qpher no longer uses an archived key
404ERR_KMS_003Decrypt, verify or verify-hash with an archived keyArchiving cannot be undone; Qpher no longer decrypts or verifies with an archived key
403ERR_KMS_020Archive requested with an API keyArchive keys in Qpher Portal