From 096f363dbce652c6fa6756324ff5cad2ca78c298 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 28 Apr 2022 11:00:53 +0000 Subject: [PATCH] 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 --- mmgen/base_proto/ethereum/misc.py | 68 ++++++++++++++++++++++++++++++ mmgen/main_tool.py | 2 + mmgen/tool/util.py | 8 ++++ test/ref/ethereum/geth-wallet.json | 1 + test/test_py_d/ts_tool.py | 9 ++++ 5 files changed, 88 insertions(+) create mode 100755 mmgen/base_proto/ethereum/misc.py create mode 100644 test/ref/ethereum/geth-wallet.json diff --git a/mmgen/base_proto/ethereum/misc.py b/mmgen/base_proto/ethereum/misc.py new file mode 100755 index 00000000..0995188c --- /dev/null +++ b/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 +# 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 diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index c342071c..0bbd7b33 100755 --- a/mmgen/main_tool.py +++ b/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', diff --git a/mmgen/tool/util.py b/mmgen/tool/util.py index 469b8969..5f4f563e 100755 --- a/mmgen/tool/util.py +++ b/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() diff --git a/test/ref/ethereum/geth-wallet.json b/test/ref/ethereum/geth-wallet.json new file mode 100644 index 00000000..1dbe7c74 --- /dev/null +++ b/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} \ No newline at end of file diff --git a/test/test_py_d/ts_tool.py b/test/test_py_d/ts_tool.py index 10ca4a2a..87e4d020 100755 --- a/test/test_py_d/ts_tool.py +++ b/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)