Skip to main content

kc: ciphertext format & cryptographic spec

We publish the exact construction of the kc: ciphertext — algorithms, parameters, and key hierarchy — so that a security reviewer can evaluate it without a sales call. Specificity is the point: you should be able to verify what we built, not take our word for it.

Wire format

A kc: value is a single ASCII string. Each variable segment is base64url (no padding):
kc:<version>:<datatype>:<b64u(key_ref)>:<b64u(eph_pubkey)>:<b64u(iv)>:<b64u(ciphertext‖tag)>:$
SegmentMeaning
versionScheme version (integer). Drives crypto-agility — the construction can change under a new version without breaking stored data. Currently 1.
datatypeOne of s (string), n (number), b (boolean), j (JSON), x (raw bytes). Lets decrypt round-trip the original type.
key_ref"<tenant_id>:<app_key_id>:<key_version>" — names the recipient keypair so decrypt needs zero out-of-band metadata.
eph_pubkeyThe ephemeral ECDH public key as a 65-byte uncompressed P-256 point (0x04 ‖ X ‖ Y).
iv12-byte AES-GCM nonce.
ciphertext‖tagAES-256-GCM output with the 16-byte authentication tag appended.
$Terminator — guards against trailing-data and makes the string recognisable by eye and by tooling.
The leading kc: and trailing :$ make detection cheap and unambiguous (POST /v1/decrypt passes non-kc: values through untouched on this basis).

Cryptographic construction (version 1)

Per-value ECIES on the NIST P-256 curve:
  1. Generate an ephemeral P-256 keypair for this value.
  2. Compute the ECDH shared secret Z between the ephemeral private key and the recipient (tenant app) public key.
  3. Derive the content-encryption key:
    CEK = HKDF-SHA256(ikm = Z, salt = eph_pubkey, info = AAD, length = 32 bytes)
    
  4. Encrypt with AES-256-GCM (12-byte random IV, 16-byte tag). The kc: header (version, datatype, key_ref, eph_pubkey, iv) is bound as the GCM additional authenticated data, so no header segment can be swapped without failing the tag.

Context binding (the AAD / HKDF info)

The HKDF info is a length-prefixed, canonical concatenation of a domain-separation label and the full context:
info = len‖"KnoxCall-kc1-ECIES-P256-AES256GCM"
     ‖ len‖tenant_id ‖ len‖app_key_id ‖ key_version(uint32 BE)
     ‖ len‖datatype  ‖ len‖purpose
purpose is the optional data-role (pci, eu, …). Because the full context is folded into key derivation, a ciphertext is cryptographically pinned to the tenant, key, version, datatype, and data-role it was created under — decrypting under any different context simply fails the GCM tag.
How this differs from raw-ECDH designs. Some implementations feed the raw ECDH shared secret straight into AES with no KDF. KnoxCall always runs HKDF-SHA256 and binds the context into the info parameter. This gives domain separation, defends against cross-context reuse, and means a stolen ciphertext cannot be replayed under a different tenant, key, or role.

Key hierarchy & custody

platform master key  (env MASTER_KEY_B64, or unsealed from AWS/GCP/Azure KMS)
        │  wraps

tenant master key    (per tenant; or wrapped by YOUR KMS under BYOK)
        │  wraps

app ecdh-p256 private key   (PKCS#8 DER, stored only as a wrapped KCT1 envelope)
        │  decrypts

kc: ciphertext
  • The app private key is never stored in the clear — only as a KCT1 envelope wrapped under the tenant master key. The public half is cached for encryption.
  • Under BYOK, the tenant master key is wrapped by your cloud KMS, so KnoxCall cannot unwrap it without a call your KMS policy authorises — and you can revoke that at any time.
  • Cryptographic erasure: destroy a key version and every kc: value produced under it becomes permanently unreadable.

Versioning & rotation

Every kc: value names its key_version in the key_ref. Rotating an ecdh-p256 key mints a new version: new values encrypt under the new version while existing values still decrypt under theirs. No bulk re-encryption is required, and a long-running re-encrypt can pin a target version to stay consistent across a concurrent rotation.

Parameter summary

ParameterValue
CurveNIST P-256 (prime256v1), ephemeral per value
KDFHKDF-SHA256, 32-byte output, salt = ephemeral public key, context-bound info
AEADAES-256-GCM, 12-byte IV, 16-byte tag
Header integritykc: header bound as GCM AAD
Key wrappingKCT1 envelope (AES-256-GCM) under the per-tenant master key
Master key custodyenv, or AWS KMS / GCP KMS / Azure Key Vault unseal; per-tenant BYOK

Migrating from Evervault ev:

KnoxCall cannot decrypt an Evervault ev: ciphertext (only your Evervault app holds that key). Migration is a one-way re-encryption you run with scripts/migrate-ev-to-kc.ts: decrypt each ev: value via your Evervault app, then re-encrypt via POST /v1/encrypt to get a kc: value.