Browse Source

mmgen-tool: new command: `decrypt_keystore`

command rename: `extract_key_from_geth_wallet` -> `decrypt_geth_keystore`
The MMGen Project 1 year ago
parent
commit
99e7057856

+ 1 - 0
alt-requirements.txt

@@ -1 +1,2 @@
 pycryptodomex
+pbkdf2

+ 82 - 0
mmgen/altcoin/util.py

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

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-14.0.0
+14.1.dev0

+ 2 - 1
mmgen/main_tool.py

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

+ 6 - 33
mmgen/proto/eth/misc.py

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

+ 16 - 5
mmgen/tool/fileutil.py

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

+ 2 - 2
test/cmdtest_py_d/ct_ethdev.py

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

+ 20 - 7
test/cmdtest_py_d/ct_tool.py

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

+ 1 - 0
test/ref/altcoin/98831F3A-keystore-wallet.json

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

+ 1 - 1
test/tooltest.py

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