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

Fixing inconsistency between generated entropy value type and the expected HDWallet.entropy value type #101

Merged
merged 17 commits into from Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
682cc9b
UPDATE. ignoring common IDE directories
daehan-koreapool Oct 17, 2022
5ecc142
UPDATE. ensuring entropy value stays as a string value for consistency
daehan-koreapool Oct 17, 2022
692682e
FIX. correctly converting bytearray into hex decoded string value
daehan-koreapool Oct 17, 2022
d6838b3
UPDATE. explicitly requiring from_entropy() method to have a string d…
daehan-koreapool Oct 17, 2022
679d533
ADD. adding a testcase for creating a HDWallet & a reward address dir…
daehan-koreapool Oct 17, 2022
ff44736
UPDATE. only ignoring bech32.py module from coverage report
daehan-koreapool Oct 17, 2022
2c2bbeb
ADD. porting over MAINNET address tests from Emurgo's cardano-seriali…
daehan-koreapool Oct 17, 2022
eec2609
ADD. adding a new phony handle to ease running a single test case
daehan-koreapool Oct 17, 2022
1929601
ADD. explicitly specifying all phony handles
daehan-koreapool Oct 17, 2022
870bc06
UPDATE. replacing print statements with logging statements
daehan-koreapool Oct 17, 2022
4879627
ADD. adding more test cases to increase test coverage
daehan-koreapool Oct 17, 2022
4900fce
REFACTOR. Enhancing HDWallet derivation UX by supporting chained exec…
daehan-koreapool Oct 17, 2022
11e0f0d
FIX. ensuring private/public root keys are passed to the child wallet
daehan-koreapool Oct 17, 2022
612029b
ADD. passing root_chain_code down to derived HDWallet instances
daehan-koreapool Oct 17, 2022
1aff88a
REFACTOR. pulling out frequently used hard-coded supported mnemonic l…
daehan-koreapool Oct 19, 2022
cedda97
UPDATE. simplifying is_mnemonic() nested loops by supporting early br…
daehan-koreapool Oct 19, 2022
899d3d9
ADD. adding more testcases for bip32 module coverage
daehan-koreapool Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coveragerc
@@ -1,7 +1,7 @@
[run]
branch = True
omit =
pycardano/crypto/*
pycardano/crypto/bech32.py

[report]
# Regexes for lines to exclude from consideration
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,8 @@
.coverage
cov_html
docs/build
dist
dist

# IDE
.idea
.code
5 changes: 4 additions & 1 deletion Makefile
@@ -1,4 +1,4 @@
.PHONY: clean clean-test clean-pyc clean-build format test help docs
.PHONY: cov cov-html clean clean-test clean-pyc clean-build qa format test test-single help docs
.DEFAULT_GOAL := help

define BROWSER_PYSCRIPT
Expand Down Expand Up @@ -57,6 +57,9 @@ clean-test: ## remove test and coverage artifacts
test: ## runs tests
poetry run pytest -s -vv -n 4

test-single: ## runs tests with "single" markers
poetry run pytest -s -vv -m single

qa: ## runs static analysis with flake8
poetry run flake8 pycardano

Expand Down
156 changes: 77 additions & 79 deletions pycardano/crypto/bip32.py
Expand Up @@ -16,9 +16,23 @@
from mnemonic import Mnemonic
from nacl import bindings

from pycardano.logging import logger

__all__ = ["BIP32ED25519PrivateKey", "BIP32ED25519PublicKey", "HDWallet"]


SUPPORTED_MNEMONIC_LANGS = {
"english",
"french",
"italian",
"japanese",
"chinese_simplified",
"chinese_traditional",
"korean",
"spanish",
}


class BIP32ED25519PrivateKey:
def __init__(self, private_key: bytes, chain_code: bytes):
self.private_key = private_key
Expand Down Expand Up @@ -109,6 +123,9 @@ def from_seed(

Args:
seed: Master key of 96 bytes from seed hex string.
entropy: Entropy hex string, default to ``None``.
passphrase: Mnemonic passphrase or password, default to ``None``.
mnemonic: Mnemonic words, default to ``None``.

Returns:
HDWallet -- Hierarchical Deterministic Wallet instance.
Expand Down Expand Up @@ -152,28 +169,18 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet:
raise ValueError("Invalid mnemonic words.")

mnemonic = unicodedata.normalize("NFKD", mnemonic)
passphrase = str(passphrase) if passphrase else ""
daehan-koreapool marked this conversation as resolved.
Show resolved Hide resolved
entropy = Mnemonic(language="english").to_entropy(words=mnemonic)

seed = bytearray(
hashlib.pbkdf2_hmac(
"sha512",
password=passphrase.encode(),
salt=entropy,
iterations=4096,
dklen=96,
)
)
seed = cls._generate_seed(passphrase, entropy)

return cls.from_seed(
seed=hexlify(seed).decode(),
mnemonic=mnemonic,
entropy=entropy,
entropy=hexlify(entropy).decode("utf-8"),
passphrase=passphrase,
)

@classmethod
def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet:
def from_entropy(cls, entropy: str, passphrase: str = "") -> HDWallet:
daehan-koreapool marked this conversation as resolved.
Show resolved Hide resolved
"""
Create master key and HDWallet from Mnemonic words.

Expand All @@ -188,12 +195,20 @@ def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet:
if not cls.is_entropy(entropy):
raise ValueError("Invalid entropy")

seed = bytearray(
seed = cls._generate_seed(passphrase, bytearray.fromhex(entropy))
return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy)

@classmethod
def _generate_seed(cls, passphrase: str, entropy: bytearray) -> bytearray:
return bytearray(
hashlib.pbkdf2_hmac(
"sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96
"sha512",
password=passphrase.encode(),
salt=entropy,
iterations=4096,
dklen=96,
)
)
return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy)

@classmethod
def _tweak_bits(cls, seed: bytearray) -> bytes:
Expand Down Expand Up @@ -264,28 +279,26 @@ def derive_from_path(self, path: str, private: bool = True) -> HDWallet:
)

derived_hdwallet = self._copy_hdwallet()

for index in path.lstrip("m/").split("/"):
if index.endswith("'"):
derived_hdwallet = self.derive_from_index(
derived_hdwallet, int(index[:-1]), private=private, hardened=True
derived_hdwallet = derived_hdwallet.derive(
int(index[:-1]), private=private, hardened=True
)
else:
derived_hdwallet = self.derive_from_index(
derived_hdwallet, int(index), private=private, hardened=False
derived_hdwallet = derived_hdwallet.derive(
int(index), private=private, hardened=False
)

return derived_hdwallet

def derive_from_index(
def derive(
self,
parent_wallet: HDWallet,
index: int,
private: bool = True,
hardened: bool = False,
) -> HDWallet:
"""
Derive keys from index.
Returns a new HDWallet derived from given index.

Args:
index: Derivation index.
Expand All @@ -298,12 +311,12 @@ def derive_from_index(
Examples:
>>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice"
>>> hdwallet = HDWallet.from_mnemonic(mnemonic_words)
>>> hdwallet_l1 = hdwallet.derive_from_index(parent_wallet=hdwallet, index=1852, hardened=True)
>>> hdwallet_l2 = hdwallet.derive_from_index(parent_wallet=hdwallet_l1, index=1815, hardened=True)
>>> hdwallet_l3 = hdwallet.derive_from_index(parent_wallet=hdwallet_l2, index=0, hardened=True)
>>> hdwallet_l4 = hdwallet.derive_from_index(parent_wallet=hdwallet_l3, index=0)
>>> hdwallet_l5 = hdwallet.derive_from_index(parent_wallet=hdwallet_l4, index=0)
>>> hdwallet_l5.public_key.hex()
>>> hdwallet = hdwallet.derive(index=1852, hardened=True)
>>> hdwallet = hdwallet.derive(index=1815, hardened=True)
>>> hdwallet = hdwallet.derive(index=0, hardened=True)
>>> hdwallet = hdwallet.derive(index=0)
>>> hdwallet = hdwallet.derive(index=0)
>>> hdwallet.public_key.hex()
'73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d'
"""

Expand All @@ -319,19 +332,19 @@ def derive_from_index(
# derive private child key
if private:
node = (
parent_wallet._xprivate_key[:32],
parent_wallet._xprivate_key[32:],
parent_wallet._public_key,
parent_wallet._chain_code,
parent_wallet._path,
self._xprivate_key[:32],
self._xprivate_key[32:],
self._public_key,
self._chain_code,
self._path,
)
derived_hdwallet = self._derive_private_child_key_by_index(node, index)
# derive public child key
else:
node = (
parent_wallet._public_key,
parent_wallet._chain_code,
parent_wallet._path,
self._public_key,
self._chain_code,
self._path,
)
derived_hdwallet = self._derive_public_child_key_by_index(node, index)

Expand Down Expand Up @@ -416,7 +429,13 @@ def _derive_private_child_key_by_index(
path += "/" + str(index)

derived_hdwallet = HDWallet(
xprivate_key=kL + kR, public_key=A, chain_code=c, path=path
xprivate_key=kL + kR,
public_key=A,
chain_code=c,
path=path,
root_xprivate_key=self.root_xprivate_key,
root_public_key=self.root_public_key,
root_chain_code=self.root_chain_code,
)

return derived_hdwallet
Expand Down Expand Up @@ -469,7 +488,14 @@ def _derive_public_child_key_by_index(
# compute path
path += "/" + str(index)

derived_hdwallet = HDWallet(public_key=A, chain_code=c, path=path)
derived_hdwallet = HDWallet(
public_key=A,
chain_code=c,
path=path,
root_xprivate_key=self.root_xprivate_key,
root_public_key=self.root_public_key,
root_chain_code=self.root_chain_code,
)

return derived_hdwallet

Expand Down Expand Up @@ -510,16 +536,7 @@ def generate_mnemonic(language: str = "english", strength: int = 256) -> str:
mnemonic (str): mnemonic words.
"""

if language and language not in [
"english",
"french",
"italian",
"japanese",
"chinese_simplified",
"chinese_traditional",
"korean",
"spanish",
]:
if language and language not in SUPPORTED_MNEMONIC_LANGS:
raise ValueError(
"invalid language, use only this options english, french, "
"italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages."
Expand All @@ -545,42 +562,22 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool:
bool. Whether the input mnemonic words is valid.
"""

if language and language not in [
"english",
"french",
"italian",
"japanese",
"chinese_simplified",
"chinese_traditional",
"korean",
"spanish",
]:
if language and language not in SUPPORTED_MNEMONIC_LANGS:
raise ValueError(
"invalid language, use only this options english, french, "
"italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages."
)
try:
mnemonic = unicodedata.normalize("NFKD", mnemonic)
if language is None:
for _language in [
"english",
"french",
"italian",
"chinese_simplified",
"chinese_traditional",
"japanese",
"korean",
"spanish",
]:
valid = False
if Mnemonic(language=_language).check(mnemonic=mnemonic) is True:
valid = True
break
return valid
else:
if language:
return Mnemonic(language=language).check(mnemonic=mnemonic)

for _language in SUPPORTED_MNEMONIC_LANGS:
if Mnemonic(language=_language).check(mnemonic=mnemonic) is True:
return True
return False
except ValueError:
print(
logger.warning(
"The input mnemonic words are not valid. Words should be in string format seperated by space."
)

Expand All @@ -599,4 +596,5 @@ def is_entropy(entropy: str) -> bool:
try:
return len(unhexlify(entropy)) in [16, 20, 24, 28, 32]
except ValueError:
print("The input entropy is not valid.")
logger.warning("The input entropy is not valid.")
return False