diff --git a/setup.py b/setup.py index d6ab0dc7..e7c8c19a 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,9 @@ "mnemonic", "bech32", "pywgkey", - "sentinel-sdk" + "sentinel-sdk", + "bcrypt", + "jwcrypto" ], package_data={ "conf": ["config/config.ini"], diff --git a/src/cli/wallet.py b/src/cli/wallet.py index 15b04002..e965c4af 100644 --- a/src/cli/wallet.py +++ b/src/cli/wallet.py @@ -18,6 +18,8 @@ from cli.v2ray import V2RayHandler, V2RayConfiguration import base64 +import bcrypt +from jwcrypto import jwe, jwk import uuid import configparser import socket @@ -57,38 +59,128 @@ def __init__(self, **kwargs): # Migrate existing wallet to v2 self.__migrate_wallets() + @staticmethod + def decode_jwt_file(fpath: str, password: str) -> dict: + encrypted_jwe = open(fpath).read() + jwkey = {'kty': 'oct', 'k': base64.b64encode(password.encode()).decode("utf-8")} + jwetoken = jwe.JWE() + jwetoken.deserialize(encrypted_jwe) + jwetoken.decrypt(jwk.JWK(**jwkey)) + return json.loads(jwetoken.payload) + + @staticmethod + def decode_wallet_record(data: bytes) -> dict: + # First byte, padding + data = data[1:] + # Prefix keyname: \r\xad\x15=\n + data = data.removeprefix(b"\r\xad\x15=\n") + # Another, padding + data = data[1:] + # Key name until: \x12&\xebZ\xe9\x87! + keyname = data[:(data.find(b"\x12&\xebZ\xe9\x87!"))] + data = data.removeprefix(keyname + b"\x12&\xebZ\xe9\x87!") + pubkey = data[:33] + data = data.removeprefix(pubkey) + # Padding privatekey \x1a%\xe1\xb0\xf7\x9b + data = data.removeprefix(b"\x1a%\xe1\xb0\xf7\x9b ") + privkey = data[:32] + data = data.removeprefix(privkey) + curve = data[2:].decode() + + s = hashlib.new("sha256", pubkey).digest() + r = hashlib.new("ripemd160", s).digest() + + hex_address = r.hex() + account_address = bech32.bech32_encode("sent", bech32.convertbits(r, 8, 5)) + + pubkey = base64.b64encode(pubkey).decode() + privkey = privkey.hex() + keyname = keyname.decode() + + return { + "keyname": keyname, + "pubkey": pubkey, + "hex_address": hex_address, + "account_address": account_address, + "privkey": privkey, + "curve": curve, + } + def __migrate_wallets(self): - # https://github.com/MathNodes/meile-gui/blob/main/src/cli/wallet.py#L215-L230 - # Before continue make sure that sentinelcli still exist - if path.isfile(sentinelcli): - CONFIG = MeileConfig.read_configuration(MeileConfig.CONFFILE) - PASSWORD = CONFIG['wallet'].get('password', '') - KEYNAME = CONFIG['wallet'].get('keyname', '') - - kr = self.__keyring(PASSWORD) - if kr.get_password("meile-gui", KEYNAME) is None: # TODO: very ungly - export_cmd = f"{sentinelcli} keys export {KEYNAME} --unsafe --unarmored-hex --keyring-backend file --keyring-dir {ConfParams.KEYRINGDIR}" - child = pexpect.spawn(export_cmd) - child.expect(".*") - child.sendline("y") - child.expect("Enter*") - child.sendline(PASSWORD) - - outputs = child.readlines() - private_key = outputs[-1].strip().decode("utf-8") - child.expect(pexpect.EOF) + CONFIG = MeileConfig.read_configuration(MeileConfig.CONFFILE) + PASSWORD = CONFIG['wallet'].get('password', '') + KEYNAME = CONFIG['wallet'].get('keyname', '') + + kr = self.__keyring(PASSWORD) + if kr.get_password("meile-gui", KEYNAME) is not None: # TODO: very ungly + # Wallet was already migrated because exist a valid keyname in our keyring + return - kr = self.__keyring(PASSWORD) - kr.set_password("meile-gui", KEYNAME, private_key) + # For each key record, we actually write 2 items: + # - one with key `.info`, with Data = the serialized protobuf key + # - another with key `.address`, with Data = the uid (i.e. the key name) + # https://github.com/cosmos/cosmos-sdk/blob/main/crypto/keyring/keyring.go + + # We have two way to export private key: + # 1. Decode `.info` and then, assuming the curve is secp256k1, .replace(b"\"\tsecp256k1", b"")[-32:] + # 2. Follow me. + + # First all check if the keyring has a valid key-hash file and if the hash can be compared with our password + keyhash_fpath = path.join(ConfParams.KEYRINGDIR, "keyring-file", "keyhash") + if path.isfile(keyhash_fpath): + keyhash = open(keyhash_fpath, "r").read() + if bcrypt.checkpw(PASSWORD.encode(), keyhash.strip().encode()) is True: + # Search for `.info` / keyname .info file + keyname_dotinfo = path.join(ConfParams.KEYRINGDIR, "keyring-file", f"{KEYNAME}.info") + if path.isfile(keyname_dotinfo) is True: + payload = HandleWalletFunctions.decode_jwt_file(keyname_dotinfo, PASSWORD) + + # Double verify, we could also remove this assert + assert payload.get('Key', None) == f"{KEYNAME}.info" + data = payload["Data"] + data = base64.b64decode(data) + + wallet_record = HandleWalletFunctions.decode_wallet_record(data) + # Double verify, we could also remove this assert + assert wallet_record["keyname"] == KEYNAME + + hex_address = wallet_record["hex_address"] + + # Anothe double verification, let's find the `.address` and verify if data match with our uuid + hex_address_fpath = path.join(ConfParams.KEYRINGDIR, "keyring-file", f"{hex_address}.address") + if path.isfile(hex_address_fpath) is True: + payload = HandleWalletFunctions.decode_jwt_file(hex_address_fpath, PASSWORD) + # Double verify, we could also remove this assert + assert payload.get('Key', None) == f"{hex_address}.address" + + data = payload["Data"] + data = base64.b64decode(data) + # Double verify, we could also remove this assert + assert data.decode() == f"{KEYNAME}.info" + + # TODO: just for debugging purpose + privkey = wallet_record["privkey"] + del wallet_record["privkey"] + print(wallet_record) + + kr.set_password("meile-gui", KEYNAME, privkey) + else: + print(f"{hex_address}.address doesn't exist") + else: + print(f"{KEYNAME}.info doesn't exist") + else: + print("bcrypt hash doesn't match") + else: + print(f"{keyhash_fpath} doesn't exist") def __keyring(self, keyring_passphrase: str): kr = CryptFileKeyring() kr.filename = "keyring.cfg" - print(ConfParams.KEYRINGDIR) + # print(ConfParams.KEYRINGDIR) kr.file_path = path.join(ConfParams.KEYRINGDIR, kr.filename) - print(kr.file_path) + # print(kr.file_path) kr.keyring_key = keyring_passphrase return kr