Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various changes #1

Merged
merged 2 commits into from
Apr 19, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions bipentropy/bipentropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@

import hmac
import hashlib
import base58
from mnemonic import Mnemonic as bip39
from pycoin.symbols.btc import network as BTC
from pycoin.encoding.bytes32 import from_bytes_32, to_bytes_32


class BIPEntropy(object):

def __get_k_from_node(self, node):
return to_bytes_32(node.secret_exponent())

def __derive_k(self, path, xprv):
path = path.replace("m/", "").replace("'", "p")
node = xprv.subkey_for_path(path)
return self.__get_k_from_node(node)

def __hmac_sha512(self, message_k):
return hmac.new(message_k, msg=b'bip-entropy-from-k', digestmod=hashlib.sha512).digest()
Copy link
Owner

@ethankosakovsky ethankosakovsky Apr 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. The format of the python function is hmac.new(key, msg, digest), where key is a shared key or salt, and message is the payload you are hashing like a password etc. See the python reference https://docs.python.org/3/library/hmac.html#hmac.new

And examples in the wild in pycoin, python-mnemonic, and Bitcoin Core here and here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, okay.


def bip39_mnemonic_to_entropy(self, path, mnemonic, passphrase=''):
bip39_seed = bip39.to_seed(mnemonic, passphrase=passphrase)
xprv = BTC.keys.bip32_seed(bip39_seed)
Expand All @@ -38,26 +50,29 @@ def bip32_xprv_to_entropy(self, path, xprv_string):
raise ValueError('ERROR: Invalid xprv')
return self.__hmac_sha512(self.__derive_k(path, xprv))

def entropy_from_wif(self, wif):
return self.__hmac_sha512(self.__get_k_from_wif(wif))
def bip32_xprv_to_xprv(self, index, xprv_string):
# app_no = 32 => XPRV to XPRV
path = "83696968p/32p/{index}p".format(index=index)
node = BTC.parse.bip32_prv(xprv_string).subkey_for_path(path)

def entropy_to_wif(self, entropy):
return BTC.keys.private(secret_exponent=int(entropy[:32].hex(), 16)).wif()
# if API to pycoin hadn't been shitcoined:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wins the internet today.

# return BIP32Node(node.chain_code(), secret_exponent=node.secret_exponent()).hwif()

def entropy_to_bip39(self, entropy, words, language='english'):
bits = (words - 1) * 11 // 8 + 1
m = bip39(language)
return m.to_mnemonic(entropy[:bits])
node._depth = 0
node._parent_fingerprint = bytes(4)
node._child_index = 0

def __get_k_from_wif(self, wif):
return base58.b58decode(wif)[1:-5]
return node.hwif(as_private=True)

def __decorate_path(self, path):
return path.replace("m/", "").replace("'", "p")
def entropy_from_wif(self, wif):
node = BTC.keys.from_text(wif)
return self.__hmac_sha512(self.__get_k_from_node(node))

def __derive_k(self, path, xprv):
child_wif = xprv.subkey_for_path(self.__decorate_path(path)).wif()
return self.__get_k_from_wif(child_wif)
def entropy_to_wif(self, entropy):
return BTC.keys.private(secret_exponent=from_bytes_32(entropy[:32])).wif()

def __hmac_sha512(self, message_k):
return hmac.new(b'bip-entropy-from-k', message_k, digestmod=hashlib.sha512).digest()
def entropy_to_bip39(self, entropy, words, language='english'):
width = (words - 1) * 11 // 8 + 1
assert 16 <= width <= 32
m = bip39(language)
return m.to_mnemonic(entropy[:width])
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
base58
pycoin
mnemonic
pytest
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def read(*path):
description="Implementation of Bitcoin BIP Entropy",
long_description=read("README.md"),
url="https://github.com/ethankosakovsky/bipentropy",
install_requires=["mnemonic", "base58", "pycoin"],
install_requires=["mnemonic", "pycoin"],
zip_safe=False,
classifiers=[
"License :: OSI Approved :: MIT License",
Expand Down
128 changes: 64 additions & 64 deletions test_entropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,71 +20,71 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from bipentropy import bipentropy
import unittest

class EntropyTest(unittest.TestCase):
def test_mnemonic(self):
e = bipentropy.BIPEntropy()
mnemonic = 'install scatter logic circle pencil average fall shoe quantum disease suspect usage'
test = e.bip39_mnemonic_to_entropy("m/83696968'/0'/0'", mnemonic)
expected = 'efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7'
self.assertEqual(test.hex(), expected)

# with password
test = e.bip39_mnemonic_to_entropy("m/83696968'/0'/0'", mnemonic, 'TREZOR')
expected = 'd24cee04c61c4a47751658d078ae9b0cc9550fe43eee643d5c10ac2e3f5edbca757b2bd74d55ff5bcc2b1608d567053660d9c7447ae1eb84b6619282fd391844'
self.assertEqual(test.hex(), expected)

def test_xprv(self):
e = bipentropy.BIPEntropy()
xprv = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'
test = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", xprv)
expected = 'efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7'
self.assertEqual(test.hex(), expected)

def test_entropy_to_mnemonic(self):
e = bipentropy.BIPEntropy()
xprv = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'
entropy = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", xprv)

words12 = 'useful guitar veteran zone perfect october explain grant clarify december flight recycle'
words15 = 'useful guitar veteran zone perfect october explain grant clarify december flight raw banana estate uncle'
words24 = 'useful guitar veteran zone perfect october explain grant clarify december flight raw banana estate unfair grow search witness echo market primary alley forward boring'

self.assertEqual(e.entropy_to_bip39(entropy, 12), words12)
self.assertEqual(e.entropy_to_bip39(entropy, 15), words15)
self.assertEqual(e.entropy_to_bip39(entropy, 24), words24)

def test_wif_from_entropy(self):
e = bipentropy.BIPEntropy()
xprv = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'
entropy = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", xprv)
self.assertEqual(e.entropy_to_wif(entropy), 'L5G6UFMvJaFt1KPvupEtT8TUN2YrFnQJm1LA2nEczWrR7MuoxB1Z')

def test_applications(self):
e = bipentropy.BIPEntropy()
xprv = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'
entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/12'/0'", xprv)
self.assertEqual(entropy[:16].hex(), '6250b68daf746d12a24d58b4787a714b')
self.assertEqual(e.entropy_to_bip39(entropy, 12),
'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose')

entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/18'/0'", xprv)
self.assertEqual(entropy[:24].hex(), '938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc')
self.assertEqual(e.entropy_to_bip39(entropy, 18),
'near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token')

entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/24'/0'", xprv)
self.assertEqual(entropy[:32].hex(), 'ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f')
self.assertEqual(e.entropy_to_bip39(entropy, 24),
'puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano')


def __main__():
unittest.main()

import pytest

XPRV = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'

def test_mnemonic():
e = bipentropy.BIPEntropy()
mnemonic = 'install scatter logic circle pencil average fall shoe quantum disease suspect usage'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to revert the test cases because hmac param order is flipped incorrectly elsewhere in this PR here

test = e.bip39_mnemonic_to_entropy("m/83696968'/0'/0'", mnemonic)
expected = '71356cdf2f6851c0499b47fe16ef121883f8c58e7d6fb4c33a9017df71f3b0fe21f92d043ee4f19676384a3d943554904caca131269dcb84151ecb2f7ca31902'
assert test.hex() == expected

# with password
test = e.bip39_mnemonic_to_entropy("m/83696968'/0'/0'", mnemonic, 'TREZOR')
expected = '5c56a2a19ebe8f86c4cbb788dd264bd96387dac2047dac799c51fb6218da0513da44bc0f4603815cc8c8c456dbd3aae79e334fb19dbeffc43fc236d58368ebdb'
assert test.hex() == expected

def test_xprv_to_entropy():
e = bipentropy.BIPEntropy()
test = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", XPRV)
expected = '71356cdf2f6851c0499b47fe16ef121883f8c58e7d6fb4c33a9017df71f3b0fe21f92d043ee4f19676384a3d943554904caca131269dcb84151ecb2f7ca31902'
assert test.hex() == expected


def test_entropy_to_mnemonic():
e = bipentropy.BIPEntropy()
entropy = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", XPRV)

words12 = 'illness problem daughter gain lunar then chapter happy wrap resist setup corn'
assert e.entropy_to_bip39(entropy, 12) == words12

words15 = 'illness problem daughter gain lunar then chapter happy wrap resist setup country display glare debate'
assert e.entropy_to_bip39(entropy, 15) == words15

words24 = 'illness problem daughter gain lunar then chapter happy wrap resist setup country display glare delay pupil regular border piano cook warfare what sentence supreme'
assert e.entropy_to_bip39(entropy, 24) == words24

def test_wif_from_entropy():
# PDG: not sure about this?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested this specific case against Coldcard, that's all.

e = bipentropy.BIPEntropy()
entropy = e.bip32_xprv_to_entropy("m/83696968'/0'/0'", XPRV)
assert e.entropy_to_wif(entropy) == 'L11mqGbaozsMYDHS7dfZ2bPGL2viSH6zHr69MKwvpxuw7cCR4M1u'

def test_applications():
e = bipentropy.BIPEntropy()
entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/12'/0'", XPRV)
assert entropy[:16].hex() == 'f0337580e36fd50ef8734cd9dcfb9a78'
assert e.entropy_to_bip39(entropy, 12) == \
'usual option gasp short wool manual tide hat supply treat track valve'

entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/18'/0'", XPRV)
assert entropy[:24].hex() == '60529dbbf2707ea89e4cd41f7e26fcebf2b492b9a99c5e95'
assert e.entropy_to_bip39(entropy, 18) == \
'gate neutral humble top among february junior once buyer van sand subject clip enable trade crime future protect'

entropy = e.bip32_xprv_to_entropy("m/83696968'/39'/0'/24'/0'", XPRV)
assert entropy[:32].hex() == '5166983339d6fc685abe49162327ac2e915fcc17132dad7a2b1e8f324b2f06bd'
assert e.entropy_to_bip39(entropy, 24) == \
'fabric crumble art inhale hurt crouch helmet since bike bomb twelve frog bicycle toward fox grant pulp spend sibling bunker caution nurse brain prison'

def test_xprv_application():
e = bipentropy.BIPEntropy()
result = e.bip32_xprv_to_xprv(0, XPRV)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would definitely prefer the library does not use fixed paths. The underlying is reason for these particular functions is the transformation, and if I want to use an arbitrary path, I shouldn't have to rewrite the underlying function to do so. You can always write a wrapper API layer around this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, the purpose of demo code like this is to fix the paths. It's the embodiment of the BIP.

assert result == 'xprv9s21ZrQH143K3KJoGoKpsDsWdDNDBKs1wqFymBpCGJtrYXrfKzykGDBadZq5SrNde22F83X9qhFZr4uyV9TptTgLqCBc6XFN9tssphdxVeg'

if __name__ == "__main__":
__main__()
pytest.main()