A deep, practical guide to encrypting private keys at rest in a crypto payment gateway: the threat model, real industry breaches, AES-256-GCM and authenticated encryption, envelope encryption and key management, and the exact zero-downtime process TronDealer used to encrypt 7,800+ wallet keys.
Picture the worst Tuesday of your life. A backup snapshot of your production database ends up somewhere it shouldn't — an S3 bucket left public, a leaked service credential, a compromised laptop, a curious contractor. If that database stored your customers' wallet private keys in plaintext, the attacker doesn't need to break anything else. They already have the keys. Every deposit address you ever generated is theirs to drain, on every chain, simultaneously, in minutes.
That single scenario is why encryption at rest is not a "nice to have" checkbox for a crypto payment gateway — it is the difference between a bad day and an extinction event. This guide walks through the whole picture: how key material actually leaks, the cryptography that protects it, and the exact, zero-downtime process we used at TronDealer to encrypt more than 7,800 live wallet keys across seven blockchains without dropping a single payment.
Most breaches are survivable: you rotate passwords, invalidate sessions, notify users. A private-key leak is not. On-chain, there is no "reset password." Whoever holds the key is the wallet. Encryption at rest is what turns a database compromise back into a survivable incident.
Developers often assume that because their site runs on HTTPS, their data is "encrypted." HTTPS (TLS) protects data in transit — while it moves between the browser and the server. It does nothing for data sitting on a disk.
The threat model for "at rest" is the one people underestimate. You are not defending against a hacker typing at a keyboard in real time. You are defending against a static copy of your data falling into the wrong hands, where the attacker has unlimited time and no rate limits.
Encryption at rest earns its keep because the leak vectors are numerous, mundane, and mostly have nothing to do with your application code being "hacked":
axios releases carrying a fake plain-crypto-js trojan (March), and the self-propagating ChainDrop worm that poisoned 444 packages starting from keyv (August) — specifically harvested .npmrc tokens, cloud credentials, and crypto wallets from build machines. Your own code can be pristine and still be the delivery vehicle.Notice the pattern: in almost every case the attacker gets a read copy of the database, not shell access to your running app. Encryption at rest is precisely the control that makes that read copy worthless.
Assume your database will leak someday — through a backup, a credential, or a dependency you don't control. Design so that when it does, the attacker gets ciphertext, not money. That assumption is the entire philosophy behind encryption at rest.
Not all encryption is equal. Storing keys "encrypted" with the wrong algorithm or the wrong parameters can be barely better than plaintext. Here is the landscape.
Everyone reaches for AES-256 and stops there. But AES is a block cipher — the mode of operation is what determines whether your encryption is actually secure:
| Mode | Confidentiality | Integrity / tamper-proof | Verdict |
|---|---|---|---|
| ECB | Weak — identical plaintext blocks produce identical ciphertext | None | Never use |
| CBC (unauthenticated) | Yes, with a random IV | None — an attacker can flip bits undetected | Avoid |
| GCM (AES-256-GCM) | Yes | Yes — built-in authentication tag | Use this |
AES-256-GCM is an AEAD cipher — Authenticated Encryption with Associated Data. It gives you two guarantees in one operation: confidentiality (the data is unreadable without the key) and integrity (any tampering with the ciphertext is detected at decrypt time, because the authentication tag won't validate). For storing something as sensitive as a signing key, integrity matters as much as secrecy — you want decryption to fail loudly if a row was altered, not silently hand a corrupted key to a signer.
The graveyard of breached systems is full of clever homegrown encryption: static IVs, ECB mode, XOR "ciphers," keys derived from predictable values, custom padding. Use a vetted library implementing a standard AEAD construction, and follow the parameter rules above. Cryptography punishes creativity.
Where does the master key live? The professional answer is envelope encryption: a hierarchy where a high-value key-encryption key (KEK) — held in a Key Management Service (KMS) or a Hardware Security Module (HSM) — encrypts the data-encryption keys (DEK) that actually encrypt your rows. The database only ever sees encrypted DEKs; the KEK never leaves the KMS boundary.
For a single-tenant deployment, a simpler-but-sound version is a single 256-bit key held in a secrets manager or an environment variable that is never written to the database, logs, or the repo. The non-negotiable principle is the same at every scale: the key and the ciphertext must be able to leak independently. If one compromise leaks both, you have encryption theater, not encryption.
Here is the exact process we followed — a real, production migration of 7,800+ live wallet keys across EVM chains, TRON, Solana, SUI, Bitcoin, Litecoin, and TON, done with zero downtime and zero missed payments.
Every private key is encrypted with AES-256-GCM. Each encryption generates a fresh random 96-bit IV and produces a 128-bit authentication tag. Nothing is ever encrypted with a reused IV. The stored value is a self-describing, versioned envelope:
enc1:<iv base64>:<authTag base64>:<ciphertext base64>The enc1: prefix versions the scheme. It lets every read instantly tell an encrypted value from a legacy plaintext one — which is what makes a gradual, non-breaking migration possible.
The 256-bit key lives in an environment variable provisioned from the deployment's secret store — never in the database, never in logs, never in the git repository. A database leak therefore yields only ciphertext; the key leaks (or doesn't) through a completely separate channel.
The encrypt function passes an already-encrypted value through unchanged; the decrypt function passes plaintext through unchanged. This single property is what makes the migration safe: you can wrap a value twice, or decrypt a value that was never encrypted, and nothing breaks. The only fatal mistake would be to forget a read site — so we made the wrapper impossible to double-apply and audited every path.
A dump of our database now exposes exactly zero usable private keys. Every signing path transparently decrypts, and the migration ran without a single interruption to live deposit detection, sweeping, or payouts.
Encrypting keys is one layer. Security that survives contact with reality is defense in depth — many independent layers, so no single failure is fatal. The practices that surround our encryption:
Encryption at rest is the control that assumes your database will leak and makes sure the leak is worthless. The recipe is not exotic: a standard authenticated cipher (AES-256-GCM), a unique IV per value, an authentication tag you actually verify, and a key that lives somewhere other than the data it protects. Wrap it in an idempotent, versioned envelope and you can even retrofit it onto a live system without downtime — as we did across seven chains and thousands of wallets.
If you accept crypto payments, ask your gateway one question: are the private keys encrypted at rest, and where does the key live? If the answer is vague, the keys are probably plaintext — and one leaked backup away from a very bad Tuesday.
Every wallet TronDealer generates for you has its private key encrypted at rest with AES-256-GCM. Read more about our guarantees on the security section of our homepage, or dive into the integration guide to start accepting payments.
Rather than sprinkle decrypt calls across dozens of call sites, we decrypt in the handful of low-level functions that turn a stored secret into a signer — one per blockchain (the keypair builder, the transaction signer). Every code path that ever signs necessarily flows through one of these, so a single, auditable edit per chain covers them all. We then verified by static search that no signer is ever constructed from a raw stored key outside these points.
Because reads accept both formats, the code ships first: new wallets encrypt immediately, and existing plaintext rows keep working (reads pass them through). Then an idempotent backfill script sweeps every wallet table, encrypting any row that isn't already in enc1: form. It is re-runnable and fails safe — if one row errors, it is logged and skipped, never aborting the rest.
Before flipping the switch, we ran a read-only verification against production: for a sample of real keys on every chain, we confirmed that decrypt(encrypt(key)) reproduces the key byte for byte, and that the wallet address derived from the round-tripped key is identical to the stored address. If the address comes out the same, signing cannot break. It did, on every sample.