Author

Topic: In theory,THE most unsafe HD Wallet - [BIP32, BIP39, BIP44, BIP84] - am I close? (Read 119 times)

copper member
Activity: 193
Merit: 255
Click "+Merit" top-right corner
This is interesting. Remember a decade ago and the fuck-up with brain wallets?

People would take SHA256(secret) and use the 32-byte output as a BTC private key. Things got really interesting when someone set secret = "", i.e. an empty string. Its SHA256 is

Code:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Here is a neat tool to convert 32-byte strings to Bitcoin key pairs. For the empty string hash above, it gives us

Code:
Private key uncompressed (WIF):     5Hvr8Y5y3QZE6cVciRcsRtmXSLbuJcsVXr5JooEVUEwwGpShw71
Private key compressed (WIF):       KwifrGCvMcyEomfJdqfKasVXLqX3rmKrw9c1bZ4YfEZUYoVyHRGv

Legacy address uncompressed:        17DCsohf9Mx9xJxJx5i21VXw8c6xeKVj2d
Legacy address compressed:          151nEUuyFiwrxxqse4wmQfwPvpWACbyBUU
Nested segwit adress:               3FiUrxVsgBuGmTBtfbySRZ4z8kFffzYzTf
Native segwit address:              bc1q8l84lncadllmcrn9wsulzj4e24nnjsv86w0jhh
Taproot address:                   

...and the infamous 50 BTC transaction belongs to one of them.

"What on Earth does any of this have to do with HD Wallets?" is a valid question.

Well, I realized we can easily recreate the madness using a master XPRV and leave out derivation paths.

Others have described the anatomy of XPRV better before me, so allow me to concatenate the bytes according to:

(pseudocode) b"\x04\x88\xad\xe4" + a string of zeros + SHA25&(""), so that we get a 156 characters long hexadecimal string like so

Code:
0488ade4000000000000000000000000000000000000000000000000000000000000000000000000000000000000e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

To verify is as an XPRV and make it human-readable, here is a one-liner that does the trick:

Code:
>>> base58.b58encode_check(binascii.unhexlify('0488ade4000000000000000000000000000000000000000000000000000000000000000000000000000000000000e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')).decode()
>>> 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAmp8jihbB12h5EN88J7v7oXcjdCgi9zWxT3Ya8znix4tadVdyM'

Lo and behold, if we run

Code:
xprivkey = "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAmp8jihbB12h5EN88J7v7oXcjdCgi9zWxT3Ya8znix4tadVdyM"
path = ""
hdw = HDWallet(xprivkey, path)
#and so forth

we get... the same private keys and public addresses!

In other words, we have resurrected the decade-old brain wallet madness and shown that the same goes for XPRV.

Bonus:

If we instead use the stupid SHA("") twice in the same string, e.g.

Code:
>>> base58.b58encode_check(binascii.unhexlify('0488ade400000000000000000000e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')).decode()
>>> 'xprv9s21ZrQH143K24sTZGBSPocQZqkzcNhss3CpUdhrJxgdUVyK3iG7QvdpuddRytfq997BhnDGVmrbqFmCEPDQhbAyXDmFfeFJh3AxsfSxCMp'

We get a seemingly new and unique XPRV, but it resolves to the same good old key pairs. If no derivation path is specified, none of these extra bytes matter.

This ultimately leads me to believe that we could scan using either ...01, ...02, ...03 like brainflayer or by feeding it with good old SHA256 that has been out in the wild for a long time.

Additionally, it will be interesting to see whether the derivation path can be the variable that leads us to the discovery of new (old) public keys and their private keys.

Mnemonic phrases and hierarchical deterministic (HD) wallets may be good. I mean, the intention was certainly good. But it is not so good if you can trick non-tech-savvy people into using XPRVs that are accepted by all major wallet software even if they are a cryptographic disaster.
copper member
Activity: 193
Merit: 255
Click "+Merit" top-right corner
Simplifying further with a one-liner (added "x01" to the 32-byte/64-character hexadecimal string as the least significant byte)

Code:
base58.b58encode_check(binascii.unhexlify('0488ade40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001')).decode()

This leads to five public addresses, of which all(!) have had activity on the blockchain this year. Therefore, I would like to call this

Code:
xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21TeqtDeZVxb

the most catastrophic XPRV of them all. Agree?

Code:
Legacy address uncompressed:   1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
Legacy address compressed:     1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Nested segwit address:         3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN
Native segwit address:         bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
Taproot address:               bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9

And we may control them. Merry Christmas and happy scripting.

copper member
Activity: 193
Merit: 255
Click "+Merit" top-right corner
My main point is that it doesn't matter how many safety stops you have along the way, when the end product, the XPRV, is a simple base58_check string with a defined, even hard-coded, structure.

I'll try with a separate demonstration using as few libraries and imports as possible.

The proper, safe, and sound protocols are supposed to spit out a high-entropy seed of 32 bytes (64 hexadecimal characters) after intricate rounds of hashing. But nothing stops us from hardcoding it to a string of zeroes.

Demo in Python:

Code:
import base58

seed = b"0000000000000000000000000000000000000000000000000000000000000000" #Intentionally bad

xprv = b"\x04\x88\xad\xe4" #BTC Mainnet, always the same
xprv += b"\x00" * 9  # Depth, parent fingerprint, and child number, let's use zeroes only
xprv += seed[32:]  # Chain code (right part of the long string of zeroes)
xprv += b"\x00" + seed[:32]  # Master key (left part of the long string of zeroes)
base58.b58encode_check(xprv).decode()

which gives

Code:
xprv9s21ZrQH143K2YBbTcxPkMQWcHDV2gnvSAeXEz4bnhHWkoYf9roKBtVTb1bPzW5tqKSbZuTdDhHuS4Sv1KiraqmxmL4ovaEZaeprrVuJGjK

which is a valid XPRV - but, according to me, with catastrophic properties

Using the same code as in the OP, we can derive from it:

Code:
Private key uncompressed:      5JBWV6pN7wQzggN7gPYzY9yVdvp8ptiYLhPydzQDmX2x3k2PcJ1
Private key compressed:        KxqP92g4DdAuc8kCUrYCxWcMPzX3YPGMKwJQzGMVMVZbxr9VXU2s

Legacy address uncompressed:   1CrdmWFByuXBxhpRC2Q3AgGjdQg6qqURXe
Legacy address compressed:     1ML9yxF3ahintKwkhi1Vxr55BhXsBRy2vZ
Nested segwit address:         3NmVSGxP5Z5aA6aUakH65sQd4mKEv7mNEF
Native segwit address:         bc1qmuq74trafulz3nem3y54jpmx6d2eu7nfvwt25h
Taproot address:               bc1pxphaqt9zu5gzvyddx6q48aztj9x40nh0j5a7fs8ucxs47yuqzeds5eawkr

and (almost) infinitely many more by playing around with the derivation path.



Edit/addition: The true power of strong and secure hashing methods, such as PBKDF2-HMAC-SHA512 - "computationally slow one-way functions," becomes their weakness in intentionally bad applications. This is because discarding outputs for which the input was catastrophic is, by definition, impossible. It will always be possible to override suggested requirements such as "at least 128 bits of entropy", as demonstrated here, where it is impossible to tell from the XPRV output alone if it was generated properly or not. (I tested this last one, too, with Electrum, which accepts it if you tick the BIP39 box, and generates an array of receiving and change addresses from it.)
legendary
Activity: 3472
Merit: 10611
Quote
See what I did there? I overrode all traces of good practice: I used an empty string ("") as the passphrase, didn't hash anything, didn't salt, used no derivation path at all ("m"), and confirmed both outputs, the master and the first derivation, equate to the same value:
I'm assuming `bip32 = BIP32.from_seed(bytes.fromhex(""))` is from
https://github.com/darosior/python-bip32/blob/1492d39312f1d9630363c292f6ab8beb8ceb16dd/bip32/bip32.py#L332
In which case the library is broken because it should have rejected your input as insecure because despite what you said in the quote above the only "good practice" that you overrode was using a 0 bit entropy whereas the doc says it should be between 128 and 512 bits
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#master-key-generation

The rest can be skipped and your derived keys would still be secure as long as the entropy was secure.
Meaning you can skip passphrase in BIP39, you can even modify the algorithm and completely remove PBKDF2 and just use a single SHA512 (removes all the hashes and usage of salt), not use any derivation path, and so on and still produce 100% secure child keys.
copper member
Activity: 1330
Merit: 899
🖤😏
You might also want to mention sending a lot of Bitcoins to burned addresses for no reason or maybe that single address has actually an owner with private key, I don't know, but I would like to know who has done what you said, to derive a key like that?

Ps, do an rmd160 on empty string, the address contains 72 Bitcoins.
copper member
Activity: 193
Merit: 255
Click "+Merit" top-right corner
DISCLAIMER: Consider this a theoretical exercise in producing the worst imaginable HD Wallet and its private keys and public addresses from a safety and good practice perspective. It contains overriding as many safety measures as possible - on purpose. Don't try this at home.

Brief introduction
Firstly, I will skip a proper intro and instead recommend those interested to read up on BIP32, BIP39, BIP44, and BIP84 as needed. These are intended to add safety and utility to Bitcoin, and I chose to try my best to misuse them.

I will use Python and some common crypto-related libraries, leaving out most whys and hows.

My idea is to create the worst imaginable BTC mainnet extended private key (XPRV) from a cryptographic viewpoint and derive the first of its corresponding key pairs. In other words: "Catastrophic private keys" in WIF format and the different flavors of public addresses they control - details you can import in Electrum and sweep clean in a matter of seconds.

This is hopefully somewhat entertaining and, in the best of worlds, even rewarding in one way or the other.

I hypothesize that if you learn to do every step wrong, you become better at making it right when needed. I look forward to your comments, corrections, and feedback!

Let's get going
We need a disastrous XPRV to begin with. Let's aim for the worst of them all. There are excellent online tools, such as this, this and this you can use to create high-entropy and human-readable mnemonic phrases based on standardized word lists, but they have safety measures, as they should have, against week phrases. Also, remember that creating cryptographically sound XPRVs involves many rounds of one-way hashing and the incremental use of random salts and checksums (PBKDF2-HMAC-SHA512).

Can we break it? I think we can:

Code:
from bip32 import BIP32

bip32 = BIP32.from_seed(bytes.fromhex(""))
master_xpriv = bip32.get_xpriv()
xpriv_from_path = bip32.get_xpriv_from_path("m")

print(master_xpriv)
print(xpriv_from_path)

See what I did there? I overrode all traces of good practice: I used an empty string ("") as the passphrase, didn't hash anything, didn't salt, used no derivation path at all ("m"), and confirmed both outputs, the master and the first derivation, equate to the same value:

Code:
xprv9s21ZrQH143K37GktPqduLh1DoBuxUQvLPY9yR3eJ4zQB4KzSYeoHNutnWNasXJJmEHCneeFhpSuC8Ppc33RXvzsuhSTLLqurx7kLvw9d6p

Please tell me if you imagine a more unsafe and stupid method to create a master XPRV for Bitcoin! (Further, consider all this a homage to the marvelous 50 BTC transaction to the address that belongs to SHA256(*empty string*) in 2015.)

Now, let's use this master XPRV, once again without derivation paths, to find the possible private keys and all(?) corresponding public addresses, given BTC standards of 2023:

Code:
import sympy
from bitcoinutils.setup import setup
from bitcoinutils.keys import P2pkhAddress, P2shAddress, PrivateKey
from bitcoinutils.hdwallet import HDWallet as hdw

setup('mainnet')

xprivkey = "xprv9s21ZrQH143K4YUcKrp6cVxQaX59ZFkN6MFdeZjt8CHVYNs55xxQSvZpHWfojWMv6zgjmzopCyWPSFAnV4RU33J4pwCcnhsB4R4mPEnTsMC"
path = ""
hdw = HDWallet(xprivkey, path)
priv1 = hdw.get_private_key()
wif1 = priv1.to_wif(compressed=False)
wif2 = priv1.to_wif()
pub1 = priv1.get_public_key()
script_witness = priv1.get_public_key().get_segwit_address().to_script_pub_key()
add1 = pub1.get_address(compressed=False).to_string()
add2 = pub1.get_address().to_string()
add3 = P2shAddress.from_script(script_witness).to_string()
add4 = pub1.get_segwit_address().to_string()
add5 = pub1.get_taproot_address().to_string()

After some prettification (actual print commands omitted), the output is

Code:
Private key uncompressed:       5JBSnNFYrX7VZcomqnzmmVZqTuubdCDJbkmR8Z7g9qJ48FmxDkV
Private key compressed:         Kxq6oEoDbZuf4y3SvN1zbP5vunX9pkVJgQB3WLkTozAGns2p7PWe

Legacy address uncompressed:    15np5mEdrT7CAaFePSDY9PwiqZqZaT4noQ
Legacy address compressed:      1ATHjwQRMgxmCm6c8Pj9snyrd39UMZyKBZ
Nested segwit address:          32ZjiUAL8FNXj9gQ7Jqcsi2zGHL4VxAop7
Native segwit address:          bc1qv7c7h5d8thdd8gxsmqaquhe8xkzk5cdmjmjyxg
Taproot address:                bc1p6tx5tl7cp49alj4n48kfca0yslgvzhvakja7fysx3y384thl2wmqj8nszu

Concluding remarks and questions
At least one of these public addresses has appeared on the blockchain before. I claim no fame in finding these. I just added what has been out for years together, but I sincerely wonder - can you think of an even more stupid and/or insecure way of using XPRV to set up an HD Wallet?

I haven't even bothered with playing around with derivation paths yet. It should be a simple loop in Python to spit out millions, if not billions, of "hardened" (not so hardened) children and change addresses that are derived from this single disastrous XPRV.

What say you? Can you possibly do worse? Smiley
Jump to: