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?