Browse Source

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
The MMGen Project 2 years ago
parent
commit
096f363dbc

+ 68 - 0
mmgen/base_proto/ethereum/misc.py

@@ -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

+ 2 - 0
mmgen/main_tool.py

@@ -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',

+ 8 - 0
mmgen/tool/util.py

@@ -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()

+ 1 - 0
test/ref/ethereum/geth-wallet.json

@@ -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}

+ 9 - 0
test/test_py_d/ts_tool.py

@@ -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)