Author

Topic: [SOLVED] Help with making BIP38 class standard-compliant (Read 116 times)

legendary
Activity: 1568
Merit: 6660
bitcoincleanup.com / bitmixlist.org
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):

Code:
pip install zpywallet

Documentation is also being written for the next release which will hopefully make things more clear.
legendary
Activity: 2464
Merit: 4415
🔐BitcoinMessage.Tools🔑
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."
legendary
Activity: 1568
Merit: 6660
bitcoincleanup.com / bitmixlist.org
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):

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

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

Code:
(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?
Jump to: