addr.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. addr.py: Address generation/display routines for the MMGen suite
  20. """
  21. from hashlib import sha256,sha512
  22. from .common import *
  23. from .obj import *
  24. from .baseconv import *
  25. from .protocol import init_proto,hash160
  26. pnm = g.proj_name
  27. def dmsg_sc(desc,data):
  28. if g.debug_addrlist:
  29. Msg(f'sc_debug_{desc}: {data}')
  30. class AddrGenerator(MMGenObject):
  31. def __new__(cls,proto,addr_type):
  32. if type(addr_type) == str:
  33. addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
  34. elif type(addr_type) == MMGenAddrType:
  35. assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
  36. else:
  37. raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
  38. addr_generators = {
  39. 'p2pkh': AddrGeneratorP2PKH,
  40. 'segwit': AddrGeneratorSegwit,
  41. 'bech32': AddrGeneratorBech32,
  42. 'ethereum': AddrGeneratorEthereum,
  43. 'zcash_z': AddrGeneratorZcashZ,
  44. 'monero': AddrGeneratorMonero,
  45. }
  46. me = super(cls,cls).__new__(addr_generators[addr_type.gen_method])
  47. me.desc = type(me).__name__
  48. me.proto = proto
  49. me.addr_type = addr_type
  50. me.pubkey_type = addr_type.pubkey_type
  51. return me
  52. class AddrGeneratorP2PKH(AddrGenerator):
  53. def to_addr(self,pubhex):
  54. assert pubhex.privkey.pubkey_type == self.pubkey_type
  55. return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False))
  56. def to_segwit_redeem_script(self,pubhex):
  57. raise NotImplementedError('Segwit redeem script not supported by this address type')
  58. class AddrGeneratorSegwit(AddrGenerator):
  59. def to_addr(self,pubhex):
  60. assert pubhex.privkey.pubkey_type == self.pubkey_type
  61. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  62. return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex))
  63. def to_segwit_redeem_script(self,pubhex):
  64. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  65. return HexStr(self.proto.pubhex2redeem_script(pubhex))
  66. class AddrGeneratorBech32(AddrGenerator):
  67. def to_addr(self,pubhex):
  68. assert pubhex.privkey.pubkey_type == self.pubkey_type
  69. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  70. return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex)))
  71. def to_segwit_redeem_script(self,pubhex):
  72. raise NotImplementedError('Segwit redeem script not supported by this address type')
  73. class AddrGeneratorEthereum(AddrGenerator):
  74. def __init__(self,proto,addr_type):
  75. try:
  76. assert not g.use_internal_keccak_module
  77. from sha3 import keccak_256
  78. except:
  79. from .keccak import keccak_256
  80. self.keccak_256 = keccak_256
  81. from .protocol import hash256
  82. self.hash256 = hash256
  83. def to_addr(self,pubhex):
  84. assert pubhex.privkey.pubkey_type == self.pubkey_type
  85. return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
  86. def to_wallet_passwd(self,sk_hex):
  87. return WalletPassword(self.hash256(sk_hex)[:32])
  88. def to_segwit_redeem_script(self,pubhex):
  89. raise NotImplementedError('Segwit redeem script not supported by this address type')
  90. # github.com/FiloSottile/zcash-mini/zcash/address.go
  91. class AddrGeneratorZcashZ(AddrGenerator):
  92. def zhash256(self,s,t):
  93. s = bytearray(s + bytes(32))
  94. s[0] |= 0xc0
  95. s[32] = t
  96. from .sha2 import Sha256
  97. return Sha256(s,preprocess=False).digest()
  98. def to_addr(self,pubhex): # pubhex is really privhex
  99. assert pubhex.privkey.pubkey_type == self.pubkey_type
  100. key = bytes.fromhex(pubhex)
  101. assert len(key) == 32, f'{len(key)}: incorrect privkey length'
  102. from nacl.bindings import crypto_scalarmult_base
  103. p2 = crypto_scalarmult_base(self.zhash256(key,1))
  104. from .protocol import _b58chk_encode
  105. ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z')
  106. ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2)
  107. return CoinAddr(self.proto,ret)
  108. def to_viewkey(self,pubhex): # pubhex is really privhex
  109. key = bytes.fromhex(pubhex)
  110. assert len(key) == 32, f'{len(key)}: incorrect privkey length'
  111. vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
  112. vk[32] &= 0xf8
  113. vk[63] &= 0x7f
  114. vk[63] |= 0x40
  115. from .protocol import _b58chk_encode
  116. ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey')
  117. ret = _b58chk_encode(ver_bytes + vk)
  118. return ZcashViewKey(self.proto,ret)
  119. def to_segwit_redeem_script(self,pubhex):
  120. raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
  121. class AddrGeneratorMonero(AddrGenerator):
  122. def __init__(self,proto,addr_type):
  123. try:
  124. assert not g.use_internal_keccak_module
  125. from sha3 import keccak_256
  126. except:
  127. from .keccak import keccak_256
  128. self.keccak_256 = keccak_256
  129. from .protocol import hash256
  130. self.hash256 = hash256
  131. if getattr(opt,'use_old_ed25519',False):
  132. from .ed25519 import edwards,encodepoint,B,scalarmult
  133. else:
  134. from .ed25519ll_djbec import scalarmult
  135. from .ed25519 import edwards,encodepoint,B
  136. self.edwards = edwards
  137. self.encodepoint = encodepoint
  138. self.scalarmult = scalarmult
  139. self.B = B
  140. def b58enc(self,addr_bytes):
  141. enc = baseconv.frombytes
  142. l = len(addr_bytes)
  143. a = ''.join([enc(addr_bytes[i*8:i*8+8],'b58',pad=11,tostr=True) for i in range(l//8)])
  144. b = enc(addr_bytes[l-l%8:],'b58',pad=7,tostr=True)
  145. return a + b
  146. def to_addr(self,sk_hex): # sk_hex instead of pubhex
  147. assert sk_hex.privkey.pubkey_type == self.pubkey_type
  148. # Source and license for scalarmultbase function:
  149. # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
  150. # Copyright (c) 2014-2016, The Monero Project
  151. # All rights reserved.
  152. def scalarmultbase(e):
  153. if e == 0: return [0, 1]
  154. Q = self.scalarmult(self.B, e//2)
  155. Q = self.edwards(Q, Q)
  156. if e & 1: Q = self.edwards(Q, self.B)
  157. return Q
  158. def hex2int_le(hexstr):
  159. return int((bytes.fromhex(hexstr)[::-1]).hex(),16)
  160. vk_hex = self.to_viewkey(sk_hex)
  161. pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
  162. pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
  163. addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
  164. return CoinAddr(
  165. proto = self.proto,
  166. addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) )
  167. def to_wallet_passwd(self,sk_hex):
  168. return WalletPassword(self.hash256(sk_hex)[:32])
  169. def to_viewkey(self,sk_hex):
  170. assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length'
  171. return MoneroViewKey(
  172. self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
  173. def to_segwit_redeem_script(self,sk_hex):
  174. raise NotImplementedError('Monero addresses incompatible with Segwit')
  175. class KeyGenerator(MMGenObject):
  176. def __new__(cls,proto,addr_type,generator=None,silent=False):
  177. if type(addr_type) == str: # allow override w/o check
  178. pubkey_type = addr_type
  179. elif type(addr_type) == MMGenAddrType:
  180. assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}'
  181. pubkey_type = addr_type.pubkey_type
  182. else:
  183. raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
  184. if pubkey_type == 'std':
  185. if cls.test_for_secp256k1(silent=silent) and generator != 1:
  186. if not opt.key_generator or opt.key_generator == 2 or generator == 2:
  187. me = super(cls,cls).__new__(KeyGeneratorSecp256k1)
  188. else:
  189. qmsg('Using (slow) native Python ECDSA library for address generation')
  190. me = super(cls,cls).__new__(KeyGeneratorPython)
  191. elif pubkey_type in ('zcash_z','monero'):
  192. me = super(cls,cls).__new__(KeyGeneratorDummy)
  193. me.desc = 'mmgen-'+pubkey_type
  194. else:
  195. raise ValueError(f'{pubkey_type}: invalid pubkey_type argument')
  196. me.proto = proto
  197. return me
  198. @classmethod
  199. def test_for_secp256k1(self,silent=False):
  200. try:
  201. from .secp256k1 import priv2pub
  202. m = 'Unable to execute priv2pub() from secp256k1 extension module'
  203. assert priv2pub(bytes.fromhex('deadbeef'*8),1),m
  204. return True
  205. except Exception as e:
  206. if not silent:
  207. ymsg(str(e))
  208. return False
  209. import ecdsa
  210. class KeyGeneratorPython(KeyGenerator):
  211. desc = 'mmgen-python-ecdsa'
  212. def __init__(self,*args,**kwargs):
  213. # secp256k1: http://www.oid-info.com/get/1.3.132.0.10
  214. p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
  215. r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
  216. b = 0x0000000000000000000000000000000000000000000000000000000000000007
  217. a = 0x0000000000000000000000000000000000000000000000000000000000000000
  218. Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
  219. Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
  220. curve_fp = ecdsa.ellipticcurve.CurveFp(p,a,b)
  221. G = ecdsa.ellipticcurve.Point(curve_fp,Gx,Gy,r)
  222. oid = (1,3,132,0,10)
  223. self.secp256k1 = ecdsa.curves.Curve('secp256k1',curve_fp,G,oid)
  224. # devdoc/guide_wallets.md:
  225. # Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
  226. # 0x02 depending on whether they're greater or less than the midpoint of the curve.
  227. def privnum2pubhex(self,numpriv,compressed=False):
  228. pko = ecdsa.SigningKey.from_secret_exponent(numpriv,self.secp256k1)
  229. # pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
  230. pubkey = pko.get_verifying_key().to_string().hex()
  231. if compressed: # discard Y coord, replace with appropriate version byte
  232. # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
  233. return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
  234. else:
  235. return '04' + pubkey
  236. def to_pubhex(self,privhex):
  237. assert type(privhex) == PrivKey
  238. return PubKey(
  239. s = self.privnum2pubhex(int(privhex,16),compressed=privhex.compressed),
  240. privkey = privhex )
  241. class KeyGeneratorSecp256k1(KeyGenerator):
  242. desc = 'mmgen-secp256k1'
  243. def to_pubhex(self,privhex):
  244. assert type(privhex) == PrivKey
  245. from .secp256k1 import priv2pub
  246. return PubKey(
  247. s = priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),
  248. privkey = privhex )
  249. class KeyGeneratorDummy(KeyGenerator):
  250. desc = 'mmgen-dummy'
  251. def to_pubhex(self,privhex):
  252. assert type(privhex) == PrivKey
  253. return PubKey(
  254. s = privhex,
  255. privkey = privhex )
  256. class AddrListEntryBase(MMGenListItem):
  257. invalid_attrs = {'proto'}
  258. def __init__(self,proto,**kwargs):
  259. self.__dict__['proto'] = proto
  260. MMGenListItem.__init__(self,**kwargs)
  261. class AddrListEntry(AddrListEntryBase):
  262. addr = ListItemAttr('CoinAddr',include_proto=True)
  263. idx = ListItemAttr('AddrIdx') # not present in flat addrlists
  264. label = ListItemAttr('TwComment',reassign_ok=True)
  265. sec = ListItemAttr('PrivKey',include_proto=True)
  266. viewkey = ListItemAttr('ViewKey',include_proto=True)
  267. wallet_passwd = ListItemAttr('WalletPassword')
  268. class PasswordListEntry(AddrListEntryBase):
  269. passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
  270. idx = ImmutableAttr('AddrIdx')
  271. label = ListItemAttr('TwComment',reassign_ok=True)
  272. sec = ListItemAttr('PrivKey',include_proto=True)
  273. class AddrListChksum(str,Hilite):
  274. color = 'pink'
  275. trunc_ok = False
  276. def __new__(cls,addrlist):
  277. ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
  278. if ea == None: ea = ()
  279. lines = [' '.join(
  280. addrlist.chksum_rec_f(e) +
  281. tuple(getattr(e,a) for a in ea if getattr(e,a))
  282. ) for e in addrlist.data]
  283. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  284. class AddrListIDStr(str,Hilite):
  285. color = 'green'
  286. trunc_ok = False
  287. def __new__(cls,addrlist,fmt_str=None):
  288. idxs = [e.idx for e in addrlist.data]
  289. prev = idxs[0]
  290. ret = prev,
  291. for i in idxs[1:]:
  292. if i == prev + 1:
  293. if i == idxs[-1]: ret += '-', i
  294. else:
  295. if prev != ret[-1]: ret += '-', prev
  296. ret += ',', i
  297. prev = i
  298. s = ''.join(map(str,ret))
  299. if fmt_str:
  300. ret = fmt_str.format(s)
  301. else:
  302. bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
  303. mt = addrlist.al_id.mmtype
  304. ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
  305. dmsg_sc('id_str',ret[8:].split('[')[0])
  306. return str.__new__(cls,ret)
  307. class AddrList(MMGenObject): # Address info for a single seed ID
  308. msgs = {
  309. 'file_header': """
  310. # {pnm} address file
  311. #
  312. # This file is editable.
  313. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  314. # A text label of {n} screen cells or less may be added to the right of each
  315. # address, and it will be appended to the tracking wallet label upon import.
  316. # The label may contain any printable ASCII symbol.
  317. """.strip().format(n=TwComment.max_screen_width,pnm=pnm),
  318. 'record_chksum': """
  319. Record this checksum: it will be used to verify the address file in the future
  320. """.strip(),
  321. 'check_chksum': 'Check this value against your records',
  322. 'removed_dup_keys': """
  323. Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
  324. """.strip().format(pnm=pnm)
  325. }
  326. entry_type = AddrListEntry
  327. main_attr = 'addr'
  328. data_desc = 'address'
  329. file_desc = 'addresses'
  330. gen_desc = 'address'
  331. gen_desc_pl = 'es'
  332. gen_addrs = True
  333. gen_passwds = False
  334. gen_keys = False
  335. has_keys = False
  336. ext = 'addrs'
  337. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
  338. line_ctr = 0
  339. def __init__(self,proto,
  340. addrfile = '',
  341. al_id = '',
  342. adata = [],
  343. seed = '',
  344. addr_idxs = '',
  345. src = '',
  346. addrlist = '',
  347. keylist = '',
  348. mmtype = None,
  349. skip_key_address_validity_check = False,
  350. ):
  351. self.skip_ka_check = skip_key_address_validity_check
  352. do_chksum = True
  353. self.update_msgs()
  354. mmtype = mmtype or proto.dfl_mmtype
  355. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  356. from .protocol import CoinProtocol
  357. self.bitcoin_addrtypes = tuple(
  358. MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
  359. self.proto = proto
  360. if seed and addr_idxs: # data from seed + idxs
  361. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  362. adata = self.generate(seed,addr_idxs)
  363. elif addrfile: # data from MMGen address file
  364. self.infile = addrfile
  365. adata = self.parse_file(addrfile) # sets self.al_id
  366. elif al_id and adata: # data from tracking wallet
  367. self.al_id = al_id
  368. do_chksum = False
  369. elif addrlist: # data from flat address list
  370. self.al_id = None
  371. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in set(addrlist)])
  372. elif keylist: # data from flat key list
  373. self.al_id = None
  374. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in set(keylist)])
  375. elif seed or addr_idxs:
  376. die(3,'Must specify both seed and addr indexes')
  377. elif al_id or adata:
  378. die(3,'Must specify both al_id and adata')
  379. else:
  380. die(3,'Incorrect arguments for {}'.format(type(self).__name__))
  381. # al_id,adata now set
  382. self.data = adata
  383. self.num_addrs = len(adata)
  384. self.fmt_data = ''
  385. self.chksum = None
  386. if self.al_id == None: return
  387. self.id_str = AddrListIDStr(self)
  388. if type(self) == KeyList: return
  389. if do_chksum:
  390. self.chksum = AddrListChksum(self)
  391. qmsg('Checksum for {} data {}: {}'.format(
  392. self.data_desc,self.id_str.hl(),self.chksum.hl()))
  393. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  394. def update_msgs(self):
  395. self.msgs = AddrList.msgs
  396. self.msgs.update(type(self).msgs)
  397. def generate(self,seed,addrnums):
  398. assert type(addrnums) is AddrIdxList
  399. seed = self.scramble_seed(seed.data)
  400. dmsg_sc('seed',seed[:8].hex())
  401. compressed = self.al_id.mmtype.compressed
  402. pubkey_type = self.al_id.mmtype.pubkey_type
  403. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  404. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  405. if self.gen_addrs:
  406. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  407. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  408. t_addrs,num,pos,out = len(addrnums),0,0,AddrListData()
  409. le = self.entry_type
  410. while pos != t_addrs:
  411. seed = sha512(seed).digest()
  412. num += 1 # round
  413. if num != addrnums[pos]: continue
  414. pos += 1
  415. if not g.debug:
  416. qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs))
  417. e = le(proto=self.proto,idx=num)
  418. # Secret key is double sha256 of seed hash round /num/
  419. e.sec = PrivKey(
  420. self.proto,
  421. sha256(sha256(seed).digest()).digest(),
  422. compressed = compressed,
  423. pubkey_type = pubkey_type )
  424. if self.gen_addrs:
  425. pubhex = kg.to_pubhex(e.sec)
  426. e.addr = ag.to_addr(pubhex)
  427. if gen_viewkey:
  428. e.viewkey = ag.to_viewkey(pubhex)
  429. if gen_wallet_passwd:
  430. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  431. if type(self) == PasswordList:
  432. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  433. dmsg('Key {:>03}: {}'.format(pos,e.passwd))
  434. out.append(e)
  435. if g.debug_addrlist:
  436. Msg('generate():\n{}'.format(e.pfmt()))
  437. qmsg('\r{}: {} {}{} generated{}'.format(
  438. self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
  439. return out
  440. def check_format(self,addr):
  441. return True # format is checked when added to list entry object
  442. def scramble_seed(self,seed):
  443. is_btcfork = self.proto.base_coin == 'BTC'
  444. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  445. dmsg_sc('str','(none)')
  446. return seed
  447. if self.proto.base_coin == 'ETH':
  448. scramble_key = self.proto.coin.lower()
  449. else:
  450. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  451. from .crypto import scramble_seed
  452. if self.proto.testnet:
  453. scramble_key += ':' + self.proto.network
  454. dmsg_sc('str',scramble_key)
  455. return scramble_seed(seed,scramble_key.encode())
  456. def encrypt(self,desc='new key list'):
  457. from .crypto import mmgen_encrypt
  458. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  459. self.ext += '.'+g.mmenc_ext
  460. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  461. tn = ('.' + self.proto.network) if self.proto.testnet else ''
  462. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  463. ask_tty = self.has_keys and not opt.quiet
  464. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  465. def idxs(self):
  466. return [e.idx for e in self.data]
  467. def addrs(self):
  468. return ['{}:{}'.format(self.al_id.sid,e.idx) for e in self.data]
  469. def addrpairs(self):
  470. return [(e.idx,e.addr) for e in self.data]
  471. def coinaddrs(self):
  472. return [e.addr for e in self.data]
  473. def comments(self):
  474. return [e.label for e in self.data]
  475. def entry(self,idx):
  476. for e in self.data:
  477. if idx == e.idx: return e
  478. def coinaddr(self,idx):
  479. for e in self.data:
  480. if idx == e.idx: return e.addr
  481. def comment(self,idx):
  482. for e in self.data:
  483. if idx == e.idx: return e.label
  484. def set_comment(self,idx,comment):
  485. for e in self.data:
  486. if idx == e.idx:
  487. e.label = comment
  488. def make_reverse_dict_addrlist(self,coinaddrs):
  489. d = MMGenDict()
  490. b = coinaddrs
  491. for e in self.data:
  492. try:
  493. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  494. except ValueError:
  495. pass
  496. return d
  497. def remove_dup_keys(self,cmplist):
  498. assert self.has_keys
  499. pop_list = []
  500. for n,d in enumerate(self.data):
  501. for e in cmplist.data:
  502. if e.sec.wif == d.sec.wif:
  503. pop_list.append(n)
  504. for n in reversed(pop_list): self.data.pop(n)
  505. if pop_list:
  506. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed)))
  507. def add_wifs(self,key_list):
  508. if not key_list: return
  509. for d in self.data:
  510. for e in key_list.data:
  511. if e.addr and e.sec and e.addr == d.addr:
  512. d.sec = e.sec
  513. def list_missing(self,key):
  514. return [d.addr for d in self.data if not getattr(d,key)]
  515. def generate_addrs_from_keys(self):
  516. # assume that the first listed mmtype is valid for flat key list
  517. at = self.proto.addr_type(self.proto.mmtypes[0])
  518. kg = KeyGenerator(self.proto,at.pubkey_type)
  519. ag = AddrGenerator(self.proto,at)
  520. d = self.data
  521. for n,e in enumerate(d,1):
  522. qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
  523. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  524. if g.debug_addrlist:
  525. Msg('generate_addrs_from_keys():\n{}'.format(e.pfmt()))
  526. qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
  527. def make_label(self):
  528. bc,mt = self.proto.base_coin,self.al_id.mmtype
  529. l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc]
  530. l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()]
  531. l_tn = [] if not self.proto.testnet else [self.proto.network.upper()]
  532. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  533. return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  534. def format(self,add_comments=False):
  535. out = [self.msgs['file_header']+'\n']
  536. if self.chksum:
  537. out.append('# {} data checksum for {}: {}'.format(
  538. capfirst(self.data_desc),self.id_str,self.chksum))
  539. out.append('# Record this value to a secure location.\n')
  540. lbl = self.make_label()
  541. dmsg_sc('lbl',lbl[9:])
  542. out.append('{} {{'.format(lbl))
  543. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  544. for e in self.data:
  545. c = ' '+e.label if add_comments and e.label else ''
  546. if type(self) == KeyList:
  547. out.append(fs.format(e.idx,'{}: {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  548. elif type(self) == PasswordList:
  549. out.append(fs.format(e.idx,e.passwd,c))
  550. else: # First line with idx
  551. out.append(fs.format(e.idx,e.addr,c))
  552. if self.has_keys:
  553. if opt.b16:
  554. out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c))
  555. out.append(fs.format('','{}: {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  556. for k in ('viewkey','wallet_passwd'):
  557. v = getattr(e,k)
  558. if v: out.append(fs.format('','{}: {}'.format(k,v),c))
  559. out.append('}')
  560. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  561. def get_line(self,lines):
  562. ret = lines.pop(0).split(None,2)
  563. self.line_ctr += 1
  564. if ret[0] == 'orig_hex:': # hacky
  565. ret = lines.pop(0).split(None,2)
  566. self.line_ctr += 1
  567. return ret if len(ret) == 3 else ret + ['']
  568. def parse_file_body(self,lines):
  569. ret = AddrListData()
  570. le = self.entry_type
  571. iifs = "{!r}: invalid identifier [expected '{}:']"
  572. while lines:
  573. idx,addr,lbl = self.get_line(lines)
  574. assert is_mmgen_idx(idx), f'invalid address index {idx!r}'
  575. self.check_format(addr)
  576. a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl })
  577. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  578. d = self.get_line(lines)
  579. assert d[0] == self.al_id.mmtype.wif_label+':',iifs.format(d[0],self.al_id.mmtype.wif_label)
  580. a.sec = PrivKey(proto=self.proto,wif=d[1])
  581. for k,dtype,add_proto in (
  582. ('viewkey',ViewKey,True),
  583. ('wallet_passwd',WalletPassword,False) ):
  584. if k in self.al_id.mmtype.extra_attrs:
  585. d = self.get_line(lines)
  586. assert d[0] == k+':',iifs.format(d[0],k)
  587. setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) )
  588. ret.append(a)
  589. if self.has_keys and not self.skip_ka_check:
  590. if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'):
  591. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  592. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  593. llen = len(ret)
  594. for n,e in enumerate(ret):
  595. qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen))
  596. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  597. "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr))
  598. qmsg(' - done')
  599. return ret
  600. def parse_file(self,fn,buf=[],exit_on_error=True):
  601. def parse_addrfile_label(lbl):
  602. """
  603. label examples:
  604. - Bitcoin legacy mainnet: no label
  605. - Bitcoin legacy testnet: 'LEGACY:TESTNET'
  606. - Bitcoin Segwit: 'SEGWIT'
  607. - Bitcoin Segwit testnet: 'SEGWIT:TESTNET'
  608. - Bitcoin Bech32 regtest: 'BECH32:REGTEST'
  609. - Litecoin legacy mainnet: 'LTC'
  610. - Litecoin Bech32 mainnet: 'LTC:BECH32'
  611. - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET'
  612. - Ethereum mainnet: 'ETH'
  613. - Ethereum Classic mainnet: 'ETC'
  614. - Ethereum regtest: 'ETH:REGTEST'
  615. """
  616. lbl = lbl.lower()
  617. # remove the network component:
  618. if lbl.endswith(':testnet'):
  619. network = 'testnet'
  620. lbl = lbl[:-8]
  621. elif lbl.endswith(':regtest'):
  622. network = 'regtest'
  623. lbl = lbl[:-8]
  624. else:
  625. network = 'mainnet'
  626. if lbl in self.bitcoin_addrtypes:
  627. coin,mmtype_key = ( 'BTC', lbl )
  628. elif ':' in lbl: # first component is coin, second is mmtype_key
  629. coin,mmtype_key = lbl.split(':')
  630. else: # only component is coin
  631. coin,mmtype_key = ( lbl, None )
  632. proto = init_proto(coin=coin,network=network)
  633. if mmtype_key == None:
  634. mmtype_key = proto.mmtypes[0]
  635. return ( proto, proto.addr_type(mmtype_key) )
  636. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  637. try:
  638. assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
  639. ls = lines[0].split()
  640. assert 1 < len(ls) < 5, f'Invalid first line for {self.gen_desc} file: {lines[0]!r}'
  641. assert ls.pop() == '{', f'{ls!r}: invalid first line'
  642. assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line'
  643. sid = ls.pop(0)
  644. assert is_mmgen_seed_id(sid), f'{sid!r}: invalid Seed ID'
  645. if type(self) == PasswordList and len(ls) == 2:
  646. ss = ls.pop().split(':')
  647. assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)'
  648. self.set_pw_fmt(ss[0])
  649. self.set_pw_len(ss[1])
  650. self.pw_id_str = MMGenPWIDString(ls.pop())
  651. proto = init_proto('btc')# FIXME: dummy protocol
  652. mmtype = MMGenPasswordType(proto,'P')
  653. elif len(ls) == 1:
  654. proto,mmtype = parse_addrfile_label(ls[0])
  655. elif len(ls) == 0:
  656. proto = init_proto('btc')
  657. mmtype = proto.addr_type('L')
  658. else:
  659. raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}')
  660. if type(self) != PasswordList:
  661. if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network:
  662. """
  663. Having caller supply protocol and checking address file protocol against it here
  664. allows us to catch all mismatches in one place. This behavior differs from that of
  665. transaction files, which determine the protocol independently, requiring the caller
  666. to check for protocol mismatches (e.g. MMGenTX.check_correct_chain())
  667. """
  668. raise ValueError(
  669. f'{self.data_desc} file is '
  670. + f'{proto.base_coin} {proto.network} but protocol is '
  671. + f'{self.proto.base_coin} {self.proto.network}' )
  672. self.base_coin = proto.base_coin
  673. self.network = proto.network
  674. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  675. data = self.parse_file_body(lines[1:-1])
  676. assert isinstance(data,list),'Invalid file body data'
  677. except Exception as e:
  678. m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
  679. self.data_desc,
  680. self.infile,
  681. (f', content line {self.line_ctr}' if self.line_ctr else ''),
  682. e )
  683. if exit_on_error:
  684. die(3,m)
  685. else:
  686. msg(m)
  687. return False
  688. return data
  689. class KeyAddrList(AddrList):
  690. data_desc = 'key-address'
  691. file_desc = 'secret keys'
  692. gen_desc = 'key/address pair'
  693. gen_desc_pl = 's'
  694. gen_addrs = True
  695. gen_keys = True
  696. has_keys = True
  697. ext = 'akeys'
  698. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  699. class KeyList(AddrList):
  700. msgs = {
  701. 'file_header': f"""
  702. # {pnm} key file
  703. #
  704. # This file is editable.
  705. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  706. """.strip()
  707. }
  708. data_desc = 'key'
  709. file_desc = 'secret keys'
  710. gen_desc = 'key'
  711. gen_desc_pl = 's'
  712. gen_addrs = False
  713. gen_keys = True
  714. has_keys = True
  715. ext = 'keys'
  716. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  717. def is_bip39_str(s):
  718. from .bip39 import bip39
  719. return bool(bip39.tohex(s.split(),wl_id='bip39'))
  720. def is_xmrseed(s):
  721. return bool(baseconv.tobytes(s.split(),wl_id='xmrseed'))
  722. from collections import namedtuple
  723. class PasswordList(AddrList):
  724. msgs = {
  725. 'file_header': f"""
  726. # {pnm} password file
  727. #
  728. # This file is editable.
  729. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  730. # A text label of {TwComment.max_screen_width} screen cells or less may be added to the right of each
  731. # password. The label may contain any printable ASCII symbol.
  732. #
  733. """.strip(),
  734. 'file_header_mn': f"""
  735. # {pnm} {{}} password file
  736. #
  737. # This file is editable.
  738. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  739. #
  740. """.strip(),
  741. 'record_chksum': """
  742. Record this checksum: it will be used to verify the password file in the future
  743. """.strip()
  744. }
  745. entry_type = PasswordListEntry
  746. main_attr = 'passwd'
  747. data_desc = 'password'
  748. file_desc = 'passwords'
  749. gen_desc = 'password'
  750. gen_desc_pl = 's'
  751. gen_addrs = False
  752. gen_keys = False
  753. gen_passwds = True
  754. has_keys = False
  755. ext = 'pws'
  756. pw_len = None
  757. dfl_pw_fmt = 'b58'
  758. pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
  759. pw_info = {
  760. 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128
  761. 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128
  762. 'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', is_bip39_str),
  763. 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic',is_xmrseed),
  764. 'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', is_hex_str),
  765. }
  766. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  767. feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
  768. hex2bip39 = False
  769. def __init__(self,proto,
  770. infile = None,
  771. seed = None,
  772. pw_idxs = None,
  773. pw_id_str = None,
  774. pw_len = None,
  775. pw_fmt = None,
  776. chk_params_only = False
  777. ):
  778. self.proto = proto # proto is ignored
  779. self.update_msgs()
  780. if infile:
  781. self.infile = infile
  782. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  783. else:
  784. if not chk_params_only:
  785. for k in (seed,pw_idxs):
  786. assert k
  787. self.pw_id_str = MMGenPWIDString(pw_id_str)
  788. self.set_pw_fmt(pw_fmt)
  789. self.set_pw_len(pw_len)
  790. if chk_params_only:
  791. return
  792. if self.hex2bip39:
  793. ymsg(self.feature_warn_fs.format(pw_fmt))
  794. self.set_pw_len_vs_seed_len(pw_len,seed)
  795. self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
  796. self.data = self.generate(seed,pw_idxs)
  797. if self.pw_fmt in ('bip39','xmrseed'):
  798. self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
  799. self.num_addrs = len(self.data)
  800. self.fmt_data = ''
  801. self.chksum = AddrListChksum(self)
  802. fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
  803. self.id_str = AddrListIDStr(self,fs)
  804. qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
  805. qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
  806. def set_pw_fmt(self,pw_fmt):
  807. if pw_fmt == 'hex2bip39':
  808. self.hex2bip39 = True
  809. self.pw_fmt = 'bip39'
  810. self.pw_fmt_disp = 'hex2bip39'
  811. else:
  812. self.pw_fmt = pw_fmt
  813. self.pw_fmt_disp = pw_fmt
  814. if self.pw_fmt not in self.pw_info:
  815. raise InvalidPasswdFormat(
  816. '{!r}: invalid password format. Valid formats: {}'.format(
  817. self.pw_fmt,
  818. ', '.join(self.pw_info) ))
  819. def chk_pw_len(self,passwd=None):
  820. if passwd is None:
  821. assert self.pw_len,'either passwd or pw_len must be set'
  822. pw_len = self.pw_len
  823. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  824. else:
  825. pw_len = len(passwd)
  826. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  827. d = self.pw_info[self.pw_fmt]
  828. if d.valid_lens:
  829. if pw_len not in d.valid_lens:
  830. die(2,fs.format(l=pw_len,b=d.desc,c='not one of ',m=d.valid_lens,pw=passwd))
  831. elif pw_len > d.max_len:
  832. die(2,fs.format(l=pw_len,b=d.desc,c='>',m=d.max_len,pw=passwd))
  833. elif pw_len < d.min_len:
  834. die(2,fs.format(l=pw_len,b=d.desc,c='<',m=d.min_len,pw=passwd))
  835. def set_pw_len(self,pw_len):
  836. d = self.pw_info[self.pw_fmt]
  837. if pw_len is None:
  838. self.pw_len = d.dfl_len
  839. return
  840. if not is_int(pw_len):
  841. die(2,"'{}': invalid user-requested password length (not an integer)".format(pw_len,d.desc))
  842. self.pw_len = int(pw_len)
  843. self.chk_pw_len()
  844. def set_pw_len_vs_seed_len(self,pw_len,seed):
  845. pf = self.pw_fmt
  846. if pf == 'hex':
  847. pw_bytes = self.pw_len // 2
  848. good_pw_len = seed.byte_len * 2
  849. elif pf == 'bip39':
  850. from .bip39 import bip39
  851. pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
  852. good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
  853. elif pf == 'xmrseed':
  854. pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
  855. try:
  856. good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len]
  857. except:
  858. die(1,'{}: unsupported seed length for Monero new-style mnemonic'.format(seed.byte_len*8))
  859. elif pf in ('b32','b58'):
  860. pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
  861. pw_bytes = pw_int.bit_length() // 8
  862. good_pw_len = len(baseconv.frombytes(b'\xff'*seed.byte_len,wl_id=pf))
  863. else:
  864. raise NotImplementedError('{!r}: unknown password format'.format(pf))
  865. if pw_bytes > seed.byte_len:
  866. m1 = 'Cannot generate passwords with more entropy than underlying seed! ({} bits)'
  867. m2 = ( 'Re-run the command with --passwd-len={}' if pf in ('bip39','hex') else
  868. 'Re-run the command, specifying a password length of {} or less' )
  869. die(1,(m1+'\n'+m2).format(len(seed.data) * 8,good_pw_len))
  870. if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
  871. m1 = 'WARNING: requested {} length has less entropy than underlying seed!'
  872. m2 = 'Is this what you want?'
  873. if not keypress_confirm((m1+'\n'+m2).format(self.pw_info[pf].desc),default_yes=True):
  874. die(1,'Exiting at user request')
  875. def make_passwd(self,hex_sec):
  876. assert self.pw_fmt in self.pw_info
  877. if self.pw_fmt == 'hex':
  878. # take most significant part
  879. return hex_sec[:self.pw_len]
  880. elif self.pw_fmt == 'bip39':
  881. from .bip39 import bip39
  882. pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  883. # take most significant part
  884. return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
  885. elif self.pw_fmt == 'xmrseed':
  886. pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
  887. # take most significant part
  888. bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
  889. bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None)
  890. return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
  891. else:
  892. # take least significant part
  893. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  894. def check_format(self,pw):
  895. if not self.pw_info[self.pw_fmt].chk_func(pw):
  896. raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
  897. pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
  898. if pwlen != self.pw_len:
  899. raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
  900. return True
  901. def scramble_seed(self,seed):
  902. # Changing either pw_fmt or pw_len will cause a different, unrelated
  903. # set of passwords to be generated: this is what we want.
  904. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  905. scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
  906. if self.hex2bip39:
  907. from .bip39 import bip39
  908. pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  909. scramble_key = '{}:{}:{}'.format('hex',pwlen,self.pw_id_str)
  910. from .crypto import scramble_seed
  911. dmsg_sc('str',scramble_key)
  912. return scramble_seed(seed,scramble_key.encode())
  913. def get_line(self,lines):
  914. self.line_ctr += 1
  915. if self.pw_fmt in ('bip39','xmrseed'):
  916. ret = lines.pop(0).split(None,self.pw_len+1)
  917. if len(ret) > self.pw_len+1:
  918. m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1])
  919. m2 = '[bare comments not allowed in BIP39 password files]'
  920. m = m1+' '+m2
  921. elif len(ret) < self.pw_len+1:
  922. m = 'invalid password length {}'.format(len(ret)-1)
  923. else:
  924. return (ret[0],' '.join(ret[1:self.pw_len+1]),'')
  925. raise ValueError(m)
  926. else:
  927. ret = lines.pop(0).split(None,2)
  928. return ret if len(ret) == 3 else ret + ['']
  929. def make_label(self):
  930. return '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
  931. class AddrData(MMGenObject):
  932. msgs = {
  933. 'too_many_acct_addresses': """
  934. ERROR: More than one address found for account: '{{}}'.
  935. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  936. Please restore your tracking wallet from a backup or create a new one and
  937. re-import your addresses.
  938. """.strip().format(pnm=pnm)
  939. }
  940. def __new__(cls,proto,*args,**kwargs):
  941. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  942. def __init__(self,proto,*args,**kwargs):
  943. self.al_ids = {}
  944. self.proto = proto
  945. self.rpc = None
  946. def seed_ids(self):
  947. return list(self.al_ids.keys())
  948. def addrlist(self,al_id):
  949. # TODO: Validate al_id
  950. if al_id in self.al_ids:
  951. return self.al_ids[al_id]
  952. def mmaddr2coinaddr(self,mmaddr):
  953. al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
  954. coinaddr = ''
  955. if al_id in self.al_ids:
  956. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  957. return coinaddr or None
  958. def coinaddr2mmaddr(self,coinaddr):
  959. d = self.make_reverse_dict([coinaddr])
  960. return (list(d.values())[0][0]) if d else None
  961. def add(self,addrlist):
  962. if type(addrlist) == AddrList:
  963. self.al_ids[addrlist.al_id] = addrlist
  964. return True
  965. else:
  966. raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
  967. def make_reverse_dict(self,coinaddrs):
  968. d = MMGenDict()
  969. for al_id in self.al_ids:
  970. d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
  971. return d
  972. class TwAddrData(AddrData,metaclass=AsyncInit):
  973. def __new__(cls,proto,*args,**kwargs):
  974. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  975. async def __init__(self,proto,wallet=None):
  976. self.proto = proto
  977. from .rpc import rpc_init
  978. self.rpc = await rpc_init(proto)
  979. self.al_ids = {}
  980. await self.add_tw_data(wallet)
  981. async def get_tw_data(self,wallet=None):
  982. vmsg('Getting address data from tracking wallet')
  983. c = self.rpc
  984. if 'label_api' in c.caps:
  985. accts = await c.call('listlabels')
  986. ll = await c.batch_call('getaddressesbylabel',[(k,) for k in accts])
  987. alists = [list(a.keys()) for a in ll]
  988. else:
  989. accts = await c.call('listaccounts',0,True)
  990. alists = await c.batch_call('getaddressesbyaccount',[(k,) for k in accts])
  991. return list(zip(accts,alists))
  992. async def add_tw_data(self,wallet):
  993. twd = await self.get_tw_data(wallet)
  994. out,i = {},0
  995. for acct,addr_array in twd:
  996. l = get_obj(TwLabel,proto=self.proto,text=acct,silent=True)
  997. if l and l.mmid.type == 'mmgen':
  998. obj = l.mmid.obj
  999. if len(addr_array) != 1:
  1000. die(2,self.msgs['too_many_acct_addresses'].format(acct))
  1001. al_id = AddrListID(SeedID(sid=obj.sid),self.proto.addr_type(obj.mmtype))
  1002. if al_id not in out:
  1003. out[al_id] = []
  1004. out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],label=l.comment))
  1005. i += 1
  1006. vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(twd)))
  1007. for al_id in out:
  1008. self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx))))