TellWang
Dashboard

Key Management

Generate encryption and signing keys, then encrypt, decrypt, and sign through your Wok. The private key material stays inside the control plane — your app holds an API key and asks the service to do the crypto, so a leaked app server never leaks the key itself.

Generate a key (shipped)

A key has a name, a type, and one or more versions. Three types today:

create.sh
curl -X POST https://tellwang.com/v1/orgs/$ORG_SLUG/kms/keys \
  -H "Authorization: Bearer $TELLWANG_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"appdata","type":"aes-256-gcm"}'

The response is metadata only — name, type, version, and a short fingerprint. The key material is never returned, not even once. List your keys with GET /kms/keys, read one with GET /kms/keys/{name}, and turn a key off (and back on) with PUT /kms/keys/{name}/state. Use $ORG_SLUG or the literal me for the org tied to your key.

Encrypt & decrypt (shipped)

Send base64 plaintext to an aes-256-gcm key; get back a self-describing ciphertext string you can store anywhere. Decrypt returns the original bytes. Pass an optional aad_b64 (additional authenticated data) to bind the ciphertext to a context — the same value must be supplied to decrypt.

encrypt.sh
# encrypt
curl -X POST .../kms/keys/appdata/encrypt -H "Authorization: Bearer $KEY" \
  -d '{"plaintext_b64":"'"$(echo -n 'card 4111…' | base64)"'"}'
# -> {"ciphertext":"twk:..."}

# decrypt
curl -X POST .../kms/keys/appdata/decrypt -H "Authorization: Bearer $KEY" \
  -d '{"ciphertext":"twk:..."}'

For large payloads, call POST /kms/keys/{name}/generate-data-key. It returns a fresh data key in the clear plus the same key wrapped under your KMS key. Encrypt the bulk data locally with the plaintext data key, discard it, and store the wrapped copy — decrypt it back when you need to read the data. This is envelope encryption, and it keeps large data off the wire.

Rotate (shipped)

POST /kms/keys/{name}/rotate generates a new version and makes it current. New writes use the new version; older versions are kept, so data and signatures produced before the rotation still decrypt and verify. The ciphertext carries the version it was made with, so nothing breaks when you rotate.

Sign & verify (shipped)

An ed25519 or rsa-4096 key signs a message and verifies a signature without ever exposing the private key. The public key is returned on the key's metadata so anyone can verify offline.

sign.sh
curl -X POST .../kms/keys/webhook-signer/sign -H "Authorization: Bearer $KEY" \
  -d '{"message_b64":"'"$(echo -n 'payload' | base64)"'"}'
# -> {"signature":"…","signature_b64":"…","version":1}

Sign Solana transactions (shipped)

A Solana address is an ed25519 public key. Create an ed25519 key and its public_key_base58 is a wallet address; /sign over a serialized transaction message is a valid Solana signature. So the KMS is a remote signer: your app builds the transaction, the control plane signs it, and the private key never touches your app server.

transfer.ts
import { Connection, Transaction, SystemProgram, PublicKey } from "@solana/web3.js";
import bs58 from "bs58";

const signer = new PublicKey(KMS_ADDRESS); // the key's public_key_base58
const tx = new Transaction().add(SystemProgram.transfer({
  fromPubkey: signer, toPubkey: new PublicKey(dest), lamports,
}));
tx.feePayer = signer;
tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;

// the key signs the message bytes; nothing private leaves the control plane
const message = tx.compileMessage().serialize();
const res = await fetch(`https://tellwang.com/v1/orgs/me/kms/keys/treasury/sign`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ message_b64: message.toString("base64") }),
});
const { signature } = await res.json(); // base58
tx.addSignature(signer, bs58.decode(signature));
await conn.sendRawTransaction(tx.serialize());

Bring your own key (shipped)

Already hold a key? Import it instead of generating one. For Solana, the import takes the formats you already have — a solana-keygen id.json array, a base58 secret key from a wallet export, or a raw seed — and echoes back the derived address so you can confirm it matches before you trust it. Symmetric keys import as 32 raw bytes; RSA imports as PEM.

import.sh
curl -X POST .../kms/keys/treasury/import -H "Authorization: Bearer $KEY" \
  -d '{"type":"ed25519","secret_key":[12,34,…]}' # the id.json array
# -> {"key":{"public_key_base58":"…","origin":"imported", …}}

Roadmap