Browse Source

mmgen-passgen: support BIP39 passwords

Examples:

  Generate three 24-word BIP39 mnemonic seed phrases from your default wallet
  for your Trezor device:

  $ mmgen-passgen --passwd-fmt=bip39 mytrezor 1-3

  Same, but generate 12-word seed phrases:

  $ mmgen-passgen --passwd-fmt=bip39 --passwd-len=12 mytrezor 1-3

Relevant tests:

  $ test/test.py ref ref3
MMGen 5 years ago
parent
commit
8705e57b8f

+ 65 - 7
mmgen/addr.py

@@ -594,7 +594,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 			out.append('# Record this value to a secure location.\n')
 			out.append('# Record this value to a secure location.\n')
 
 
 		if type(self) == PasswordList:
 		if type(self) == PasswordList:
-			lbl = '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
+			lbl = '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
 		else:
 		else:
 			bc,mt = g.proto.base_coin,self.al_id.mmtype
 			bc,mt = g.proto.base_coin,self.al_id.mmtype
 			l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
 			l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
@@ -780,6 +780,10 @@ class KeyList(AddrList):
 	ext      = 'keys'
 	ext      = 'keys'
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
 
 
+def is_bip39_str(s):
+	from mmgen.bip39 import bip39
+	return bool(bip39.tohex(s.split(),wl_id='bip39'))
+
 from collections import namedtuple
 from collections import namedtuple
 class PasswordList(AddrList):
 class PasswordList(AddrList):
 	msgs = {
 	msgs = {
@@ -792,6 +796,13 @@ class PasswordList(AddrList):
 # password.  The label may contain any printable ASCII symbol.
 # password.  The label may contain any printable ASCII symbol.
 #
 #
 """.strip().format(n=TwComment.max_screen_width,pnm=pnm),
 """.strip().format(n=TwComment.max_screen_width,pnm=pnm),
+	'file_header_bip39': """
+# {pnm} BIP39 password file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
+#
+""".strip().format(pnm=pnm),
 	'record_chksum': """
 	'record_chksum': """
 Record this checksum: it will be used to verify the password file in the future
 Record this checksum: it will be used to verify the password file in the future
 """.strip()
 """.strip()
@@ -813,10 +824,14 @@ Record this checksum: it will be used to verify the password file in the future
 	pw_info     = {
 	pw_info     = {
 		'b32':   pwinfo(10, 42 ,24, None,       'base32 password',       is_b32_str),   # 32**24 < 2**128
 		'b32':   pwinfo(10, 42 ,24, None,       'base32 password',       is_b32_str),   # 32**24 < 2**128
 		'b58':   pwinfo(8,  36 ,20, None,       'base58 password',       is_b58_str),   # 58**20 < 2**128
 		'b58':   pwinfo(8,  36 ,20, None,       'base58 password',       is_b58_str),   # 58**20 < 2**128
+		'bip39': pwinfo(12, 24 ,24, [12,18,24], 'BIP39 mnemonic',        is_bip39_str),
 		'hex':   pwinfo(32, 64 ,64, [32,48,64], 'hexadecimal password',  is_hex_str),
 		'hex':   pwinfo(32, 64 ,64, [32,48,64], 'hexadecimal password',  is_hex_str),
 	}
 	}
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
 
 
+	feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature.  Use at your own risk!'
+	hex2bip39 = False
+
 	def __init__(   self,infile=None,seed=None,
 	def __init__(   self,infile=None,seed=None,
 					pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
 					pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
 					chk_params_only=False):
 					chk_params_only=False):
@@ -835,24 +850,35 @@ Record this checksum: it will be used to verify the password file in the future
 			self.set_pw_len(pw_len)
 			self.set_pw_len(pw_len)
 			if chk_params_only:
 			if chk_params_only:
 				return
 				return
+			if self.hex2bip39:
+				ymsg(self.feature_warn_fs.format(pw_fmt))
 			self.set_pw_len_vs_seed_len(pw_len,seed)
 			self.set_pw_len_vs_seed_len(pw_len,seed)
 			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
 			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
 			self.data = self.generate(seed,pw_idxs)
 			self.data = self.generate(seed,pw_idxs)
 
 
+		if self.pw_fmt == 'bip39':
+			self.msgs['file_header'] = self.msgs['file_header_bip39']
+
 		self.num_addrs = len(self.data)
 		self.num_addrs = len(self.data)
 		self.fmt_data = ''
 		self.fmt_data = ''
 		self.chksum = AddrListChksum(self)
 		self.chksum = AddrListChksum(self)
 
 
-		fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
+		fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
 		self.id_str = AddrListIDStr(self,fs)
 		self.id_str = AddrListIDStr(self,fs)
 		qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
 		qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
 		qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
 		qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
 
 
 	def set_pw_fmt(self,pw_fmt):
 	def set_pw_fmt(self,pw_fmt):
-		if pw_fmt not in self.pw_info:
+		if pw_fmt == 'hex2bip39':
+			self.hex2bip39 = True
+			self.pw_fmt = 'bip39'
+			self.pw_fmt_disp = 'hex2bip39'
+		else:
+			self.pw_fmt = pw_fmt
+			self.pw_fmt_disp = pw_fmt
+		if self.pw_fmt not in self.pw_info:
 			m = '{!r}: invalid password format.  Valid formats: {}'
 			m = '{!r}: invalid password format.  Valid formats: {}'
-			raise InvalidPasswdFormat(m.format(pw_fmt,', '.join(sorted(self.pw_info))))
-		self.pw_fmt = pw_fmt
+			raise InvalidPasswdFormat(m.format(self.pw_fmt,', '.join(sorted(self.pw_info))))
 
 
 	def chk_pw_len(self,passwd=None):
 	def chk_pw_len(self,passwd=None):
 		if passwd is None:
 		if passwd is None:
@@ -888,6 +914,10 @@ Record this checksum: it will be used to verify the password file in the future
 		if pf == 'hex':
 		if pf == 'hex':
 			pw_bytes = self.pw_len // 2
 			pw_bytes = self.pw_len // 2
 			good_pw_len = seed.byte_len * 2
 			good_pw_len = seed.byte_len * 2
+		elif pf == 'bip39':
+			from mmgen.bip39 import bip39
+			pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
+			good_pw_len = bip39.seedlen2nwords(len(seed.data),in_bytes=True)
 		elif pf in ('b32','b58'):
 		elif pf in ('b32','b58'):
 			pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
 			pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
 			pw_bytes = pw_int.bit_length() // 8
 			pw_bytes = pw_int.bit_length() // 8
@@ -901,7 +931,7 @@ Record this checksum: it will be used to verify the password file in the future
 					'Re-run the command, specifying a password length of {} or less' )
 					'Re-run the command, specifying a password length of {} or less' )
 			die(1,(m1+'\n'+m2).format(len(seed.data) * 8,good_pw_len))
 			die(1,(m1+'\n'+m2).format(len(seed.data) * 8,good_pw_len))
 
 
-		if pf == 'hex' and pw_bytes < seed.byte_len:
+		if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
 			m1 = 'WARNING: requested {} length has less entropy than underlying seed!'
 			m1 = 'WARNING: requested {} length has less entropy than underlying seed!'
 			m2 = 'Is this what you want?'
 			m2 = 'Is this what you want?'
 			if not keypress_confirm((m1+'\n'+m2).format(self.pw_info[pf].desc),default_yes=True):
 			if not keypress_confirm((m1+'\n'+m2).format(self.pw_info[pf].desc),default_yes=True):
@@ -912,6 +942,11 @@ Record this checksum: it will be used to verify the password file in the future
 		if self.pw_fmt == 'hex':
 		if self.pw_fmt == 'hex':
 			# take most significant part
 			# take most significant part
 			return hex_sec[:self.pw_len]
 			return hex_sec[:self.pw_len]
+		elif self.pw_fmt == 'bip39':
+			from mmgen.bip39 import bip39
+			pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
+			# take most significant part
+			return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
 		else:
 		else:
 			# take least significant part
 			# take least significant part
 			return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
 			return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
@@ -919,7 +954,7 @@ Record this checksum: it will be used to verify the password file in the future
 	def check_format(self,pw):
 	def check_format(self,pw):
 		if not self.pw_info[self.pw_fmt].chk_func(pw):
 		if not self.pw_info[self.pw_fmt].chk_func(pw):
 			raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
 			raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
-		pwlen = len(pw)
+		pwlen = len(pw.split()) if self.pw_fmt == 'bip39' else len(pw)
 		if pwlen != self.pw_len:
 		if pwlen != self.pw_len:
 			raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
 			raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
 		return True
 		return True
@@ -929,9 +964,32 @@ Record this checksum: it will be used to verify the password file in the future
 		# set of passwords to be generated: this is what we want.
 		# set of passwords to be generated: this is what we want.
 		# NB: In original implementation, pw_id_str was 'baseN', not 'bN'
 		# NB: In original implementation, pw_id_str was 'baseN', not 'bN'
 		scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
 		scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
+
+		if self.hex2bip39:
+			from mmgen.bip39 import bip39
+			pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
+			scramble_key = '{}:{}:{}'.format('hex',pwlen,self.pw_id_str)
+
 		from mmgen.crypto import scramble_seed
 		from mmgen.crypto import scramble_seed
 		return scramble_seed(seed,scramble_key.encode())
 		return scramble_seed(seed,scramble_key.encode())
 
 
+	def get_line(self,lines):
+		self.line_ctr += 1
+		if self.pw_fmt == 'bip39':
+			ret = lines.pop(0).split(None,self.pw_len+1)
+			if len(ret) > self.pw_len+1:
+				m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1])
+				m2 = '[bare comments not allowed in BIP39 password files]'
+				m = m1+' '+m2
+			elif len(ret) < self.pw_len+1:
+				m = 'invalid password length {}'.format(len(ret)-1)
+			else:
+				return (ret[0],' '.join(ret[1:self.pw_len+1]),'')
+			raise ValueError(m)
+		else:
+			ret = lines.pop(0).split(None,2)
+			return ret if len(ret) == 3 else ret + ['']
+
 class AddrData(MMGenObject):
 class AddrData(MMGenObject):
 	msgs = {
 	msgs = {
 	'too_many_acct_addresses': """
 	'too_many_acct_addresses': """

+ 15 - 0
mmgen/bip39.py

@@ -2092,6 +2092,21 @@ zoo
 	}
 	}
 	digits = { 'bip39': words }
 	digits = { 'bip39': words }
 
 
+	@classmethod
+	def nwords2seedlen(cls,nwords,in_bytes=False,in_hex=False):
+		for k,v in cls.constants.items():
+			if v[1] == nwords:
+				return int(k)//8 if in_bytes else int(k)//4 if in_hex else int(k)
+		raise MnemonicError('{!r}: invalid word length for BIP39 mnemonic'.format(nwords))
+
+	@classmethod
+	def seedlen2nwords(cls,seed_len,in_bytes=False,in_hex=False):
+		seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
+		try:
+			return cls.constants[str(seed_bits)][1]
+		except:
+			raise ValueError('{!r}: invalid seed length for BIP39 mnemonic'.format(seed_bits))
+
 	@classmethod
 	@classmethod
 	def tohex(cls,words,wl_id,pad=None):
 	def tohex(cls,words,wl_id,pad=None):
 		assert isinstance(words,(list,tuple)),'words must be list or tuple'
 		assert isinstance(words,(list,tuple)),'words must be list or tuple'

+ 5 - 1
mmgen/main_passgen.py

@@ -93,6 +93,10 @@ EXAMPLES:
   Generate ten base32 passwords of length {i32.dfl_len} for Alice's email account:
   Generate ten base32 passwords of length {i32.dfl_len} for Alice's email account:
   {g.prog_name} --passwd-fmt=b32 alice@nowhere.com 1-10
   {g.prog_name} --passwd-fmt=b32 alice@nowhere.com 1-10
 
 
+  Generate three BIP39 mnemonic seed phrases of length {i39.dfl_len} for Alice's
+  Trezor device:
+  {g.prog_name} --passwd-fmt=bip39 mytrezor 1-3
+
   All passwords are cryptographically unlinkable with each other, including
   All passwords are cryptographically unlinkable with each other, including
   passwords with the same format but different length, so Alice needn't worry
   passwords with the same format but different length, so Alice needn't worry
   about inadvertent reuse of private data.
   about inadvertent reuse of private data.
@@ -117,7 +121,7 @@ FMT CODES:
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
 		),
 		),
 		'notes': lambda s: s.format(
 		'notes': lambda s: s.format(
-				o=opts,g=g,i58=pwi['b58'],i32=pwi['b32'],
+				o=opts,g=g,i58=pwi['b58'],i32=pwi['b32'],i39=pwi['bip39'],
 				ml=MMGenPWIDString.max_len,
 				ml=MMGenPWIDString.max_len,
 				fs="', '".join(MMGenPWIDString.forbidden),
 				fs="', '".join(MMGenPWIDString.forbidden),
 				n_pw=help_notes('passwd'),
 				n_pw=help_notes('passwd'),

+ 6 - 0
test/ref/98831F3A-фубар@crypto.org-bip39-12[1,4,1100].pws

@@ -0,0 +1,6 @@
+# checksum: BF57 02A3 5229 CF18
+98831F3A фубар@crypto.org bip39:12 {
+  1     end later squirrel find token lens spring bubble solution museum uncover cat
+  4     auto nice blood insane buffalo client slender warrior thumb glass sing sunset
+  1100  swing rude gas illness silly cherry differ payment retreat whip sausage quiz
+}

+ 6 - 0
test/ref/98831F3A-фубар@crypto.org-bip39-18[1,4,1100].pws

@@ -0,0 +1,6 @@
+# checksum: 31D3 1656 B7DC 27CF
+98831F3A фубар@crypto.org bip39:18 {
+  1     dirt broom flag tragic wool grape image window paddle wrap enough negative jewel tiger theory have relief nephew
+  4     shrug catch neutral artwork zebra circle defy cream wild resist east custom recycle ability enact cable office inhale
+  1100  kitchen visa way various scrap odor syrup armed crater expire cushion table work better design afford umbrella market
+}

+ 6 - 0
test/ref/98831F3A-фубар@crypto.org-bip39-24[1,4,1100].pws

@@ -0,0 +1,6 @@
+# checksum: E565 3A59 7D91 4671
+98831F3A фубар@crypto.org bip39:24 {
+  1     letter sphere indoor maze surprise orange much state clinic sibling demise suggest tobacco economy resist lawsuit flock napkin report air rule catch cheap toilet
+  4     perfect squeeze jump term critic elegant satisfy apology chair soap they visual chimney keep absurd goddess time move party blade crawl enhance token uncover
+  1100  accident dad dilemma fault uphold dutch pony scout luxury puppy wave response gossip chronic denial spoon energy ugly uncover enlist regular ceiling found gesture
+}

+ 6 - 0
test/ref/98831F3A-фубар@crypto.org-hex2bip39-12[1,4,1100].pws

@@ -0,0 +1,6 @@
+# checksum: 93AD 4AE2 03D1 8A0A
+98831F3A фубар@crypto.org hex2bip39:12 {
+  1     session mobile spirit butter hand hip people rebuild flight there tower armed
+  4     cycle humor able term august budget lend cover usual grocery mountain journey
+  1100  reward tongue emotion kid humor sight lunar bread remove clinic fun valve
+}

+ 16 - 0
test/test_py_d/ts_ref.py

@@ -48,6 +48,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		'ref_passwdfile_hex_32': '98831F3A-фубар@crypto.org-hex-32[1,4,1100].pws',
 		'ref_passwdfile_hex_32': '98831F3A-фубар@crypto.org-hex-32[1,4,1100].pws',
 		'ref_passwdfile_hex_48': '98831F3A-фубар@crypto.org-hex-48[1,4,1100].pws',
 		'ref_passwdfile_hex_48': '98831F3A-фубар@crypto.org-hex-48[1,4,1100].pws',
 		'ref_passwdfile_hex_64': '98831F3A-фубар@crypto.org-hex-64[1,4,1100].pws',
 		'ref_passwdfile_hex_64': '98831F3A-фубар@crypto.org-hex-64[1,4,1100].pws',
+		'ref_passwdfile_bip39_12': '98831F3A-фубар@crypto.org-bip39-12[1,4,1100].pws',
+		'ref_passwdfile_bip39_18': '98831F3A-фубар@crypto.org-bip39-18[1,4,1100].pws',
+		'ref_passwdfile_bip39_24': '98831F3A-фубар@crypto.org-bip39-24[1,4,1100].pws',
+		'ref_passwdfile_hex2bip39_12': '98831F3A-фубар@crypto.org-hex2bip39-12[1,4,1100].pws',
 		'ref_tx_file': { # data shared with ref_altcoin, autosign
 		'ref_tx_file': { # data shared with ref_altcoin, autosign
 			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
 			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
 					'0C7115[15.86255,14,tl=1320969600].testnet.rawtx'),
 					'0C7115[15.86255,14,tl=1320969600].testnet.rawtx'),
@@ -90,6 +94,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		'ref_passwdfile_hex_32_chksum': '05C7 3678 E25E BC32',
 		'ref_passwdfile_hex_32_chksum': '05C7 3678 E25E BC32',
 		'ref_passwdfile_hex_48_chksum': '7DBB FFD0 633E DE6F',
 		'ref_passwdfile_hex_48_chksum': '7DBB FFD0 633E DE6F',
 		'ref_passwdfile_hex_64_chksum': 'F11D CB0A 8AE3 4D21',
 		'ref_passwdfile_hex_64_chksum': 'F11D CB0A 8AE3 4D21',
+		'ref_passwdfile_bip39_12_chksum': 'BF57 02A3 5229 CF18',
+		'ref_passwdfile_bip39_18_chksum': '31D3 1656 B7DC 27CF',
+		'ref_passwdfile_bip39_24_chksum': 'E565 3A59 7D91 4671',
+		'ref_passwdfile_hex2bip39_12_chksum': '93AD 4AE2 03D1 8A0A',
 	}
 	}
 	cmd_group = ( # TODO: move to tooltest2
 	cmd_group = ( # TODO: move to tooltest2
 		('ref_words_to_subwallet_chk1','subwallet generation from reference words file (long subseed)'),
 		('ref_words_to_subwallet_chk1','subwallet generation from reference words file (long subseed)'),
@@ -110,6 +118,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		('ref_passwdfile_chk_hex_32','saved reference password file (hexadecimal, 32 chars)'),
 		('ref_passwdfile_chk_hex_32','saved reference password file (hexadecimal, 32 chars)'),
 		('ref_passwdfile_chk_hex_48','saved reference password file (hexadecimal, 48 chars)'),
 		('ref_passwdfile_chk_hex_48','saved reference password file (hexadecimal, 48 chars)'),
 		('ref_passwdfile_chk_hex_64','saved reference password file (hexadecimal, 64 chars)'),
 		('ref_passwdfile_chk_hex_64','saved reference password file (hexadecimal, 64 chars)'),
+		('ref_passwdfile_chk_bip39_12','saved reference password file (BIP39, 12 words)'),
+		('ref_passwdfile_chk_bip39_18','saved reference password file (BIP39, 18 words)'),
+		('ref_passwdfile_chk_bip39_24','saved reference password file (BIP39, 24 words)'),
+		('ref_passwdfile_chk_hex2bip39_12','saved reference password file (hex-to-BIP39, 12 words)'),
 
 
 #	Create the fake inputs:
 #	Create the fake inputs:
 #	('txcreate8',          'transaction creation (8)'),
 #	('txcreate8',          'transaction creation (8)'),
@@ -233,6 +245,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 	def ref_passwdfile_chk_hex_32(self): return self.ref_passwdfile_chk(key='hex_32',pat='Hexadec.*len.* 32\n')
 	def ref_passwdfile_chk_hex_32(self): return self.ref_passwdfile_chk(key='hex_32',pat='Hexadec.*len.* 32\n')
 	def ref_passwdfile_chk_hex_48(self): return self.ref_passwdfile_chk(key='hex_48',pat='Hexadec.*len.* 48\n')
 	def ref_passwdfile_chk_hex_48(self): return self.ref_passwdfile_chk(key='hex_48',pat='Hexadec.*len.* 48\n')
 	def ref_passwdfile_chk_hex_64(self): return self.ref_passwdfile_chk(key='hex_64',pat='Hexadec.*len.* 64\n')
 	def ref_passwdfile_chk_hex_64(self): return self.ref_passwdfile_chk(key='hex_64',pat='Hexadec.*len.* 64\n')
+	def ref_passwdfile_chk_bip39_12(self): return self.ref_passwdfile_chk(key='bip39_12',pat='BIP39.*len.* 12\n')
+	def ref_passwdfile_chk_bip39_18(self): return self.ref_passwdfile_chk(key='bip39_18',pat='BIP39.*len.* 18\n')
+	def ref_passwdfile_chk_bip39_24(self): return self.ref_passwdfile_chk(key='bip39_24',pat='BIP39.*len.* 24\n')
+	def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat='BIP39.*len.* 12\n')
 
 
 	def ref_tx_chk(self):
 	def ref_tx_chk(self):
 		fn = self.sources['ref_tx_file'][g.coin.lower()][bool(self.tn_ext)]
 		fn = self.sources['ref_tx_file'][g.coin.lower()][bool(self.tn_ext)]

+ 28 - 0
test/test_py_d/ts_ref_3seed.py

@@ -77,6 +77,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		'ref_b32passwdgen_1': '37B6 C218 2ABC 7508',
 		'ref_b32passwdgen_1': '37B6 C218 2ABC 7508',
 		'ref_hexpasswdgen_1': '8E99 E696 84CE E7D5',
 		'ref_hexpasswdgen_1': '8E99 E696 84CE E7D5',
 		'ref_hexpasswdgen_half_1': '8E99 E696 84CE E7D5',
 		'ref_hexpasswdgen_half_1': '8E99 E696 84CE E7D5',
+		'ref_bip39_12_passwdgen_1': '834F CF45 0B33 8AF0',
+		'ref_bip39_18_passwdgen_1': '834F CF45 0B33 8AF0',
+		'ref_bip39_24_passwdgen_1': '834F CF45 0B33 8AF0',
+		'ref_hex2bip39_24_passwdgen_1': '91AF E735 A31D 72A0',
 		'refaddrgen_legacy_2': {
 		'refaddrgen_legacy_2': {
 			'btc': ('8C17 A5FA 0470 6E89','764C 66F9 7502 AAEA'),
 			'btc': ('8C17 A5FA 0470 6E89','764C 66F9 7502 AAEA'),
 			'ltc': ('2B77 A009 D5D0 22AD','51D1 979D 0A35 F24B'),
 			'ltc': ('2B77 A009 D5D0 22AD','51D1 979D 0A35 F24B'),
@@ -114,6 +118,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		'ref_b32passwdgen_2': '2A28 C5C7 36EC 217A',
 		'ref_b32passwdgen_2': '2A28 C5C7 36EC 217A',
 		'ref_hexpasswdgen_2': '88F9 0D48 3A7E 7CC2',
 		'ref_hexpasswdgen_2': '88F9 0D48 3A7E 7CC2',
 		'ref_hexpasswdgen_half_2': '59F3 8F48 861E 1186',
 		'ref_hexpasswdgen_half_2': '59F3 8F48 861E 1186',
+		'ref_bip39_12_passwdgen_2': 'D32D B8D7 A840 250B',
+		'ref_bip39_18_passwdgen_2': '0FAA 78DD A6BA 31AD',
+		'ref_bip39_24_passwdgen_2': '0FAA 78DD A6BA 31AD',
+		'ref_hex2bip39_24_passwdgen_2': '0E8E 23C9 923F 7C2D',
 		'refaddrgen_legacy_3': {
 		'refaddrgen_legacy_3': {
 			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
 			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
 			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
 			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
@@ -151,6 +159,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		'ref_b32passwdgen_3': 'F6C1 CDFB 97D9 FCAE',
 		'ref_b32passwdgen_3': 'F6C1 CDFB 97D9 FCAE',
 		'ref_hexpasswdgen_3': 'BD4F A0AC 8628 4BE4',
 		'ref_hexpasswdgen_3': 'BD4F A0AC 8628 4BE4',
 		'ref_hexpasswdgen_half_3': 'FBDD F733 FFB9 21C1',
 		'ref_hexpasswdgen_half_3': 'FBDD F733 FFB9 21C1',
+		'ref_bip39_12_passwdgen_3': 'A86E EA14 974A 1B0E',
+		'ref_bip39_18_passwdgen_3': 'EF87 9904 88E2 5884',
+		'ref_bip39_24_passwdgen_3': 'EBE8 2A8F 8F8C 7DBD',
+		'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E',
 	}
 	}
 	cmd_group = (
 	cmd_group = (
 		# reading
 		# reading
@@ -182,6 +194,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		('ref_b32passwdgen',     (['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
 		('ref_b32passwdgen',     (['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
 		('ref_hexpasswdgen',     (['mmdat',pwfile],'new refwallet passwd file chksum (hex)')),
 		('ref_hexpasswdgen',     (['mmdat',pwfile],'new refwallet passwd file chksum (hex)')),
 		('ref_hexpasswdgen_half',(['mmdat',pwfile],'new refwallet passwd file chksum (hex, half-length)')),
 		('ref_hexpasswdgen_half',(['mmdat',pwfile],'new refwallet passwd file chksum (hex, half-length)')),
+		('ref_bip39_12_passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (BIP39, 12 words)')),
+		('ref_bip39_18_passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (BIP39, up to 18 words)')),
+		('ref_bip39_24_passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (BIP39, up to 24 words)')),
+		('ref_hex2bip39_24_passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')),
 	)
 	)
 
 
 	def __init__(self,trunner,cfgs,spawn):
 	def __init__(self,trunner,cfgs,spawn):
@@ -339,3 +355,15 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 	def ref_hexpasswdgen_half(self,wf,pf):
 	def ref_hexpasswdgen_half(self,wf,pf):
 		ea = ['--passwd-fmt=hex','--passwd-len=h','--accept-defaults']
 		ea = ['--passwd-fmt=hex','--passwd-len=h','--accept-defaults']
 		return self.addrgen(wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea,stdout=1)
 		return self.addrgen(wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea,stdout=1)
+
+	def ref_bip39_passwdgen(self,wf,pf,req_pw_len,pw_fmt='bip39',stdout=False):
+		pw_len = min(req_pw_len,{'1':12,'2':18,'3':24}[self.test_name[-1]])
+		ea = ['--passwd-fmt='+pw_fmt,'--passwd-len={}'.format(pw_len),'--accept-defaults']
+		return self.addrgen(
+			wf,pf,check_ref=True,ftype='passbip39',id_str='фубар@crypto.org',extra_args=ea,stdout=stdout)
+
+	def ref_bip39_12_passwdgen(self,wf,pf): return self.ref_bip39_passwdgen(wf,pf,12,stdout=True)
+	def ref_bip39_18_passwdgen(self,wf,pf): return self.ref_bip39_passwdgen(wf,pf,18,stdout=True)
+	def ref_bip39_24_passwdgen(self,wf,pf): return self.ref_bip39_passwdgen(wf,pf,24)
+
+	def ref_hex2bip39_24_passwdgen(self,wf,pf): return self.ref_bip39_passwdgen(wf,pf,24,'hex2bip39')