From 8519b68b893dca573c2706cf6a5fa6154e841021 Mon Sep 17 00:00:00 2001 From: MMGen Date: Tue, 9 Jul 2019 12:44:16 +0000 Subject: [PATCH] Complete BIP39 mnemonic support - provided as an alternative to MMGen's native mnemonic format # Run the BIP39 unit test: $ test/unit_tests.py -v bip39 # Generate a random 128-bit BIP39 seed phrase: $ mmgen-tool mn_rand128 fmt=bip39 # Export your default wallet to BIP39 format: $ mmgen-walletconv -o bip39 ... BIP39 mnemonic data written to file '98831F3A[256].bip39' # Generate ten addresses from the exported wallet: $ mmgen-addrgen '98831F3A[256].bip39' 1-10 ... Addresses written to file '98831F3A[1-10].addrs' # Generate ten addresses directly from your BIP39 seed phrase: $ mmgen-addrgen -q -i bip39 1-10 ... Addresses written to file '98831F3A[1-10].addrs' # Export subwallet 10L of your default wallet to BIP39 format: $ mmgen-subwalletgen -o bip39 10L ... BIP39 mnemonic data written to file 'A17F8E90[256].bip39' --- mmgen/bip39.py | 2163 ++++++++++++++++++++++++++++++++ mmgen/exception.py | 1 + mmgen/mn_electrum.py | 4 +- mmgen/mn_tirosh.py | 4 +- mmgen/seed.py | 46 +- mmgen/tool.py | 72 +- mmgen/util.py | 24 +- setup.py | 1 + test/ref/1378FC64.bip39 | 1 + test/ref/98831F3A.bip39 | 1 + test/ref/FE3C6545.bip39 | 1 + test/test_py_d/common.py | 3 +- test/test_py_d/ts_main.py | 10 +- test/test_py_d/ts_misc.py | 11 +- test/test_py_d/ts_ref.py | 4 +- test/test_py_d/ts_ref_3seed.py | 19 +- test/test_py_d/ts_wallet.py | 20 +- test/tooltest2.py | 37 +- test/unit_tests_d/ut_bip39.py | 164 +++ 19 files changed, 2507 insertions(+), 79 deletions(-) create mode 100755 mmgen/bip39.py create mode 100644 test/ref/1378FC64.bip39 create mode 100644 test/ref/98831F3A.bip39 create mode 100644 test/ref/FE3C6545.bip39 create mode 100755 test/unit_tests_d/ut_bip39.py diff --git a/mmgen/bip39.py b/mmgen/bip39.py new file mode 100755 index 00000000..fc0f0fd6 --- /dev/null +++ b/mmgen/bip39.py @@ -0,0 +1,2163 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +bip39.py - Data and routines for BIP39 mnemonic seed phrases +""" + +from hashlib import sha256 + +from mmgen.exception import * +from mmgen.util import baseconv,is_hex_str + +# implements a subset of the baseconv API +class bip39(baseconv): + + words = tuple(""" +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo +""".split()) + + mn_base = 2048 + mn_ids = ('bip39',) + wl_chksums = { 'bip39': 'f18b9a84' } + # ENT CS MS + constants = { + '128': (4, 12), + '160': (5, 15), + '192': (6, 18), + '224': (7, 21), + '256': (8, 24), + } + + @classmethod + def tohex(cls,words,wl_id,pad=None): + assert isinstance(words,(list,tuple)),'words must be list or tuple' + assert wl_id == 'bip39',"'wl_id' must be 'bip39'" + + wl = cls.digits[wl_id] + + for w in words: + if w not in wl: + raise MnemonicError('{!r} is not in the BIP39 word list'.format(w)) + + res = ''.join(['{:011b}'.format(wl.index(w)) for w in words]) + + for k in cls.constants: + if len(words) == cls.constants[k][1]: + bitlen = int(k) + break + else: + raise MnemonicError('{}: invalid seed phrase length'.format(len(words))) + + if pad != None: + assert pad * 4 == bitlen, '{}: invalid pad length'.format(pad) + + seed_bin = res[:bitlen] + chk_bin = res[bitlen:] + + seed_hex = '{:0{w}x}'.format(int(seed_bin,2),w=bitlen//4) + seed_bytes = bytes.fromhex(seed_hex) + + chk_len = cls.constants[str(bitlen)][0] + chk_hex_chk = sha256(seed_bytes).hexdigest() + chk_bin_chk = '{:0{w}b}'.format(int(chk_hex_chk,16),w=256)[:chk_len] + + if chk_bin != chk_bin_chk: + raise MnemonicError('{}: invalid checksum (should be {})'.format(chk_bin,chk_bin_chk)) + + return seed_hex + + @classmethod + def fromhex(cls,seed_hex,wl_id,pad=None,tostr=False): + assert is_hex_str(seed_hex),"{!r}: not a hexadecimal string".format(seed_hex) + assert wl_id == 'bip39',"'wl_id' must be 'bip39'" + assert tostr == False,"'tostr' must be False for 'bip39'" + + wl = cls.digits[wl_id] + seed_bytes = bytes.fromhex(seed_hex) + bitlen = len(seed_bytes) * 8 + + assert str(bitlen) in cls.constants,'{}: invalid seed bit length'.format(bitlen) + chk_len,mn_len = cls.constants[str(bitlen)] + + if pad != None: + assert mn_len == pad, '{}: invalid pad length'.format(pad) + + chk_hex = sha256(seed_bytes).hexdigest() + + seed_bin = '{:0{w}b}'.format(int(seed_hex,16),w=bitlen) + chk_bin = '{:0{w}b}'.format(int(chk_hex,16),w=256)[:chk_len] + + res = seed_bin + chk_bin + + return tuple(wl[int(res[i*11:(i+1)*11],2)] for i in range(mn_len)) + + @classmethod + def b58encode(cls,*args,**kwargs): + raise NotImplementedError('not implemented') + + @classmethod + def b58decode(cls,*args,**kwargs): + raise NotImplementedError('not implemented') diff --git a/mmgen/exception.py b/mmgen/exception.py index b6e701ad..4a5766ba 100755 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -26,6 +26,7 @@ class BadAgeFormat(Exception): mmcode = 1 class BadFilename(Exception): mmcode = 1 class SocketError(Exception): mmcode = 1 class UserAddressNotInWallet(Exception): mmcode = 1 +class MnemonicError(Exception): mmcode = 1 # 2: yellow hl, message only class InvalidTokenAddress(Exception): mmcode = 2 diff --git a/mmgen/mn_electrum.py b/mmgen/mn_electrum.py index 486b5cd7..92dbe5f6 100755 --- a/mmgen/mn_electrum.py +++ b/mmgen/mn_electrum.py @@ -21,7 +21,7 @@ # https://github.com/spesmilo/electrum/blob/1.9.5/lib/mnemonic.py # Electrum - lightweight Bitcoin client. Copyright (C) 2011 thomasv@gitorious -words = """ +words = tuple(""" able about above @@ -1648,4 +1648,4 @@ young yours yourself youth -""" +""".split()) diff --git a/mmgen/mn_tirosh.py b/mmgen/mn_tirosh.py index 1aef4883..4a19454e 100755 --- a/mmgen/mn_tirosh.py +++ b/mmgen/mn_tirosh.py @@ -48,7 +48,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -words = """ +words = tuple(""" abraham absent absorb @@ -1682,4 +1682,4 @@ zigzag zipper zodiac zoom -""" +""".split()) diff --git a/mmgen/seed.py b/mmgen/seed.py index 87d3d99e..19e8d94b 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -25,6 +25,7 @@ import os from mmgen.common import * from mmgen.obj import * from mmgen.crypto import * +from mmgen.bip39 import bip39 pnm = g.proj_name @@ -33,11 +34,11 @@ def check_usr_seed_len(seed_len): m = "ERROR: requested seed length ({}) doesn't match seed length of source ({})" die(1,m.format((opt.seed_len,seed_len))) -def is_mnemonic(s): +def _is_mnemonic(s,fmt): oq_save = opt.quiet opt.quiet = True try: - SeedSource(in_data=s,in_fmt='words') + SeedSource(in_data=s,in_fmt=fmt) ret = True except: ret = False @@ -45,6 +46,9 @@ def is_mnemonic(s): opt.quiet = oq_save return ret +def is_bip39_mnemonic(s): return _is_mnemonic(s,fmt='bip39') +def is_mmgen_mnemonic(s): return _is_mnemonic(s,fmt='words') + class SeedBase(MMGenObject): data = MMGenImmutableAttr('data',bytes,typeconv=False) @@ -743,14 +747,20 @@ an empty passphrase, just hit ENTER twice. d.key_id = make_chksum_8(key) d.enc_seed = encrypt_seed(self.seed.data,key) -class Mnemonic (SeedSourceUnenc): +class MMGenMnemonic(SeedSourceUnenc): stdin_ok = True fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m' - desc = 'mnemonic data' + desc = 'MMGen native mnemonic data' + mn_name = 'MMGen native' ext = 'mmwords' mn_lens = [i // 32 * 3 for i in g.seed_lens] - wl_id = 'electrum' # or 'tirosh' + wl_id = 'mmgen' + conv_cls = baseconv + + def __init__(self,*args,**kwargs): + self.conv_cls.init_mn(self.wl_id) + super().__init__(*args,**kwargs) def _get_data_from_user(self,desc): @@ -768,12 +778,15 @@ class Mnemonic (SeedSourceUnenc): msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r') return self.mn_lens[int(r)-1] + msg('{} {}'.format(blue('Mnemonic type:'),yellow(self.mn_name))) + while True: mn_len = choose_mn_len() prompt = 'Mnemonic length of {} words chosen. OK?'.format(mn_len) if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): break - wl = baseconv.digits[self.wl_id] + + wl = self.conv_cls.digits[self.wl_id] longest_word = max(len(w) for w in wl) from string import ascii_lowercase @@ -832,8 +845,8 @@ class Mnemonic (SeedSourceUnenc): hexseed = self.seed.hexdata - mn = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) - ret = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) + mn = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) + ret = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) # Internal error, so just die on fail compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error') @@ -851,12 +864,12 @@ class Mnemonic (SeedSourceUnenc): return False for n,w in enumerate(mn,1): - if w not in baseconv.digits[self.wl_id]: - msg('Invalid mnemonic: word #{} is not in the wordlist'.format(n)) + if w not in self.conv_cls.digits[self.wl_id]: + msg('Invalid mnemonic: word #{} is not in the {} wordlist'.format(n,self.wl_id.upper())) return False - hexseed = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) - ret = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) + hexseed = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) + ret = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) if len(hexseed) * 4 not in g.seed_lens: msg('Invalid mnemonic (produces too large a number)') @@ -872,6 +885,15 @@ class Mnemonic (SeedSourceUnenc): return True +class BIP39Mnemonic(MMGenMnemonic): + + fmt_codes = ('bip39',) + desc = 'BIP39 mnemonic data' + mn_name = 'BIP39' + ext = 'bip39' + wl_id = 'bip39' + conv_cls = bip39 + class SeedFile (SeedSourceUnenc): stdin_ok = True diff --git a/mmgen/tool.py b/mmgen/tool.py index 14f7bc9f..59a3ba55 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -24,6 +24,7 @@ from mmgen.protocol import hash160 from mmgen.common import * from mmgen.crypto import * from mmgen.addr import * +from mmgen.bip39 import bip39 NL = ('\n','\r\n')[g.platform=='win'] @@ -97,7 +98,7 @@ def _usage(cmd=None,exit_val=1): Msg(m2) elif cmd in MMGenToolCmd._user_commands(): docstr = getattr(MMGenToolCmd,cmd).__doc__.strip() - msg('{}\n'.format(capfirst(docstr))) + msg('{}'.format(capfirst(docstr))) msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd))) else: die(1,"'{}': no such tool command".format(cmd)) @@ -224,8 +225,12 @@ def init_generators(arg=None): kg = KeyGenerator(at) ag = AddrGenerator(at) -wordlists = 'electrum','tirosh' -dfl_wl_id = 'electrum' +dfl_mnemonic_fmt = 'mmgen' +mnemonic_fmts = { + 'mmgen': { 'fmt': 'words', 'conv_cls': baseconv }, + 'bip39': { 'fmt': 'bip39', 'conv_cls': bip39 }, +} +mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts)) class MMGenToolCmdBase(object): @@ -454,53 +459,68 @@ class MMGenToolCmdCoin(MMGenToolCmdBase): class MMGenToolCmdMnemonic(MMGenToolCmdBase): """ - seed mnemonic utilities (wordlist: choose 'electrum' (default) or 'tirosh') + seed phrase utilities (valid formats: 'mmgen' (default), 'bip39') - IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're - computed using a different algorithm and are NOT Electrum-compatible! + IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum + wordlist, however seed phrases are computed using a different algorithm + and are NOT Electrum-compatible! + + BIP39 support is fully compatible with the standard, allowing users to + import and export seed entropy from BIP39-compatible wallets. However, + 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. """ - def _do_random_mn(self,nbytes:int,wordlist:str): + def _do_random_mn(self,nbytes:int,fmt:str): assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32' hexrand = get_random(nbytes).hex() Vmsg('Seed: {}'.format(hexrand)) - return self.hex2mn(hexrand,wordlist=wordlist) + return self.hex2mn(hexrand,fmt=fmt) - def mn_rand128(self,wordlist=dfl_wl_id): + def mn_rand128(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ): "generate random 128-bit mnemonic seed phrase" - return self._do_random_mn(16,wordlist) + return self._do_random_mn(16,fmt) - def mn_rand192(self,wordlist=dfl_wl_id): + def mn_rand192(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ): "generate random 192-bit mnemonic seed phrase" - return self._do_random_mn(24,wordlist) + return self._do_random_mn(24,fmt) - def mn_rand256(self,wordlist=dfl_wl_id): + def mn_rand256(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ): "generate random 256-bit mnemonic seed phrase" - return self._do_random_mn(32,wordlist) + return self._do_random_mn(32,fmt) - def hex2mn(self,hexstr:'sstr',wordlist=dfl_wl_id): - "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic" - opt.out_fmt = 'words' + def _get_mnemonic_fmt(self,fmt): + if fmt not in mnemonic_fmts: + m = '{!r}: invalid format (valid options: {})' + die(1,m.format(fmt,', '.join(mnemonic_fmts))) + return mnemonic_fmts[fmt]['fmt'] + + def hex2mn( self, hexstr:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ): + "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic seed phrase" + opt.out_fmt = self._get_mnemonic_fmt(fmt) from mmgen.seed import SeedSource s = SeedSource(seed_bin=bytes.fromhex(hexstr)) s._format() return ' '.join(s.ssdata.mnemonic) - def mn2hex(self,seed_mnemonic:'sstr',wordlist=dfl_wl_id): - "convert a 12, 18 or 24-word mnemonic to a hexadecimal number" + def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ): + "convert a 12, 18 or 24-word mnemonic seed phrase to a hexadecimal number" + in_fmt = self._get_mnemonic_fmt(fmt) opt.quiet = True from mmgen.seed import SeedSource - return SeedSource(in_data=seed_mnemonic,in_fmt='words').seed.hexdata + return SeedSource(in_data=seed_mnemonic,in_fmt=in_fmt).seed.hexdata - def mn_stats(self,wordlist=dfl_wl_id): + def mn_stats(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ): "show stats for mnemonic wordlist" - wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist)) - baseconv.check_wordlist(wordlist) + conv_cls = mnemonic_fmts[fmt]['conv_cls'] + fmt in conv_cls.digits or die(1,"'{}': not a valid format".format(fmt)) + conv_cls.check_wordlist(fmt) return True - def mn_printlist(self,wordlist=dfl_wl_id,enum=False,pager=False): + def mn_printlist( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, enum=False, pager=False ): "print mnemonic wordlist" - wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist)) - ret = baseconv.digits[wordlist] + self._get_mnemonic_fmt(fmt) # perform check + ret = mnemonic_fmts[fmt]['conv_cls'].digits[fmt] if enum: ret = ['{:>4} {}'.format(n,e) for n,e in enumerate(ret)] return '\n'.join(ret) diff --git a/mmgen/util.py b/mmgen/util.py index 3cb8f1aa..49d0e227 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -283,9 +283,8 @@ def is_utf8(s): return is_ascii(s,enc='utf8') class baseconv(object): mn_base = 1626 # tirosh list is 1633 words long! + mn_ids = ('mmgen','tirosh') digits = { - 'electrum': tuple(__import__('mmgen.mn_electrum',fromlist=['words']).words.split()), - 'tirosh': tuple(__import__('mmgen.mn_tirosh',fromlist=['words']).words.split()[:mn_base]), 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'), 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), 'b16': tuple('0123456789abcdef'), @@ -293,13 +292,25 @@ class baseconv(object): 'b8': tuple('01234567'), } wl_chksums = { - 'electrum': '5ca31424', - 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) + 'mmgen': '5ca31424', + 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) # 'tirosh1633': '1a5faeff' } b58pad_lens = [(16,22), (24,33), (32,44)] b58pad_lens_rev = [(v,k) for k,v in b58pad_lens] + @classmethod + def init_mn(cls,mn_id): + assert mn_id in cls.mn_ids + if mn_id == 'mmgen': + from mmgen.mn_electrum 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] + else: # bip39 + cls.digits[mn_id] = cls.words + @classmethod def b58encode(cls,s,pad=None): pad = cls._get_pad(s,pad,'b58encode',cls.b58pad_lens,(bytes,)) @@ -327,6 +338,7 @@ class baseconv(object): @classmethod def get_wordlist_chksum(cls,wl_id): + cls.init_mn(wl_id) return sha256(' '.join(cls.digits[wl_id]).encode()).hexdigest()[:8] @classmethod @@ -364,7 +376,7 @@ class baseconv(object): @classmethod def fromhex(cls,hexnum,wl_id,pad=None,tostr=False): - if wl_id in ('electrum','tirosh'): + if wl_id in ('mmgen','tirosh'): assert tostr == False,"'tostr' must be False for '{}'".format(wl_id) if not is_hex_str(hexnum): @@ -379,8 +391,6 @@ class baseconv(object): o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]] return ''.join(o) if tostr else o -baseconv.check_wordlists() - def match_ext(addr,ext): return addr.split('.')[-1] == ext diff --git a/setup.py b/setup.py index 99de5eba..acff062b 100755 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ setup( 'mmgen.addr', 'mmgen.altcoin', 'mmgen.bech32', + 'mmgen.bip39', 'mmgen.color', 'mmgen.common', 'mmgen.crypto', diff --git a/test/ref/1378FC64.bip39 b/test/ref/1378FC64.bip39 new file mode 100644 index 00000000..9928315b --- /dev/null +++ b/test/ref/1378FC64.bip39 @@ -0,0 +1 @@ +earth hip style decade say bulb cattle strike install air once labor asset bronze piece pact digital hollow diff --git a/test/ref/98831F3A.bip39 b/test/ref/98831F3A.bip39 new file mode 100644 index 00000000..f24e2f17 --- /dev/null +++ b/test/ref/98831F3A.bip39 @@ -0,0 +1 @@ +earth hip style decade say bulb cattle strike install air once labor asset bronze piece pact digital gym dry candy finger across define doll diff --git a/test/ref/FE3C6545.bip39 b/test/ref/FE3C6545.bip39 new file mode 100644 index 00000000..47f08af6 --- /dev/null +++ b/test/ref/FE3C6545.bip39 @@ -0,0 +1 @@ +earth hip style decade say bulb cattle strike install air once large diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index d8bab00b..6cd09749 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -47,6 +47,7 @@ non_mmgen_fn = 'coinkey' ref_dir = os.path.join('test','ref') dfl_words_file = os.path.join(ref_dir,'98831F3A.mmwords') mn_words_mmgen = os.path.join(ref_dir,'FE3C6545.mmwords') +mn_words_bip39 = os.path.join(ref_dir,'FE3C6545.bip39') from mmgen.obj import MMGenTXLabel,TwComment @@ -149,7 +150,7 @@ def get_label(do_shuffle=False): def stealth_mnemonic_entry(t,mn,fmt): wnum = 1 - max_wordlen = { 'words': 12 }[fmt] + max_wordlen = { 'words': 12, 'bip39': 8 }[fmt] def get_pad_chars(n): ret = '' diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index d027dc51..c3429537 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -94,6 +94,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): ('export_seed', (1,'seed export to mmseed format', [[['mmdat'],1]])), ('export_hex', (1,'seed export to hexadecimal format', [[['mmdat'],1]])), ('export_mnemonic', (1,'seed export to mmwords format', [[['mmdat'],1]])), + ('export_bip39', (1,'seed export to bip39 format', [[['mmdat'],1]])), ('export_incog', (1,'seed export to mmincog format', [[['mmdat'],1]])), ('export_incog_hex',(1,'seed export to mmincog hex format', [[['mmdat'],1]])), ('export_incog_hidden',(1,'seed export to hidden mmincog format', [[['mmdat'],1]])), @@ -227,7 +228,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t.license() t.passphrase('MMGen wallet',self.cfgs['1']['wpasswd']) t.expect('Generating subseed 3L') - fn = t.written_to_file('Mnemonic data') + fn = t.written_to_file('MMGen native mnemonic data') assert fn[-8:] == '.mmwords','incorrect file extension: {}'.format(fn[-8:]) return t @@ -523,7 +524,10 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return self.export_seed(wf,desc=desc,out_fmt=out_fmt,pf=pf) def export_mnemonic(self,wf): - return self.export_seed(wf,desc='mnemonic data',out_fmt='words') + return self.export_seed(wf,desc='MMGen native mnemonic data',out_fmt='words') + + def export_bip39(self,wf): + return self.export_seed(wf,desc='BIP39 mnemonic data',out_fmt='bip39') def export_incog(self,wf,desc='incognito data',out_fmt='i',add_args=[]): uargs = ['-p1',self.usr_rand_arg] + add_args @@ -560,7 +564,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return self.addrgen_seed(wf,foo,desc=desc,in_fmt=in_fmt) def addrgen_mnemonic(self,wf,foo): - return self.addrgen_seed(wf,foo,desc='mnemonic data',in_fmt='words') + return self.addrgen_seed(wf,foo,desc='MMGen native mnemonic data',in_fmt='words') def addrgen_incog(self,wf=[],foo='',in_fmt='i',desc='incognito data',args=[]): t = self.spawn('mmgen-addrgen', args + self.segwit_arg + ['-i'+in_fmt,'-d',self.tmpdir]+ diff --git a/test/test_py_d/ts_misc.py b/test/test_py_d/ts_misc.py index f1588e86..88511f01 100755 --- a/test/test_py_d/ts_misc.py +++ b/test/test_py_d/ts_misc.py @@ -116,7 +116,8 @@ class TestSuiteInput(TestSuiteBase): cmd_group = ( ('password_entry_noecho', (1,"utf8 password entry", [])), ('password_entry_echo', (1,"utf8 password entry (echoed)", [])), - ('mnemonic_entry', (1,"stealth mnemonic entry", [])), + ('mnemonic_entry_mmgen', (1,"stealth mnemonic entry (MMGen native)", [])), + ('mnemonic_entry_bip39', (1,"stealth mnemonic entry (BIP39)", [])), ) def password_entry(self,prompt,cmd_args): @@ -141,21 +142,23 @@ class TestSuiteInput(TestSuiteBase): return 'skip' # pexpect double-escapes utf8, so skip return self.password_entry('Enter passphrase (echoed): ',['--echo-passphrase']) - def _mnemonic_entry(self,fmt,wf): + def _mnemonic_entry(self,fmt,mn_name,wf): mn = read_from_file(wf).strip().split() mn = ['foo'] + mn[:5] + ['grac','graceful'] + mn[5:] t = self.spawn('mmgen-walletconv',['-S','-i',fmt,'-o',fmt]) + t.expect('Mnemonic type: {}'.format(mn_name)) t.expect('words: ','1') t.expect('(Y/n): ','y') stealth_mnemonic_entry(t,mn,fmt=fmt) sid_chk = 'FE3C6545' - sid = t.expect_getend('Valid mnemonic data for Seed ID ')[:8] + sid = t.expect_getend('Valid {} mnemonic data for Seed ID '.format(mn_name))[:8] assert sid == sid_chk,'Seed ID mismatch! {} != {}'.format(sid,sid_chk) t.expect('to confirm: ','YES\n') t.read() return t - def mnemonic_entry(self): return self._mnemonic_entry('words',mn_words_mmgen) + def mnemonic_entry_mmgen(self): return self._mnemonic_entry('words','MMGen native',mn_words_mmgen) + def mnemonic_entry_bip39(self): return self._mnemonic_entry('bip39','BIP39',mn_words_bip39) class TestSuiteTool(TestSuiteMain,TestSuiteBase): "tests for interactive 'mmgen-tool' commands" diff --git a/test/test_py_d/ts_ref.py b/test/test_py_d/ts_ref.py index 2814a471..5b5b0baf 100755 --- a/test/test_py_d/ts_ref.py +++ b/test/test_py_d/ts_ref.py @@ -125,12 +125,12 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-subwalletgen',args,extra_desc='(generate subwallet)') t.expect('Generating subseed {}'.format(ss_idx)) chk_sid = self.chk_data['ref_subwallet_sid']['98831F3A:{}'.format(ss_idx)] - fn = t.written_to_file('Mnemonic data') + fn = t.written_to_file('MMGen native mnemonic data') assert chk_sid in fn,'incorrect filename: {} (does not contain {})'.format(fn,chk_sid) ok() t = self.spawn('mmgen-walletchk',[fn],extra_desc='(check subwallet)') - t.expect(r'Valid mnemonic data for Seed ID ([0-9A-F]*)\b',regex=True) + t.expect(r'Valid MMGen native mnemonic data for Seed ID ([0-9A-F]*)\b',regex=True) sid = t.p.match.group(1) assert sid == chk_sid,'subseed ID {} does not match expected value {}'.format(sid,chk_sid) t.read() diff --git a/test/test_py_d/ts_ref_3seed.py b/test/test_py_d/ts_ref_3seed.py index 23dfed8e..4d720def 100755 --- a/test/test_py_d/ts_ref_3seed.py +++ b/test/test_py_d/ts_ref_3seed.py @@ -151,12 +151,14 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): ('ref_wallet_chk', ([],'saved reference wallet')), ('ref_seed_chk', ([],'saved seed file')), ('ref_hex_chk', ([],'saved mmhex file')), - ('ref_mn_chk', ([],'saved mnemonic file')), + ('ref_mn_chk', ([],'saved native MMGen mnemonic file')), + ('ref_bip39_chk', ([],'saved BIP39 mnemonic file')), ('ref_hincog_chk', ([],'saved hidden incog reference wallet')), ('ref_brain_chk', ([],'saved brainwallet')), # in ts_shared # generating new reference ('abc' brainwallet) files: ('ref_walletgen_brain', ([],'generating new reference wallet + filename check (brain)')), - ('ref_walletconv_words', (['mmdat',pwfile],'wallet filename (words)')), + ('ref_walletconv_words', (['mmdat',pwfile],'wallet filename (native mnemonic)')), + ('ref_walletconv_bip39', (['mmdat',pwfile],'wallet filename (bip39)')), ('ref_walletconv_seed', (['mmdat',pwfile],'wallet filename (seed)')), ('ref_walletconv_hexseed',(['mmdat',pwfile],'wallet filename (hex seed)')), ('ref_walletconv_incog', (['mmdat',pwfile],'wallet filename (incog)')), @@ -201,8 +203,12 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): return self.ref_ss_chk(ss=HexSeedFile) def ref_mn_chk(self): - from mmgen.seed import Mnemonic - return self.ref_ss_chk(ss=Mnemonic) + from mmgen.seed import MMGenMnemonic + return self.ref_ss_chk(ss=MMGenMnemonic) + + def ref_bip39_chk(self): + from mmgen.seed import BIP39Mnemonic + return self.ref_ss_chk(ss=BIP39Mnemonic) def ref_hincog_chk(self,desc='hidden incognito data'): source = TestSuiteWalletConv.sources[str(self.seed_len)] @@ -263,7 +269,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): return t def ref_walletconv_words(self,fn,pf): - return self.ref_walletconv(fn,pf,ofmt='mn',desc='Mnemonic data',ext='mmwords') + return self.ref_walletconv(fn,pf,ofmt='mn',desc='MMGen native mnemonic data',ext='mmwords') + + def ref_walletconv_bip39(self,fn,pf): + return self.ref_walletconv(fn,pf,ofmt='bip39',desc='BIP39 mnemonic data',ext='bip39') def ref_walletconv_seed(self,fn,pf): return self.ref_walletconv(fn,pf,ofmt='mmseed',desc='Seed data',ext='mmseed') diff --git a/test/test_py_d/ts_wallet.py b/test/test_py_d/ts_wallet.py index d11000e6..a6128f42 100755 --- a/test/test_py_d/ts_wallet.py +++ b/test/test_py_d/ts_wallet.py @@ -59,7 +59,8 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): cmd_group = ( # reading ('ref_wallet_conv', 'conversion of saved reference wallet'), - ('ref_mn_conv', 'conversion of saved mnemonic'), + ('ref_mn_conv', 'conversion of saved MMGen native mnemonic'), + ('ref_bip39_conv', 'conversion of saved BIP39 mnemonic'), ('ref_seed_conv', 'conversion of saved seed file'), ('ref_hex_conv', 'conversion of saved hexadecimal seed file'), ('ref_brain_conv', 'conversion of ref brainwallet'), @@ -69,7 +70,8 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): ('ref_hincog_conv_old','conversion of saved hidden incog wallet (old format)'), # writing ('ref_wallet_conv_out', 'ref seed conversion to wallet'), - ('ref_mn_conv_out', 'ref seed conversion to mnemonic'), + ('ref_mn_conv_out', 'ref seed conversion to MMGen native mnemonic'), + ('ref_bip39_conv_out', 'ref seed conversion to BIP39 mnemonic'), ('ref_hex_conv_out', 'ref seed conversion to hex seed'), ('ref_seed_conv_out', 'ref seed conversion to seed'), ('ref_incog_conv_out', 'ref seed conversion to incog data'), @@ -88,10 +90,13 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): wf = joinpath(ref_dir,self.sources[str(self.seed_len)]['ref_wallet']) return self.walletconv_in(wf,'MMGen wallet',pw=True,oo=True) - def ref_mn_conv(self,ext='mmwords',desc='Mnemonic data'): + def ref_mn_conv(self,ext='mmwords',desc='MMGen native mnemonic data'): wf = joinpath(ref_dir,self.seed_id+'.'+ext) return self.walletconv_in(wf,desc,oo=True) + def ref_bip39_conv(self): + return self.ref_mn_conv(ext='bip39',desc='BIP39 mnemonic data') + def ref_seed_conv(self): return self.ref_mn_conv(ext='mmseed',desc='Seed data') @@ -123,7 +128,10 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): return self.walletconv_out('MMGen wallet','w',pw=True) def ref_mn_conv_out(self): - return self.walletconv_out('mnemonic data','mn') + return self.walletconv_out('MMGen native mnemonic data','mn') + + def ref_bip39_conv_out(self): + return self.walletconv_out('BIP39 mnemonic data','bip39') def ref_seed_conv_out(self): return self.walletconv_out('seed data','seed') @@ -181,14 +189,14 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): else: t.expect(['Passphrase is OK',' are correct']) # Output - wf = t.written_to_file('Mnemonic data',oo=oo) + wf = t.written_to_file('MMGen native mnemonic data',oo=oo) t.p.wait() # back check of result msg('' if opt.profile else ' OK') return self.walletchk( wf, pf = None, extra_desc = '(check)', - desc = 'mnemonic data', + desc = 'MMGen native mnemonic data', sid = self.seed_id ) def walletconv_out(self,desc,out_fmt='w',uopts=[],uopts_chk=[],pw=False): diff --git a/test/tooltest2.py b/test/tooltest2.py index f10438a5..c86be297 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -36,7 +36,7 @@ os.environ['MMGEN_TEST_SUITE'] = '1' from mmgen.common import * from test.common import * from mmgen.obj import is_wif,is_coin_addr -from mmgen.seed import is_mnemonic +from mmgen.seed import is_bip39_mnemonic,is_mmgen_mnemonic NL = ('\n','\r\n')[g.platform=='win'] @@ -94,10 +94,11 @@ kafile_code = ( "\nopt.use_old_ed25519 = None" + "\nopt.passwd_file = 'test/ref/keyaddrfile_password'" ) +from test.unit_tests_d.ut_bip39 import unit_test as bip39 tests = { 'Mnemonic': { 'hex2mn': [ - ( ['deadbeefdeadbeefdeadbeefdeadbeef'], + ( ['deadbeefdeadbeefdeadbeefdeadbeef','fmt=mmgen'], 'table cast forgive master funny gaze sadness ripple million paint moral match' ), ( ['deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'], ('swirl maybe anymore mix scale stray fog use approach page crime rhyme ' + @@ -116,9 +117,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') ), - ], + ] + [([a,'fmt=bip39'],b) for a,b in bip39.vectors], 'mn2hex': [ - ( ['table cast forgive master funny gaze sadness ripple million paint moral match'], + ( ['table cast forgive master funny gaze sadness ripple million paint moral match','fmt=mmgen'], 'deadbeefdeadbeefdeadbeefdeadbeef' ), ( ['swirl maybe anymore mix scale stray fog use approach page crime rhyme ' + 'class former strange window snap soon'], @@ -137,12 +138,30 @@ 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'), + ] + [([b,'fmt=bip39'],a) for a,b in bip39.vectors], + 'mn_rand128': [ + ( [], is_mmgen_mnemonic, ['-r0']), + ( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']), + ( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']), + ], + 'mn_rand192': [ + ( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']), + ( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']), + ], + 'mn_rand256': [ + ( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']), + ( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']), + ], + 'mn_stats': [ + ( [], is_str ), + ( ['fmt=mmgen'], is_str ), + ( ['fmt=bip39'], is_str ), + ], + 'mn_printlist': [ + ( [], is_str ), + ( ['fmt=mmgen'], is_str ), + ( ['fmt=bip39'], is_str ), ], - 'mn_rand128': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], - 'mn_rand192': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], - 'mn_rand256': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], - 'mn_stats': [ ( [], is_str ), ( ['wordlist=tirosh'], is_str ), ], - 'mn_printlist': [ ( [], is_str ), ( ['wordlist=tirosh'], is_str ), ], }, 'Util': { 'hextob32': [ diff --git a/test/unit_tests_d/ut_bip39.py b/test/unit_tests_d/ut_bip39.py new file mode 100755 index 00000000..2f403c06 --- /dev/null +++ b/test/unit_tests_d/ut_bip39.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +test/unit_tests_d/ut_bip39: BIP39 unit test for the MMGen suite +""" + +from mmgen.common import * +from mmgen.exception import * +from mmgen.bip39 import * + +class unit_test(object): + + vectors = ( + ( "00000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ), + ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank yellow" + ), + ( "80808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above" + ), + ( "ffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" + ), + ( "000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent" + ), + ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will" + ), + ( "808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always" + ), + ( "ffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when" + ), + ( "0000000000000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" + ), + ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title" + ), + ( "8080808080808080808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless" + ), + ( "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote" + ), + ( "9e885d952ad362caeb4efe34a8e91bd2", + "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic" + ), + ( "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", + "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog" + ), + ( "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", + "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length" + ), + ( "c0ba5a8e914111210f2bd131f3d5e08d", + "scheme spot photo card baby mountain device kick cradle pact join borrow" + ), + ( "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", + "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave" + ), + ( "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside" + ), + ( "23db8160a31d3e0dca3688ed941adbf3", + "cat swing flag economy stadium alone churn speed unique patch report train" + ), + ( "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", + "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access" + ), + ( "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform" + ), + ( "f30f8c1da665478f49b001d94c5fc452", + "vessel ladder alter error federal sibling chat ability sun glass valve picture" + ), + ( "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", + "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump" + ), + ( "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold" + ) + ) + + def run_test(self,name): + + msg_r('Testing BIP39 conversion routines...') + qmsg('') + + from mmgen.bip39 import bip39 + + bip39.check_wordlists() + bip39.check_wordlist('bip39') + + vmsg('') + qmsg('Checking seed to mnemonic conversion:') + for v in self.vectors: + chk = tuple(v[1].split()) + vmsg(' '+v[1]) + res = bip39.fromhex(v[0],'bip39') + assert res == chk, 'mismatch:\nres: {}\nchk: {}'.format(res,chk) + + vmsg('') + qmsg('Checking mnemonic to seed conversion:') + for v in self.vectors: + chk = v[0] + vmsg(' '+chk) + res = bip39.tohex(v[1].split(),'bip39') + assert res == chk, 'mismatch:\nres: {}\nchk: {}'.format(res,chk) + + vmsg('') + qmsg('Checking error handling:') + + bad_data = ( + ('bad hex', 'AssertionError', 'not a hexadecimal'), + ('bad id (tohex)', 'AssertionError', "must be 'bip39'"), + ('bad seed len', 'AssertionError', 'invalid seed bit length'), + ('bad mnemonic type', 'AssertionError', 'must be list'), + ('bad id (fromhex)', 'AssertionError', "must be 'bip39'"), + ('tostr = True', 'AssertionError', "'tostr' must be"), + ('bad pad length (fromhex)', 'AssertionError', "invalid pad len"), + ('bad pad length (tohex)', 'AssertionError', "invalid pad len"), + ('bad word', 'MnemonicError', "not in the BIP39 word list"), + ('bad checksum', 'MnemonicError', "checksum"), + ('bad seed phrase length', 'MnemonicError', "phrase len"), + ) + + good_mn = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong".split() + bad_len_mn = "zoo zoo zoo".split() + bad_chksum_mn = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo".split() + bad_word_mn = "admire zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo".split() + bad_seed = 'deadbeef' + good_seed = 'deadbeef' * 4 + + def bad0(): bip39.fromhex('xx','bip39') + def bad1(): bip39.fromhex(good_seed,'foo') + def bad2(): bip39.fromhex(bad_seed,'bip39') + def bad3(): bip39.tohex('string','bip39') + def bad4(): bip39.tohex(good_mn,'foo') + def bad5(): bip39.fromhex(good_seed,'bip39',tostr=True) + def bad6(): bip39.fromhex(good_seed,'bip39',pad=23) + def bad7(): bip39.tohex(good_mn,'bip39',pad=23) + def bad8(): bip39.tohex(bad_word_mn,'bip39') + def bad9(): bip39.tohex(bad_chksum_mn,'bip39') + def bad10(): bip39.tohex(bad_len_mn,'bip39') + + for i in range(len(bad_data)): + try: + vmsg_r(' {:26}'.format(bad_data[i][0]+':')) + locals()['bad'+str(i)]() + except Exception as e: + n = type(e).__name__ + vmsg(' {:15} [{}]'.format(n,e.args[0])) + assert n == bad_data[i][1] + assert bad_data[i][2] in e.args[0] + else: + rdie(3,"\nillegal action '{}' failed to raise exception".format(bad_data[n][0])) + + vmsg('') + msg('OK') + + return True