Last active
January 24, 2026 14:38
-
-
Save atx/6cca74e88209a1c19e9344dd1d118223 to your computer and use it in GitHub Desktop.
This is a tool that ingests legacy Parity brain wallet seeds (not-BIP39-compatible) and outputs a wallet.json file that can be imported for example into MetaMask. 100% vibe coded, worked for my wallet but who knows if it is correct...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Parity Brain Wallet Recovery Tool | |
| Recovers an Ethereum wallet from a Parity brain wallet phrase and outputs | |
| a standard Ethereum JSON keystore file (v3 format). | |
| Algorithm source: parity-ethereum/accounts/ethkey/src/brain.rs | |
| Usage: | |
| python3 recover_parity_wallet.py "phrase words here" "password" [--output wallet.json] | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import uuid | |
| from dataclasses import dataclass | |
| from hashlib import pbkdf2_hmac | |
| from Crypto.Cipher import AES | |
| from Crypto.Hash import keccak | |
| # SECP256K1 curve order | |
| SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | |
| def keccak256(data: bytes) -> bytes: | |
| """Compute Keccak-256 hash.""" | |
| k = keccak.new(digest_bits=256) | |
| k.update(data) | |
| return k.digest() | |
| def is_valid_secp256k1_secret(secret: bytes) -> bool: | |
| """Check if bytes represent a valid SECP256K1 private key.""" | |
| secret_int = int.from_bytes(secret, 'big') | |
| # Must be in range [1, order-1] | |
| return 0 < secret_int < SECP256K1_ORDER | |
| def derive_public_key(private_key: bytes) -> bytes: | |
| """Derive uncompressed public key from private key using SECP256K1.""" | |
| # Using ecdsa library for SECP256K1 operations | |
| from ecdsa import SigningKey, SECP256k1 | |
| sk = SigningKey.from_string(private_key, curve=SECP256k1) | |
| vk = sk.get_verifying_key() | |
| # Return uncompressed public key (64 bytes, without 0x04 prefix) | |
| return vk.to_string() | |
| def derive_address(public_key: bytes) -> bytes: | |
| """Derive Ethereum address from public key.""" | |
| # Address is last 20 bytes of keccak256(public_key) | |
| return keccak256(public_key)[-20:] | |
| @dataclass | |
| class Keypair: | |
| private_key: bytes | |
| public_key: bytes | |
| address: bytes | |
| def derive_keypair_from_phrase(phrase: str) -> Keypair: | |
| """ | |
| Derive keypair from Parity brain wallet phrase. | |
| Algorithm from parity-ethereum/accounts/ethkey/src/brain.rs: | |
| 1. Initial hash: secret = keccak256(phrase.encode('utf-8')) | |
| 2. Loop: secret = keccak256(secret) | |
| 3. After i > 16384, check: | |
| - is_valid_secp256k1_secret(secret) | |
| - address[0] == 0x00 (first byte must be zero) | |
| 4. Return first valid keypair | |
| """ | |
| # Initial hash | |
| secret = keccak256(phrase.encode('utf-8')) | |
| i = 0 | |
| while True: | |
| secret = keccak256(secret) | |
| if i <= 16384: | |
| i += 1 | |
| else: | |
| # After 16385 iterations, try to create keypair | |
| if is_valid_secp256k1_secret(secret): | |
| public_key = derive_public_key(secret) | |
| address = derive_address(public_key) | |
| # Parity vanity constraint: first byte must be 0x00 | |
| if address[0] == 0x00: | |
| return Keypair( | |
| private_key=secret, | |
| public_key=public_key, | |
| address=address | |
| ) | |
| def create_keystore(private_key: bytes, address: bytes, password: str) -> dict: | |
| """ | |
| Create Ethereum JSON keystore v3 format. | |
| Uses PBKDF2 for key derivation and AES-128-CTR for encryption. | |
| """ | |
| # Generate random salt and IV | |
| salt = os.urandom(32) | |
| iv = os.urandom(16) | |
| # PBKDF2 key derivation (matches Parity's default: 10240 iterations) | |
| derived_key = pbkdf2_hmac( | |
| 'sha256', | |
| password.encode('utf-8'), | |
| salt, | |
| iterations=10240, | |
| dklen=32 | |
| ) | |
| # AES-128-CTR encryption | |
| # Use first 16 bytes of derived key for encryption | |
| cipher = AES.new(derived_key[:16], AES.MODE_CTR, nonce=b'', initial_value=iv) | |
| ciphertext = cipher.encrypt(private_key) | |
| # MAC = keccak256(derived_key[16:32] + ciphertext) | |
| mac = keccak256(derived_key[16:32] + ciphertext) | |
| return { | |
| "address": address.hex(), | |
| "crypto": { | |
| "cipher": "aes-128-ctr", | |
| "cipherparams": { | |
| "iv": iv.hex() | |
| }, | |
| "ciphertext": ciphertext.hex(), | |
| "kdf": "pbkdf2", | |
| "kdfparams": { | |
| "c": 10240, | |
| "dklen": 32, | |
| "prf": "hmac-sha256", | |
| "salt": salt.hex() | |
| }, | |
| "mac": mac.hex() | |
| }, | |
| "id": str(uuid.uuid4()), | |
| "version": 3 | |
| } | |
| def decrypt_keystore(keystore: dict, password: str) -> bytes: | |
| """Decrypt a keystore to verify it works correctly.""" | |
| crypto = keystore['crypto'] | |
| salt = bytes.fromhex(crypto['kdfparams']['salt']) | |
| iv = bytes.fromhex(crypto['cipherparams']['iv']) | |
| ciphertext = bytes.fromhex(crypto['ciphertext']) | |
| expected_mac = bytes.fromhex(crypto['mac']) | |
| # Derive key | |
| derived_key = pbkdf2_hmac( | |
| 'sha256', | |
| password.encode('utf-8'), | |
| salt, | |
| iterations=crypto['kdfparams']['c'], | |
| dklen=crypto['kdfparams']['dklen'] | |
| ) | |
| # Verify MAC | |
| computed_mac = keccak256(derived_key[16:32] + ciphertext) | |
| if computed_mac != expected_mac: | |
| raise ValueError("MAC verification failed - wrong password or corrupted keystore") | |
| # Decrypt | |
| cipher = AES.new(derived_key[:16], AES.MODE_CTR, nonce=b'', initial_value=iv) | |
| return cipher.decrypt(ciphertext) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Recover Ethereum wallet from Parity brain wallet phrase' | |
| ) | |
| parser.add_argument('phrase', help='The brain wallet phrase (space-separated words)') | |
| parser.add_argument('password', help='Password to encrypt the JSON keystore') | |
| parser.add_argument('--output', '-o', help='Output file path (default: stdout)') | |
| parser.add_argument('--verbose', '-v', action='store_true', help='Show derived address') | |
| parser.add_argument('--verify', action='store_true', help='Verify keystore can be decrypted') | |
| args = parser.parse_args() | |
| # Derive keypair | |
| if args.verbose: | |
| print(f"Deriving keypair from phrase...", file=sys.stderr) | |
| keypair = derive_keypair_from_phrase(args.phrase) | |
| if args.verbose: | |
| print(f"Secret: {keypair.private_key.hex()}", file=sys.stderr) | |
| print(f"Address: 0x{keypair.address.hex()}", file=sys.stderr) | |
| # Create keystore | |
| keystore = create_keystore(keypair.private_key, keypair.address, args.password) | |
| # Verify keystore if requested | |
| if args.verify: | |
| decrypted = decrypt_keystore(keystore, args.password) | |
| if decrypted != keypair.private_key: | |
| print("ERROR: Keystore verification failed!", file=sys.stderr) | |
| sys.exit(1) | |
| print("Keystore verification: OK", file=sys.stderr) | |
| # Output | |
| output = json.dumps(keystore, indent=2) | |
| if args.output: | |
| with open(args.output, 'w') as f: | |
| f.write(output) | |
| if args.verbose: | |
| print(f"Keystore written to: {args.output}", file=sys.stderr) | |
| else: | |
| print(output) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment