diff --git a/docs/en/generate_keypair.rst b/docs/en/generate_keypair.rst index a6fd9040..fcbdd910 100644 --- a/docs/en/generate_keypair.rst +++ b/docs/en/generate_keypair.rst @@ -62,7 +62,7 @@ Vou can also generate a mnemonic phrase and later use it to generate a keypair: print(f"Public Key: {keypair.public_key}") print(f"Secret Seed: {keypair.secret}") -Lastly, you can also use the Shamir secret shamir method to split a mnemonic +Lastly, you can also use the Shamir secret sharing method to split a mnemonic phrase into multiple phrases. In the following example, we need exactly 2 phrases in order to reconstruct the secret: @@ -76,3 +76,21 @@ phrases in order to reconstruct the secret: keypair = Keypair.from_shamir_mnemonic_phrases(mnemonic_phrases[:2]) # any combinations print(f"Public Key: {keypair.public_key}") print(f"Secret Seed: {keypair.secret}") + +If you want to convert an existing mnemonic phrase to Shamir, you need to get +the corresponding entropy. You can use these lower level functions: + +.. code-block:: python + :linenos: + + import shamir_mnemonic + from stellar_sdk.sep.mnemonic import StellarMnemonic + + seed_raw = StellarMnemonic("english").to_entropy(mnemonic) + mnemonic_phrases = shamir_mnemonic.generate_mnemonics( + group_threshold=1, + groups=[(2, 3)], + master_secret=seed_raw, + passphrase=passphrase.encode(), + )[0] + print(f"Mnemonic phrases: {mnemonic_phrases}") diff --git a/stellar_sdk/keypair.py b/stellar_sdk/keypair.py index 3e6eefbf..a4e46262 100644 --- a/stellar_sdk/keypair.py +++ b/stellar_sdk/keypair.py @@ -259,7 +259,10 @@ def from_mnemonic_phrase( @staticmethod def generate_shamir_mnemonic_phrases( - member_threshold: int, member_count: int, passphrase: str = "" + member_threshold: int, + member_count: int, + passphrase: str = "", + strength: int = 256, ) -> List[str]: """Generate mnemonic phrases using Shamir secret sharing method. @@ -270,6 +273,10 @@ def generate_shamir_mnemonic_phrases( :param member_threshold: Number of members required to reconstruct the secret key. :param member_count: Number of shares the secret is split into. :param passphrase: An optional passphrase used to decrypt the secret key. + :param strength: The complexity of the mnemonics in terms of bites, its possible + value is ``128``, ``160``, ``192``, ``224`` and ``256``. + Strengths of ``128`` and ``256`` lead respectively to + shares with 20 and 33 words. :return: A list of mnemonic phrases. """ try: @@ -278,12 +285,19 @@ def generate_shamir_mnemonic_phrases( message = "shamir_mnemonic must be installed to use method `generate_shamir_mnemonic_phrases`." raise ModuleNotFoundError(message) from exc - secrets = Keypair.random().secret.encode() + # it can be a multiple of 16, one can use a higher entropy if they want + # still for simplicity in the public API we show common values + if strength % 16 != 0: + raise ValueError( + f"Strength should be one of the following (128, 160, 192, 224, 256), but it is not ({strength})." + ) + + entropy = os.urandom(strength // 8) try: phrases = shamir_mnemonic.generate_mnemonics( group_threshold=1, groups=[(member_threshold, member_count)], - master_secret=secrets, + master_secret=entropy, passphrase=passphrase.encode(), )[0] except shamir_mnemonic.utils.MnemonicError as exc: @@ -314,12 +328,14 @@ def from_shamir_mnemonic_phrases( raise ModuleNotFoundError(message) from exc try: + # Shamir -> entropy main_seed = shamir_mnemonic.combine_mnemonics( mnemonics=mnemonic_phrases, passphrase=passphrase.encode() ) except shamir_mnemonic.utils.MnemonicError as exc: raise ValueError(exc) from exc + # Entropy -> SLIP-10 -> ED25519 derived_seed = StellarMnemonic.derive(seed=main_seed, index=index) return cls.from_raw_ed25519_seed(derived_seed) diff --git a/stellar_sdk/sep/mnemonic.py b/stellar_sdk/sep/mnemonic.py index 699446cc..fb1908da 100644 --- a/stellar_sdk/sep/mnemonic.py +++ b/stellar_sdk/sep/mnemonic.py @@ -47,6 +47,12 @@ def __init__(self, language: Union[str, Language] = Language.ENGLISH) -> None: super().__init__(language) def to_seed(self, mnemonic: str, passphrase: str = "", index: int = 0) -> bytes: # type: ignore[override] + """Derive an ED25519 key from a mnemonic.""" + bip39_seed = self.to_bip39_seed(mnemonic=mnemonic, passphrase=passphrase) + return self.derive(bip39_seed[:64], index) + + def to_bip39_seed(self, mnemonic: str, passphrase: str = "") -> bytes: + """Derive a BIP-39 key from a mnemonic.""" if not self.check(mnemonic): raise ValueError( "Invalid mnemonic, please check if the mnemonic is correct, " @@ -60,7 +66,7 @@ def to_seed(self, mnemonic: str, passphrase: str = "", index: int = 0) -> bytes: stretched = hashlib.pbkdf2_hmac( "sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS ) - return self.derive(stretched[:64], index) + return stretched def generate(self, strength: int = 128) -> str: if strength not in (128, 160, 192, 224, 256): @@ -71,6 +77,7 @@ def generate(self, strength: int = 128) -> str: @staticmethod def derive(seed: bytes, index: int) -> bytes: + """Derive an ED25519 key from a BIP-39 seed.""" # References https://github.com/satoshilabs/slips/blob/master/slip-0010.md master_hmac = hmac.new(StellarMnemonic.SEED_MODIFIER, digestmod=hashlib.sha512) master_hmac.update(seed) diff --git a/tests/test_keypair.py b/tests/test_keypair.py index b42a0e41..ab12d64a 100644 --- a/tests/test_keypair.py +++ b/tests/test_keypair.py @@ -1,6 +1,7 @@ import itertools import pytest +import shamir_mnemonic from stellar_sdk import Keypair, StrKey from stellar_sdk.exceptions import ( @@ -9,7 +10,240 @@ Ed25519SecretSeedInvalidError, MissingEd25519SecretSeedError, ) -from stellar_sdk.sep.mnemonic import Language +from stellar_sdk.sep.mnemonic import Language, StellarMnemonic + +SEP_5_CASES = ( + { + "mnemonic": "illness spike retreat truth genius clock brain pass fit cave bargain toe", + "passphrase": "", + "accounts": ( + ( + "GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6", + "SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN", + ), + ( + "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", + "SCEPFFWGAG5P2VX5DHIYK3XEMZYLTYWIPWYEKXFHSK25RVMIUNJ7CTIS", + ), + ( + "GAY5PRAHJ2HIYBYCLZXTHID6SPVELOOYH2LBPH3LD4RUMXUW3DOYTLXW", + "SDAILLEZCSA67DUEP3XUPZJ7NYG7KGVRM46XA7K5QWWUIGADUZCZWTJP", + ), + ( + "GAOD5NRAEORFE34G5D4EOSKIJB6V4Z2FGPBCJNQI6MNICVITE6CSYIAE", + "SBMWLNV75BPI2VB4G27RWOMABVRTSSF7352CCYGVELZDSHCXWCYFKXIX", + ), + ( + "GBCUXLFLSL2JE3NWLHAWXQZN6SQC6577YMAU3M3BEMWKYPFWXBSRCWV4", + "SCPCY3CEHMOP2TADSV2ERNNZBNHBGP4V32VGOORIEV6QJLXD5NMCJUXI", + ), + ( + "GBRQY5JFN5UBG5PGOSUOL4M6D7VRMAYU6WW2ZWXBMCKB7GPT3YCBU2XZ", + "SCK27SFHI3WUDOEMJREV7ZJQG34SCBR6YWCE6OLEXUS2VVYTSNGCRS6X", + ), + ( + "GBY27SJVFEWR3DUACNBSMJB6T4ZPR4C7ZXSTHT6GMZUDL23LAM5S2PQX", + "SDJ4WDPOQAJYR3YIAJOJP3E6E4BMRB7VZ4QAEGCP7EYVDW6NQD3LRJMZ", + ), + ( + "GAY7T23Z34DWLSTEAUKVBPHHBUE4E3EMZBAQSLV6ZHS764U3TKUSNJOF", + "SA3HXJUCE2N27TBIZ5JRBLEBF3TLPQEBINP47E6BTMIWW2RJ5UKR2B3L", + ), + ( + "GDJTCF62UUYSAFAVIXHPRBR4AUZV6NYJR75INVDXLLRZLZQ62S44443R", + "SCD5OSHUUC75MSJG44BAT3HFZL2HZMMQ5M4GPDL7KA6HJHV3FLMUJAME", + ), + ( + "GBTVYYDIYWGUQUTKX6ZMLGSZGMTESJYJKJWAATGZGITA25ZB6T5REF44", + "SCJGVMJ66WAUHQHNLMWDFGY2E72QKSI3XGSBYV6BANDFUFE7VY4XNXXR", + ), + ), + }, + { + "mnemonic": "resource asthma orphan phone ice canvas fire useful arch jewel impose vague theory cushion top", + "passphrase": "", + "accounts": ( + ( + "GAVXVW5MCK7Q66RIBWZZKZEDQTRXWCZUP4DIIFXCCENGW2P6W4OA34RH", + "SAKS7I2PNDBE5SJSUSU2XLJ7K5XJ3V3K4UDFAHMSBQYPOKE247VHAGDB", + ), + ( + "GDFCYVCICATX5YPJUDS22KM2GW5QU2KKSPPPT2IC5AQIU6TP3BZSLR5K", + "SAZ2H5GLAVWCUWNPQMB6I3OHRI63T2ACUUAWSH7NAGYYPXGIOPLPW3Q4", + ), + ( + "GAUA3XK3SGEQFNCBM423WIM5WCZ4CR4ZDPDFCYSFLCTODGGGJMPOHAAE", + "SDVSSLPL76I33DKAI4LFTOAKCHJNCXUERGPCMVFT655Z4GRLWM6ZZTSC", + ), + ( + "GAH3S77QXTAPZ77REY6LGFIJ2XWVXFOKXHCFLA6HQTL3POLVZJDHHUDM", + "SCH56YSGOBYVBC6DO3ZI2PY62GBVXT4SEJSXJOBQYGC2GCEZSB5PEVBZ", + ), + ( + "GCSCZVGV2Y3EQ2RATJ7TE6PVWTW5OH5SMG754AF6W6YM3KJF7RMNPB4Y", + "SBWBM73VUNBGBMFD4E2BA7Q756AKVEAAVTQH34RYEUFD6X64VYL5KXQ2", + ), + ( + "GDKWYAJE3W6PWCXDZNMFNFQSPTF6BUDANE6OVRYMJKBYNGL62VKKCNCC", + "SAVS4CDQZI6PSA5DPCC42S5WLKYIPKXPCJSFYY4N3VDK25T2XX2BTGVX", + ), + ( + "GCDTVB4XDLNX22HI5GUWHBXJFBCPB6JNU6ZON7E57FA3LFURS74CWDJH", + "SDFC7WZT3GDQVQUQMXN7TC7UWDW5E3GSMFPHUT2TSTQ7RKWTRA4PLBAL", + ), + ( + "GBTDPL5S4IOUQHDLCZ7I2UXJ2TEHO6DYIQ3F2P5OOP3IS7JSJI4UMHQJ", + "SA6UO2FIYC6AS2MSDECLR6F7NKCJTG67F7R4LV2GYB4HCZYXJZRLPOBB", + ), + ( + "GD3KWA24OIM7V3MZKDAVSLN3NBHGKVURNJ72ZCTAJSDTF7RIGFXPW5FQ", + "SBDNHDDICLLMBIDZ2IF2D3LH44OVUGGAVHQVQ6BZQI5IQO6AB6KNJCOV", + ), + ( + "GB3C6RRQB3V7EPDXEDJCMTS45LVDLSZQ46PTIGKZUY37DXXEOAKJIWSV", + "SDHRG2J34MGDAYHMOVKVJC6LX2QZMCTIKRO5I4JQ6BJQ36KVL6QUTT72", + ), + ), + }, + { + "mnemonic": "bench hurt jump file august wise shallow faculty impulse spring exact slush thunder author capable act festival slice deposit sauce coconut afford frown better", + "passphrase": "", + "accounts": ( + ( + "GC3MMSXBWHL6CPOAVERSJITX7BH76YU252WGLUOM5CJX3E7UCYZBTPJQ", + "SAEWIVK3VLNEJ3WEJRZXQGDAS5NVG2BYSYDFRSH4GKVTS5RXNVED5AX7", + ), + ( + "GB3MTYFXPBZBUINVG72XR7AQ6P2I32CYSXWNRKJ2PV5H5C7EAM5YYISO", + "SBKSABCPDWXDFSZISAVJ5XKVIEWV4M5O3KBRRLSPY3COQI7ZP423FYB4", + ), + ( + "GDYF7GIHS2TRGJ5WW4MZ4ELIUIBINRNYPPAWVQBPLAZXC2JRDI4DGAKU", + "SD5CCQAFRIPB3BWBHQYQ5SC66IB2AVMFNWWPBYGSUXVRZNCIRJ7IHESQ", + ), + ( + "GAFLH7DGM3VXFVUID7JUKSGOYG52ZRAQPZHQASVCEQERYC5I4PPJUWBD", + "SBSGSAIKEF7JYQWQSGXKB4SRHNSKDXTEI33WZDRR6UHYQCQ5I6ZGZQPK", + ), + ( + "GAXG3LWEXWCAWUABRO6SMAEUKJXLB5BBX6J2KMHFRIWKAMDJKCFGS3NN", + "SBIZH53PIRFTPI73JG7QYA3YAINOAT2XMNAUARB3QOWWVZVBAROHGXWM", + ), + ( + "GA6RUD4DZ2NEMAQY4VZJ4C6K6VSEYEJITNSLUQKLCFHJ2JOGC5UCGCFQ", + "SCVM6ZNVRUOP4NMCMMKLTVBEMAF2THIOMHPYSSMPCD2ZU7VDPARQQ6OY", + ), + ( + "GCUDW6ZF5SCGCMS3QUTELZ6LSAH6IVVXNRPRLAUNJ2XYLCA7KH7ZCVQS", + "SBSHUZQNC45IAIRSAHMWJEJ35RY7YNW6SMOEBZHTMMG64NKV7Y52ZEO2", + ), + ( + "GBJ646Q524WGBN5X5NOAPIF5VQCR2WZCN6QZIDOSY6VA2PMHJ2X636G4", + "SC2QO2K2B4EBNBJMBZIKOYSHEX4EZAZNIF4UNLH63AQYV6BE7SMYWC6E", + ), + ( + "GDHX4LU6YBSXGYTR7SX2P4ZYZSN24VXNJBVAFOB2GEBKNN3I54IYSRM4", + "SCGMC5AHAAVB3D4JXQPCORWW37T44XJZUNPEMLRW6DCOEARY3H5MAQST", + ), + ( + "GDXOY6HXPIDT2QD352CH7VWX257PHVFR72COWQ74QE3TEV4PK2KCKZX7", + "SCPA5OX4EYINOPAUEQCPY6TJMYICUS5M7TVXYKWXR3G5ZRAJXY3C37GF", + ), + ), + }, + { + "mnemonic": "cable spray genius state float twenty onion head street palace net private method loan turn phrase state blanket interest dry amazing dress blast tube", + "passphrase": "p4ssphr4se", + "accounts": ( + ( + "GDAHPZ2NSYIIHZXM56Y36SBVTV5QKFIZGYMMBHOU53ETUSWTP62B63EQ", + "SAFWTGXVS7ELMNCXELFWCFZOPMHUZ5LXNBGUVRCY3FHLFPXK4QPXYP2X", + ), + ( + "GDY47CJARRHHL66JH3RJURDYXAMIQ5DMXZLP3TDAUJ6IN2GUOFX4OJOC", + "SBQPDFUGLMWJYEYXFRM5TQX3AX2BR47WKI4FDS7EJQUSEUUVY72MZPJF", + ), + ( + "GCLAQF5H5LGJ2A6ACOMNEHSWYDJ3VKVBUBHDWFGRBEPAVZ56L4D7JJID", + "SAF2LXRW6FOSVQNC4HHIIDURZL4SCGCG7UEGG23ZQG6Q2DKIGMPZV6BZ", + ), + ( + "GBC36J4KG7ZSIQ5UOSJFQNUP4IBRN6LVUFAHQWT2ODEQ7Y3ASWC5ZN3B", + "SDCCVBIYZDMXOR4VPC3IYMIPODNEDZCS44LDN7B5ZWECIE57N3BTV4GQ", + ), + ( + "GA6NHA4KPH5LFYD6LZH35SIX3DU5CWU3GX6GCKPJPPTQCCQPP627E3CB", + "SA5TRXTO7BG2Z6QTQT3O2LC7A7DLZZ2RBTGUNCTG346PLVSSHXPNDVNT", + ), + ( + "GBOWMXTLABFNEWO34UJNSJJNVEF6ESLCNNS36S5SX46UZT2MNYJOLA5L", + "SDEOED2KPHV355YNOLLDLVQB7HDPQVIGKXCAJMA3HTM4325ZHFZSKKUC", + ), + ( + "GBL3F5JUZN3SQKZ7SL4XSXEJI2SNSVGO6WZWNJLG666WOJHNDDLEXTSZ", + "SDYNO6TLFNV3IM6THLNGUG5FII4ET2H7NH3KCT6OAHIUSHKR4XBEEI6A", + ), + ( + "GA5XPPWXL22HFFL5K5CE37CEPUHXYGSP3NNWGM6IK6K4C3EFHZFKSAND", + "SDXMJXAY45W3WEFWMYEPLPIF4CXAD5ECQ37XKMGY5EKLM472SSRJXCYD", + ), + ( + "GDS5I7L7LWFUVSYVAOHXJET2565MGGHJ4VHGVJXIKVKNO5D4JWXIZ3XU", + "SAIZA26BUP55TDCJ4U7I2MSQEAJDPDSZSBKBPWQTD5OQZQSJAGNN2IQB", + ), + ( + "GBOSMFQYKWFDHJWCMCZSMGUMWCZOM4KFMXXS64INDHVCJ2A2JAABCYRR", + "SDXDYPDNRMGOF25AWYYKPHFAD3M54IT7LCLG7RWTGR3TS32A4HTUXNOS", + ), + ), + }, + { + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "passphrase": "", + "accounts": ( + ( + "GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX", + "SBUV3MRWKNS6AYKZ6E6MOUVF2OYMON3MIUASWL3JLY5E3ISDJFELYBRZ", + ), + ( + "GDVSYYTUAJ3ACHTPQNSTQBDQ4LDHQCMNY4FCEQH5TJUMSSLWQSTG42MV", + "SCHDCVCWGAKGIMTORV6K5DYYV3BY4WG3RA4M6MCBGJLHUCWU2MC6DL66", + ), + ( + "GBFPWBTN4AXHPWPTQVQBP4KRZ2YVYYOGRMV2PEYL2OBPPJDP7LECEVHR", + "SAPLVTLUXSDLFRDGCCFLPDZMTCEVMP3ZXTM74EBJCVKZKM34LGQPF7K3", + ), + ( + "GCCCOWAKYVFY5M6SYHOW33TSNC7Z5IBRUEU2XQVVT34CIZU7CXZ4OQ4O", + "SDQYXOP2EAUZP4YOEQ5BUJIQ3RDSP5XV4ZFI6C5Y3QCD5Y63LWPXT7PW", + ), + ( + "GCQ3J35MKPKJX7JDXRHC5YTXTULFMCBMZ5IC63EDR66QA3LO7264ZL7Q", + "SCT7DUHYZD6DRCETT6M73GWKFJI4D56P3SNWNWNJ7ANLJZS6XIFYYXSB", + ), + ( + "GDTA7622ZA5PW7F7JL7NOEFGW62M7GW2GY764EQC2TUJ42YJQE2A3QUL", + "SDTWG5AFDI6GRQNLPWOC7IYS7AKOGMI2GX4OXTBTZHHYPMNZ2PX4ONWU", + ), + ( + "GD7A7EACTPTBCYCURD43IEZXGIBCEXNBHN3OFWV2FOX67XKUIGRCTBNU", + "SDJMWY4KFRS4PTA5WBFVCPS2GKYLXOMCLQSBNEIBG7KRGHNQOM25KMCP", + ), + ( + "GAF4AGPVLQXFKEWQV3DZU5YEFU6YP7XJHAEEQH4G3R664MSF77FLLRK3", + "SDOJH5JRCNGT57QTPTJEQGBEBZJPXE7XUDYDB24VTOPP7PH3ALKHAHFG", + ), + ( + "GABTYCZJMCP55SS6I46SR76IHETZDLG4L37MLZRZKQDGBLS5RMP65TSX", + "SC6N6GYQ2VA4T7CUP2BWGBRT2P6L2HQSZIUNQRHNDLISF6ND7TW4P4ER", + ), + ( + "GAKFARYSPI33KUJE7HYLT47DCX2PFWJ77W3LZMRBPSGPGYPMSDBE7W7X", + "SALJ5LPBTXCFML2CQ7ORP7WJNJOZSVBVRQAAODMVHMUF4P4XXFZB7MKY", + ), + ), + }, +) class TestKeypair: @@ -249,239 +483,7 @@ def test_generate_mnemonic_phrase_invalid_strength_raise(self): Keypair.generate_mnemonic_phrase(strength=strength) def test_from_mnemonic_phrase(self): - cases = ( - { - "mnemonic": "illness spike retreat truth genius clock brain pass fit cave bargain toe", - "passphrase": "", - "accounts": ( - ( - "GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6", - "SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN", - ), - ( - "GBAW5XGWORWVFE2XTJYDTLDHXTY2Q2MO73HYCGB3XMFMQ562Q2W2GJQX", - "SCEPFFWGAG5P2VX5DHIYK3XEMZYLTYWIPWYEKXFHSK25RVMIUNJ7CTIS", - ), - ( - "GAY5PRAHJ2HIYBYCLZXTHID6SPVELOOYH2LBPH3LD4RUMXUW3DOYTLXW", - "SDAILLEZCSA67DUEP3XUPZJ7NYG7KGVRM46XA7K5QWWUIGADUZCZWTJP", - ), - ( - "GAOD5NRAEORFE34G5D4EOSKIJB6V4Z2FGPBCJNQI6MNICVITE6CSYIAE", - "SBMWLNV75BPI2VB4G27RWOMABVRTSSF7352CCYGVELZDSHCXWCYFKXIX", - ), - ( - "GBCUXLFLSL2JE3NWLHAWXQZN6SQC6577YMAU3M3BEMWKYPFWXBSRCWV4", - "SCPCY3CEHMOP2TADSV2ERNNZBNHBGP4V32VGOORIEV6QJLXD5NMCJUXI", - ), - ( - "GBRQY5JFN5UBG5PGOSUOL4M6D7VRMAYU6WW2ZWXBMCKB7GPT3YCBU2XZ", - "SCK27SFHI3WUDOEMJREV7ZJQG34SCBR6YWCE6OLEXUS2VVYTSNGCRS6X", - ), - ( - "GBY27SJVFEWR3DUACNBSMJB6T4ZPR4C7ZXSTHT6GMZUDL23LAM5S2PQX", - "SDJ4WDPOQAJYR3YIAJOJP3E6E4BMRB7VZ4QAEGCP7EYVDW6NQD3LRJMZ", - ), - ( - "GAY7T23Z34DWLSTEAUKVBPHHBUE4E3EMZBAQSLV6ZHS764U3TKUSNJOF", - "SA3HXJUCE2N27TBIZ5JRBLEBF3TLPQEBINP47E6BTMIWW2RJ5UKR2B3L", - ), - ( - "GDJTCF62UUYSAFAVIXHPRBR4AUZV6NYJR75INVDXLLRZLZQ62S44443R", - "SCD5OSHUUC75MSJG44BAT3HFZL2HZMMQ5M4GPDL7KA6HJHV3FLMUJAME", - ), - ( - "GBTVYYDIYWGUQUTKX6ZMLGSZGMTESJYJKJWAATGZGITA25ZB6T5REF44", - "SCJGVMJ66WAUHQHNLMWDFGY2E72QKSI3XGSBYV6BANDFUFE7VY4XNXXR", - ), - ), - }, - { - "mnemonic": "resource asthma orphan phone ice canvas fire useful arch jewel impose vague theory cushion top", - "passphrase": "", - "accounts": ( - ( - "GAVXVW5MCK7Q66RIBWZZKZEDQTRXWCZUP4DIIFXCCENGW2P6W4OA34RH", - "SAKS7I2PNDBE5SJSUSU2XLJ7K5XJ3V3K4UDFAHMSBQYPOKE247VHAGDB", - ), - ( - "GDFCYVCICATX5YPJUDS22KM2GW5QU2KKSPPPT2IC5AQIU6TP3BZSLR5K", - "SAZ2H5GLAVWCUWNPQMB6I3OHRI63T2ACUUAWSH7NAGYYPXGIOPLPW3Q4", - ), - ( - "GAUA3XK3SGEQFNCBM423WIM5WCZ4CR4ZDPDFCYSFLCTODGGGJMPOHAAE", - "SDVSSLPL76I33DKAI4LFTOAKCHJNCXUERGPCMVFT655Z4GRLWM6ZZTSC", - ), - ( - "GAH3S77QXTAPZ77REY6LGFIJ2XWVXFOKXHCFLA6HQTL3POLVZJDHHUDM", - "SCH56YSGOBYVBC6DO3ZI2PY62GBVXT4SEJSXJOBQYGC2GCEZSB5PEVBZ", - ), - ( - "GCSCZVGV2Y3EQ2RATJ7TE6PVWTW5OH5SMG754AF6W6YM3KJF7RMNPB4Y", - "SBWBM73VUNBGBMFD4E2BA7Q756AKVEAAVTQH34RYEUFD6X64VYL5KXQ2", - ), - ( - "GDKWYAJE3W6PWCXDZNMFNFQSPTF6BUDANE6OVRYMJKBYNGL62VKKCNCC", - "SAVS4CDQZI6PSA5DPCC42S5WLKYIPKXPCJSFYY4N3VDK25T2XX2BTGVX", - ), - ( - "GCDTVB4XDLNX22HI5GUWHBXJFBCPB6JNU6ZON7E57FA3LFURS74CWDJH", - "SDFC7WZT3GDQVQUQMXN7TC7UWDW5E3GSMFPHUT2TSTQ7RKWTRA4PLBAL", - ), - ( - "GBTDPL5S4IOUQHDLCZ7I2UXJ2TEHO6DYIQ3F2P5OOP3IS7JSJI4UMHQJ", - "SA6UO2FIYC6AS2MSDECLR6F7NKCJTG67F7R4LV2GYB4HCZYXJZRLPOBB", - ), - ( - "GD3KWA24OIM7V3MZKDAVSLN3NBHGKVURNJ72ZCTAJSDTF7RIGFXPW5FQ", - "SBDNHDDICLLMBIDZ2IF2D3LH44OVUGGAVHQVQ6BZQI5IQO6AB6KNJCOV", - ), - ( - "GB3C6RRQB3V7EPDXEDJCMTS45LVDLSZQ46PTIGKZUY37DXXEOAKJIWSV", - "SDHRG2J34MGDAYHMOVKVJC6LX2QZMCTIKRO5I4JQ6BJQ36KVL6QUTT72", - ), - ), - }, - { - "mnemonic": "bench hurt jump file august wise shallow faculty impulse spring exact slush thunder author capable act festival slice deposit sauce coconut afford frown better", - "passphrase": "", - "accounts": ( - ( - "GC3MMSXBWHL6CPOAVERSJITX7BH76YU252WGLUOM5CJX3E7UCYZBTPJQ", - "SAEWIVK3VLNEJ3WEJRZXQGDAS5NVG2BYSYDFRSH4GKVTS5RXNVED5AX7", - ), - ( - "GB3MTYFXPBZBUINVG72XR7AQ6P2I32CYSXWNRKJ2PV5H5C7EAM5YYISO", - "SBKSABCPDWXDFSZISAVJ5XKVIEWV4M5O3KBRRLSPY3COQI7ZP423FYB4", - ), - ( - "GDYF7GIHS2TRGJ5WW4MZ4ELIUIBINRNYPPAWVQBPLAZXC2JRDI4DGAKU", - "SD5CCQAFRIPB3BWBHQYQ5SC66IB2AVMFNWWPBYGSUXVRZNCIRJ7IHESQ", - ), - ( - "GAFLH7DGM3VXFVUID7JUKSGOYG52ZRAQPZHQASVCEQERYC5I4PPJUWBD", - "SBSGSAIKEF7JYQWQSGXKB4SRHNSKDXTEI33WZDRR6UHYQCQ5I6ZGZQPK", - ), - ( - "GAXG3LWEXWCAWUABRO6SMAEUKJXLB5BBX6J2KMHFRIWKAMDJKCFGS3NN", - "SBIZH53PIRFTPI73JG7QYA3YAINOAT2XMNAUARB3QOWWVZVBAROHGXWM", - ), - ( - "GA6RUD4DZ2NEMAQY4VZJ4C6K6VSEYEJITNSLUQKLCFHJ2JOGC5UCGCFQ", - "SCVM6ZNVRUOP4NMCMMKLTVBEMAF2THIOMHPYSSMPCD2ZU7VDPARQQ6OY", - ), - ( - "GCUDW6ZF5SCGCMS3QUTELZ6LSAH6IVVXNRPRLAUNJ2XYLCA7KH7ZCVQS", - "SBSHUZQNC45IAIRSAHMWJEJ35RY7YNW6SMOEBZHTMMG64NKV7Y52ZEO2", - ), - ( - "GBJ646Q524WGBN5X5NOAPIF5VQCR2WZCN6QZIDOSY6VA2PMHJ2X636G4", - "SC2QO2K2B4EBNBJMBZIKOYSHEX4EZAZNIF4UNLH63AQYV6BE7SMYWC6E", - ), - ( - "GDHX4LU6YBSXGYTR7SX2P4ZYZSN24VXNJBVAFOB2GEBKNN3I54IYSRM4", - "SCGMC5AHAAVB3D4JXQPCORWW37T44XJZUNPEMLRW6DCOEARY3H5MAQST", - ), - ( - "GDXOY6HXPIDT2QD352CH7VWX257PHVFR72COWQ74QE3TEV4PK2KCKZX7", - "SCPA5OX4EYINOPAUEQCPY6TJMYICUS5M7TVXYKWXR3G5ZRAJXY3C37GF", - ), - ), - }, - { - "mnemonic": "cable spray genius state float twenty onion head street palace net private method loan turn phrase state blanket interest dry amazing dress blast tube", - "passphrase": "p4ssphr4se", - "accounts": ( - ( - "GDAHPZ2NSYIIHZXM56Y36SBVTV5QKFIZGYMMBHOU53ETUSWTP62B63EQ", - "SAFWTGXVS7ELMNCXELFWCFZOPMHUZ5LXNBGUVRCY3FHLFPXK4QPXYP2X", - ), - ( - "GDY47CJARRHHL66JH3RJURDYXAMIQ5DMXZLP3TDAUJ6IN2GUOFX4OJOC", - "SBQPDFUGLMWJYEYXFRM5TQX3AX2BR47WKI4FDS7EJQUSEUUVY72MZPJF", - ), - ( - "GCLAQF5H5LGJ2A6ACOMNEHSWYDJ3VKVBUBHDWFGRBEPAVZ56L4D7JJID", - "SAF2LXRW6FOSVQNC4HHIIDURZL4SCGCG7UEGG23ZQG6Q2DKIGMPZV6BZ", - ), - ( - "GBC36J4KG7ZSIQ5UOSJFQNUP4IBRN6LVUFAHQWT2ODEQ7Y3ASWC5ZN3B", - "SDCCVBIYZDMXOR4VPC3IYMIPODNEDZCS44LDN7B5ZWECIE57N3BTV4GQ", - ), - ( - "GA6NHA4KPH5LFYD6LZH35SIX3DU5CWU3GX6GCKPJPPTQCCQPP627E3CB", - "SA5TRXTO7BG2Z6QTQT3O2LC7A7DLZZ2RBTGUNCTG346PLVSSHXPNDVNT", - ), - ( - "GBOWMXTLABFNEWO34UJNSJJNVEF6ESLCNNS36S5SX46UZT2MNYJOLA5L", - "SDEOED2KPHV355YNOLLDLVQB7HDPQVIGKXCAJMA3HTM4325ZHFZSKKUC", - ), - ( - "GBL3F5JUZN3SQKZ7SL4XSXEJI2SNSVGO6WZWNJLG666WOJHNDDLEXTSZ", - "SDYNO6TLFNV3IM6THLNGUG5FII4ET2H7NH3KCT6OAHIUSHKR4XBEEI6A", - ), - ( - "GA5XPPWXL22HFFL5K5CE37CEPUHXYGSP3NNWGM6IK6K4C3EFHZFKSAND", - "SDXMJXAY45W3WEFWMYEPLPIF4CXAD5ECQ37XKMGY5EKLM472SSRJXCYD", - ), - ( - "GDS5I7L7LWFUVSYVAOHXJET2565MGGHJ4VHGVJXIKVKNO5D4JWXIZ3XU", - "SAIZA26BUP55TDCJ4U7I2MSQEAJDPDSZSBKBPWQTD5OQZQSJAGNN2IQB", - ), - ( - "GBOSMFQYKWFDHJWCMCZSMGUMWCZOM4KFMXXS64INDHVCJ2A2JAABCYRR", - "SDXDYPDNRMGOF25AWYYKPHFAD3M54IT7LCLG7RWTGR3TS32A4HTUXNOS", - ), - ), - }, - { - "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - "passphrase": "", - "accounts": ( - ( - "GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX", - "SBUV3MRWKNS6AYKZ6E6MOUVF2OYMON3MIUASWL3JLY5E3ISDJFELYBRZ", - ), - ( - "GDVSYYTUAJ3ACHTPQNSTQBDQ4LDHQCMNY4FCEQH5TJUMSSLWQSTG42MV", - "SCHDCVCWGAKGIMTORV6K5DYYV3BY4WG3RA4M6MCBGJLHUCWU2MC6DL66", - ), - ( - "GBFPWBTN4AXHPWPTQVQBP4KRZ2YVYYOGRMV2PEYL2OBPPJDP7LECEVHR", - "SAPLVTLUXSDLFRDGCCFLPDZMTCEVMP3ZXTM74EBJCVKZKM34LGQPF7K3", - ), - ( - "GCCCOWAKYVFY5M6SYHOW33TSNC7Z5IBRUEU2XQVVT34CIZU7CXZ4OQ4O", - "SDQYXOP2EAUZP4YOEQ5BUJIQ3RDSP5XV4ZFI6C5Y3QCD5Y63LWPXT7PW", - ), - ( - "GCQ3J35MKPKJX7JDXRHC5YTXTULFMCBMZ5IC63EDR66QA3LO7264ZL7Q", - "SCT7DUHYZD6DRCETT6M73GWKFJI4D56P3SNWNWNJ7ANLJZS6XIFYYXSB", - ), - ( - "GDTA7622ZA5PW7F7JL7NOEFGW62M7GW2GY764EQC2TUJ42YJQE2A3QUL", - "SDTWG5AFDI6GRQNLPWOC7IYS7AKOGMI2GX4OXTBTZHHYPMNZ2PX4ONWU", - ), - ( - "GD7A7EACTPTBCYCURD43IEZXGIBCEXNBHN3OFWV2FOX67XKUIGRCTBNU", - "SDJMWY4KFRS4PTA5WBFVCPS2GKYLXOMCLQSBNEIBG7KRGHNQOM25KMCP", - ), - ( - "GAF4AGPVLQXFKEWQV3DZU5YEFU6YP7XJHAEEQH4G3R664MSF77FLLRK3", - "SDOJH5JRCNGT57QTPTJEQGBEBZJPXE7XUDYDB24VTOPP7PH3ALKHAHFG", - ), - ( - "GABTYCZJMCP55SS6I46SR76IHETZDLG4L37MLZRZKQDGBLS5RMP65TSX", - "SC6N6GYQ2VA4T7CUP2BWGBRT2P6L2HQSZIUNQRHNDLISF6ND7TW4P4ER", - ), - ( - "GAKFARYSPI33KUJE7HYLT47DCX2PFWJ77W3LZMRBPSGPGYPMSDBE7W7X", - "SALJ5LPBTXCFML2CQ7ORP7WJNJOZSVBVRQAAODMVHMUF4P4XXFZB7MKY", - ), - ), - }, - ) - for data in cases: + for data in SEP_5_CASES: for i in range(len(data["accounts"])): kp = Keypair.from_mnemonic_phrase( mnemonic_phrase=data["mnemonic"], @@ -575,39 +577,88 @@ def test_raise_from_shamir_mnemonic_phrases(self): Keypair.from_shamir_mnemonic_phrases([shares[0], shares_1]) @pytest.mark.parametrize( - "member_threshold, member_count, passphrase", + "member_threshold, member_count, strength, n_words, passphrase", [ - (1, 1, ""), - (1, 1, "abcde"), - (2, 3, "0"), + (1, 1, 128, 20, ""), + (1, 1, 128, 20, "abcde"), + (2, 3, 256, 33, "0"), + (3, 4, 128, 20, ""), ], ) def test_generate_shamir_mnemonic_phrases( - self, member_threshold, member_count, passphrase + self, member_threshold, member_count, strength, n_words, passphrase ): - Keypair.generate_shamir_mnemonic_phrases( + mnemonic_phrases = Keypair.generate_shamir_mnemonic_phrases( member_threshold=member_threshold, member_count=member_count, passphrase=passphrase, + strength=strength, ) + assert len(mnemonic_phrases) == member_count + for member in mnemonic_phrases: + assert len(member.split(" ")) == n_words + + for perms in itertools.permutations(mnemonic_phrases, member_threshold): + Keypair.from_shamir_mnemonic_phrases( + mnemonic_phrases=perms, passphrase=passphrase + ) + @pytest.mark.parametrize( - "member_threshold, member_count, err_msg", + "member_threshold, member_count, strength, err_msg", [ - (0, 1, "threshold must be a positive"), - (1, 2, "multiple member shares with member threshold 1"), - (2, 1, "threshold must not exceed the number of shares"), - (3, 1000, "shares must not exceed 16"), + (0, 1, 128, "threshold must be a positive"), + (1, 2, 128, "multiple member shares with member threshold 1"), + (2, 1, 128, "threshold must not exceed the number of shares"), + (3, 1000, 128, "shares must not exceed 16"), + (1, 1, 42, "Strength should be"), ], ) def test_raise_generate_shamir_mnemonic_phrases( - self, member_threshold, member_count, err_msg + self, member_threshold, member_count, strength, err_msg ): with pytest.raises(ValueError, match=err_msg): Keypair.generate_shamir_mnemonic_phrases( - member_threshold=member_threshold, member_count=member_count + member_threshold=member_threshold, + member_count=member_count, + strength=strength, ) + def test_shamir_sep5(self): + for case in SEP_5_CASES: + mnemonic, passphrase, accounts = ( + case["mnemonic"], + case["passphrase"], + case["accounts"], + ) + + # Entropy from mnemonic + seed_raw = StellarMnemonic().to_bip39_seed(mnemonic, passphrase=passphrase) + + # Shamir from the entropy + shamir_phrases = shamir_mnemonic.generate_mnemonics( + group_threshold=1, + groups=[(2, 3)], + master_secret=seed_raw, + passphrase=passphrase.encode(), + )[0] + + # consistency checks + for perms in itertools.permutations(shamir_phrases, 2): + for idx in range(10): + reconstructed_kp = Keypair.from_shamir_mnemonic_phrases( + mnemonic_phrases=perms, passphrase=passphrase, index=idx + ) + + kp = Keypair.from_mnemonic_phrase( + mnemonic_phrase=mnemonic, passphrase=passphrase, index=idx + ) + assert reconstructed_kp.public_key == kp.public_key + assert reconstructed_kp.secret == kp.secret + + assert reconstructed_kp.public_key == accounts[idx][0] + assert reconstructed_kp.secret == accounts[idx][1] + def test_xdr_public_key(self): public_key = "GBRF6PKZYP4J4WI2A3NF4CGF23SL34GRKA5LTQZCQFEUT2YJDZO2COXH" kp = Keypair.from_public_key(public_key)