Private Key Architecture
Agentic Wallet implements strict key isolation to ensure that private keys never leave the wallet-engine boundary. This architecture protects against key exfiltration by agents, protocol adapters, or external services.
Key Isolation Principles
Exclusive Signing Authority Only wallet-engine can access private keys
No Key Transmission Keys never sent over network or to agents
Provider Abstraction Pluggable backends for different custody models
Encrypted at Rest All key material encrypted when stored
Key Provider Interface
All signer backends implement a common KeyProvider interface:
// From services/wallet-engine/src/key-provider/key-provider.ts
export interface KeyProvider {
save ( walletId : string , keypair : Keypair ) : Promise < void >;
load ( walletId : string ) : Promise < Keypair >;
provenance () : KeyProvenance ;
}
This abstraction allows seamless switching between storage backends without code changes:
save(): Persist a new wallet keypair securely
load(): Retrieve keypair for signing operations
provenance(): Report backend type and custody model for audit trails
Wallet-Engine Exclusive Signing
The transaction-engine requests signatures via the wallet-engine API, never directly accessing keys:
Sign Request
Transaction-engine sends unsigned transaction to wallet-engine: POST http://localhost:3002/wallets/:walletId/sign
Content-Type : application/json
{
"transaction" : "base64-encoded-unsigned-tx"
}
Key Retrieval
Wallet-engine loads keypair from the configured key provider: const keypair = await keyProvider . load ( walletId );
Signing
Wallet-engine signs the transaction using Solana’s built-in signing: transaction . sign ([ keypair ]);
Return Signature
Signed transaction returned to transaction-engine (keys never leave wallet-engine)
The wallet-engine service must be isolated from public network access. Only transaction-engine should be able to reach it.
Key Lifecycle
Wallet Creation
New wallets are generated with cryptographically secure random keys:
// From services/wallet-engine/src/app.ts
const keypair = Keypair . generate (); // Solana's secure key generation
const publicKey = keypair . publicKey . toBase58 ();
const walletId = randomUUID ();
await keyProvider . save ( walletId , keypair );
Keypairs generated using Solana’s Keypair.generate() (ed25519)
Each wallet assigned a unique UUID identifier
Public key derived from keypair and stored in metadata
Private key immediately encrypted and stored via key provider
Key Storage
Key material is never stored in plaintext . All backends encrypt keys at rest:
Backend Encryption Method encrypted-fileAES-256-GCM with WALLET_KEY_ENCRYPTION_SECRET memoryKeys held in process memory only (ephemeral) kmsEnvelope encryption: data key wrapped by KMS master secret hsmKeys wrapped with HSM module secret + slot PIN mpcSecret split into 3 shares, each encrypted separately
Key Loading
Keys are loaded into memory only when needed for signing:
// From services/wallet-engine/src/key-provider/encrypted-file-key-provider.ts
async load ( walletId : string ): Promise < Keypair > {
const keyFile = path . join ( this . keysDir , ` ${ walletId } .json` );
const encrypted = await fs . readFile ( keyFile , 'utf8' );
const secretBytes = JSON . parse ( decryptText ( encrypted , this . encryptionSecret ));
return Keypair.fromSecretKey(Uint8Array.from(secretBytes));
}
Keys decrypted on-demand per signing request
Keypair object exists in memory only during signing operation
No persistent in-memory key cache (reduces memory dump risk)
Key Rotation
Key rotation is currently manual but supported:
Generate New Wallet
Create a new wallet with POST /api/v1/wallets
Transfer Assets
Move SOL and SPL tokens from old wallet to new wallet
Update Agent Configuration
Point agents to the new walletId
Archive Old Wallet
Optionally delete old wallet key file or mark inactive
Automated key rotation is a planned feature. For production environments using KMS/HSM backends, implement rotation policies at the KMS/HSM layer.
Key Storage Locations
Key files are stored in the wallet-engine data directory:
WALLET_ENGINE_DATA_DIR = /path/to/wallet-engine/data
Default: services/wallet-engine/data/keys/
File Naming Conventions
Backend File Pattern Example encrypted-file{walletId}.jsonf47ac10b-58cc-4372-a567-0e02b2c3d479.jsonkms{walletId}.kms.jsonf47ac10b-58cc-4372-a567-0e02b2c3d479.kms.jsonhsm{walletId}.hsm.jsonf47ac10b-58cc-4372-a567-0e02b2c3d479.hsm.jsonmpc{walletId}.mpc.jsonf47ac10b-58cc-4372-a567-0e02b2c3d479.mpc.jsonmemory(none, ephemeral) -
Encryption Details
Encrypted-File Backend
Uses AES-256-GCM authenticated encryption:
// From services/wallet-engine/src/crypto/encryption.ts
export const encryptText = ( plaintext : string , secret : string ) : string => {
const iv = randomBytes ( 16 );
const key = createHash ( 'sha256' ). update ( secret ). digest ();
const cipher = createCipheriv ( 'aes-256-gcm' , key , iv );
let encrypted = cipher . update ( plaintext , 'utf8' , 'base64' );
encrypted += cipher . final ( 'base64' );
const authTag = cipher . getAuthTag ();
return JSON . stringify ({
iv: iv . toString ( 'base64' ),
encrypted ,
authTag: authTag . toString ( 'base64' ),
});
};
Key derivation : SHA-256 hash of WALLET_KEY_ENCRYPTION_SECRET
Initialization vector (IV) : 16 random bytes per encryption
Authentication tag : GCM tag for integrity verification
Format : JSON envelope with IV, ciphertext, and auth tag
The WALLET_KEY_ENCRYPTION_SECRET must be cryptographically random and kept secure. If this secret is compromised, all encrypted-file keys are at risk.
KMS Backend
Uses envelope encryption with a data key wrapped by the master secret:
// From services/wallet-engine/src/key-provider/kms-key-provider.ts
const dataKey = randomBytes ( 32 ). toString ( 'base64' );
const wrappedDataKey = encryptText ( dataKey , this . secretForWrap ());
const encryptedSecret = encryptText (
Buffer . from ( keypair . secretKey ). toString ( 'base64' ),
dataKey
);
Envelope structure:
{
"v" : 1 ,
"keyId" : "wallet-engine-kms-key" ,
"wrappedDataKey" : "..." ,
"encryptedSecret" : "..." ,
"createdAt" : "2026-03-08T10:00:00.000Z"
}
Data key : Random 32-byte key generated per wallet
Master secret : Configured via WALLET_KMS_MASTER_SECRET
Key ID : Optional identifier for key rotation scenarios
HSM Backend
Wraps keys using HSM slot, PIN, and module secret:
// From services/wallet-engine/src/key-provider/hsm-key-provider.ts
private unwrapSecret (): string {
return ` ${ this . moduleSecret } : ${ this . slotId } : ${ this . pin } ` ;
}
const wrappedSecret = encryptText (
Buffer . from ( keypair . secretKey ). toString ( 'base64' ),
this . unwrapSecret ()
);
Envelope structure:
{
"v" : 1 ,
"slotId" : "slot-0" ,
"wrappedSecret" : "..." ,
"createdAt" : "2026-03-08T10:00:00.000Z"
}
Slot ID : HSM slot identifier (default: slot-0)
PIN : HSM access PIN from WALLET_HSM_PIN
Module secret : Shared secret from WALLET_HSM_MODULE_SECRET
The current HSM implementation is a software simulation. For production, integrate with real HSM hardware (e.g., AWS CloudHSM, Thales Luna, YubiHSM).
MPC Backend
Splits keys into 3 shares using Shamir-like secret sharing over GF(256):
// From services/wallet-engine/src/key-provider/mpc-key-provider.ts
const shares = splitSecret ( Uint8Array . from ( keypair . secretKey ));
// shares: [{id: 1, bytes}, {id: 2, bytes}, {id: 3, bytes}]
const wrappedShares : StoredShare [] = shares . map (( share ) => {
const nodeSecret = this . nodeSecrets [ share . id - 1 ];
return {
id: share . id ,
wrapped: encryptText ( Buffer . from ( share . bytes ). toString ( 'base64' ), nodeSecret ),
};
});
Envelope structure:
{
"v" : 1 ,
"threshold" : 2 ,
"shares" : [
{ "id" : 1 , "wrapped" : "..." },
{ "id" : 2 , "wrapped" : "..." },
{ "id" : 3 , "wrapped" : "..." }
],
"createdAt" : "2026-03-08T10:00:00.000Z"
}
3-of-3 split : Secret divided into 3 shares
2-of-3 reconstruction : Any 2 shares can reconstruct the key
Node secrets : Each share encrypted with a different WALLET_MPC_NODE{1,2,3}_SECRET
GF(256) arithmetic : Galois field operations for share generation/reconstruction
For production MPC deployments, the 3 node secrets should be stored in separate infrastructure zones to ensure fault tolerance and security.
Production Security Recommendations
Choose Appropriate Backend
Strong Secrets
Generate secrets with openssl rand -base64 32 or equivalent
Secret Management
Store secrets in external secret managers (AWS Secrets Manager, Vault, etc.)
Network Isolation
Run wallet-engine in an isolated network segment, not exposed to public internet
File System Permissions
Set restrictive permissions on key storage directory: chmod 700 $WALLET_ENGINE_DATA_DIR /keys
chmod 600 $WALLET_ENGINE_DATA_DIR /keys/ *
Backup Strategy
Implement secure encrypted backups of key files
Audit Logging
Enable audit logging for all key load/sign operations
Key Rotation Policy
Establish periodic key rotation schedule (e.g., quarterly)
Key Provenance Tracking
Each key provider reports its provenance for audit purposes:
export interface KeyProvenance {
backend : 'encrypted-file' | 'memory' | 'kms' | 'hsm' | 'mpc' ;
custody : 'local' | 'external' ;
deterministicAddressing : boolean ;
}
Example responses:
// Encrypted-file backend
{
"backend" : "encrypted-file" ,
"custody" : "local" ,
"deterministicAddressing" : false
}
// KMS backend
{
"backend" : "kms" ,
"custody" : "external" ,
"deterministicAddressing" : false
}
backend : Identifies the key storage mechanism
custody : local means keys stored on disk, external means KMS/HSM/MPC
deterministicAddressing : Reserved for future HD wallet support
Auto-Funding (Devnet Only)
For devnet development, wallets can be auto-funded on creation:
WALLET_AUTOFUND_PAYER_PRIVATE_KEY =< base58-or-json-array >
WALLET_AUTOFUND_DEFAULT_LAMPORTS = 2000000
npm run wallets -- create --label demo --auto-fund
Auto-funding is for devnet testing only. Never use auto-funding in production or with mainnet keys.
Related Pages
Security Overview Trust boundaries and protection layers
Signer Backends Detailed guide for each backend option
Policy Enforcement Policy evaluation before signing