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.
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)>:$
| Segment | Meaning |
|---|
version | Scheme version (integer). Drives crypto-agility — the construction can change under a new version without breaking stored data. Currently 1. |
datatype | One 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_pubkey | The ephemeral ECDH public key as a 65-byte uncompressed P-256 point (0x04 ‖ X ‖ Y). |
iv | 12-byte AES-GCM nonce. |
ciphertext‖tag | AES-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:
-
Generate an ephemeral P-256 keypair for this value.
-
Compute the ECDH shared secret
Z between the ephemeral private key and the recipient (tenant app) public key.
-
Derive the content-encryption key:
CEK = HKDF-SHA256(ikm = Z, salt = eph_pubkey, info = AAD, length = 32 bytes)
-
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
| Parameter | Value |
|---|
| Curve | NIST P-256 (prime256v1), ephemeral per value |
| KDF | HKDF-SHA256, 32-byte output, salt = ephemeral public key, context-bound info |
| AEAD | AES-256-GCM, 12-byte IV, 16-byte tag |
| Header integrity | kc: header bound as GCM AAD |
| Key wrapping | KCT1 envelope (AES-256-GCM) under the per-tenant master key |
| Master key custody | env, 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.