---
title: "Ethereum Keystore File (JSON Wallet): Structure and Security"
url: "https://bip39-phrase.com/json-wallets-explained/"
language: "en"
published: "2025-01-26T00:00:00+00:00"
updated: "2026-10-19T00:00:00+00:00"
---

[ Keys & Addresses ](/topics/keys-and-addresses/) Published January 26, 2025 Updated October 19, 2026 

# Ethereum Keystore File (JSON Wallet): Structure and Security

![Ethereum Keystore File (JSON Wallet): Structure and Security](/wp-content/uploads/2025/01/json-wallets-explained-hero.webp) 

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](https://ethereum.org/en/developers/docs/data-structures-and-encoding/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.

```json
{
  "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](/format-evm-addresses/).

## How Does the Password Decrypt the Key?

Decryption works in three steps:

1. 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.
2. 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.
3. The first half of the derived key and the `iv` decrypt 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](https://github.com/ethereum/go-ethereum/blob/master/accounts/keystore/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:

```json
{
  "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 `keystore` folder of its data directory. Its [account management guide](https://geth.ethereum.org/docs/fundamentals/account-management) gives the naming format `UTC--<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](https://support.metamask.io/start/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](/convert-seed-phrase-to-private-key-guide/).

### Validator Keystores Are a Different Format

Keystores from Ethereum staking follow [EIP-2335](https://eips.ethereum.org/EIPS/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](https://github.com/ethereum/eth-account) library reads and writes keystore files:

```python
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](/protect-crypto-wallet-from-phishing/)).
- **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](/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](/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](/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](https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/accountcmd.go) describes as the way to change an account’s password; in Python, pass the result of `Account.decrypt` to `Account.encrypt`.

[ Back to Blog ](/blog/)
