Skip to content

Commit

Permalink
Add method to create intermediate passwords for EC multiplied BIP38 keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryp Toon committed Mar 1, 2024
1 parent ccd0351 commit 8553404
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
4 changes: 4 additions & 0 deletions bitcoinlib/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
# Mnemonics
DEFAULT_LANGUAGE = 'english'

# BIP38
BIP38_MAGIC_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e251
BIP38_MAGIC_NO_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e253

# Networks
DEFAULT_NETWORK = 'bitcoin'
NETWORK_DENOMINATORS = { # source: https://en.bitcoin.it/wiki/Units, https://en.wikipedia.org/wiki/Metric_prefix
Expand Down
30 changes: 22 additions & 8 deletions bitcoinlib/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,10 +987,12 @@ def bip38_decrypt(encrypted_privkey, password):
password = password.encode('utf-8')
addresshash = d[0:4]
d = d[4:-4]
try:
key = scrypt(password, addresshash, 64, 16384, 8, 8)
except Exception:
key = scrypt.hash(password, addresshash, 16384, 8, 8, 64)

key = scrypt_hash(password, addresshash, 64, 16384, 8, 8)
# try:
# key = scrypt(password, addresshash, 64, 16384, 8, 8)
# except Exception:
# key = scrypt.hash(password, addresshash, 16384, 8, 8, 64)
derivedhalf1 = key[0:32]
derivedhalf2 = key[32:64]
encryptedhalf1 = d[0:16]
Expand Down Expand Up @@ -1028,10 +1030,7 @@ def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'):
if isinstance(password, str):
password = password.encode('utf-8')
addresshash = double_sha256(address)[0:4]
try:
key = scrypt(password, addresshash, 64, 16384, 8, 8)
except Exception:
key = scrypt.hash(password, addresshash, 16384, 8, 8, 64)
key = scrypt_hash(password, addresshash, 64, 16384, 8, 8)
derivedhalf1 = key[0:32]
derivedhalf2 = key[32:64]
aes = AES.new(derivedhalf2, AES.MODE_ECB)
Expand All @@ -1045,6 +1044,21 @@ def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'):
return base58encode(encrypted_privkey)


def scrypt_hash(password, salt, key_len=64, N=16384, r=8, p=1, buflen=64):
"""
Wrapper for Scrypt method for scrypt or Cryptodome library
For documentation see methods in referring libraries
"""
try: # Try scrypt from Cryptodome
key = scrypt(password, salt, key_len, N, r, p, buflen // key_len)
except TypeError: # Use scrypt module
key = scrypt.hash(password, salt, N, r, p, key_len)
return key



class Quantity:
"""
Class to convert very large or very small numbers to a readable format.
Expand Down
49 changes: 49 additions & 0 deletions bitcoinlib/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,55 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig
return npath


def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt=os.urandom(8)):
"""
Intermediate passphrase generator for EC multiplied BIP38 encrypted private keys.
Source: https://github.com/meherett/python-bip38/blob/master/bip38/bip38.py
:param passphrase: Passphrase or password text
:type passphrase: str
:param lot: Lot number between 100000 <= lot <= 999999 range, default to ``None``
:type lot: int
:param sequence: Sequence number between 0 <= sequence <= 4095 range, default to ``None``
:type sequence: int
:param owner_salt: Owner salt, default to ``os.urandom(8)``
:type owner_salt: str, bytes
:returns: str -- Intermediate passphrase
>>> bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1, owner_salt="75ed1cdeb254cb38")
'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ'
"""

owner_salt = to_bytes(owner_salt)
if len(owner_salt) not in [4, 8]:
raise ValueError(f"Invalid owner salt length (expected: 4 or 8 bytes, got: {len(owner_salt)})")
if len(owner_salt) == 4 and (not lot or not sequence):
raise ValueError(f"Invalid owner salt length for non lot/sequence (expected: 8 bytes, got:"
f" {len(owner_salt)})")
if (lot and not sequence) or (not lot and sequence):
raise ValueError(f"Both lot & sequence are required, got: (lot {lot}) (sequence {sequence})")

if lot and sequence:
lot, sequence = int(lot), int(sequence)
if not 100000 <= lot <= 999999:
raise ValueError(f"Invalid lot, (expected: 100000 <= lot <= 999999, got: {lot})")
if not 0 <= sequence <= 4095:
raise ValueError(f"Invalid lot, (expected: 0 <= sequence <= 4095, got: {sequence})")

pre_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt[:4], 32, 16384, 8, 8)
owner_entropy = owner_salt[:4] + int.to_bytes((lot * 4096 + sequence), 4, 'big')
pass_factor = double_sha256(pre_factor + owner_entropy)
magic = int.to_bytes(BIP38_MAGIC_LOT_AND_SEQUENCE, 8, 'big')
else:
pass_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt, 32, 16384, 8, 8)
magic = int.to_bytes(BIP38_MAGIC_NO_LOT_AND_SEQUENCE, 8, 'big')
owner_entropy = owner_salt

return pubkeyhash_to_addr_base58(magic + owner_entropy + HDKey(pass_factor).public_byte, prefix=b'')


class Address(object):
"""
Class to store, convert and analyse various address types as representation of public keys or scripts hashes
Expand Down
10 changes: 9 additions & 1 deletion tests/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

# Number of bulktests for generation of private, public keys and HDKeys. Set to 0 to disable
# WARNING: Can be slow for a larger number of tests
BULKTESTCOUNT = 100
BULKTESTCOUNT = 250


class TestKeyClasses(unittest.TestCase):
Expand Down Expand Up @@ -600,6 +600,14 @@ def test_bip38_hdkey_method(self):
k = HDKey(pkwif, witness_type='legacy')
self.assertEqual(k.encrypt('Satoshi'), bip38_wif)

def test_bip38_intermediate_password(self):
password1 = 'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ'
intpwd1 = bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1,
owner_salt="75ed1cdeb254cb38")
self.assertEqual(password1, intpwd1)
self.assertEqual(bip38_intermediate_password(passphrase="TestingOneTwoThree")[:10], 'passphrase')



class TestKeysBulk(unittest.TestCase):
"""
Expand Down

0 comments on commit 8553404

Please sign in to comment.