kek is a single-binary CLI for encrypting and decrypting Solana keypairs and arbitrary messages with AWS KMS. It supports two payload schemes so downstream consumers can decrypt straight into the keypair format they expect (raw bytes or a base58 string).
curl -fsSL https://raw.githubusercontent.com/mirrorworld-universe/solana-kms-keypair-encryption-kit/main/install.sh | bash
# or pin a release:
curl -fsSL https://raw.githubusercontent.com/mirrorworld-universe/solana-kms-keypair-encryption-kit/main/install.sh | KEK_REF=v0.2.0 bashThe script delegates to cargo install --git, which lands the binary at ~/.cargo/bin/kek. If the repository is private, configure your git credentials (SSH key, gh, or a credential helper) as a mirrorworld-universe org member before running — GitHub enforces access at the repo level.
Prerequisites: rustup (for cargo).
git clone git@github.com:mirrorworld-universe/solana-kms-keypair-encryption-kit.git
cd solana-kms-keypair-encryption-kit
cargo install --path . # installs `kek` into ~/.cargo/bin
# or, without installing:
cargo build --release # binary at target/release/kekMake sure ~/.cargo/bin is on your PATH. Verify with:
kek --versionkek uses the standard AWS SDK credential chain. Set whichever is most convenient:
export AWS_ACCESS_KEY_ID=…
export AWS_SECRET_ACCESS_KEY=…
export AWS_REGION=us-west-2 # optional; defaults to us-west-2
# or use ~/.aws/credentials, SSO, an EC2/ECS role, etc.Required IAM permissions: kms:Encrypt, kms:Decrypt, and kms:CreateKey if you ever omit --kms-key-id.
Both schemes wrap the payload in base64 before handing it to KMS. The difference is what that base64 layer wraps — pick the one whose decoded plaintext matches what your downstream consumer expects:
| Scheme | Encrypt pipeline | Decrypted plaintext (after base64.decode) |
|---|---|---|
bytes |
keypair_bytes → base64 → KMS encrypt |
Raw 64-byte Uint8Array (Solana CLI form) |
base58 |
base58_string → base64 → KMS encrypt |
Base58 private key string (Phantom form) |
Consumers that decode the KMS plaintext as Uint8Array.from(...) need the bytes scheme. Consumers that decode it into a string and pass it to Keypair.fromBase58String() need the base58 scheme.
flowchart LR
K["id.json<br/>(64-byte array)"]
subgraph bytes_scheme["bytes scheme"]
direction LR
Bb["UInt8Array<br/>(64 bytes)"] -->|"STANDARD.encode"| Bc["base64 string"]
end
subgraph base58_scheme["base58 scheme"]
direction LR
Sa["UInt8Array<br/>(64 bytes)"] -->|"bs58.encode"| Sb["base58 string"]
Sb -->|"STANDARD.encode"| Sc["base64 string"]
end
K --> Bb
K --> Sa
Bc --> KMS[("AWS KMS<br/>Encrypt")]
Sc --> KMS
KMS --> CT["ciphertext blob<br/>(base64-encoded for transport)"]
CT -.->|"decrypt: KMS → base64 → bytes"| Bb
CT -.->|"decrypt: KMS → base64 → base58 → bytes"| Sa
Solid arrows = encryption. Dashed arrows = the matching decryption path consumers must run.
Run kek --help for the full reference. Summary:
| Command | Purpose |
|---|---|
encrypt-keypair |
Encrypt a Solana id.json as raw bytes (UInt8Array) |
encrypt-keypair-base58 |
Encrypt a Solana id.json as a base58 private key |
decrypt-keypair |
Decrypt a bytes-scheme ciphertext (alias: decrypt) |
decrypt-keypair-base58 |
Decrypt a base58-scheme ciphertext |
encrypt-message |
Encrypt an arbitrary UTF-8 string |
decrypt-message |
Decrypt an encrypt-message ciphertext |
serialize-base58 |
Print an id.json as a base58 private key (no KMS) |
Every encrypt subcommand accepts --kms-key-id <ID>. If omitted, a fresh symmetric KMS key is created and its ID is printed to stderr.
kek encrypt-keypair ./id.json <PUBKEY> --kms-key-id <KMS_KEY_ID>
kek decrypt-keypair <CIPHERTEXT> <KMS_KEY_ID>Downstream decryption in TypeScript:
import { KMSClient, DecryptCommand } from "@aws-sdk/client-kms";
import { Keypair } from "@solana/web3.js";
const { Plaintext } = await new KMSClient({}).send(
new DecryptCommand({
CiphertextBlob: Buffer.from(ciphertext, "base64"),
KeyId: kmsKeyId,
}),
);
const base64 = Buffer.from(Plaintext!).toString("utf8");
const bytes = Buffer.from(base64, "base64");
const keypair = Keypair.fromSecretKey(new Uint8Array(bytes));kek encrypt-keypair-base58 ./id.json <PUBKEY> --kms-key-id <KMS_KEY_ID>
kek decrypt-keypair-base58 <CIPHERTEXT> <KMS_KEY_ID>Downstream decryption in TypeScript:
import { KMSClient, DecryptCommand } from "@aws-sdk/client-kms";
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";
const { Plaintext } = await new KMSClient({}).send(
new DecryptCommand({
CiphertextBlob: Buffer.from(ciphertext, "base64"),
KeyId: kmsKeyId,
}),
);
const base64 = Buffer.from(Plaintext!).toString("utf8");
const base58 = Buffer.from(base64, "base64").toString("utf8");
const keypair = Keypair.fromSecretKey(bs58.decode(base58));kek encrypt-message "hello" --kms-key-id <KMS_KEY_ID>
kek decrypt-message <CIPHERTEXT> <KMS_KEY_ID>kek serialize-base58 ./id.jsoncargo test # offline tests, no AWS calls
KMS_TEST_KEY_ID=<your-key-id> cargo test -- --ignored # live KMS round-tripThe --ignored test exercises the full encrypt + decrypt path against a real KMS key. The key ID must come from KMS_TEST_KEY_ID — never hard-code it.
Releases follow Semantic Versioning. See the Releases page for changelogs. Pin a specific release in install.sh with KEK_REF=v0.2.0.
- Decrypt output goes to stdout. The decrypted base58 secret is printed plainly so you can verify the result. Redirect to a file or pipe into another process when handling production keys; mind shell history and terminal scrollback.
- Never commit keypair material. The bundled
.gitignoreblocks*.keypair.json,id.json,keypair*.json,*.secret,*.log, and.env*. Add patterns if your repo introduces new filenames. - Use a least-privilege IAM role. The binary only needs
kms:Encrypt,kms:Decrypt, and (optionally, for the auto-create path)kms:CreateKey. Scope to specific key ARNs in production. - Prefer existing KMS keys. Running an encrypt command without
--kms-key-idcreates a new symmetric KMS key with no rotation policy or alias attached. Use it for experimentation only; supply--kms-key-idin production. - Memory is not zeroized. Secret bytes live in process memory until the CLI exits. The tool is single-shot, so the window is short — but do not embed this code in a long-lived process without adding a zeroize layer.