Ethereum Keystore File (JSON Wallet): Structure and Security
An Ethereum keystore file, also called a JSON wallet or a UTC file, is a small JSON document that holds one private key encrypted with a password. Its format is the Web3 Secret Storage definition, version 3: a key derivation function, scrypt or PBKDF2, stretches the password, AES-128-CTR encrypts the key, and a Keccak-256 checksum tells a wrong password from a right one. Whoever copies the file still needs the password, but a weak password can be guessed offline, and a forgotten one cannot be reset.
Keystore Files at a Glance
| Ethereum keystore file | |
|---|---|
| Standard | Web3 Secret Storage, version 3 |
| Holds | One private key, so one account, usually with its address in plain text |
| Password stretching | scrypt (Geth’s default) or PBKDF2-HMAC-SHA256 |
| Encryption | AES-128-CTR |
| Wrong password | Rejected by the Keccak-256 MAC before anything is decrypted |
| File name in Geth | UTC--<date>--<address> |
| Opens in | Geth, MyEtherWallet, the MetaMask browser extension, libraries such as eth-account |
| Backs up | One account only, unlike a seed phrase, which backs up every account of a wallet |
What Is Inside a Keystore File?
Below is a real, working example. It encrypts the public test private key 7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d from the Web3 Secret Storage specification with the password testpassword, using scrypt with Geth’s standard parameters. Never put funds on a test key.
{
"address": "008aeeda4d805471df9b2a5b0f38a0c3bcba786b",
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "b7d483dbb5ed84542dfa20b08ef05745"
},
"ciphertext": "614f2c44298a81a6ec31bb5127a2076c012d2431f3e911117b4940f6d8f1de40",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 262144,
"p": 1,
"r": 8,
"salt": "c906d7cdfb8cf20e81e919670e83542ca57950de83a1f364b8dc6cae3153f580"
},
"mac": "d0dee0c7c7bbe2c2b0959c315aa8dccc2de300be3e3cb7e1c5ddc66d397cbe9b"
},
"id": "4d2424cf-56e4-44a5-9005-b129b2aec863",
"version": 3
}
| Field | What it holds | Size for a 32-byte key |
|---|---|---|
version | Format version | always 3 |
id | A random UUID that identifies the file | 16 bytes |
address | The account address, unencrypted (optional) | 20 bytes (40 hex characters) |
crypto.ciphertext | The private key, encrypted with AES-128-CTR | 32 bytes (64 hex characters) |
crypto.cipherparams.iv | Random initialization vector for AES | 16 bytes (32 hex characters) |
crypto.kdf, kdfparams | How the password becomes a key: scrypt or PBKDF2, with a random salt | salt: 32 bytes by convention |
crypto.mac | Keccak-256 checksum that tells a wrong password from a right one | 32 bytes (64 hex characters) |
The ciphertext has the same length as the private key, which is why it is 64 hex characters and not more. The address is stored in lowercase without 0x; wallets display the same address with its mixed-case EIP-55 checksum.
How Does the Password Decrypt the Key?
Decryption works in three steps:
- The password and salt go through the key derivation function (here scrypt with N = 262,144, r = 8, p = 1), which produces a 32-byte derived key.
- The wallet computes Keccak-256 of the second half of the derived key followed by the ciphertext. If the result differs from
mac, the password is wrong and nothing is decrypted. - The first half of the derived key and the
ivdecrypt the ciphertext with AES-128-CTR, which returns the 32-byte private key.
Step 1 is deliberately expensive. Geth’s source code describes its standard scrypt settings as needing 256 MB of memory and about a second of CPU time on a modern processor (passphrase.go); in our test, a pure-JavaScript scrypt took about 0.4 seconds per attempt on an Apple M4 Pro. Every password guess pays that cost, which slows an attacker down but does not save a short or common password. Geth’s --lightkdf option writes much cheaper settings, N = 4,096 and p = 6, which make guessing far faster.
Scrypt or PBKDF2: Which KDF Does a File Use?
The crypto.kdf field says. Geth writes scrypt, while the specification names PBKDF2-HMAC-SHA256 as the function every implementation must support. This is the specification’s PBKDF2 test vector; the password testpassword decrypts it to the same test key:
{
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "6087dab2f9fdbbfaddc31a909735c1e6"
},
"ciphertext": "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46",
"kdf": "pbkdf2",
"kdfparams": {
"c": 262144,
"dklen": 32,
"prf": "hmac-sha256",
"salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"
},
"mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"
},
"id": "3198bc9c-6672-5ab3-d995-4942343ae5b6",
"version": 3
}
The cost here is the iteration count c: 262,144 rounds of HMAC-SHA256. Unlike scrypt, PBKDF2 needs almost no memory, so it is cheaper to attack with graphics cards; we checked that both example files decrypt to the published key.
Which Wallets Create or Open Keystore Files?
The format is the same everywhere; only the file name and the KDF settings differ:
- Geth keeps every account as a keystore file in the
keystorefolder of its data directory. Its account management guide gives the naming formatUTC--<date>--<address>and says it is safe to copy individual key files between Ethereum nodes. - MyEtherWallet can create a keystore file as one of its software wallet options and can open one to access a wallet.
- MetaMask imports a keystore file only in the browser extension: open the account selector, choose Add wallet, then Import an account, set Select Type to JSON File, choose the file and enter the password that protects the file, not your MetaMask password (MetaMask: use an existing wallet). The mobile app cannot. MetaMask also notes that imported accounts do not come back when you restore from your Secret Recovery Phrase, so keep the file and its password, or move the funds to an account derived from your phrase. MetaMask does not keep its own accounts in this format: its backup is the Secret Recovery Phrase, and it exports a single account as a raw private key; see how to export a private key from MetaMask and other wallets.
Validator Keystores Are a Different Format
Keystores from Ethereum staking follow EIP-2335, which stores the BLS12-381 keys that validators sign with. The structure looks similar, but the version is 4, the checksum uses SHA-256 instead of Keccak, the file adds fields such as pubkey and path, and the password is normalized to NFKD with control characters removed. These files belong in validator software, not in a wallet like MetaMask.
Code Example: Encrypting and Decrypting in Python
The eth-account library reads and writes keystore files:
from eth_account import Account
private_key = "0x7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" # public test key
keystore = Account.encrypt(private_key, "testpassword") # scrypt by default
restored = Account.decrypt(keystore, "testpassword")
print(keystore["crypto"]["kdf"]) # scrypt
print(restored.hex()) # 7a28b5ba57c5...fe9d
print(Account.from_key(restored).address) # 0x008AeEda4D805471dF9b2A5B0f38A0C3bCBA786b
Every call to encrypt picks a new salt and IV, so the same key and password give a different file each time, and all of them decrypt to the same key. Account.decrypt is also how you get the raw private key out of a keystore file offline.
How Secure Is a Keystore File?
- The password is the whole defense. Anyone who copies the file can test passwords offline for as long as they like. A long, random passphrase, such as several unrelated words, puts that out of practical reach; a short or reused one does not.
- There is no reset. Geth’s guide warns that a lost password cannot be retrieved and the account stays locked forever. Keep the password somewhere safe and separate from the file.
- File plus password equals the key. Together they control the account on every EVM network at once, so treat them like a private key: never upload the file to a website, and remember that nobody legitimate, including wallet support, asks for your keystore file and its password (how phishing works).
- One key, one account. A keystore backs up a single account, while a seed phrase recreates every account a wallet derives from it; the difference is explained in seed phrase vs private key. If an account was derived from your phrase, the phrase recreates its key at any time, so the keystore is only a convenience; the seed phrase to private key converter shows that derivation on Ethereum’s standard path.
- Old copies keep the old password. Changing the password writes a new file, but any earlier copy still opens with the old one. Delete or destroy copies you no longer trust.
Bitcoin has a similar format for a single key: BIP38 encrypts one private key with a passphrase into a string starting with 6P.
Frequently Asked Questions
Can I open a keystore file without the password?
No. The private key is encrypted with a key derived from the password, and no wallet or service can reset it. The only way back is to find the password, for example by testing variations you might have used.
Is a UTC file the same as a keystore file?
Yes. Geth names keystore files UTC--<date>--<address>, so wallets and guides call the same thing a UTC file, a JSON file or a keystore file.
Does a keystore file contain my seed phrase?
No. A version 3 keystore holds a single private key and no seed phrase, so it restores one account, not a whole wallet.
How do I change the password of a keystore file?
Decrypt it with the old password and encrypt the key again with the new one. Geth does this with geth account update <address>, which its command help describes as the way to change an account’s password; in Python, pass the result of Account.decrypt to Account.encrypt.