Browse Source

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 5 years ago
parent
commit
8519b68b89

+ 2163 - 0
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 <mmgen@tuta.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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')

+ 1 - 0
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

+ 2 - 2
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())

+ 2 - 2
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())

+ 34 - 12
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

+ 46 - 26
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 _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',wordlist=dfl_wl_id):
-		"convert a 16, 24 or 32-byte hexadecimal number to a mnemonic"
-		opt.out_fmt = 'words'
+	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)

+ 17 - 7
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
 

+ 1 - 0
setup.py

@@ -84,6 +84,7 @@ setup(
 			'mmgen.addr',
 			'mmgen.altcoin',
 			'mmgen.bech32',
+			'mmgen.bip39',
 			'mmgen.color',
 			'mmgen.common',
 			'mmgen.crypto',

+ 1 - 0
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

+ 1 - 0
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

+ 1 - 0
test/ref/FE3C6545.bip39

@@ -0,0 +1 @@
+earth hip style decade say bulb cattle strike install air once large

+ 2 - 1
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 = ''

+ 7 - 3
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]+

+ 7 - 4
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"

+ 2 - 2
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()

+ 14 - 5
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')

+ 14 - 6
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):

+ 28 - 9
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': [

+ 164 - 0
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