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

Update to support looking up mnemonic by just the first 4 letters of each word #16704

Merged
merged 14 commits into from
Nov 1, 2023
Merged
27 changes: 26 additions & 1 deletion chia/util/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,32 @@ def bytes_to_mnemonic(mnemonic_bytes: bytes) -> str:
return " ".join(mnemonics)


def bytes_from_mnemonic(mnemonic_str: str) -> bytes:
def mnemonic_from_short_words(mnemonic_str: str) -> str:
"""
Since the first 4 letters of each word is unique (or the full word, if less than 4 characters), and its common
practice to only store the first 4 letters of each word in many offline storage solutions, also support looking
up words by the first 4 characters
"""
mnemonic: List[str] = mnemonic_str.split(" ")
if len(mnemonic) not in [12, 15, 18, 21, 24]:
raise ValueError("Invalid mnemonic length")

four_char_dict = {word[: min(4, len(word))]: word for word in bip39_word_list().splitlines()}
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
full_words: List[str] = []
for i in range(0, len(mnemonic)):
word = mnemonic[i]
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
full_word = four_char_dict.get(word[: min(4, len(word))])
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
if not full_word:
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(f"'{word}' is not in the mnemonic dictionary; may be misspelled")
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
full_words.append(full_word)

return " ".join(full_words)


def bytes_from_mnemonic(mnemonic_str: str) -> bytes:
full_mnemonic_str = mnemonic_from_short_words(mnemonic_str)
mnemonic: List[str] = full_mnemonic_str.split(" ")

word_list = {word: i for i, word in enumerate(bip39_word_list().splitlines())}
bit_array = BitArray()
for i in range(0, len(mnemonic)):
Expand Down Expand Up @@ -123,6 +144,10 @@ def mnemonic_to_seed(mnemonic: str) -> bytes:
"""
Uses BIP39 standard to derive a seed from entropy bytes.
"""
# If there are only ASCII characters (as typically expected in a seed phrase), we can check if its just shortened
# 4 letter versions of each word
if not any(ord(c) >= 128 for c in mnemonic):
mnemonic = mnemonic_from_short_words(mnemonic)
salt_str: str = "mnemonic"
salt = unicodedata.normalize("NFKD", salt_str).encode("utf-8")
mnemonic_normalized = unicodedata.normalize("NFKD", mnemonic).encode("utf-8")
Expand Down
23 changes: 23 additions & 0 deletions tests/core/util/test_keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
bytes_from_mnemonic,
bytes_to_mnemonic,
generate_mnemonic,
mnemonic_from_short_words,
mnemonic_to_seed,
)

Expand Down Expand Up @@ -165,6 +166,28 @@ def test_bip39_test_vectors(self):
assert bytes_to_mnemonic(entropy_bytes) == mnemonic
assert mnemonic_to_seed(mnemonic) == seed

def test_bip39_test_vectors_short(self):
"""
Tests that the first 4 letters of each mnemonic phrase matches as if it were the full phrase
"""
with open("tests/util/bip39_test_vectors_short.json") as f:
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
all_vectors = json.loads(f.read())
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved

with open("tests/util/bip39_test_vectors.json") as f:
full_vectors = json.loads(f.read())
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved

for idx, vector_list in enumerate(all_vectors["english"]):
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
entropy_bytes = bytes.fromhex(vector_list[0])
mnemonic = vector_list[1]
seed = bytes.fromhex(vector_list[2])

full_mnemonic = full_vectors["english"][idx][1]

assert mnemonic_from_short_words(mnemonic) == full_mnemonic
assert bytes_from_mnemonic(mnemonic) == entropy_bytes
assert bytes_to_mnemonic(entropy_bytes) == full_mnemonic
assert mnemonic_to_seed(mnemonic) == seed

def test_utf8_nfkd(self):
# Test code from trezor:
# Copyright (c) 2013 Pavol Rusnak
Expand Down
124 changes: 124 additions & 0 deletions tests/util/bip39_test_vectors_short.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"english": [
cmmarslender marked this conversation as resolved.
Show resolved Hide resolved
[
"00000000000000000000000000000000",
"aban aban aban aban aban aban aban aban aban aban aban abou",
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"lega winn than year wave saus wort usef lega winn than yell",
"878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096"
],
[
"80808080808080808080808080808080",
"lett advi cage absu amou doct acou avoi lett advi cage abov",
"77d6be9708c8218738934f84bbbb78a2e048ca007746cb764f0673e4b1812d176bbb173e1a291f31cf633f1d0bad7d3cf071c30e98cd0688b5bcce65ecaceb36"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wron",
"b6a6d8921942dd9806607ebc2750416b289adea669198769f2e15ed926c3aa92bf88ece232317b4ea463e84b0fcd3b53577812ee449ccc448eb45e6f544e25b6"
],
[
"000000000000000000000000000000000000000000000000",
"aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban agen",
"4975bb3d1faf5308c86a30893ee903a976296609db223fd717e227da5a813a34dc1428b71c84a787fc51f3b9f9dc28e9459f48c08bd9578e9d1b170f2d7ea506"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"lega winn than year wave saus wort usef lega winn than year wave saus wort usef lega will",
"b059400ce0f55498a5527667e77048bb482ff6daa16c37b4b9e8af70c85b3f4df588004f19812a1a027c9a51e5e94259a560268e91cd10e206451a129826e740"
],
[
"808080808080808080808080808080808080808080808080",
"lett advi cage absu amou doct acou avoi lett advi cage absu amou doct acou avoi lett alwa",
"04d5f77103510c41d610f7f5fb3f0badc77c377090815cee808ea5d2f264fdfabf7c7ded4be6d4c6d7cdb021ba4c777b0b7e57ca8aa6de15aeb9905dba674d66"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"d2911131a6dda23ac4441d1b66e2113ec6324354523acfa20899a2dcb3087849264e91f8ec5d75355f0f617be15369ffa13c3d18c8156b97cd2618ac693f759f"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban art",
"408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf705489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"lega winn than year wave saus wort usef lega winn than year wave saus wort usef lega winn than year wave saus wort titl",
"761914478ebf6fe16185749372e91549361af22b386de46322cf8b1ba7e92e80c4af05196f742be1e63aab603899842ddadf4e7248d8e43870a4b6ff9bf16324"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"lett advi cage absu amou doct acou avoi lett advi cage absu amou doct acou avoi lett advi cage absu amou doct acou bles",
"848bbe19cad445e46f35fd3d1a89463583ac2b60b5eb4cfcf955731775a5d9e17a81a71613fed83f1ae27b408478fdec2bbc75b5161d1937aa7cdf4ad686ef5f"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"e28a37058c7f5112ec9e16a3437cf363a2572d70b6ceb3b6965447623d620f14d06bb321a26b33ec15fcd84a3b5ddfd5520e230c924c87aaa0d559749e044fef"
],
[
"9e885d952ad362caeb4efe34a8e91bd2",
"ozon dril grab fibe curt grac pudd than crui elde eigh picn",
"e2f88a043776c828063d4c3c97173944d32cf847a925b6e40b0b8bd0b4bead3ba734bdda5250d4698b310a71c9934e1a48e562315ce22bf85f89459df0e73a6c"
],
[
"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
"grav mach nort sort syst fema filt atti volu fold club stay feat offi ecol stab narr fog",
"31736b4f31612ece84404c74a5d3b938a092480eb89c11b81491f1a3657eb2fe50024610fbe814df55913a87ef741020fcf076a75a29aea0aba638126ba4c8bb"
],
[
"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
"hams diag priv dutc caus dela priv meat slid todd razo book happ fanc gosp tenn mapl dile loan word shrug infl dela leng",
"17e4b5661796eeff8904550f8572289317ece7c1cc1316469f8f4c986c1ffd7b9f4c3aeac3e1713ffc21fa33707d09d57a2ece358d72111ef7c7658e7b33f2d5"
],
[
"c0ba5a8e914111210f2bd131f3d5e08d",
"sche spot phot card baby moun devi kick crad pact join borr",
"00e1e39a39e23735adab690d0ffbefda6cacc063b4a5fcc605de060bd370adc54c94370d966b9a3fae5ed16bd58a02224cbefca4146a083951b70be2a2ce3dcc"
],
[
"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
"horn tena knee tale spon spel gate clip puls soap slus warm silv neph swap uncl crac brav",
"e893fc34fa2a78ceaaea78c246b69257ad283fa538d88f3c4520beb618a2062b8ec4920ed1793fff6ad443523ed18c03da433004d0a1e9497e194621607bc9e2"
],
[
"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
"pand eyeb bull gori call smok muff tast mesh disc soft ostr alco spee nati flas devo leve hobb quic inne driv ghos insi",
"3e066d7dee2dbf8fcd3fe240a3975658ca118a8f6f4ca81cf99104944604b05a5090a79d99e545704b914ca0397fedb82fd00fd6a72098703709c891a065ee49"
],
[
"23db8160a31d3e0dca3688ed941adbf3",
"cat swin flag econ stad alon chur spee uniq patc repo trai",
"7ea73b3a398f8a71f7dde589d972b0358d3fa8b9e91317ecc544e42752b1bb251a1926b1f4c69eec0a80c0396aa0f7df29f7d73411d3106eba539f3d584fcdf8"
],
[
"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
"ligh rule cinn wrap dras word prid squi upgr then inco fata apar sust crac supp prou acce",
"d0ca8861283a7124515e825b7a06de8e0ad0dd5ac7888013efe6e3c300d4745bbd2c729f3355d769d23718579e7b735999a8f0b38e22b5bff45c9085af449056"
],
[
"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
"all hour make firs lead exte hole alie behi guar gosp lava path outp cens muse juni mass reop famo sing adva salt refo",
"fc795be0c3f18c50dddb34e72179dc597d64055497ecc1e69e2e56a5409651bc139aae8070d4df0ea14d8d2a518a9a00bb1cc6e92e053fe34051f6821df9164c"
],
[
"f30f8c1da665478f49b001d94c5fc452",
"vess ladd alte erro fede sibl chat abil sun glas valv pict",
"e2d7f9a462875c1325f44f321392edc8eaafebf1547c89d72d10b41b4ee23af3fb0ab010f39f5cbea3b3aa671161b58262b6a508bcbe2d34ee272a942534d45f"
],
[
"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
"scis invi lock mapl supr raw rapi void cong musc digi eleg litt bris hair mang cong clum",
"a555426999448df9022c3afc2ed0e4aebff3a0ac37d8a395f81412e14994efc960ed168d39a80478e0467a3d5cfc134fef2767c1d3a27f18e3afeb11bfc8e6ad"
],
[
"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
"void come effo suff camp surv warr heav shoo prim clut crus open amaz scre patr grou spac poin ten exis slus invo unfo",
"b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c"
]
]
}