diff --git a/mmgen/addr.py b/mmgen/addr.py index 49ab68c8..80b19806 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -783,6 +783,9 @@ def is_bip39_str(s): from mmgen.bip39 import bip39 return bool(bip39.tohex(s.split(),wl_id='bip39')) +def is_xmrseed(s): + return bool(baseconv.tobytes(s.split(),wl_id='xmrseed')) + from collections import namedtuple class PasswordList(AddrList): msgs = { @@ -824,6 +827,7 @@ Record this checksum: it will be used to verify the password file in the future 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128 'bip39': pwinfo(12, 24 ,24, [12,18,24], 'BIP39 mnemonic', is_bip39_str), + 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic', is_xmrseed), 'hex': pwinfo(32, 64 ,64, [32,48,64], 'hexadecimal password', is_hex_str), } chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd) @@ -855,7 +859,7 @@ Record this checksum: it will be used to verify the password file in the future self.al_id = AddrListID(seed.sid,MMGenPasswordType('P')) self.data = self.generate(seed,pw_idxs) - if self.pw_fmt == 'bip39': + if self.pw_fmt in ('bip39','xmrseed'): self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper()) self.num_addrs = len(self.data) @@ -916,7 +920,13 @@ Record this checksum: it will be used to verify the password file in the future elif pf == 'bip39': from mmgen.bip39 import bip39 pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True) - good_pw_len = bip39.seedlen2nwords(len(seed.data),in_bytes=True) + good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True) + elif pf == 'xmrseed': + pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] + try: + good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len] + except: + die(1,'{}: unsupported seed length for Monero new-style mnemonic'.format(seed.byte_len*8)) elif pf in ('b32','b58'): pw_int = (32 if pf == 'b32' else 58) ** self.pw_len pw_bytes = pw_int.bit_length() // 8 @@ -946,6 +956,13 @@ Record this checksum: it will be used to verify the password file in the future pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True) # take most significant part return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39')) + elif self.pw_fmt == 'xmrseed': + pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2 + # take most significant part + bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex]) + from mmgen.protocol import MoneroProtocol + bytes_preproc = MoneroProtocol.preprocess_key(bytes_trunc,None) + return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed')) else: # take least significant part return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:] @@ -953,7 +970,7 @@ Record this checksum: it will be used to verify the password file in the future def check_format(self,pw): if not self.pw_info[self.pw_fmt].chk_func(pw): raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc)) - pwlen = len(pw.split()) if self.pw_fmt == 'bip39' else len(pw) + pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw) if pwlen != self.pw_len: raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len)) return True @@ -975,7 +992,7 @@ Record this checksum: it will be used to verify the password file in the future def get_line(self,lines): self.line_ctr += 1 - if self.pw_fmt == 'bip39': + if self.pw_fmt in ('bip39','xmrseed'): ret = lines.pop(0).split(None,self.pw_len+1) if len(ret) > self.pw_len+1: m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1]) diff --git a/mmgen/baseconv.py b/mmgen/baseconv.py index 4dfa62e2..19f44e6a 100755 --- a/mmgen/baseconv.py +++ b/mmgen/baseconv.py @@ -22,6 +22,7 @@ baseconv.py: base conversion class for the MMGen suite from hashlib import sha256 from mmgen.exception import * +from mmgen.util import die def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58']) def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32']) @@ -38,6 +39,7 @@ class baseconv(object): 'tirosh':('Tirosh mnemonic', 'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet 'mmgen': ('MMGen native mnemonic', 'MMGen native mnemonic seed phrase created using old Electrum wordlist and simple base conversion'), + 'xmrseed': ('Monero mnemonic', 'Monero new-style mnemonic seed phrase'), } # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet # https://tools.ietf.org/html/rfc4648 @@ -52,6 +54,7 @@ class baseconv(object): mn_base = 1626 # tirosh list is 1633 words long! wl_chksums = { 'mmgen': '5ca31424', + 'xmrseed':'3c381ebb', 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) # 'tirosh1633': '1a5faeff' } @@ -59,11 +62,13 @@ class baseconv(object): 'b58': { 16:22, 24:33, 32:44 }, 'b6d': { 16:50, 24:75, 32:100 }, 'mmgen': { 16:12, 24:18, 32:24 }, + 'xmrseed': { 32:25 }, } seedlen_map_rev = { 'b58': { 22:16, 33:24, 44:32 }, 'b6d': { 50:16, 75:24, 100:32 }, 'mmgen': { 12:16, 18:24, 24:32 }, + 'xmrseed': { 25:32 }, } @classmethod @@ -73,6 +78,9 @@ class baseconv(object): if mn_id == 'mmgen': from mmgen.mn_electrum import words cls.digits[mn_id] = words + elif mn_id == 'xmrseed': + from mmgen.mn_monero import words + cls.digits[mn_id] = words elif mn_id == 'tirosh': from mmgen.mn_tirosh import words cls.digits[mn_id] = words[:cls.mn_base] @@ -127,6 +135,12 @@ class baseconv(object): m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)" raise BaseConversionPadError(m.format(pad)) + @staticmethod + def monero_mn_checksum(words): + from binascii import crc32 + wstr = ''.join(word[:3] for word in words) + return words[crc32(wstr.encode()) % len(words)] + @classmethod def tohex(cls,words_arg,wl_id,pad=None): "convert string or list data of base 'wl_id' to hex string" @@ -161,6 +175,21 @@ class baseconv(object): m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format' raise BaseConversionError(m.format(w=words_arg,d=desc)) + if wl_id == 'xmrseed': + if len(words) not in cls.seedlen_map_rev['xmrseed']: + die(2,'{}: invalid length for Monero mnemonic'.format(len(words))) + + z = cls.monero_mn_checksum(words[:-1]) + assert z == words[-1],'{!r}: invalid Monero checksum (should be {!r})'.format(words[-1],z) + words = tuple(words[:-1]) + + ret = b'' + for i in range(len(words)//3): + w1,w2,w3 = [wl.index(w) for w in words[3*i:3*i+3]] + x = w1 + base*((w2-w1)%base) + base*base*((w3-w2)%base) + ret += x.to_bytes(4,'big')[::-1] + return ret + ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))]) bl = ret.bit_length() return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big') @@ -198,10 +227,26 @@ class baseconv(object): wl = cls.digits[wl_id] base = len(wl) - num = int.from_bytes(bytestr,'big') - ret = [] - while num: - ret.append(num % base) - num //= base - o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] - return ''.join(o) if tostr else o + if wl_id == 'xmrseed': + if len(bytestr) not in cls.seedlen_map['xmrseed']: + die(2,'{}: invalid seed byte length for Monero mnemonic'.format(len(bytestr))) + + def num2base_monero(num): + w1 = num % base + w2 = (num//base + w1) % base + w3 = (num//base//base + w2) % base + return [wl[w1], wl[w2], wl[w3]] + + o = [] + for i in range(len(bytestr)//4): + o += num2base_monero(int.from_bytes(bytestr[i*4:i*4+4][::-1],'big')) + o.append(cls.monero_mn_checksum(o)) + else: + num = int.from_bytes(bytestr,'big') + ret = [] + while num: + ret.append(num % base) + num //= base + o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] + + return (' ' if wl_id in ('mmgen','xmrseed') else '').join(o) if tostr else o diff --git a/mmgen/mn_monero.py b/mmgen/mn_monero.py new file mode 100755 index 00000000..2450d677 --- /dev/null +++ b/mmgen/mn_monero.py @@ -0,0 +1,1632 @@ +#!/usr/bin/env python3 +# +# Source: https://github.com/monero-project/monero/blob/master/src/mnemonics/english.h + +words = tuple(""" +abbey +abducts +ability +ablaze +abnormal +abort +abrasive +absorb +abyss +academy +aces +aching +acidic +acoustic +acquire +across +actress +acumen +adapt +addicted +adept +adhesive +adjust +adopt +adrenalin +adult +adventure +aerial +afar +affair +afield +afloat +afoot +afraid +after +against +agenda +aggravate +agile +aglow +agnostic +agony +agreed +ahead +aided +ailments +aimless +airport +aisle +ajar +akin +alarms +album +alchemy +alerts +algebra +alkaline +alley +almost +aloof +alpine +already +also +altitude +alumni +always +amaze +ambush +amended +amidst +ammo +amnesty +among +amply +amused +anchor +android +anecdote +angled +ankle +annoyed +answers +antics +anvil +anxiety +anybody +apart +apex +aphid +aplomb +apology +apply +apricot +aptitude +aquarium +arbitrary +archer +ardent +arena +argue +arises +army +around +arrow +arsenic +artistic +ascend +ashtray +aside +asked +asleep +aspire +assorted +asylum +athlete +atlas +atom +atrium +attire +auburn +auctions +audio +august +aunt +austere +autumn +avatar +avidly +avoid +awakened +awesome +awful +awkward +awning +awoken +axes +axis +axle +aztec +azure +baby +bacon +badge +baffles +bagpipe +bailed +bakery +balding +bamboo +banjo +baptism +basin +batch +bawled +bays +because +beer +befit +begun +behind +being +below +bemused +benches +berries +bested +betting +bevel +beware +beyond +bias +bicycle +bids +bifocals +biggest +bikini +bimonthly +binocular +biology +biplane +birth +biscuit +bite +biweekly +blender +blip +bluntly +boat +bobsled +bodies +bogeys +boil +boldly +bomb +border +boss +both +bounced +bovine +bowling +boxes +boyfriend +broken +brunt +bubble +buckets +budget +buffet +bugs +building +bulb +bumper +bunch +business +butter +buying +buzzer +bygones +byline +bypass +cabin +cactus +cadets +cafe +cage +cajun +cake +calamity +camp +candy +casket +catch +cause +cavernous +cease +cedar +ceiling +cell +cement +cent +certain +chlorine +chrome +cider +cigar +cinema +circle +cistern +citadel +civilian +claim +click +clue +coal +cobra +cocoa +code +coexist +coffee +cogs +cohesive +coils +colony +comb +cool +copy +corrode +costume +cottage +cousin +cowl +criminal +cube +cucumber +cuddled +cuffs +cuisine +cunning +cupcake +custom +cycling +cylinder +cynical +dabbing +dads +daft +dagger +daily +damp +dangerous +dapper +darted +dash +dating +dauntless +dawn +daytime +dazed +debut +decay +dedicated +deepest +deftly +degrees +dehydrate +deity +dejected +delayed +demonstrate +dented +deodorant +depth +desk +devoid +dewdrop +dexterity +dialect +dice +diet +different +digit +dilute +dime +dinner +diode +diplomat +directed +distance +ditch +divers +dizzy +doctor +dodge +does +dogs +doing +dolphin +domestic +donuts +doorway +dormant +dosage +dotted +double +dove +down +dozen +dreams +drinks +drowning +drunk +drying +dual +dubbed +duckling +dude +duets +duke +dullness +dummy +dunes +duplex +duration +dusted +duties +dwarf +dwelt +dwindling +dying +dynamite +dyslexic +each +eagle +earth +easy +eating +eavesdrop +eccentric +echo +eclipse +economics +ecstatic +eden +edgy +edited +educated +eels +efficient +eggs +egotistic +eight +either +eject +elapse +elbow +eldest +eleven +elite +elope +else +eluded +emails +ember +emerge +emit +emotion +empty +emulate +energy +enforce +enhanced +enigma +enjoy +enlist +enmity +enough +enraged +ensign +entrance +envy +epoxy +equip +erase +erected +erosion +error +eskimos +espionage +essential +estate +etched +eternal +ethics +etiquette +evaluate +evenings +evicted +evolved +examine +excess +exhale +exit +exotic +exquisite +extra +exult +fabrics +factual +fading +fainted +faked +fall +family +fancy +farming +fatal +faulty +fawns +faxed +fazed +feast +february +federal +feel +feline +females +fences +ferry +festival +fetches +fever +fewest +fiat +fibula +fictional +fidget +fierce +fifteen +fight +films +firm +fishing +fitting +five +fixate +fizzle +fleet +flippant +flying +foamy +focus +foes +foggy +foiled +folding +fonts +foolish +fossil +fountain +fowls +foxes +foyer +framed +friendly +frown +fruit +frying +fudge +fuel +fugitive +fully +fuming +fungal +furnished +fuselage +future +fuzzy +gables +gadget +gags +gained +galaxy +gambit +gang +gasp +gather +gauze +gave +gawk +gaze +gearbox +gecko +geek +gels +gemstone +general +geometry +germs +gesture +getting +geyser +ghetto +ghost +giant +giddy +gifts +gigantic +gills +gimmick +ginger +girth +giving +glass +gleeful +glide +gnaw +gnome +goat +goblet +godfather +goes +goggles +going +goldfish +gone +goodbye +gopher +gorilla +gossip +gotten +gourmet +governing +gown +greater +grunt +guarded +guest +guide +gulp +gumball +guru +gusts +gutter +guys +gymnast +gypsy +gyrate +habitat +hacksaw +haggled +hairy +hamburger +happens +hashing +hatchet +haunted +having +hawk +haystack +hazard +hectare +hedgehog +heels +hefty +height +hemlock +hence +heron +hesitate +hexagon +hickory +hiding +highway +hijack +hiker +hills +himself +hinder +hippo +hire +history +hitched +hive +hoax +hobby +hockey +hoisting +hold +honked +hookup +hope +hornet +hospital +hotel +hounded +hover +howls +hubcaps +huddle +huge +hull +humid +hunter +hurried +husband +huts +hybrid +hydrogen +hyper +iceberg +icing +icon +identity +idiom +idled +idols +igloo +ignore +iguana +illness +imagine +imbalance +imitate +impel +inactive +inbound +incur +industrial +inexact +inflamed +ingested +initiate +injury +inkling +inline +inmate +innocent +inorganic +input +inquest +inroads +insult +intended +inundate +invoke +inwardly +ionic +irate +iris +irony +irritate +island +isolated +issued +italics +itches +items +itinerary +itself +ivory +jabbed +jackets +jaded +jagged +jailed +jamming +january +jargon +jaunt +javelin +jaws +jazz +jeans +jeers +jellyfish +jeopardy +jerseys +jester +jetting +jewels +jigsaw +jingle +jittery +jive +jobs +jockey +jogger +joining +joking +jolted +jostle +journal +joyous +jubilee +judge +juggled +juicy +jukebox +july +jump +junk +jury +justice +juvenile +kangaroo +karate +keep +kennel +kept +kernels +kettle +keyboard +kickoff +kidneys +king +kiosk +kisses +kitchens +kiwi +knapsack +knee +knife +knowledge +knuckle +koala +laboratory +ladder +lagoon +lair +lakes +lamb +language +laptop +large +last +later +launching +lava +lawsuit +layout +lazy +lectures +ledge +leech +left +legion +leisure +lemon +lending +leopard +lesson +lettuce +lexicon +liar +library +licks +lids +lied +lifestyle +light +likewise +lilac +limits +linen +lion +lipstick +liquid +listen +lively +loaded +lobster +locker +lodge +lofty +logic +loincloth +long +looking +lopped +lordship +losing +lottery +loudly +love +lower +loyal +lucky +luggage +lukewarm +lullaby +lumber +lunar +lurk +lush +luxury +lymph +lynx +lyrics +macro +madness +magically +mailed +major +makeup +malady +mammal +maps +masterful +match +maul +maverick +maximum +mayor +maze +meant +mechanic +medicate +meeting +megabyte +melting +memoir +menu +merger +mesh +metro +mews +mice +midst +mighty +mime +mirror +misery +mittens +mixture +moat +mobile +mocked +mohawk +moisture +molten +moment +money +moon +mops +morsel +mostly +motherly +mouth +movement +mowing +much +muddy +muffin +mugged +mullet +mumble +mundane +muppet +mural +musical +muzzle +myriad +mystery +myth +nabbing +nagged +nail +names +nanny +napkin +narrate +nasty +natural +nautical +navy +nearby +necklace +needed +negative +neither +neon +nephew +nerves +nestle +network +neutral +never +newt +nexus +nibs +niche +niece +nifty +nightly +nimbly +nineteen +nirvana +nitrogen +nobody +nocturnal +nodes +noises +nomad +noodles +northern +nostril +noted +nouns +novelty +nowhere +nozzle +nuance +nucleus +nudged +nugget +nuisance +null +number +nuns +nurse +nutshell +nylon +oaks +oars +oasis +oatmeal +obedient +object +obliged +obnoxious +observant +obtains +obvious +occur +ocean +october +odds +odometer +offend +often +oilfield +ointment +okay +older +olive +olympics +omega +omission +omnibus +onboard +oncoming +oneself +ongoing +onion +online +onslaught +onto +onward +oozed +opacity +opened +opposite +optical +opus +orange +orbit +orchid +orders +organs +origin +ornament +orphans +oscar +ostrich +otherwise +otter +ouch +ought +ounce +ourselves +oust +outbreak +oval +oven +owed +owls +owner +oxidant +oxygen +oyster +ozone +pact +paddles +pager +pairing +palace +pamphlet +pancakes +paper +paradise +pastry +patio +pause +pavements +pawnshop +payment +peaches +pebbles +peculiar +pedantic +peeled +pegs +pelican +pencil +people +pepper +perfect +pests +petals +phase +pheasants +phone +phrases +physics +piano +picked +pierce +pigment +piloted +pimple +pinched +pioneer +pipeline +pirate +pistons +pitched +pivot +pixels +pizza +playful +pledge +pliers +plotting +plus +plywood +poaching +pockets +podcast +poetry +point +poker +polar +ponies +pool +popular +portents +possible +potato +pouch +poverty +powder +pram +present +pride +problems +pruned +prying +psychic +public +puck +puddle +puffin +pulp +pumpkins +punch +puppy +purged +push +putty +puzzled +pylons +pyramid +python +queen +quick +quote +rabbits +racetrack +radar +rafts +rage +railway +raking +rally +ramped +randomly +rapid +rarest +rash +rated +ravine +rays +razor +react +rebel +recipe +reduce +reef +refer +regular +reheat +reinvest +rejoices +rekindle +relic +remedy +renting +reorder +repent +request +reruns +rest +return +reunion +revamp +rewind +rhino +rhythm +ribbon +richly +ridges +rift +rigid +rims +ringing +riots +ripped +rising +ritual +river +roared +robot +rockets +rodent +rogue +roles +romance +roomy +roped +roster +rotate +rounded +rover +rowboat +royal +ruby +rudely +ruffled +rugged +ruined +ruling +rumble +runway +rural +rustled +ruthless +sabotage +sack +sadness +safety +saga +sailor +sake +salads +sample +sanity +sapling +sarcasm +sash +satin +saucepan +saved +sawmill +saxophone +sayings +scamper +scenic +school +science +scoop +scrub +scuba +seasons +second +sedan +seeded +segments +seismic +selfish +semifinal +sensible +september +sequence +serving +session +setup +seventh +sewage +shackles +shelter +shipped +shocking +shrugged +shuffled +shyness +siblings +sickness +sidekick +sieve +sifting +sighting +silk +simplest +sincerely +sipped +siren +situated +sixteen +sizes +skater +skew +skirting +skulls +skydive +slackens +sleepless +slid +slower +slug +smash +smelting +smidgen +smog +smuggled +snake +sneeze +sniff +snout +snug +soapy +sober +soccer +soda +software +soggy +soil +solved +somewhere +sonic +soothe +soprano +sorry +southern +sovereign +sowed +soya +space +speedy +sphere +spiders +splendid +spout +sprig +spud +spying +square +stacking +stellar +stick +stockpile +strained +stunning +stylishly +subtly +succeed +suddenly +suede +suffice +sugar +suitcase +sulking +summon +sunken +superior +surfer +sushi +suture +swagger +swept +swiftly +sword +swung +syllabus +symptoms +syndrome +syringe +system +taboo +tacit +tadpoles +tagged +tail +taken +talent +tamper +tanks +tapestry +tarnished +tasked +tattoo +taunts +tavern +tawny +taxi +teardrop +technical +tedious +teeming +tell +template +tender +tepid +tequila +terminal +testing +tether +textbook +thaw +theatrics +thirsty +thorn +threaten +thumbs +thwart +ticket +tidy +tiers +tiger +tilt +timber +tinted +tipsy +tirade +tissue +titans +toaster +tobacco +today +toenail +toffee +together +toilet +token +tolerant +tomorrow +tonic +toolbox +topic +torch +tossed +total +touchy +towel +toxic +toyed +trash +trendy +tribal +trolling +truth +trying +tsunami +tubes +tucks +tudor +tuesday +tufts +tugs +tuition +tulips +tumbling +tunnel +turnip +tusks +tutor +tuxedo +twang +tweezers +twice +twofold +tycoon +typist +tyrant +ugly +ulcers +ultimate +umbrella +umpire +unafraid +unbending +uncle +under +uneven +unfit +ungainly +unhappy +union +unjustly +unknown +unlikely +unmask +unnoticed +unopened +unplugs +unquoted +unrest +unsafe +until +unusual +unveil +unwind +unzip +upbeat +upcoming +update +upgrade +uphill +upkeep +upload +upon +upper +upright +upstairs +uptight +upwards +urban +urchins +urgent +usage +useful +usher +using +usual +utensils +utility +utmost +utopia +uttered +vacation +vague +vain +value +vampire +vane +vapidly +vary +vastness +vats +vaults +vector +veered +vegan +vehicle +vein +velvet +venomous +verification +vessel +veteran +vexed +vials +vibrate +victim +video +viewpoint +vigilant +viking +village +vinegar +violin +vipers +virtual +visited +vitals +vivid +vixen +vocal +vogue +voice +volcano +vortex +voted +voucher +vowels +voyage +vulture +wade +waffle +wagtail +waist +waking +wallets +wanted +warped +washing +water +waveform +waxing +wayside +weavers +website +wedge +weekday +weird +welders +went +wept +were +western +wetsuit +whale +when +whipped +whole +wickets +width +wield +wife +wiggle +wildly +winter +wipeout +wiring +wise +withdrawn +wives +wizard +wobbly +woes +woken +wolf +womanly +wonders +woozy +worry +wounded +woven +wrap +wrist +wrong +yacht +yahoo +yanks +yard +yawning +yearbook +yellow +yesterday +yeti +yields +yodel +yoga +younger +yoyo +zapped +zeal +zebra +zero +zesty +zigzags +zinger +zippers +zodiac +zombie +zones +zoom +""".split()) diff --git a/mmgen/tool.py b/mmgen/tool.py index a6858cb1..c07e5527 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -234,6 +234,7 @@ dfl_mnemonic_fmt = 'mmgen' mnemonic_fmts = { 'mmgen': { 'fmt': 'words', 'conv_cls': lambda: baseconv }, 'bip39': { 'fmt': 'bip39', 'conv_cls': conv_cls_bip39 }, + 'xmrseed': { 'fmt': 'xmrseed','conv_cls': lambda: baseconv }, } mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts)) @@ -473,7 +474,7 @@ class MMGenToolCmdCoin(MMGenToolCmdBase): class MMGenToolCmdMnemonic(MMGenToolCmdBase): """ - seed phrase utilities (valid formats: 'mmgen' (default), 'bip39') + seed phrase utilities (valid formats: 'mmgen' (default), 'bip39', 'xmrseed') IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum wordlist, however seed phrases are computed using a different algorithm @@ -484,10 +485,24 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase): users should be aware that BIP39 support does not imply BIP32 support! MMGen uses its own key derivation scheme differing from the one described by the BIP32 protocol. + + For Monero ('xmrseed') seed phrases, input data is reduced to a spendkey + before conversion so that a canonical seed phrase is produced. This is + required because Monero seeds, unlike ordinary wallet seeds, are tied + to a concrete key/address pair. To manually generate a Monero spendkey, + use the 'hex2wif' command. """ + + @staticmethod + def _xmr_reduce(bytestr): + from mmgen.protocol import MoneroProtocol + return MoneroProtocol.preprocess_key(bytestr,None) + def _do_random_mn(self,nbytes:int,fmt:str): assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32' randbytes = get_random(nbytes) + if fmt == 'xmrseed': + randbytes = self._xmr_reduce(randbytes) if opt.verbose: msg('Seed: {}'.format(randbytes.hex())) return self.hex2mn(randbytes.hex(),fmt=fmt) @@ -518,6 +533,8 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase): return ' '.join(bip39.fromhex(hexstr,fmt)) else: bytestr = bytes.fromhex(hexstr) + if fmt == 'xmrseed': + bytestr = self._xmr_reduce(bytestr) return baseconv.frombytes(bytestr,fmt,'seed',tostr=True) def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ): diff --git a/setup.py b/setup.py index 34f1788a..a7d48018 100755 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ setup( 'mmgen.keccak', 'mmgen.license', 'mmgen.mn_electrum', + 'mmgen.mn_monero', 'mmgen.mn_tirosh', 'mmgen.obj', 'mmgen.opts', diff --git a/test/ref/98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws b/test/ref/98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws new file mode 100644 index 00000000..1d6bf2de --- /dev/null +++ b/test/ref/98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws @@ -0,0 +1,6 @@ +# B488 21D3 4539 968D +98831F3A фубар@crypto.org xmrseed:25 { + 1 amended jailed extra apart hinder upbeat dating onboard yesterday aided irony guide cogs apricot aggravate twofold tepid nuance ripped vessel soothe woven vials when aided + 4 godfather arrow hobby mailed educated abnormal boss cavernous skirting alumni voted hunter vessel unsafe mohawk pylons amused rift jury code assorted oscar himself inquest oscar + 1100 molten icon lectures amaze foes lurk camp divers goldfish zombie neutral drunk topic abort boldly recipe natural verification circle polar woozy biplane yoyo adapt abort +} diff --git a/test/scrambletest.py b/test/scrambletest.py index c37177e5..a3db4969 100755 --- a/test/scrambletest.py +++ b/test/scrambletest.py @@ -88,6 +88,7 @@ passwd_data = { 'bip39_dfl_αω':td('95b383d5092a55df', 'bip39:24:αω','-αω-bip39-24','αω bip39:24','treat athlete brand top beauty poverty senior unhappy vacant domain yellow scale fossil aim lonely fatal sun nuclear such ancient stage require stool similar'), 'bip39_18_αω': td('29e5a605ffa36142', 'bip39:18:αω','-αω-bip39-18','αω bip39:18','better legal various ketchup then range festival either tomato cradle say absorb solar earth alter pattern canyon liar'), 'bip39_12_αω': td('efa13cb309d7fc1d', 'bip39:12:αω','-αω-bip39-12','αω bip39:12','lady oppose theme fit position merry reopen acquire tuna dentist young chunk'), +'xmrseed_dfl_αω':td('62f5b72a5ca89cab', 'xmrseed:25:αω','-αω-xmrseed-25','αω xmrseed:25','tequila eden skulls giving jester hospital dreams bakery adjust nanny cactus inwardly films amply nanny soggy vials muppet yellow woken ashtray organs exhale foes eden'), } cvr_opts = ' -m trace --count --coverdir={} --file={}'.format(*init_coverage()) if opt.coverage else '' diff --git a/test/test_py_d/ts_ref.py b/test/test_py_d/ts_ref.py index cece846d..20639d5d 100755 --- a/test/test_py_d/ts_ref.py +++ b/test/test_py_d/ts_ref.py @@ -53,6 +53,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): 'ref_passwdfile_bip39_12': '98831F3A-фубар@crypto.org-bip39-12[1,4,1100].pws', 'ref_passwdfile_bip39_18': '98831F3A-фубар@crypto.org-bip39-18[1,4,1100].pws', 'ref_passwdfile_bip39_24': '98831F3A-фубар@crypto.org-bip39-24[1,4,1100].pws', + 'ref_passwdfile_xmrseed_25': '98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws', 'ref_passwdfile_hex2bip39_12': '98831F3A-фубар@crypto.org-hex2bip39-12[1,4,1100].pws', 'ref_tx_file': { # data shared with ref_altcoin, autosign 'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx', @@ -99,6 +100,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): 'ref_passwdfile_bip39_12_chksum': 'BF57 02A3 5229 CF18', 'ref_passwdfile_bip39_18_chksum': '31D3 1656 B7DC 27CF', 'ref_passwdfile_bip39_24_chksum': 'E565 3A59 7D91 4671', + 'ref_passwdfile_xmrseed_25_chksum': 'B488 21D3 4539 968D', 'ref_passwdfile_hex2bip39_12_chksum': '93AD 4AE2 03D1 8A0A', } cmd_group = ( # TODO: move to tooltest2 @@ -123,6 +125,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): ('ref_passwdfile_chk_bip39_12','saved reference password file (BIP39, 12 words)'), ('ref_passwdfile_chk_bip39_18','saved reference password file (BIP39, 18 words)'), ('ref_passwdfile_chk_bip39_24','saved reference password file (BIP39, 24 words)'), + ('ref_passwdfile_chk_xmrseed_25','saved reference password file (Monero new-style mnemonic, 25 words)'), ('ref_passwdfile_chk_hex2bip39_12','saved reference password file (hex-to-BIP39, 12 words)'), # Create the fake inputs: @@ -252,6 +255,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): def ref_passwdfile_chk_bip39_12(self): return self.ref_passwdfile_chk(key='bip39_12',pat=r'BIP39.*len.* 12\b') def ref_passwdfile_chk_bip39_18(self): return self.ref_passwdfile_chk(key='bip39_18',pat=r'BIP39.*len.* 18\b') def ref_passwdfile_chk_bip39_24(self): return self.ref_passwdfile_chk(key='bip39_24',pat=r'BIP39.*len.* 24\b') + def ref_passwdfile_chk_xmrseed_25(self): return self.ref_passwdfile_chk(key='xmrseed_25',pat=r'Mon.*len.* 25\b') def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat=r'BIP39.*len.* 12\b') def ref_tx_chk(self): diff --git a/test/test_py_d/ts_ref_3seed.py b/test/test_py_d/ts_ref_3seed.py index 3fb25242..55280393 100755 --- a/test/test_py_d/ts_ref_3seed.py +++ b/test/test_py_d/ts_ref_3seed.py @@ -42,6 +42,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): 'sids': ('FE3C6545', '1378FC64', '98831F3A'), } shared_deps = ['mmdat',pwfile] + skip_cmds = ( + 'ref_xmrseed_25_passwdgen_1', + 'ref_xmrseed_25_passwdgen_2', + ) cmd_group = ( # reading saved reference wallets ('ref_wallet_chk', ([],'saved reference wallet')), @@ -319,6 +323,7 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed): 'ref_bip39_18_passwdgen_3': 'EF87 9904 88E2 5884', 'ref_bip39_24_passwdgen_3': 'EBE8 2A8F 8F8C 7DBD', 'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E', + 'ref_xmrseed_25_passwdgen_3': '91AE E76A 2827 C8CC', } cmd_group = ( @@ -339,6 +344,7 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed): ('ref_bip39_12_passwdgen', ([],'new refwallet passwd file chksum (BIP39, 12 words)')), ('ref_bip39_18_passwdgen', ([],'new refwallet passwd file chksum (BIP39, up to 18 words)')), ('ref_bip39_24_passwdgen', ([],'new refwallet passwd file chksum (BIP39, up to 24 words)')), + ('ref_xmrseed_25_passwdgen', ([],'new refwallet passwd file chksum (Monero new-style mnemonic, 25 words)')), ('ref_hex2bip39_24_passwdgen',([],'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')), ) @@ -383,6 +389,8 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed): def mn_pwgen(self,req_pw_len,pwfmt,ftype='passbip39',stdout=False): pwlen = min(req_pw_len,{'1':12,'2':18,'3':24}[self.test_name[-1]]) + if pwfmt == 'xmrseed': + pwlen += 1 ea = ['--accept-defaults'] return self.pwgen(ftype,'фубар@crypto.org',pwfmt,pwlen,ea,stdout=stdout) @@ -390,3 +398,4 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed): def ref_bip39_18_passwdgen(self): return self.mn_pwgen(18,'bip39',stdout=True) def ref_bip39_24_passwdgen(self): return self.mn_pwgen(24,'bip39') def ref_hex2bip39_24_passwdgen(self): return self.mn_pwgen(24,'hex2bip39') + def ref_xmrseed_25_passwdgen(self): return self.mn_pwgen(24,'xmrseed',ftype='passxmrseed') diff --git a/test/tooltest2.py b/test/tooltest2.py index 7c69244b..4a8b3d22 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -32,6 +32,7 @@ from mmgen.common import * from test.common import * from mmgen.obj import is_wif,is_coin_addr from mmgen.seed import is_bip39_mnemonic,is_mmgen_mnemonic +from mmgen.addr import is_xmrseed from mmgen.baseconv import * NL = ('\n','\r\n')[g.platform=='win'] @@ -112,6 +113,9 @@ tests = { ( ['0000000000000000000000000000000000000000000000000000000000000001'], ('able able able able able able able able able able able able ' + 'able able able able able able able able able able able about') ), + ( ['e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f','fmt=xmrseed'], + ('viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure ' + + 'jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template') ), ] + [([a,'fmt=bip39'],b) for a,b in bip39.vectors], 'mn2hex': [ ( ['table cast forgive master funny gaze sadness ripple million paint moral match','fmt=mmgen'], @@ -133,6 +137,10 @@ tests = { ( ['able able able able able able able able able able able able ' + 'able able able able able able able able able able able about'], '0000000000000000000000000000000000000000000000000000000000000001'), + ( ['viewpoint donuts ardent template unveil agile meant unafraid urgent athlete ' + + 'rustled mime azure jaded hawk baby jagged haystack baby jagged haystack ' + + 'ramped oncoming point template','fmt=xmrseed'], + 'e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f'), ] + [([b,'fmt=bip39'],a) for a,b in bip39.vectors], 'mn_rand128': [ ( [], is_mmgen_mnemonic, ['-r0']), @@ -146,16 +154,19 @@ tests = { 'mn_rand256': [ ( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']), ( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']), + ( ['fmt=xmrseed'], is_xmrseed, ['-r0']), ], 'mn_stats': [ ( [], is_str ), ( ['fmt=mmgen'], is_str ), ( ['fmt=bip39'], is_str ), + ( ['fmt=xmrseed'], is_str ), ], 'mn_printlist': [ ( [], is_str ), ( ['fmt=mmgen'], is_str ), ( ['fmt=bip39'], is_str ), + ( ['fmt=xmrseed','enum=true'], is_str ), ], }, 'Util': { diff --git a/test/unit_tests_d/ut_baseconv.py b/test/unit_tests_d/ut_baseconv.py index 9e853d06..860f8397 100755 --- a/test/unit_tests_d/ut_baseconv.py +++ b/test/unit_tests_d/ut_baseconv.py @@ -9,6 +9,23 @@ from mmgen.exception import * class unit_test(object): vectors = { + 'xmrseed': ( + # 42nsXK8WbVGTNayQ6Kjw5UdgqbQY5KCCufdxdCgF7NgTfjC69Mna7DJSYyie77hZTQ8H92G2HwgFhgEUYnDzrnLnQdF28r3 + (('0000000000000000000000000000000000000000000000000000000000000001','seed'), # 0x1 + 'abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey bamboo jaws jerseys abbey'), + + # 49voQEbjouUQSDikRWKUt1PGbS47TBde4hiGyftN46CvTDd8LXCaimjHRGtofCJwY5Ed5QhYwc12P15AH5w7SxUAMCz1nr1 + (('1c95988d7431ecd670cf7d73f45befc6feffffffffffffffffffffffffffff0f','seed'), # 0xffffffff * 8 + 'powder directed sayings enmity bacon vapidly entrance bumper noodles iguana sleepless nasty flying soil software foamy solved soggy foamy solved soggy jury yawning ankle solved'), + + # 41i7saPWA53EoHenmJVRt34dubPxsXwoWMnw8AdMyx4mTD1svf7qYzcVjxxRfteLNdYrAxWUMmiPegFW9EfoNgXx7vDMExv + (('e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f','seed'), # 0xdeadbeef * 8 + 'viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template'), + + # 42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm + (('148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e','seed'), # Monero repo + 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'), + ), 'b58': ( (('00',None),'1'), (('00',1),'1'), @@ -161,7 +178,10 @@ class unit_test(object): for (hexstr,pad),ret_chk in data: if type(pad) == int: pad = len(hexstr) - ret = baseconv.tohex(ret_chk,wl_id=base,pad=pad) + ret = baseconv.tohex( + ret_chk.split() if base == 'xmrseed' else ret_chk, + wl_id=base, + pad=pad) if pad == None: assert int(ret,16) == int(hexstr,16), rerr.format(int(ret,16),int(hexstr,16)) else: