Solana Keychain
Enterprise-grade signing with pluggable backends, AWS KMS, Fireblocks, Turnkey, and more.
Overview#
By default, DAEMON stores wallet keypairs locally using your OS keychain (via Electron's safeStorage). This works great for solo development, but teams and enterprises need more: hardware security modules, cloud KMS, custodial platforms, and audit trails.
Solana Keychain is a unified signing library maintained by the Solana Foundation. It lets DAEMON support 10 signing backends through a single interface, without changing how the rest of the app works.
Supported Backends#
| Backend | Package | Use Case |
|---|---|---|
| Local Keypair | Built-in (default) | Solo development, testing |
| AWS KMS | @solana/keychain-aws-kms | Cloud-managed signing with IAM policies |
| GCP KMS | @solana/keychain-gcp-kms | Google Cloud key management |
| HashiCorp Vault | @solana/keychain-vault | Self-hosted secret management |
| Turnkey | @solana/keychain-turnkey | Non-custodial wallet infrastructure |
| Privy | @solana/keychain-privy | Embedded wallet platform |
| Fireblocks | @solana/keychain-fireblocks | Institutional custody with policies |
| Coinbase CDP | @solana/keychain-cdp | Coinbase Developer Platform |
| Dfns | @solana/keychain-dfns | Programmable key management |
| Crossmint | @solana/keychain-crossmint | Managed wallet transactions |
| Para | @solana/keychain-para | MPC-based signing |
How It Works#
DAEMON's wallet architecture already isolates all signing in the Electron main process. The renderer (UI) never sees private keys, it only sends signing requests over IPC. This makes keychain integration clean: we only change where the signing happens, nothow the UI works.
Current Flow (Local Keypair)
Renderer: wallet:swap-execute(walletId, ...)
│
Main Process:
withKeypair(walletId, async (keypair) => {
SecureKeyService.getKey() // OS keychain
→ safeStorage.decryptString()
→ bs58.decode()
→ Keypair.fromSecretKey()
transaction.sign([keypair])
connection.sendRawTransaction()
keypair.secretKey.fill(0) // zero memory
})
│
Renderer: ← { ok: true, data: signature }New Flow (With Keychain)
Renderer: wallet:swap-execute(walletId, ...)
│
Main Process:
withSigner(walletId, async (signer) => {
// Resolves backend based on wallet config:
// 'memory' → SecureKeyService (current behavior)
// 'aws-kms' → createAwsKmsSigner(config)
// 'vault' → createVaultSigner(config)
// etc.
signer.signTransactions([transaction])
connection.sendRawTransaction()
})
│
Renderer: ← { ok: true, data: signature }Architecture#
Backend (Electron Main Process)
A new KeychainService acts as a factory that creates the right signer based on each wallet's configuration:
import {
createAwsKmsSigner,
createTurnkeySigner,
createVaultSigner,
type SolanaSigner,
} from "@solana/keychain";
type SignerType = "memory" | "aws-kms" | "gcp-kms"
| "vault" | "turnkey" | "privy" | "fireblocks";
async function createSigner(
walletId: string,
config: SignerConfig
): Promise<SolanaSigner> {
switch (config.type) {
case "memory":
return createMemorySigner(walletId);
case "aws-kms":
return createAwsKmsSigner({
keyId: config.keyId,
publicKey: config.publicKey,
region: config.region,
credentials: {
accessKeyId: SecureKeyService.getKey(`AWS_KEY_${walletId}`),
secretAccessKey: SecureKeyService.getKey(`AWS_SECRET_${walletId}`),
},
});
case "vault":
return createVaultSigner({
keyName: config.keyName,
publicKey: config.publicKey,
vaultAddr: config.vaultAddr,
vaultToken: SecureKeyService.getKey(`VAULT_TOKEN_${walletId}`),
});
// ... other backends
}
}The withSigner Wrapper
Replaces the existing withKeypair(), same pattern, different signer source:
async function withSigner<T>(
walletId: string,
fn: (signer: SolanaSigner) => Promise<T>
): Promise<T> {
const wallet = db.getWallet(walletId);
const signerConfig = wallet.signer_config
? JSON.parse(wallet.signer_config)
: { type: "memory" };
const signer = await createSigner(walletId, signerConfig);
if (!(await signer.isAvailable())) {
throw new Error("Signer backend not available");
}
return fn(signer);
}Database Changes
ALTER TABLE wallets ADD COLUMN signer_type TEXT DEFAULT 'memory';
ALTER TABLE wallets ADD COLUMN signer_config TEXT;
-- Backend credentials stored in existing secure_keys table
-- e.g., AWS_KEY_{walletId}, VAULT_TOKEN_{walletId}New IPC Handlers
| Handler | Purpose |
|---|---|
keychain:backends | List available signing backends |
keychain:configure | Set up a backend for a wallet |
keychain:test | Test backend connectivity |
Frontend (Renderer)#
A new Signing Backend section in wallet settings lets users pick and configure their backend:
Backend Selector
Radio-style list showing all available backends. Local Keypair is the default. Each option shows the backend name, description, and connection status.
Configuration Forms
Backend-specific forms for entering credentials (API keys, regions, key IDs). All secrets are sent to the main process and stored encrypted, never held in the renderer.
Connection Test
One-click test button that calls the backend's isAvailable() method and reports success or error with actionable diagnostics.
Wallet Badge
Small indicator on the wallet panel showing which backend is active (e.g., "AWS KMS" badge next to the wallet name).
What Changes vs What Stays#
| Component | Changes? | Details |
|---|---|---|
| SecureKeyService | No | Still used for local keys + backend credentials |
| withKeypair() | Yes → withSigner() | Wrapper resolves signer type |
| Signing logic | Yes | Uses signer.signTransactions() instead of keypair.sign() |
| IPC contract | Minimal | Existing handlers unchanged, 3 new handlers added |
| Wallet UI | Yes | New backend settings section |
| Transaction history | No | Identical, just stores signatures |
| Helius / portfolio | No | Read-only, doesn't involve signing |
| Jupiter swaps | Signing only | Quote flow unchanged |
| PumpFun launches | Signing only | Token creation flow unchanged |
| Rate limiting | No | All existing security checks stay |
Backend Setup Examples#
AWS KMS
Requirements: an Ed25519 KMS key with SIGN_VERIFY usage.
// Key must be ECC_NIST_EDWARDS25519 algorithm
// IAM policy needs: kms:Sign and kms:DescribeKey
{
backend: "aws-kms",
keyId: "arn:aws:kms:us-east-1:123456:key/uuid",
publicKey: "YourSolanaBase58Address",
region: "us-east-1"
}HashiCorp Vault
Requirements: Vault transit engine with an Ed25519 key.
// Setup:
// vault secrets enable transit
// vault write -f transit/keys/solana-key type=ed25519
{
backend: "vault",
vaultAddr: "https://vault.example.com:8200",
keyName: "solana-key",
publicKey: "YourSolanaBase58Address"
}Turnkey
Non-custodial wallet infrastructure with policy controls.
{
backend: "turnkey",
organizationId: "your-org-id",
privateKeyId: "your-key-id",
publicKey: "YourSolanaBase58Address"
}Migration Path#
- Phase 1, Add @solana/keychain, build KeychainService, create withSigner() wrapper. Zero user-facing changes, local keypair behavior is identical.
- Phase 2, Add keychain IPC handlers and backend selector UI. Users can now switch backends per-wallet.
- Phase 3, Add backend-specific config forms one at a time (AWS KMS first, then Vault, Turnkey, etc.).
Security#
- Keys never in renderer, All signing stays in the Electron main process behind IPC
- Credentials encrypted, Backend API keys stored via SecureKeyService (OS keychain encryption)
- Memory cleanup, Local keypairs zeroed after use, remote backends never expose raw keys
- Health checks, isAvailable() called before every signing operation
- Audit trail, All transactions logged to transaction_history table regardless of backend
- Security audited, The solana-keychain SDK has undergone independent security review by Accretion