Skip to content

Latest commit

 

History

History
221 lines (175 loc) · 11.7 KB

README.md

File metadata and controls

221 lines (175 loc) · 11.7 KB

Decrypt Ethereum Private Key

Rust project that decrypts an Ethereum keyfile to recover the original private key.

I recently built the same functionality in a Python project, and wanted to see how to create the same functionality in Rust.

Be careful with your private keys. If you use this repo to decrypt your private key from an Ethereum keyfile and a malicious person gets hold of it, they gain control over the funds held by that private key.

The objective of this project is to replicate a key component of any wallet - decrypting the private key so that it can be used to sign transactions.

If you want to make a backup of Ethereum keys, just backup the keyfiles - the private key is encrypted already, and any Ethereum client should be able to use the keyfile format. This assumes of course that you have used a strong passphrase to secure your keys.

Note: the scrypt key derivation function used in this project is very slow. I have a similar project in Python here which is much quicker. I'm still trying to work out why the Rust implementation of scrypt takes so much longer (minutes rather than ~ 1 second) to achieve the same result.

Table of Contents

Introduction

In cryptocurrencies like Bitcoin and Ethereum, private keys define ownership of assets on a public blockchain. As such, it is vitally important that such keys are not exposed - access to private keys is synonymous with access to funds.

For this reason, private keys are generally encrypted before being stored.

Bitcoin

In the case of the Bitcoin Core client (the original cryptocurrency client), private keys are stored in an internal database. By default, this is named wallet.dat and located in the wallets subdirectory of the Bitcoin data directory. The wallet file is a Berkeley DB file that contains keys and related transactions. The wallet file is not a text file and is not human-readable, and users have the choice whether or not to encrypt the wallet.

Wallet encryption involves encrypting the private keys with a random master key which is in turn symmetrically encrypted using a key derived from passphrase - full description of the relevant encryption protocols. Keys are decrypted only when necessary, either by GUI prompt or by means of the walletpassphrase command.

An encrypted wallet file is fairly tightly coupled to the Bitcoin Core client - you need the core client to parse the wallet. However, Bitcoin Core provides an option for exporting private keys by means of the dumpprivkey CLI command - keys might then be imported into other wallet software.

Ethereum

Ethereum keyfiles are defined in the Web3 Secret Storage Definition. Communication with nodes takes place through RPC calls to a JSON-RPC interface, which uses JSON as a data format.

Go Ethereum is the official Golang implementation of the Ethereum protocol. It's CLI client, geth, does not allow private keys to be exported in plaintext. This is in contrast to Bitcoin Core where the dumpprivkey command provides access to decrypted private keys.

The Ethereum approach is interesting in that keyfiles contain information relating to their decryption. You could easily print an Ethereum keyfile and have a paper-backup of the key, the security of which is determined by your passphrase.

Ethereum keyfiles are JSON text files that are comprised of a symmetrically encrypted private key along with additional metadata relating to the encryption scheme.

Keyfiles are stored by default in a keystore directory, and are human readable. Each keyfile provides the encrypted key, along with the metadata required to decrypt it.

Build & Usage Instructions

Requires rustc.

If cargo is installed:

  • Build an executable: cargo build --release
  • Run ./target/release/decrypt-ethereum-private-key with a path to an Ethereum keyfile as the first command-line argument - a sample keyfile is provided in this repo's root.
  • Enter password when prompted - for the test file, the password is password123.
  • If the password is correct, the private key will be output to stdout.
  • If testing against the supplied test keyfile, check the result against correct-result.

Tests

Run cargo test to test the key derivation, authentication and decryption functions.

Generate an Ethereum Keyfile

For testing purposes, you need a private key and an associated keyfile (which contains the encrypted private key). Geth can be used for this.

Geth will generate a keyfile from a supplied private key, which should be 32 bytes long expressed as a hex string:

# cd into a temporary working directory
cd $(mktemp -d)

# Make a private key from 32 pseudo-random bytes
head -c 32 /dev/random | xxd -ps -c 32 > plain_key.txt

# Make an Ethereum key file - assumes geth is installed, prompts for password
geth --datadir . account import plain_key.txt

The sample keyfile shown below is generated from a private key 82633960e2a725ab641067a12b05fcaeca860d45ba785f634318490261e5d1a1 - 32 pseudo-random bytes - encrypted with the password "password123":

{
  "address": "15d5d89632dc2d185aa27907ad42b1012ef1c982",
  "crypto": {
    "cipher": "aes-128-ctr",
    "ciphertext": "050d93d6a4e396a0cb74d021d0de9b1ed7860c0fd843b28acefbd3dc61314a19",
    "cipherparams": {
      "iv": "6aa1de28f8f43a522e6ac987c18bf66e"
    },
    "kdf": "scrypt",
    "kdfparams": {
      "dklen": 32,
      "n": 262144,
      "p": 1,
      "r": 8,
      "salt": "b04dcccf351dba67460e5bf322493ab25b4e1b314df970503ed43c392166d4c8"
    },
    "mac": "c9a7a0c880289d267c49bf828ace98ecb89c64d600bbeed718dac9f605083e61"
  },
  "id": "62b2bcce-9ba7-49a4-8f67-59fb366ac7dd",
  "version": 3
}

This keyfile is included in this repo: UTC--2019-12-17T09-17-16.419911545Z--15d5d89632dc2d185aa27907ad42b1012ef1c982.

The correct private key is also provided in correct-result.

Encryption of Keys in Ethereum

The keyfile holds the encrypted private key in the crypto.ciphertext field.

The encryption scheme is an AES-128-CTR cipher, using scrypt as a key derivation function (to derive a block cipher key from a text-based password) and message authentication code (MAC) to authenticate the password.

The private key is symmetrically encrypted using AES-128 with block cipher mode CTR. In this case, the scrypt key derivation function is used to generate an AES symmetric key from the original password. An initialization vector is also required for decryption - and this is held in the crypto.cipherparams.iv field.

Relevant fields are:

  • crypto.cipher: Denotes the cryptographic block-cipher algorithm, key size in bits and block cipher mode of operation.
  • crypto.ciphertext: The encrypted private key.
  • crypto.cipherparams.iv: The initialization vector required for AES in counter (CTR) mode.
  • crypto.kdf: Denotes the key derivation function used - in this case, scrypt.
  • crypto.kdfparams: These variables are used in the kdf function - see decrypt.rs, scrypt wikipedia
  • crypto.mac: Message authentication code - used to check the authenticity of the key derived from the user-supplied password.

Key Derivation

Requires the user-supplied password and the crypto.kdfparams.

Uses crypto::scrypt::scrypt function from the rust-crypto crate

From decrypt.rs:

/**
 * Derive key for decryption by means of scrypt with the provided parameters from the original
 * keyfile along with the user-supplied password (`data.password`).
 * */
pub fn derive_key(data: &Data) -> Result<Vec<u8>, &'static str> {
    let mut n = data.n as f64;
    n = n.log2();
    let log_2_n = n as u8;
    let params = ScryptParams::new(log_2_n, data.r, data.p);
    let mut result: Vec<u8> = vec![0; data.dklen as usize];
    scrypt(&data.password, &data.salt, &params, &mut result);
    Ok(result.to_vec())
}

NOTE: in debug builds (i.e. cargo run) this implementation of scrypt is very slow. It's much faster in the release build, but for debugging/development purposes, you may wish to skip the key derivation step in the interests of speeding things up.

Verify Password by Message Authentication

Once the key has been derived from the password, it is authenticated by:

  • Taking the second-leftmost 16 bytes from the derived key.
  • Concatenating this value (key excluding first 16 bytes) with the ciphertext bytes.
  • Comparing the SHA-3 (keccak-256) hash of this value with the value of the crypto.mac field.
  • If these values are the same, the key is authentic.

If the values do not match, the supplied password is incorrect.

Decryption

Once the encryption key has been derived (and authenticated) from the user-supplied password and the KDF parameters, it can be used to decrypt crypto.cipertext - yielding the decrypted private key.

Note that for AES 128 bit counter mode the aes_key must be 16 bytes, but the Ethereum keyfile key derivation algorithm uses scrypt to derive a 32 byte key from a user-supplied password.

The web3 protocol requires using the first 16 bytes of the derived key to AES decrypt ciphertext in AES-128 counter mode.

pub fn decrypt(data: &Data, key: &Vec<u8>) -> Result<Vec<u8>, &'static str> {
    use aes_ctr::Aes128Ctr;
    use aes_ctr::stream_cipher::generic_array::GenericArray;
    use aes_ctr::stream_cipher::{
        NewStreamCipher, SyncStreamCipher
    };
    let aes_key = GenericArray::from_slice(key.split_at(16).0);
    let initialization_vector = GenericArray::from_slice(&data.iv);
    let mut ct = (data.ct).clone();
    let mut cipher = Aes128Ctr::new(&aes_key, &initialization_vector);
    cipher.apply_keystream(&mut ct);
    Ok(ct.to_vec())
}

References