mmgen-tool: new command: decrypt_keystore

command rename: `extract_key_from_geth_wallet` -> `decrypt_geth_keystore`
This commit is contained in:
The MMGen Project 2023-11-30 10:53:40 +00:00
commit 99e7057856
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
10 changed files with 132 additions and 50 deletions

View file

@ -1 +1,2 @@
pycryptodomex
pbkdf2

82
mmgen/altcoin/util.py Executable file
View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2023 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-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
altcoin.util: various altcoin-related utilities
"""
from mmgen.util import die
def decrypt_keystore(data,passwd,mac_algo=None,mac_params={}):
"""
Decrypt the encrypted data in a cross-chain keystore
Returns the decrypted data as a bytestring
"""
cdata = data['crypto']
parms = cdata['kdfparams']
valid_kdfs = ['scrypt','pbkdf2']
valid_ciphers = ['aes-128-ctr','aes-256-ctr']
if (kdf := cdata['kdf']) not in valid_kdfs:
die(1, f'unsupported key derivation function {kdf!r} (must be one of {valid_kdfs})')
if (cipher := cdata['cipher']) not in valid_ciphers:
die(1,f'unsupported cipher {cipher!r} (must be one of {valid_ciphers})')
# Derive encryption key from password:
if kdf == 'scrypt':
if not mac_algo:
die(1,f'the ‘mac_algo’ parameter is required for scrypt kdf')
from hashlib import scrypt
hashed_pw = scrypt(
password = passwd,
salt = bytes.fromhex(parms['salt']),
n = parms['n'],
r = parms['r'],
p = parms['p'],
maxmem = 0,
dklen = parms['dklen'])
elif kdf == 'pbkdf2':
if (prf := parms.get('prf')) != 'hmac-sha256':
die(1, f"unsupported hash function {prf!r} (must be 'hmac-sha256')")
from pbkdf2 import PBKDF2
hashed_pw = PBKDF2(
passphrase = passwd,
salt = bytes.fromhex(parms['salt']),
iterations = parms['c'],
digestmodule = 'sha256',
).read(parms['dklen'])
# see:
# https://github.com/xchainjs/xchainjs-lib.git
# https://github.com/xchainjs/foundry-primitives-js.git
from hashlib import blake2b
mac_algo = mac_algo or blake2b
mac_params = mac_params or {'digest_size': 32}
mac = mac_algo(
hashed_pw[16:32] + bytes.fromhex(cdata['ciphertext']),
**mac_params
).digest().hex()
if mac != cdata['mac']:
die(1, 'incorrect password')
# Decrypt data:
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
cipher_len = int(cipher.split('-')[1]) // 8
c = Cipher(
algorithms.AES(hashed_pw[:cipher_len]),
modes.CTR(bytes.fromhex(cdata['cipherparams']['iv'])),
backend = default_backend())
encryptor = c.encryptor()
return encryptor.update(bytes.fromhex(cdata['ciphertext'])) + encryptor.finalize()

View file

@ -1 +1 @@
14.0.0
14.1.dev0

View file

@ -145,7 +145,8 @@ mods = {
'encrypt',
),
'fileutil': (
'extract_key_from_geth_wallet',
'decrypt_keystore',
'decrypt_geth_keystore',
'find_incog_data',
'rand2file',
),

View file

@ -12,10 +12,9 @@
proto.eth.misc: miscellaneous utilities for Ethereum base protocol
"""
from ...util import die
from ...util2 import get_keccak
def extract_key_from_geth_keystore_wallet(cfg,wallet_fn,passwd,check_addr=True):
def decrypt_geth_keystore(cfg,wallet_fn,passwd,check_addr=True):
"""
Decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key
"""
@ -24,37 +23,11 @@ def extract_key_from_geth_keystore_wallet(cfg,wallet_fn,passwd,check_addr=True):
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
mac_chk = get_keccak(cfg)(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()
from ...altcoin.util import decrypt_keystore
key = decrypt_keystore(
wallet_data,
passwd,
mac_algo = get_keccak())
# Optionally check that Ethereum private key produces correct address
if check_addr:

View file

@ -148,9 +148,20 @@ class tool_cmd(tool_cmd_base):
return True
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"
def decrypt_keystore(self, wallet_file:str, output_hex=False):
"decrypt the data in a keystore wallet, returning the decrypted data in binary format"
from ..ui import line_input
from ..proto.eth.misc import extract_key_from_geth_keystore_wallet
passwd = line_input( self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase ).strip().encode()
return extract_key_from_geth_keystore_wallet( self.cfg, wallet_file, passwd, check_addr ).hex()
passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
import json
with open(wallet_file) as fh:
data = json.loads(fh.read())
from ..altcoin.util import decrypt_keystore
ret = decrypt_keystore(data[0]['keystore'], passwd)
return ret.hex() if output_hex else ret
def decrypt_geth_keystore(self, wallet_file:str, check_addr=True):
"decrypt the private key in a Geth keystore wallet, returning the decrypted key in hex format"
from ..ui import line_input
passwd = line_input(self.cfg, 'Enter passphrase: ', echo=self.cfg.echo_passphrase).strip().encode()
from ..proto.eth.misc import decrypt_geth_keystore
return decrypt_geth_keystore(self.cfg, wallet_file, passwd, check_addr).hex()

View file

@ -482,8 +482,8 @@ class CmdTestEthdev(CmdTestBase,CmdTestShared):
wallet_fn = os.path.join( self.keystore_dir, os.listdir(self.keystore_dir)[0] )
from mmgen.proto.eth.misc import extract_key_from_geth_keystore_wallet
key = extract_key_from_geth_keystore_wallet(
from mmgen.proto.eth.misc import decrypt_geth_keystore
key = decrypt_geth_keystore(
cfg = cfg,
wallet_fn = wallet_fn,
passwd = b'' )

View file

@ -40,7 +40,8 @@ class CmdTestTool(CmdTestMain,CmdTestBase):
('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_decrypt_keystore',(9,"'mmgen-tool decrypt_keystore'", [])),
('tool_decrypt_geth_keystore',(9,"'mmgen-tool decrypt_geth_keystore'", [])),
('tool_api', (9,'tool API (initialization, config methods, wif2addr)',[])),
# ('tool_encrypt_ref', (9,"'mmgen-tool encrypt' (reference text)", [])),
)
@ -99,16 +100,28 @@ class CmdTestTool(CmdTestMain,CmdTestBase):
t.req_exit_val = 2
return t
def tool_extract_key_from_geth_wallet(self):
def _decrypt_keystore(self,cmd,fn,pw,chk):
if cfg.no_altcoin:
return 'skip'
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)
t = self.spawn('mmgen-tool',['-d',self.tmpdir,cmd,fn])
t.expect('Enter passphrase: ',pw+'\n')
t.expect(chk)
return t
def tool_decrypt_keystore(self):
return self._decrypt_keystore(
cmd = 'decrypt_keystore',
fn = 'test/ref/altcoin/98831F3A-keystore-wallet.json',
pw = 'abc',
chk = read_from_file('test/ref/98831F3A.bip39') )
def tool_decrypt_geth_keystore(self):
return self._decrypt_keystore(
cmd = 'decrypt_geth_keystore',
fn = 'test/ref/ethereum/geth-wallet.json',
pw = '',
chk = '9627ddb68354f5e0ff45fb2da49d7a20a013b7257a83ef4adbbbd87aeaccc75e')
def tool_api(self):
t = self.spawn(
'tool_api_test.py',

View file

@ -0,0 +1 @@
[{"id":1700757398916,"name":"fake-test-wallet","keystore":{"crypto":{"cipher":"aes-128-ctr","ciphertext":"05e0926498f428b0693c6d47b5d22ffb664f376143cca66fa144b4004063ec3a9ec5fae950d4232717b22a1981fd0d9fe6657cfd400625b4a489fd1b02f06ca0055a3b616ad46b02cdffe860b8eaf3d3fe38f69a0f9018da293a417aa54a9519f5c1a4bdde0486d5eb778cf548e1475c4900348db103ba101ba09f3a2aa6684ad05a779fc651eca2ee079e07","cipherparams":{"iv":"f8a1c1f53f59ab43a27b683d1e97221c"},"kdf":"pbkdf2","kdfparams":{"prf":"hmac-sha256","dklen":32,"salt":"8dfbb24067ffd72225570e33f54327050f0aff01a65b7b94cb4661d5cfeae441","c":26214},"mac":"2ad462309e453b7ed98472b8836c2b765bfbde0303ea8cc50cb830a43cf01d8c"},"id":"bccb2fc2-8af5-4309-9ebc-a0edcfc73882","version":1,"meta":"xchain-keystore"},"selected":true}]

View file

@ -180,7 +180,7 @@ if cfg.testing_status:
'addrfile_chksum','keyaddrfile_chksum','passwdfile_chksum',
'add_label','remove_label','remove_address','twview',
'getbalance','listaddresses','listaddress',
'daemon_version','extract_key_from_geth_wallet',
'daemon_version','decrypt_geth_keystore',
'mn2hex_interactive','rand2file',
'rescan_address','rescan_blockchain','resolve_address',
'twexport','twimport','txhist'