Browse Source

slip39: implement extendable backups

master
Ondřej Vejpustek 2 years ago
parent
commit
70f0ed992f
  1. 119
      electrum/slip39.py
  2. 99
      tests/slip39-vectors.json
  3. 5
      tests/test_mnemonic.py
  4. 119
      tests/test_wallet_vertical.py

119
electrum/slip39.py

@ -63,11 +63,19 @@ def _xor(a: bytes, b: bytes) -> bytes:
_ID_LENGTH_BITS = 15
"""The length of the random identifier in bits."""
_ITERATION_EXP_LENGTH_BITS = 5
_ITERATION_EXP_LENGTH_BITS = 4
"""The length of the iteration exponent in bits."""
_ID_EXP_LENGTH_WORDS = _bits_to_words(_ID_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS)
"""The length of the random identifier and iteration exponent in words."""
_EXTENDABLE_BACKUP_FLAG_LENGTH_BITS = 1
"""The length of the extendable backup flag in bits."""
_ID_EXP_LENGTH_WORDS = _bits_to_words(
_ID_LENGTH_BITS + _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS
)
"""The length of the random identifier, extendable backup flag and iteration exponent in words."""
_INDEX_LENGTH_BITS = 4
"""The length of the group index, group threshold, group count, and member index in bits."""
_CHECKSUM_LENGTH_WORDS = 3
"""The length of the RS1024 checksum in words."""
@ -75,8 +83,11 @@ _CHECKSUM_LENGTH_WORDS = 3
_DIGEST_LENGTH_BYTES = 4
"""The length of the digest of the shared secret in bytes."""
_CUSTOMIZATION_STRING = b"shamir"
"""The customization string used in the RS1024 checksum and in the PBKDF2 salt."""
_CUSTOMIZATION_STRING_NON_EXTENDABLE = b"shamir"
"""The customization string used in the RS1024 checksum and in the PBKDF2 salt when extendable backup flag is not set."""
_CUSTOMIZATION_STRING_EXTENDABLE = b"shamir_extendable"
"""The customization string used in the RS1024 checksum when extendable backup flag is set."""
_GROUP_PREFIX_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 1
"""The length of the prefix of the mnemonic that is common to a share group."""
@ -120,6 +131,7 @@ class Share:
def __init__(
self,
identifier: int,
extendable_backup_flag: bool,
iteration_exponent: int,
group_index: int,
group_threshold: int,
@ -130,6 +142,7 @@ class Share:
):
self.index = None
self.identifier = identifier
self.extendable_backup_flag = extendable_backup_flag
self.iteration_exponent = iteration_exponent
self.group_index = group_index
self.group_threshold = group_threshold
@ -142,6 +155,7 @@ class Share:
"""Return the values that uniquely identify a matching set of shares."""
return (
self.identifier,
self.extendable_backup_flag,
self.iteration_exponent,
self.group_threshold,
self.group_count,
@ -153,8 +167,15 @@ class EncryptedSeed:
Represents the encrypted master seed for BIP-32.
"""
def __init__(self, identifier: int, iteration_exponent: int, encrypted_master_secret: bytes):
def __init__(
self,
identifier: int,
extendable_backup_flag: bool,
iteration_exponent: int,
encrypted_master_secret: bytes,
):
self.identifier = identifier
self.extendable_backup_flag = extendable_backup_flag
self.iteration_exponent = iteration_exponent
self.encrypted_master_secret = encrypted_master_secret
@ -169,7 +190,7 @@ class EncryptedSeed:
ems_len = len(self.encrypted_master_secret)
l = self.encrypted_master_secret[: ems_len // 2]
r = self.encrypted_master_secret[ems_len // 2 :]
salt = _get_salt(self.identifier)
salt = _get_salt(self.identifier, self.extendable_backup_flag)
for i in reversed(range(_ROUND_COUNT)):
(l, r) = (
r,
@ -190,6 +211,7 @@ def recover_ems(mnemonics: List[str]) -> EncryptedSeed:
(
identifier,
extendable_backup_flag,
iteration_exponent,
group_threshold,
group_count,
@ -212,7 +234,9 @@ def recover_ems(mnemonics: List[str]) -> EncryptedSeed:
]
encrypted_master_secret = _recover_secret(group_threshold, group_shares)
return EncryptedSeed(identifier, iteration_exponent, encrypted_master_secret)
return EncryptedSeed(
identifier, extendable_backup_flag, iteration_exponent, encrypted_master_secret
)
def decode_mnemonic(mnemonic: str) -> Share:
@ -227,12 +251,19 @@ def decode_mnemonic(mnemonic: str) -> Share:
if padding_len > 8:
raise Slip39Error(_('Invalid length.'))
if not _rs1024_verify_checksum(mnemonic_data):
idExpExtInt = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
identifier = idExpExtInt >> (
_EXTENDABLE_BACKUP_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS
)
extendable_backup_flag = bool(
(idExpExtInt >> _ITERATION_EXP_LENGTH_BITS)
& ((1 << _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS) - 1)
)
iteration_exponent = idExpExtInt & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
if not _rs1024_verify_checksum(mnemonic_data, extendable_backup_flag):
raise Slip39Error(_('Invalid mnemonic checksum.'))
id_exp_int = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])
identifier = id_exp_int >> _ITERATION_EXP_LENGTH_BITS
iteration_exponent = id_exp_int & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)
tmp = _int_from_indices(
mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]
)
@ -242,7 +273,7 @@ def decode_mnemonic(mnemonic: str) -> Share:
group_count,
member_index,
member_threshold,
) = _int_to_indices(tmp, 5, 4)
) = _int_to_indices(tmp, 5, _INDEX_LENGTH_BITS)
value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]
if group_count < group_threshold:
@ -256,6 +287,7 @@ def decode_mnemonic(mnemonic: str) -> Share:
return Share(
identifier,
extendable_backup_flag,
iteration_exponent,
group_index,
group_threshold + 1,
@ -314,6 +346,7 @@ def process_mnemonics(mnemonics: List[str]) -> Tuple[Optional[EncryptedSeed], st
groups_completed += 1
identifier = shares[0].identifier
extendable_backup_flag = shares[0].extendable_backup_flag
iteration_exponent = shares[0].iteration_exponent
group_threshold = shares[0].group_threshold
group_count = shares[0].group_count
@ -323,7 +356,14 @@ def process_mnemonics(mnemonics: List[str]) -> Tuple[Optional[EncryptedSeed], st
status += ":<br/>"
for group_index in range(group_count):
group_prefix = _make_group_prefix(identifier, iteration_exponent, group_index, group_threshold, group_count)
group_prefix = _make_group_prefix(
identifier,
extendable_backup_flag,
iteration_exponent,
group_index,
group_threshold,
group_count,
)
status += _group_status(groups[group_index], group_prefix)
if groups_completed >= group_threshold:
@ -350,16 +390,25 @@ _EMPTY = '<span style="color:red;">&#x2715;</span>'
_INPROGRESS = '<span style="color:orange;">&#x26ab;</span>'
_ERROR_STYLE = '<span style="color:red; font-weight:bold;">' + _('Error') + ': %s</span>'
def _make_group_prefix(identifier, iteration_exponent, group_index, group_threshold, group_count):
def _make_group_prefix(
identifier,
extendable_backup_flag,
iteration_exponent,
group_index,
group_threshold,
group_count,
):
wordlist = get_wordlist()
val = identifier
val <<= _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS
val += int(extendable_backup_flag)
val <<= _ITERATION_EXP_LENGTH_BITS
val += iteration_exponent
val <<= 4
val <<= _INDEX_LENGTH_BITS
val += group_index
val <<= 4
val <<= _INDEX_LENGTH_BITS
val += group_threshold - 1
val <<= 4
val <<= _INDEX_LENGTH_BITS
val += group_count - 1
val >>= 2
prefix = ' '.join(wordlist[idx] for idx in _int_to_indices(val, _GROUP_PREFIX_LENGTH_WORDS, _RADIX_BITS))
@ -413,6 +462,13 @@ def _mnemonic_to_indices(mnemonic: str) -> List[int]:
"""
def _get_customization_string(extendable_backup_flag: bool) -> bytes:
if extendable_backup_flag:
return _CUSTOMIZATION_STRING_EXTENDABLE
else:
return _CUSTOMIZATION_STRING_NON_EXTENDABLE
def _rs1024_polymod(values: Indices) -> int:
GEN = (
0xE0E040,
@ -435,11 +491,14 @@ def _rs1024_polymod(values: Indices) -> int:
return chk
def _rs1024_verify_checksum(data: Indices) -> bool:
def _rs1024_verify_checksum(data: Indices, extendable_backup_flag: bool) -> bool:
"""
Verifies a checksum of the given mnemonic, which was already parsed into Indices.
"""
return _rs1024_polymod(tuple(_CUSTOMIZATION_STRING) + data) == 1
return (
_rs1024_polymod(tuple(_get_customization_string(extendable_backup_flag)) + data)
== 1
)
"""
@ -532,10 +591,13 @@ def _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) ->
)
def _get_salt(identifier: int) -> bytes:
return _CUSTOMIZATION_STRING + identifier.to_bytes(
_bits_to_bytes(_ID_LENGTH_BITS), "big"
)
def _get_salt(identifier: int, extendable_backup_flag: bool) -> bytes:
if extendable_backup_flag:
return bytes()
else:
return _CUSTOMIZATION_STRING_NON_EXTENDABLE + identifier.to_bytes(
_bits_to_bytes(_ID_LENGTH_BITS), "big"
)
def _create_digest(random_data: bytes, shared_secret: bytes) -> bytes:
@ -562,6 +624,7 @@ def _decode_mnemonics(
mnemonics: List[str],
) -> Tuple[int, int, int, int, MnemonicGroups]:
identifiers = set()
extendable_backup_flags = set()
iteration_exponents = set()
group_thresholds = set()
group_counts = set()
@ -571,6 +634,7 @@ def _decode_mnemonics(
for mnemonic in mnemonics:
share = decode_mnemonic(mnemonic)
identifiers.add(share.identifier)
extendable_backup_flags.add(share.extendable_backup_flag)
iteration_exponents.add(share.iteration_exponent)
group_thresholds.add(share.group_threshold)
group_counts.add(share.group_count)
@ -581,7 +645,11 @@ def _decode_mnemonics(
)
group[1].add((share.member_index, share.share_value))
if len(identifiers) != 1 or len(iteration_exponents) != 1:
if (
len(identifiers) != 1
or len(extendable_backup_flags) != 1
or len(iteration_exponents) != 1
):
raise Slip39Error(
"Invalid set of mnemonics. All mnemonics must begin with the same {} words.".format(
_ID_EXP_LENGTH_WORDS
@ -606,6 +674,7 @@ def _decode_mnemonics(
return (
identifiers.pop(),
extendable_backup_flags.pop(),
iteration_exponents.pop(),
group_thresholds.pop(),
group_counts.pop(),

99
tests/slip39-vectors.json

@ -4,13 +4,15 @@
[
"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard"
],
"bb54aac4b89dc868ba37d9cc21b2cece"
"bb54aac4b89dc868ba37d9cc21b2cece",
"xprv9s21ZrQH143K4QViKpwKCpS2zVbz8GrZgpEchMDg6KME9HZtjfL7iThE9w5muQA4YPHKN1u5VM1w8D4pvnjxa2BmpGMfXr7hnRrRHZ93awZ"
],
[
"2. Mnemonic with invalid checksum (128 bits)",
[
"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney"
],
"",
""
],
[
@ -18,6 +20,7 @@
[
"duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness"
],
"",
""
],
[
@ -26,13 +29,15 @@
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed",
"shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking"
],
"b43ceb7e57a0ea8766221624d01b0864"
"b43ceb7e57a0ea8766221624d01b0864",
"xprv9s21ZrQH143K2nNuAbfWPHBtfiSCS14XQgb3otW4pX655q58EEZeC8zmjEUwucBu9dPnxdpbZLCn57yx45RBkwJHnwHFjZK4XPJ8SyeYjYg"
],
[
"5. Basic sharing 2-of-3 (128 bits)",
[
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed"
],
"",
""
],
[
@ -41,6 +46,7 @@
"adequate smoking academic acid debut wine petition glen cluster slow rhyme slow simple epidemic rumor junk tracks treat olympic tolerate",
"adequate stay academic agency agency formal party ting frequent learn upstairs remember smear leaf damage anatomy ladle market hush corner"
],
"",
""
],
[
@ -49,6 +55,7 @@
"peasant leaves academic acid desert exact olympic math alive axle trial tackle drug deny decent smear dominant desert bucket remind",
"peasant leader academic agency cultural blessing percent network envelope medal junk primary human pumps jacket fragment payroll ticket evoke voice"
],
"",
""
],
[
@ -58,6 +65,7 @@
"liberty category beard email beyond should fancy romp founder easel pink holy hairy romp loyalty material victim owner toxic custody",
"liberty category academic easy being hazard crush diminish oral lizard reaction cluster force dilemma deploy force club veteran expect photo"
],
"",
""
],
[
@ -66,6 +74,7 @@
"average senior academic leaf broken teacher expect surface hour capture obesity desire negative dynamic dominant pistol mineral mailman iris aide",
"average senior academic agency curious pants blimp spew clothes slice script dress wrap firm shaft regular slavery negative theater roster"
],
"",
""
],
[
@ -75,6 +84,7 @@
"music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow",
"music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce"
],
"",
""
],
[
@ -83,6 +93,7 @@
"device stay academic always dive coal antenna adult black exceed stadium herald advance soldier busy dryer daughter evaluate minister laser",
"device stay academic always dwarf afraid robin gravity crunch adjust soul branch walnut coastal dream costume scholar mortgage mountain pumps"
],
"",
""
],
[
@ -91,6 +102,7 @@
"hour painting academic academic device formal evoke guitar random modern justice filter withdraw trouble identify mailman insect general cover oven",
"hour painting academic agency artist again daisy capital beaver fiber much enjoy suitable symbolic identify photo editor romp float echo"
],
"",
""
],
[
@ -99,6 +111,7 @@
"guilt walnut academic acid deliver remove equip listen vampire tactics nylon rhythm failure husband fatigue alive blind enemy teaspoon rebound",
"guilt walnut academic agency brave hamster hobo declare herd taste alpha slim criminal mild arcade formal romp branch pink ambition"
],
"",
""
],
[
@ -106,6 +119,7 @@
[
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice"
],
"",
""
],
[
@ -114,6 +128,7 @@
"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join",
"eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter"
],
"",
""
],
[
@ -122,6 +137,7 @@
"eraser senior decision shadow artist work morning estate greatest pipeline plan ting petition forget hormone flexible general goat admit surface",
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice"
],
"",
""
],
[
@ -133,7 +149,8 @@
"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate",
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
],
"7c3397a292a5941682d7a4ae2d898d11"
"7c3397a292a5941682d7a4ae2d898d11",
"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV"
],
[
"18. Threshold number of groups and members in each group (128 bits, case 2)",
@ -142,7 +159,8 @@
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join"
],
"7c3397a292a5941682d7a4ae2d898d11"
"7c3397a292a5941682d7a4ae2d898d11",
"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV"
],
[
"19. Threshold number of groups and members in each group (128 bits, case 3)",
@ -150,20 +168,23 @@
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
"eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market"
],
"7c3397a292a5941682d7a4ae2d898d11"
"7c3397a292a5941682d7a4ae2d898d11",
"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV"
],
[
"20. Valid mnemonic without sharing (256 bits)",
[
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck"
],
"989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92"
"989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92",
"xprv9s21ZrQH143K41mrxxMT2FpiheQ9MFNmWVK4tvX2s28KLZAhuXWskJCKVRQprq9TnjzzzEYePpt764csiCxTt22xwGPiRmUjYUUdjaut8RM"
],
[
"21. Mnemonic with invalid checksum (256 bits)",
[
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect lunar"
],
"",
""
],
[
@ -171,6 +192,7 @@
[
"theory painting academic academic campus sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips facility obtain sister"
],
"",
""
],
[
@ -179,13 +201,15 @@
"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap",
"humidity disease academic agency actress jacket gross physics cylinder solution fake mortgage benefit public busy prepare sharp friar change work slow purchase ruler again tricycle involve viral wireless mixture anatomy desert cargo upgrade"
],
"c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae"
"c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae",
"xprv9s21ZrQH143K3a4GRMgK8WnawupkwkP6gyHxRsXnMsYPTPH21fWwNcAytijtfyftqNfiaY8LgQVdBQvHZ9FBvtwdjC7LCYxjYruJFuLzyMQ"
],
[
"24. Basic sharing 2-of-3 (256 bits)",
[
"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap"
],
"",
""
],
[
@ -194,6 +218,7 @@
"smear husband academic acid deadline scene venture distance dive overall parking bracelet elevator justice echo burning oven chest duke nylon",
"smear isolate academic agency alpha mandate decorate burden recover guard exercise fatal force syndrome fumes thank guest drift dramatic mule"
],
"",
""
],
[
@ -202,6 +227,7 @@
"finger trash academic acid average priority dish revenue academic hospital spirit western ocean fact calcium syndrome greatest plan losing dictate",
"finger traffic academic agency building lilac deny paces subject threaten diploma eclipse window unknown health slim piece dragon focus smirk"
],
"",
""
],
[
@ -211,6 +237,7 @@
"flavor pink beard email diet teaspoon freshman identify document rebound cricket prune headset loyalty smell emission skin often square rebound",
"flavor pink academic easy credit cage raisin crazy closet lobe mobile become drink human tactics valuable hand capture sympathy finger"
],
"",
""
],
[
@ -219,6 +246,7 @@
"column flea academic leaf debut extra surface slow timber husky lawsuit game behavior husky swimming already paper episode tricycle scroll",
"column flea academic agency blessing garbage party software stadium verify silent umbrella therapy decorate chemical erode dramatic eclipse replace apart"
],
"",
""
],
[
@ -228,6 +256,7 @@
"smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit",
"smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful"
],
"",
""
],
[
@ -236,6 +265,7 @@
"fishing recover academic always device craft trend snapshot gums skin downtown watch device sniff hour clock public maximum garlic born",
"fishing recover academic always aircraft view software cradle fangs amazing package plastic evaluate intend penalty epidemic anatomy quarter cage apart"
],
"",
""
],
[
@ -244,6 +274,7 @@
"evoke garden academic academic answer wolf scandal modern warmth station devote emerald market physics surface formal amazing aquatic gesture medical",
"evoke garden academic agency deal revenue knit reunion decrease magazine flexible company goat repair alarm military facility clogs aide mandate"
],
"",
""
],
[
@ -252,6 +283,7 @@
"river deal academic acid average forbid pistol peanut custody bike class aunt hairy merit valid flexible learn ajar very easel",
"river deal academic agency camera amuse lungs numb isolate display smear piece traffic worthy year patrol crush fact fancy emission"
],
"",
""
],
[
@ -259,6 +291,7 @@
[
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium"
],
"",
""
],
[
@ -267,6 +300,7 @@
"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen",
"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install"
],
"",
""
],
[
@ -275,6 +309,7 @@
"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club",
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium"
],
"",
""
],
[
@ -286,7 +321,8 @@
"wildlife deal ceramic snake agree voter main lecture axis kitchen physics arcade velvet spine idea scroll promise platform firm sharp patrol divorce ancestor fantasy forbid goat ajar believe swimming cowboy symbolic plastic spelling",
"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club"
],
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b",
"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c"
],
[
"37. Threshold number of groups and members in each group (256 bits, case 2)",
@ -295,7 +331,8 @@
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install"
],
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b",
"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c"
],
[
"38. Threshold number of groups and members in each group (256 bits, case 3)",
@ -303,13 +340,15 @@
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs"
],
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b",
"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c"
],
[
"39. Mnemonic with insufficient length",
[
"junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder"
],
"",
""
],
[
@ -317,6 +356,7 @@
[
"fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter"
],
"",
""
],
[
@ -326,6 +366,41 @@
"herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace",
"herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult"
],
"ad6f2ad8b59bbbaa01369b9006208d9a"
"ad6f2ad8b59bbbaa01369b9006208d9a",
"xprv9s21ZrQH143K2R4HJxcG1eUsudvHM753BZ9vaGkpYCoeEhCQx147C5qEcupPHxcXYfdYMwJmsKXrHDhtEwutxTTvFzdDCZVQwHneeQH8ioH"
],
[
"42. Valid extendable mnemonic without sharing (128 bits)",
[
"testify swimming academic academic column loyalty smear include exotic bedroom exotic wrist lobe cover grief golden smart junior estimate learn"
],
"1679b4516e0ee5954351d288a838f45e",
"xprv9s21ZrQH143K2w6eTpQnB73CU8Qrhg6gN3D66Jr16n5uorwoV7CwxQ5DofRPyok5DyRg4Q3BfHfCgJFk3boNRPPt1vEW1ENj2QckzVLQFXu"
],
[
"43. Extendable basic sharing 2-of-3 (128 bits)",
[
"enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish",
"enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce"
],
"48b1a4b80b8c209ad42c33672bdaa428",
"xprv9s21ZrQH143K4FS1qQdXYAFVAHiSAnjj21YAKGh2CqUPJ2yQhMmYGT4e5a2tyGLiVsRgTEvajXkxhg92zJ8zmWZas9LguQWz7WZShfJg6RS"
],
[
"44. Valid extendable mnemonic without sharing (256 bits)",
[
"impulse calcium academic academic alcohol sugar lyrics pajamas column facility finance tension extend space birthday rainbow swimming purple syndrome facility trial warn duration snapshot shadow hormone rhyme public spine counter easy hawk album"
],
"8340611602fe91af634a5f4608377b5235fa2d757c51d720c0c7656249a3035f",
"xprv9s21ZrQH143K2yJ7S8bXMiGqp1fySH8RLeFQKQmqfmmLTRwWmAYkpUcWz6M42oGoFMJRENmvsGQmunWTdizsi8v8fku8gpbVvYSiCYJTF1Y"
],
[
"45. Extendable basic sharing 2-of-3 (256 bits)",
[
"western apart academic always artist resident briefing sugar woman oven coding club ajar merit pecan answer prisoner artist fraction amount desktop mild false necklace muscle photo wealthy alpha category unwrap spew losing making",
"western apart academic acid answer ancient auction flip image penalty oasis beaver multiple thunder problem switch alive heat inherit superior teaspoon explain blanket pencil numb lend punish endless aunt garlic humidity kidney observe"
],
"8dc652d6d6cd370d8c963141f6d79ba440300f25c467302c1d966bff8f62300d",
"xprv9s21ZrQH143K2eFW2zmu3aayWWd6MJZBG7RebW35fiKcoCZ6jFi6U5gzffB9McDdiKTecUtRqJH9GzueCXiQK1LaQXdgthS8DgWfC8Uu3z7"
]
]
]

5
tests/test_mnemonic.py

@ -247,7 +247,7 @@ class Test_slip39(ElectrumTestCase):
test_vector_file = os.path.join(os.path.dirname(__file__), "slip39-vectors.json")
with open(test_vector_file, "r") as f:
vectors = json.load(f)
for description, mnemonics, expected_secret in vectors:
for description, mnemonics, expected_secret, extended_private_key in vectors:
if expected_secret:
encrypted_seed = slip39.recover_ems(mnemonics)
assert bytes.fromhex(expected_secret) == encrypted_seed.decrypt("TREZOR"), 'Incorrect secret for test vector "{}".'.format(description)
@ -257,3 +257,6 @@ class Test_slip39(ElectrumTestCase):
self.fail(
'Failed to raise exception for test vector "{}".'.format(description)
)
def test_make_group_prefix(self):
self.assertEqual(slip39._make_group_prefix(5, 0, 4, 3, 2, 1), "academic cover decision")

119
tests/test_wallet_vertical.py

@ -497,7 +497,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3tsrkg3gs')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_basic_3of6_bip44_standard(self, mock_save_db):
async def test_slip39_non_extendable_basic_3of6_bip44_standard(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
xprv9s21ZrQH143K2pMWi8jrTawHaj16uKk4CSbvo4Zt61tcrmuUDMx2o1Byzcr3saXNGNvHP8zZgXVdJHsXVdzYFPavxvCyaGyGr1WkAYG83ce
@ -524,7 +524,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '1Aw4wpXsAyEHSgMZqPdyewoAtJqH9Jaso3')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_basic_2of5_bip49_p2sh_segwit(self, mock_save_db):
async def test_slip39_non_extendable_basic_2of5_bip49_p2sh_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
xprv9s21ZrQH143K2o6EXEHpVy8TCYoMmkBnDCCESLdR2ieKwmcNG48ck2XJQY4waS7RUQcXqR9N7HnQbUVEDMWYyREdF1idQqxFHuCfK7fqFni
@ -550,7 +550,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '3FVvdRhR7racZhmcvrGAqX9eJoP8Sw3ypp')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_groups_128bit_bip84_native_segwit(self, mock_save_db):
async def test_slip39_non_extendable_groups_128bit_bip84_native_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV
@ -580,7 +580,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1q8l6hcvlczu4mtjcnlwhczw7vdxnvwccpjl3cwz')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_groups_256bit_bip49_p2sh_segwit(self, mock_save_db):
async def test_slip39_non_extendable_groups_256bit_bip49_p2sh_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c
@ -607,6 +607,117 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_receiving_addresses()[0], '3FoqkcrEHgkKQ3iXStantygCetRGSRMMNE')
self.assertEqual(w.get_change_addresses()[0], '32tvTmBLfLofu8ps4SWpUJC4fS699jiWvC')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_extendable_basic_3of6_bip44_standard(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
xprv9yba7duYBT5g7SbaN1oCX43xeDtjKXNUZ2uSmJ3efHsWYaLkqzdjg2bjLYYzQ9rmXdNzDHYWXv5m9aBCqbFbZzAoGcAceH1K8cPYVDpsJLH
"""
mnemonics = [
"judicial dramatic academic agree craft physics memory born prize academic black listen elder station premium dance sympathy flip always kitchen",
"judicial dramatic academic arcade clogs timber taught recover burning judicial desktop square ecology budget nervous overall tidy knife fused knit",
"judicial dramatic academic axle destroy justice username elegant filter seafood device ranked behavior pecan infant lunar answer identify hour enjoy",
]
encrypted_seed = slip39.recover_ems(mnemonics)
root_seed = encrypted_seed.decrypt('TREZOR')
self.assertEqual("255415e2b20ad13cef7adca1e336eaec", root_seed.hex())
ks = keystore.from_bip43_rootseed(root_seed, derivation="m/44'/0'/0'")
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'xprv9yba7duYBT5g7SbaN1oCX43xeDtjKXNUZ2uSmJ3efHsWYaLkqzdjg2bjLYYzQ9rmXdNzDHYWXv5m9aBCqbFbZzAoGcAceH1K8cPYVDpsJLH')
self.assertEqual(ks.xpub, 'xpub6CavX9SS1pdyKvg3U3LCtBzhCFjDiz6KvFq3ZgTGDdQVRNfuPXwzDpvDBqbg1kEsDgEeHo6uWeYsZWALRejoJMVCq4rprrHkbw8Jyu3uaMb')
w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)
self.assertEqual(w.txin_type, 'p2pkh')
self.assertEqual(w.get_receiving_addresses()[0], '1N4hqJRTVqUbwT5WCbbsQSwKRPPPzG1TSo')
self.assertEqual(w.get_change_addresses()[0], '1FW3QQzbYRSUoNDDYGWPvSCoom8fBhPC9k')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_extendable_basic_2of5_bip49_p2sh_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
yprvAJP391MZiYGpkDnSkAfHBGrEKNxpkFVbx9hap59M2hxD1i7kmnaBUC2yo8tzz5AwxSv3ekJRrSGYWA8ec7XmQGLvX4xkWwCRqiadT5fuTfh
"""
mnemonics = [
"station type academic acid away gather venture pupal speak treat ruler pecan soldier cowboy paces wavy review similar born moment",
"station type academic aquatic bundle mineral twice temple miracle ruin earth olympic system dining inform alive branch false easy manual",
]
encrypted_seed = slip39.recover_ems(mnemonics)
root_seed = encrypted_seed.decrypt('TREZOR')
ks = keystore.from_bip43_rootseed(root_seed, derivation="m/49'/0'/0'")
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'yprvAJP391MZiYGpkDnSkAfHBGrEKNxpkFVbx9hap59M2hxD1i7kmnaBUC2yo8tzz5AwxSv3ekJRrSGYWA8ec7XmQGLvX4xkWwCRqiadT5fuTfh')
self.assertEqual(ks.xpub, 'ypub6XNPYWtTYuq7xhrurCCHYQnxsQoK9iDTKNdBcTYxb3VBtWSuKKtS1zMTeQTDeVe1Y8mzGue1oDYyvjczspnPznLmyruzxVTU785W2QpbTW9')
w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)
self.assertEqual(w.txin_type, 'p2wpkh-p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '38diDMcH7japAtpJjVKviBroQfTdvgpdqX')
self.assertEqual(w.get_change_addresses()[0], '36Hd2PnEvJpN9pUdhpZWh3aQccbRp46FVc')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_extendable_groups_128bit_bip84_native_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
zprvAe6okUFoH5tieuTJJxN84xjPCvWkhFiiP87myHqTNmfux4wY8XnLG7DxezL5Dt2jXu5FrsMc4wEPhAJovAGhH1cAPjmkhh3KcSCMRyuQghd
"""
# SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)
mnemonics = [
"fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else",
"fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory",
"fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps",
"fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture",
]
encrypted_seed = slip39.recover_ems(mnemonics)
root_seed = encrypted_seed.decrypt('TREZOR')
ks = keystore.from_bip43_rootseed(root_seed, derivation="m/84'/0'/0'")
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'zprvAe6okUFoH5tieuTJJxN84xjPCvWkhFiiP87myHqTNmfux4wY8XnLG7DxezL5Dt2jXu5FrsMc4wEPhAJovAGhH1cAPjmkhh3KcSCMRyuQghd')
self.assertEqual(ks.xpub, 'zpub6s6A9ynh7TT1sPXmQyu8S6g7kxMF6iSZkM3NmgF4w7CtpsGgg56aouYSWHgAoMy186a8FRT8zkmhcwV5SWKFFQfMpvV8C9Ft4woWSzD5sXz')
w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)
self.assertEqual(w.txin_type, 'p2wpkh')
self.assertEqual(w.get_receiving_addresses()[0], 'bc1qs2svwhfz47qv9qju2waa6prxzv5f522fc4p06t')
self.assertEqual(w.get_change_addresses()[0], 'bc1qmjq5nenac3vjwltldk5qsq4yd8mttw2dpkmx06')
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_slip39_extendable_groups_256bit_bip49_p2sh_segwit(self, mock_save_db):
"""
BIP32 Root Key for passphrase "TREZOR":
yprvAJbhup8ey3hmPhgVsXKySTS54BfywUZR6SvQ2jrjdsUgNd4P8B5HR7ute93zXVTXKUvrmvnav1spLzEkDuT7Cy3bf3hWtYoH6A5p8vNzbEC
"""
# SLIP39 shares (256 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6):
mnemonics = [
"smart surprise acrobat romp deal omit pupal capacity invasion should glen smear segment frost surprise ancestor plan frost cultural herd",
"smart surprise beard romp closet antenna pencil rapids goat artwork race industry segment parcel briefing glad voice camera priority satoshi",
]
encrypted_seed = slip39.recover_ems(mnemonics)
root_seed = encrypted_seed.decrypt('TREZOR')
ks = keystore.from_bip43_rootseed(root_seed, derivation="m/49'/0'/0'")
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
self.assertEqual(ks.xprv, 'yprvAJbhup8ey3hmPhgVsXKySTS54BfywUZR6SvQ2jrjdsUgNd4P8B5HR7ute93zXVTXKUvrmvnav1spLzEkDuT7Cy3bf3hWtYoH6A5p8vNzbEC')
self.assertEqual(ks.xpub, 'ypub6Xb4KKfYoRG4cBkxyYryobNocDWULwHGTfqzq8GMCD1fFRPXfiPXxvENVQYVbi64BJzdPnPUiJ4iY37X5BA594dqxyE4FwccHdhydU9RhPJ')
w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)
self.assertEqual(w.txin_type, 'p2wpkh-p2sh')
self.assertEqual(w.get_receiving_addresses()[0], '3JDN4wF5BphZqcJFFYuDA7N1apzfPYyJLG')
self.assertEqual(w.get_change_addresses()[0], '3J8zNvhJndqzBcuPuarzUn1kWs9N4ZY7HS')
class TestWalletKeystoreAddressIntegrityForTestnet(ElectrumTestCase):
TESTNET = True

Loading…
Cancel
Save