Title: [SOLVED] Help with making BIP38 class standard-compliant
Post by: NotATether on June 05, 2023, 10:21:39 AM
A minor error: I forgot to switch off the compressed mode in the BIP38 encryption. Doing that resolved the test case. I'm still mildly frustrated that none of the test vectors have bytes values in them, as the Base58 can make slightly incorrect results look wildly different. Leaving this thread open in case someone has something to say about this.I have a BIP38 class which encrypts and decrypts private keys (see https://github.com/ZenulAbidin/zpywallet for details), but it's creating the wrong encrypted keys. I am 100% sure the AES encryption/decryption works properly. Code (/zpywallet/utils/bip38.py): import hashlib import binascii import base58 import scrypt
from .utils.keys import PrivateKey from .utils.utils import encrypt, decrypt
class Bip38PrivateKey: BLOCK_SIZE = 16 KEY_LEN = 32 IV_LEN = 16
def __init__(self, privkey: PrivateKey, passphrase, compressed=True, segwit=False, witness_version=0): '''BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted privkey.''' if "BASE58" not in privkey.network.ADDRESS_MODE and "BECH32" not in privkey.network.ADDRESS_MODE: raise ValueError("BIP38 requires Base58 or Bech32 addresses") flagbyte = b'\xe0' if compressed else b'\xc0' addr = privkey.public_key.bech32_address(compressed, witness_version) if segwit else privkey.public_key.base58_address(compressed) addresshash = hashlib.sha256(hashlib.sha256(addr).digest()).digest()[0:4] key = scrypt.hash(passphrase, addresshash, 16384, 8, 8) derivedhalf1 = key[0:32] derivedhalf2 = key[32:64] encryptedhalf1 = encrypt(binascii.unhexlify('%0.32x' % (int(binascii.hexlify(bytes(privkey)[0:16]), 16) ^ int(binascii.hexlify(derivedhalf1[0:16]), 16))), derivedhalf2) encryptedhalf2 = encrypt(binascii.unhexlify('%0.32x' % (int(binascii.hexlify(bytes(privkey)[16:32]), 16) ^ int(binascii.hexlify(derivedhalf1[16:32]), 16))), derivedhalf2) self.flagbyte = flagbyte self.addresshash = addresshash self.encryptedhalf1 = encryptedhalf1 self.encryptedhalf2 = encryptedhalf2 encrypted_privkey = b'\x01\x42' + self.flagbyte + self.addresshash + self.encryptedhalf1 + self.encryptedhalf2 encrypted_privkey += hashlib.sha256(hashlib.sha256(encrypted_privkey).digest()).digest()[:4] # b58check for encrypted privkey self._encrypted_privkey = base58.b58encode(encrypted_privkey)
@property def base58(self): return self._encrypted_privkey.decode()
def private_key(self, passphrase, compressed=True, segwit=False, witness_version=0): '''BIP0038 non-ec-multiply decryption. Returns WIF privkey.''' d = base58.b58decode(self._encrypted_privkey) d = d[2:] #flagbyte = d[0:1] d = d[1:] #WIF compression #if flagbyte == b'\xc0': # compressed = False #if flagbyte == b'\xe0': # compressed = True addresshash = d[0:4] d = d[4:-4] key = scrypt.hash(passphrase,addresshash, 16384, 8, 8) derivedhalf1 = key[0:32] derivedhalf2 = key[32:64] encryptedhalf1 = d[0:16] encryptedhalf2 = d[16:32] decryptedhalf2 = decrypt(encryptedhalf2, derivedhalf2) decryptedhalf1 = decrypt(encryptedhalf1, derivedhalf2) priv = decryptedhalf1 + decryptedhalf2 priv = PrivateKey.from_bytes(binascii.unhexlify('%064x' % (int(binascii.hexlify(priv), 16) ^ int(binascii.hexlify(derivedhalf1), 16)))) pub = priv.public_key
addr = pub.bech32_address(compressed, witness_version) if segwit else pub.base58_address(compressed) if hashlib.sha256(hashlib.sha256(addr).digest()).digest()[0:4] != addresshash: raise ValueError('Verification failed. Password is incorrect.') else: return priv
Input parameters from BIP-0038 test vector: No compression, no EC multiply Test 1:
Passphrase: TestingOneTwoThree Encrypted: 6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg Unencrypted (WIF): 5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR Unencrypted (hex): CBF4B9F70470856BB4F40F80B87EDB90865997FFEE6DF315AB166D713AF433A5
only the WIF is used as input. Debug output from PDB: (Pdb) p flagbyte b'\xe0' (Pdb) p addr b'164MQi977u9GUteHr4EPH27VkkdxmfCvGW' (Pdb) p addresshash b'C\xbeAy' (Pdb) p key b's\x1e\xf3\xc77\xb5]\xf4\x99\x8bD\xfa\x8aTz?8\xdfBM\xa2@\xde8\x9b\x11\xd1\x87[\xa4wg//\xe8\x1b\x052\xb5\x95\x0e>\xa6\xff\xf9,e\xd4g\xaa}\x05Ii\x82\x1d\xe24Oz\x86\xd4%i' (Pdb) p derivedhalf1 b's\x1e\xf3\xc77\xb5]\xf4\x99\x8bD\xfa\x8aTz?8\xdfBM\xa2@\xde8\x9b\x11\xd1\x87[\xa4wg' (Pdb) p derivedhalf2 b'//\xe8\x1b\x052\xb5\x95\x0e>\xa6\xff\xf9,e\xd4g\xaa}\x05Ii\x82\x1d\xe24Oz\x86\xd4%i' (Pdb) encryptedhalf1 b'p\xe4\xa0\x80_\x15\xa7~\xfcs\x8fy@h\xd8\x83' (Pdb) encryptedhalf2 b'|)\x85\xa6\x94_\x7f\xe0\xdb?u\xdc0^\xaf|' (Pdb) encrypted_privkey b'\x01B\xe0C\xbeAyp\xe4\xa0\x80_\x15\xa7~\xfcs\x8fy@h\xd8\x83|)\x85\xa6\x94_\x7f\xe0\xdb?u\xdc0^\xaf|\xbba\xce$' (Pdb) hashlib.sha256(hashlib.sha256(encrypted_privkey).digest()).digest()[:4] b'8J\xa7\x12' (Pdb) self._encrypted_privkey b'6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo' As you can see, instead of 6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg, I am getting 6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo which is wrong. So what is wrong with the BIP38 encryption?
Title: Re: [SOLVED] Help with making BIP38 class standard-compliant
Post by: witcher_sense on June 05, 2023, 11:44:14 AM
A minor error: I forgot to switch off the compressed mode in the BIP38 encryption. Doing that resolved the test case. I'm still mildly frustrated that none of the test vectors have bytes values in them, as the Base58 can make slightly incorrect results look wildly different. Leaving this thread open in case someone has something to say about this. Good to know you solved your problem. Could you also explain how to run this software or this particular module to test it? What is the entry point for this wallet? I tried to run the wallet.py file and a couple of others, but always get an "ImportError: attempted relative import with no known parent package."
Title: Re: [SOLVED] Help with making BIP38 class standard-compliant
Post by: NotATether on June 05, 2023, 01:44:27 PM
A minor error: I forgot to switch off the compressed mode in the BIP38 encryption. Doing that resolved the test case. I'm still mildly frustrated that none of the test vectors have bytes values in them, as the Base58 can make slightly incorrect results look wildly different. Leaving this thread open in case someone has something to say about this. Good to know you solved your problem. Could you also explain how to run this software or this particular module to test it? What is the entry point for this wallet? I tried to run the wallet.py file and a couple of others, but always get an "ImportError: attempted relative import with no known parent package." Don't use git clone because the latest commits broke a few things, instead you should install the latest version from PyPI (v0.4.0 at the time of writing): Documentation is also being written for the next release which will hopefully make things more clear.
|