mmgen-tool: add extract_key_from_geth_wallet command

- decrypt the encrypted private key in a Geth keystore wallet and output
  the decrypted key in hexadecimal format

Usage:

    $ mmgen-tool extract_key_from_geth_wallet geth-keystore-wallet.json

Testing:

    $ test/test.py -e tool_extract_key_from_geth_wallet
This commit is contained in:
The MMGen Project 2022-04-28 11:00:53 +00:00
commit 096f363dbc
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
5 changed files with 88 additions and 0 deletions

View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen
# https://gitlab.com/mmgen/mmgen
"""
base_proto.ethereum.misc: miscellaneous utilities for Ethereum base protocol
"""
from ...util import die
def extract_key_from_geth_keystore_wallet(wallet_fn,passwd,check_addr=True):
"""
Decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key
"""
import json
with open(wallet_fn) as fp:
wallet_data = json.loads(fp.read())
cdata = wallet_data['crypto']
assert cdata['cipher'] == 'aes-128-ctr', f'incorrect cipher: "{cdata["cipher"]}" != "aes-128-ctr"'
assert cdata['kdf'] == 'scrypt', f'incorrect KDF: "{cdata["kdf"]}" != "scrypt"'
# Derive encryption key from password
from hashlib import scrypt
sp = cdata['kdfparams']
hashed_pw = scrypt(
password = passwd,
salt = bytes.fromhex( sp['salt'] ),
n = sp['n'],
r = sp['r'],
p = sp['p'],
maxmem = 0,
dklen = sp['dklen'] )
# Check password by comparing generated MAC to stored MAC
from ...util import get_keccak
mac_chk = get_keccak()(hashed_pw[16:32] + bytes.fromhex( cdata['ciphertext'] )).digest().hex()
if mac_chk != cdata['mac']:
die(1,'Incorrect passphrase')
# Decrypt Ethereum private key
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
c = Cipher(
algorithms.AES(hashed_pw[:16]),
modes.CTR(bytes.fromhex( cdata['cipherparams']['iv'] )),
backend = default_backend() )
encryptor = c.encryptor()
key = encryptor.update( bytes.fromhex(cdata['ciphertext']) ) + encryptor.finalize()
# Optionally check that Ethereum private key produces correct address
if check_addr:
from ...tool.coin import tool_cmd
from ...protocol import init_proto
t = tool_cmd( proto=init_proto('eth') )
addr = t.wif2addr(key.hex())
addr_chk = wallet_data['address']
assert addr == addr_chk, f'incorrect address: ({addr} != {addr_chk})'
return key

View file

@ -32,6 +32,7 @@ opts_data = {
-d, --outdir= d Specify an alternate directory 'd' for output
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
-k, --use-internal-keccak-module Force use of the internal keccak module
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
@ -83,6 +84,7 @@ mods = {
'bytespec',
'bytestob58',
'eth_checksummed_addr',
'extract_key_from_geth_wallet',
'hash160',
'hash256',
'hexdump',

View file

@ -178,3 +178,11 @@ class tool_cmd(tool_cmd_base):
"create a checksummed Ethereum address"
from ..protocol import init_proto
return init_proto('eth').checksummed_addr(addr)
def extract_key_from_geth_wallet( self, wallet_file:str, check_addr=True ):
"decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key"
from ..util import line_input
from ..opts import opt
from ..base_proto.ethereum.misc import extract_key_from_geth_keystore_wallet
passwd = line_input( 'Enter passphrase: ', echo=opt.echo_passphrase ).strip().encode()
return extract_key_from_geth_keystore_wallet( wallet_file, passwd, check_addr ).hex()

View file

@ -0,0 +1 @@
{"address":"50f8b08aadb66d5e6d9df924ec1173ab4540ef82","crypto":{"cipher":"aes-128-ctr","ciphertext":"2fefcef71b5f7f16a04b6b76b6f6db145a242f4f79e1cda75633d0d0d46a7419","cipherparams":{"iv":"0f47f4bcd638d2e2d5e4997e382c15fc"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"fd29f4b7f22b5dd0fcc554a91cc46da6e27cd854cf98d84105487b9c13f6e968"},"mac":"40323ca744ef7b43cd672c5129dd49f7ad68e4ad6f9a38874a1d92f9509da12d"},"id":"5c4d8652-874c-4838-be13-33666a2c2b8d","version":3}

View file

@ -26,6 +26,7 @@ class TestSuiteTool(TestSuiteMain,TestSuiteBase):
('tool_encrypt', (9,"'mmgen-tool encrypt' (random data)", [])),
('tool_decrypt', (9,"'mmgen-tool decrypt' (random data)", [[[enc_infn+'.mmenc'],9]])),
('tool_twview_bad_comment',(9,"'mmgen-tool twview' (with bad comment)", [])),
('tool_extract_key_from_geth_wallet',(9,"'mmgen-tool extract_key_from_geth_wallet'", [])),
('tool_api', (9,'tool API (initialization, config methods, wif2addr)',[])),
# ('tool_encrypt_ref', (9,"'mmgen-tool encrypt' (reference text)", [])),
)
@ -85,6 +86,14 @@ class TestSuiteTool(TestSuiteMain,TestSuiteBase):
t.req_exit_val = 2
return t
def tool_extract_key_from_geth_wallet(self):
fn = 'test/ref/ethereum/geth-wallet.json'
key = '9627ddb68354f5e0ff45fb2da49d7a20a013b7257a83ef4adbbbd87aeaccc75e'
t = self.spawn('mmgen-tool',['-d',self.tmpdir,'extract_key_from_geth_wallet',fn])
t.expect('Enter passphrase: ','\n')
t.expect(key)
return t
def tool_api(self):
t = self.spawn('tool_api_test.py',cmd_dir='test/misc')
t.expect('legacy.*compressed.*segwit.*bech32',regex=True)