addr.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  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:
  206. return False
  207. import ecdsa
  208. class KeyGeneratorPython(KeyGenerator):
  209. desc = 'mmgen-python-ecdsa'
  210. def __init__(self,*args,**kwargs):
  211. # secp256k1: http://www.oid-info.com/get/1.3.132.0.10
  212. p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
  213. r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
  214. b = 0x0000000000000000000000000000000000000000000000000000000000000007
  215. a = 0x0000000000000000000000000000000000000000000000000000000000000000
  216. Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
  217. Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
  218. curve_fp = ecdsa.ellipticcurve.CurveFp(p,a,b)
  219. G = ecdsa.ellipticcurve.Point(curve_fp,Gx,Gy,r)
  220. oid = (1,3,132,0,10)
  221. self.secp256k1 = ecdsa.curves.Curve('secp256k1',curve_fp,G,oid)
  222. # devdoc/guide_wallets.md:
  223. # Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
  224. # 0x02 depending on whether they're greater or less than the midpoint of the curve.
  225. def privnum2pubhex(self,numpriv,compressed=False):
  226. pko = ecdsa.SigningKey.from_secret_exponent(numpriv,self.secp256k1)
  227. # pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
  228. pubkey = (pko.get_verifying_key().to_string()).hex()
  229. if compressed: # discard Y coord, replace with appropriate version byte
  230. # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
  231. return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
  232. else:
  233. return '04' + pubkey
  234. def to_pubhex(self,privhex):
  235. assert type(privhex) == PrivKey
  236. return PubKey(
  237. s = self.privnum2pubhex(int(privhex,16),compressed=privhex.compressed),
  238. privkey = privhex )
  239. class KeyGeneratorSecp256k1(KeyGenerator):
  240. desc = 'mmgen-secp256k1'
  241. def to_pubhex(self,privhex):
  242. assert type(privhex) == PrivKey
  243. from .secp256k1 import priv2pub
  244. return PubKey(
  245. s = priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),
  246. privkey = privhex )
  247. class KeyGeneratorDummy(KeyGenerator):
  248. desc = 'mmgen-dummy'
  249. def to_pubhex(self,privhex):
  250. assert type(privhex) == PrivKey
  251. return PubKey(
  252. s = privhex,
  253. privkey = privhex )
  254. class AddrListEntryBase(MMGenListItem):
  255. invalid_attrs = {'proto'}
  256. def __init__(self,proto,**kwargs):
  257. self.__dict__['proto'] = proto
  258. MMGenListItem.__init__(self,**kwargs)
  259. class AddrListEntry(AddrListEntryBase):
  260. addr = ListItemAttr('CoinAddr',include_proto=True)
  261. idx = ListItemAttr('AddrIdx') # not present in flat addrlists
  262. label = ListItemAttr('TwComment',reassign_ok=True)
  263. sec = ListItemAttr('PrivKey',include_proto=True)
  264. viewkey = ListItemAttr('ViewKey',include_proto=True)
  265. wallet_passwd = ListItemAttr('WalletPassword')
  266. class PasswordListEntry(AddrListEntryBase):
  267. passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
  268. idx = ImmutableAttr('AddrIdx')
  269. label = ListItemAttr('TwComment',reassign_ok=True)
  270. sec = ListItemAttr('PrivKey',include_proto=True)
  271. class AddrListChksum(str,Hilite):
  272. color = 'pink'
  273. trunc_ok = False
  274. def __new__(cls,addrlist):
  275. ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
  276. if ea == None: ea = ()
  277. lines = [' '.join(
  278. addrlist.chksum_rec_f(e) +
  279. tuple(getattr(e,a) for a in ea if getattr(e,a))
  280. ) for e in addrlist.data]
  281. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  282. class AddrListIDStr(str,Hilite):
  283. color = 'green'
  284. trunc_ok = False
  285. def __new__(cls,addrlist,fmt_str=None):
  286. idxs = [e.idx for e in addrlist.data]
  287. prev = idxs[0]
  288. ret = prev,
  289. for i in idxs[1:]:
  290. if i == prev + 1:
  291. if i == idxs[-1]: ret += '-', i
  292. else:
  293. if prev != ret[-1]: ret += '-', prev
  294. ret += ',', i
  295. prev = i
  296. s = ''.join(map(str,ret))
  297. if fmt_str:
  298. ret = fmt_str.format(s)
  299. else:
  300. bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
  301. mt = addrlist.al_id.mmtype
  302. ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
  303. dmsg_sc('id_str',ret[8:].split('[')[0])
  304. return str.__new__(cls,ret)
  305. class AddrList(MMGenObject): # Address info for a single seed ID
  306. msgs = {
  307. 'file_header': """
  308. # {pnm} address file
  309. #
  310. # This file is editable.
  311. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  312. # A text label of {n} screen cells or less may be added to the right of each
  313. # address, and it will be appended to the tracking wallet label upon import.
  314. # The label may contain any printable ASCII symbol.
  315. """.strip().format(n=TwComment.max_screen_width,pnm=pnm),
  316. 'record_chksum': """
  317. Record this checksum: it will be used to verify the address file in the future
  318. """.strip(),
  319. 'check_chksum': 'Check this value against your records',
  320. 'removed_dup_keys': """
  321. Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
  322. """.strip().format(pnm=pnm)
  323. }
  324. entry_type = AddrListEntry
  325. main_attr = 'addr'
  326. data_desc = 'address'
  327. file_desc = 'addresses'
  328. gen_desc = 'address'
  329. gen_desc_pl = 'es'
  330. gen_addrs = True
  331. gen_passwds = False
  332. gen_keys = False
  333. has_keys = False
  334. ext = 'addrs'
  335. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
  336. line_ctr = 0
  337. def __init__(self,proto,
  338. addrfile = '',
  339. al_id = '',
  340. adata = [],
  341. seed = '',
  342. addr_idxs = '',
  343. src = '',
  344. addrlist = '',
  345. keylist = '',
  346. mmtype = None,
  347. skip_key_address_validity_check = False,
  348. ):
  349. self.skip_ka_check = skip_key_address_validity_check
  350. do_chksum = True
  351. self.update_msgs()
  352. mmtype = mmtype or proto.dfl_mmtype
  353. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  354. from .protocol import CoinProtocol
  355. self.bitcoin_addrtypes = tuple(
  356. MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
  357. self.proto = proto
  358. if seed and addr_idxs: # data from seed + idxs
  359. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  360. adata = self.generate(seed,addr_idxs)
  361. elif addrfile: # data from MMGen address file
  362. self.infile = addrfile
  363. adata = self.parse_file(addrfile) # sets self.al_id
  364. elif al_id and adata: # data from tracking wallet
  365. self.al_id = al_id
  366. do_chksum = False
  367. elif addrlist: # data from flat address list
  368. self.al_id = None
  369. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in set(addrlist)])
  370. elif keylist: # data from flat key list
  371. self.al_id = None
  372. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in set(keylist)])
  373. elif seed or addr_idxs:
  374. die(3,'Must specify both seed and addr indexes')
  375. elif al_id or adata:
  376. die(3,'Must specify both al_id and adata')
  377. else:
  378. die(3,'Incorrect arguments for {}'.format(type(self).__name__))
  379. # al_id,adata now set
  380. self.data = adata
  381. self.num_addrs = len(adata)
  382. self.fmt_data = ''
  383. self.chksum = None
  384. if self.al_id == None: return
  385. self.id_str = AddrListIDStr(self)
  386. if type(self) == KeyList: return
  387. if do_chksum:
  388. self.chksum = AddrListChksum(self)
  389. qmsg('Checksum for {} data {}: {}'.format(
  390. self.data_desc,self.id_str.hl(),self.chksum.hl()))
  391. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  392. def update_msgs(self):
  393. self.msgs = AddrList.msgs
  394. self.msgs.update(type(self).msgs)
  395. def generate(self,seed,addrnums):
  396. assert type(addrnums) is AddrIdxList
  397. seed = self.scramble_seed(seed.data)
  398. dmsg_sc('seed',seed[:8].hex())
  399. compressed = self.al_id.mmtype.compressed
  400. pubkey_type = self.al_id.mmtype.pubkey_type
  401. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  402. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  403. if self.gen_addrs:
  404. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  405. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  406. t_addrs,num,pos,out = len(addrnums),0,0,AddrListData()
  407. le = self.entry_type
  408. while pos != t_addrs:
  409. seed = sha512(seed).digest()
  410. num += 1 # round
  411. if num != addrnums[pos]: continue
  412. pos += 1
  413. if not g.debug:
  414. qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs))
  415. e = le(proto=self.proto,idx=num)
  416. # Secret key is double sha256 of seed hash round /num/
  417. e.sec = PrivKey(
  418. self.proto,
  419. sha256(sha256(seed).digest()).digest(),
  420. compressed = compressed,
  421. pubkey_type = pubkey_type )
  422. if self.gen_addrs:
  423. pubhex = kg.to_pubhex(e.sec)
  424. e.addr = ag.to_addr(pubhex)
  425. if gen_viewkey:
  426. e.viewkey = ag.to_viewkey(pubhex)
  427. if gen_wallet_passwd:
  428. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  429. if type(self) == PasswordList:
  430. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  431. dmsg('Key {:>03}: {}'.format(pos,e.passwd))
  432. out.append(e)
  433. if g.debug_addrlist:
  434. Msg('generate():\n{}'.format(e.pfmt()))
  435. qmsg('\r{}: {} {}{} generated{}'.format(
  436. self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
  437. return out
  438. def check_format(self,addr):
  439. return True # format is checked when added to list entry object
  440. def scramble_seed(self,seed):
  441. is_btcfork = self.proto.base_coin == 'BTC'
  442. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  443. dmsg_sc('str','(none)')
  444. return seed
  445. if self.proto.base_coin == 'ETH':
  446. scramble_key = self.proto.coin.lower()
  447. else:
  448. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  449. from .crypto import scramble_seed
  450. if self.proto.testnet:
  451. scramble_key += ':' + self.proto.network
  452. dmsg_sc('str',scramble_key)
  453. return scramble_seed(seed,scramble_key.encode())
  454. def encrypt(self,desc='new key list'):
  455. from .crypto import mmgen_encrypt
  456. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  457. self.ext += '.'+g.mmenc_ext
  458. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  459. tn = ('.' + self.proto.network) if self.proto.testnet else ''
  460. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  461. ask_tty = self.has_keys and not opt.quiet
  462. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  463. def idxs(self):
  464. return [e.idx for e in self.data]
  465. def addrs(self):
  466. return ['{}:{}'.format(self.al_id.sid,e.idx) for e in self.data]
  467. def addrpairs(self):
  468. return [(e.idx,e.addr) for e in self.data]
  469. def coinaddrs(self):
  470. return [e.addr for e in self.data]
  471. def comments(self):
  472. return [e.label for e in self.data]
  473. def entry(self,idx):
  474. for e in self.data:
  475. if idx == e.idx: return e
  476. def coinaddr(self,idx):
  477. for e in self.data:
  478. if idx == e.idx: return e.addr
  479. def comment(self,idx):
  480. for e in self.data:
  481. if idx == e.idx: return e.label
  482. def set_comment(self,idx,comment):
  483. for e in self.data:
  484. if idx == e.idx:
  485. e.label = comment
  486. def make_reverse_dict_addrlist(self,coinaddrs):
  487. d = MMGenDict()
  488. b = coinaddrs
  489. for e in self.data:
  490. try:
  491. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  492. except ValueError:
  493. pass
  494. return d
  495. def remove_dup_keys(self,cmplist):
  496. assert self.has_keys
  497. pop_list = []
  498. for n,d in enumerate(self.data):
  499. for e in cmplist.data:
  500. if e.sec.wif == d.sec.wif:
  501. pop_list.append(n)
  502. for n in reversed(pop_list): self.data.pop(n)
  503. if pop_list:
  504. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed)))
  505. def add_wifs(self,key_list):
  506. if not key_list: return
  507. for d in self.data:
  508. for e in key_list.data:
  509. if e.addr and e.sec and e.addr == d.addr:
  510. d.sec = e.sec
  511. def list_missing(self,key):
  512. return [d.addr for d in self.data if not getattr(d,key)]
  513. def generate_addrs_from_keys(self):
  514. # assume that the first listed mmtype is valid for flat key list
  515. at = self.proto.addr_type(self.proto.mmtypes[0])
  516. kg = KeyGenerator(self.proto,at.pubkey_type)
  517. ag = AddrGenerator(self.proto,at)
  518. d = self.data
  519. for n,e in enumerate(d,1):
  520. qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
  521. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  522. if g.debug_addrlist:
  523. Msg('generate_addrs_from_keys():\n{}'.format(e.pfmt()))
  524. qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
  525. def make_label(self):
  526. bc,mt = self.proto.base_coin,self.al_id.mmtype
  527. l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc]
  528. l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()]
  529. l_tn = [] if not self.proto.testnet else [self.proto.network.upper()]
  530. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  531. return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  532. def format(self,add_comments=False):
  533. out = [self.msgs['file_header']+'\n']
  534. if self.chksum:
  535. out.append('# {} data checksum for {}: {}'.format(
  536. capfirst(self.data_desc),self.id_str,self.chksum))
  537. out.append('# Record this value to a secure location.\n')
  538. lbl = self.make_label()
  539. dmsg_sc('lbl',lbl[9:])
  540. out.append('{} {{'.format(lbl))
  541. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  542. for e in self.data:
  543. c = ' '+e.label if add_comments and e.label else ''
  544. if type(self) == KeyList:
  545. out.append(fs.format(e.idx,'{}: {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  546. elif type(self) == PasswordList:
  547. out.append(fs.format(e.idx,e.passwd,c))
  548. else: # First line with idx
  549. out.append(fs.format(e.idx,e.addr,c))
  550. if self.has_keys:
  551. if opt.b16:
  552. out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c))
  553. out.append(fs.format('','{}: {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  554. for k in ('viewkey','wallet_passwd'):
  555. v = getattr(e,k)
  556. if v: out.append(fs.format('','{}: {}'.format(k,v),c))
  557. out.append('}')
  558. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  559. def get_line(self,lines):
  560. ret = lines.pop(0).split(None,2)
  561. self.line_ctr += 1
  562. if ret[0] == 'orig_hex:': # hacky
  563. ret = lines.pop(0).split(None,2)
  564. self.line_ctr += 1
  565. return ret if len(ret) == 3 else ret + ['']
  566. def parse_file_body(self,lines):
  567. ret = AddrListData()
  568. le = self.entry_type
  569. iifs = "{!r}: invalid identifier [expected '{}:']"
  570. while lines:
  571. idx,addr,lbl = self.get_line(lines)
  572. assert is_mmgen_idx(idx), f'invalid address index {idx!r}'
  573. self.check_format(addr)
  574. a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl })
  575. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  576. d = self.get_line(lines)
  577. assert d[0] == self.al_id.mmtype.wif_label+':',iifs.format(d[0],self.al_id.mmtype.wif_label)
  578. a.sec = PrivKey(proto=self.proto,wif=d[1])
  579. for k,dtype,add_proto in (
  580. ('viewkey',ViewKey,True),
  581. ('wallet_passwd',WalletPassword,False) ):
  582. if k in self.al_id.mmtype.extra_attrs:
  583. d = self.get_line(lines)
  584. assert d[0] == k+':',iifs.format(d[0],k)
  585. setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) )
  586. ret.append(a)
  587. if self.has_keys and not self.skip_ka_check:
  588. if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'):
  589. kg = KeyGenerator(self.proto,self.al_id.mmtype)
  590. ag = AddrGenerator(self.proto,self.al_id.mmtype)
  591. llen = len(ret)
  592. for n,e in enumerate(ret):
  593. qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen))
  594. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  595. "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr))
  596. qmsg(' - done')
  597. return ret
  598. def parse_file(self,fn,buf=[],exit_on_error=True):
  599. def parse_addrfile_label(lbl):
  600. """
  601. label examples:
  602. - Bitcoin legacy mainnet: no label
  603. - Bitcoin legacy testnet: 'LEGACY:TESTNET'
  604. - Bitcoin Segwit: 'SEGWIT'
  605. - Bitcoin Segwit testnet: 'SEGWIT:TESTNET'
  606. - Bitcoin Bech32 regtest: 'BECH32:REGTEST'
  607. - Litecoin legacy mainnet: 'LTC'
  608. - Litecoin Bech32 mainnet: 'LTC:BECH32'
  609. - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET'
  610. - Ethereum mainnet: 'ETH'
  611. - Ethereum Classic mainnet: 'ETC'
  612. - Ethereum regtest: 'ETH:REGTEST'
  613. """
  614. lbl = lbl.lower()
  615. # remove the network component:
  616. if lbl.endswith(':testnet'):
  617. network = 'testnet'
  618. lbl = lbl[:-8]
  619. elif lbl.endswith(':regtest'):
  620. network = 'regtest'
  621. lbl = lbl[:-8]
  622. else:
  623. network = 'mainnet'
  624. if lbl in self.bitcoin_addrtypes:
  625. coin,mmtype_key = ( 'BTC', lbl )
  626. elif ':' in lbl: # first component is coin, second is mmtype_key
  627. coin,mmtype_key = lbl.split(':')
  628. else: # only component is coin
  629. coin,mmtype_key = ( lbl, None )
  630. proto = init_proto(coin=coin,network=network)
  631. if mmtype_key == None:
  632. mmtype_key = proto.mmtypes[0]
  633. return ( proto, proto.addr_type(mmtype_key) )
  634. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  635. try:
  636. assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
  637. ls = lines[0].split()
  638. assert 1 < len(ls) < 5, f'Invalid first line for {self.gen_desc} file: {lines[0]!r}'
  639. assert ls.pop() == '{', f'{ls!r}: invalid first line'
  640. assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line'
  641. sid = ls.pop(0)
  642. assert is_mmgen_seed_id(sid), f'{sid!r}: invalid Seed ID'
  643. if type(self) == PasswordList and len(ls) == 2:
  644. ss = ls.pop().split(':')
  645. assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)'
  646. self.set_pw_fmt(ss[0])
  647. self.set_pw_len(ss[1])
  648. self.pw_id_str = MMGenPWIDString(ls.pop())
  649. proto = init_proto('btc')# FIXME: dummy protocol
  650. mmtype = MMGenPasswordType(proto,'P')
  651. elif len(ls) == 1:
  652. proto,mmtype = parse_addrfile_label(ls[0])
  653. elif len(ls) == 0:
  654. proto = init_proto('btc')
  655. mmtype = proto.addr_type('L')
  656. else:
  657. raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}')
  658. if type(self) != PasswordList:
  659. if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network:
  660. """
  661. Having caller supply protocol and checking address file protocol against it here
  662. allows us to catch all mismatches in one place. This behavior differs from that of
  663. transaction files, which determine the protocol independently, requiring the caller
  664. to check for protocol mismatches (e.g. MMGenTX.check_correct_chain())
  665. """
  666. raise ValueError(
  667. f'{self.data_desc} file is '
  668. + f'{proto.base_coin} {proto.network} but protocol is '
  669. + f'{self.proto.base_coin} {self.proto.network}' )
  670. self.base_coin = proto.base_coin
  671. self.network = proto.network
  672. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  673. data = self.parse_file_body(lines[1:-1])
  674. assert isinstance(data,list),'Invalid file body data'
  675. except Exception as e:
  676. m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
  677. self.data_desc,
  678. self.infile,
  679. (f', content line {self.line_ctr}' if self.line_ctr else ''),
  680. e )
  681. if exit_on_error:
  682. die(3,m)
  683. else:
  684. msg(m)
  685. return False
  686. return data
  687. class KeyAddrList(AddrList):
  688. data_desc = 'key-address'
  689. file_desc = 'secret keys'
  690. gen_desc = 'key/address pair'
  691. gen_desc_pl = 's'
  692. gen_addrs = True
  693. gen_keys = True
  694. has_keys = True
  695. ext = 'akeys'
  696. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  697. class KeyList(AddrList):
  698. msgs = {
  699. 'file_header': f"""
  700. # {pnm} key file
  701. #
  702. # This file is editable.
  703. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  704. """.strip()
  705. }
  706. data_desc = 'key'
  707. file_desc = 'secret keys'
  708. gen_desc = 'key'
  709. gen_desc_pl = 's'
  710. gen_addrs = False
  711. gen_keys = True
  712. has_keys = True
  713. ext = 'keys'
  714. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  715. def is_bip39_str(s):
  716. from .bip39 import bip39
  717. return bool(bip39.tohex(s.split(),wl_id='bip39'))
  718. def is_xmrseed(s):
  719. return bool(baseconv.tobytes(s.split(),wl_id='xmrseed'))
  720. from collections import namedtuple
  721. class PasswordList(AddrList):
  722. msgs = {
  723. 'file_header': f"""
  724. # {pnm} password file
  725. #
  726. # This file is editable.
  727. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  728. # A text label of {TwComment.max_screen_width} screen cells or less may be added to the right of each
  729. # password. The label may contain any printable ASCII symbol.
  730. #
  731. """.strip(),
  732. 'file_header_mn': f"""
  733. # {pnm} {{}} password file
  734. #
  735. # This file is editable.
  736. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  737. #
  738. """.strip(),
  739. 'record_chksum': """
  740. Record this checksum: it will be used to verify the password file in the future
  741. """.strip()
  742. }
  743. entry_type = PasswordListEntry
  744. main_attr = 'passwd'
  745. data_desc = 'password'
  746. file_desc = 'passwords'
  747. gen_desc = 'password'
  748. gen_desc_pl = 's'
  749. gen_addrs = False
  750. gen_keys = False
  751. gen_passwds = True
  752. has_keys = False
  753. ext = 'pws'
  754. pw_len = None
  755. dfl_pw_fmt = 'b58'
  756. pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
  757. pw_info = {
  758. 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128
  759. 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128
  760. 'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', is_bip39_str),
  761. 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic',is_xmrseed),
  762. 'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', is_hex_str),
  763. }
  764. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  765. feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
  766. hex2bip39 = False
  767. def __init__(self,proto,
  768. infile = None,
  769. seed = None,
  770. pw_idxs = None,
  771. pw_id_str = None,
  772. pw_len = None,
  773. pw_fmt = None,
  774. chk_params_only = False
  775. ):
  776. self.proto = proto # proto is ignored
  777. self.update_msgs()
  778. if infile:
  779. self.infile = infile
  780. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  781. else:
  782. if not chk_params_only:
  783. for k in (seed,pw_idxs):
  784. assert k
  785. self.pw_id_str = MMGenPWIDString(pw_id_str)
  786. self.set_pw_fmt(pw_fmt)
  787. self.set_pw_len(pw_len)
  788. if chk_params_only:
  789. return
  790. if self.hex2bip39:
  791. ymsg(self.feature_warn_fs.format(pw_fmt))
  792. self.set_pw_len_vs_seed_len(pw_len,seed)
  793. self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
  794. self.data = self.generate(seed,pw_idxs)
  795. if self.pw_fmt in ('bip39','xmrseed'):
  796. self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
  797. self.num_addrs = len(self.data)
  798. self.fmt_data = ''
  799. self.chksum = AddrListChksum(self)
  800. fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
  801. self.id_str = AddrListIDStr(self,fs)
  802. qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
  803. qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
  804. def set_pw_fmt(self,pw_fmt):
  805. if pw_fmt == 'hex2bip39':
  806. self.hex2bip39 = True
  807. self.pw_fmt = 'bip39'
  808. self.pw_fmt_disp = 'hex2bip39'
  809. else:
  810. self.pw_fmt = pw_fmt
  811. self.pw_fmt_disp = pw_fmt
  812. if self.pw_fmt not in self.pw_info:
  813. raise InvalidPasswdFormat(
  814. '{!r}: invalid password format. Valid formats: {}'.format(
  815. self.pw_fmt,
  816. ', '.join(self.pw_info) ))
  817. def chk_pw_len(self,passwd=None):
  818. if passwd is None:
  819. assert self.pw_len,'either passwd or pw_len must be set'
  820. pw_len = self.pw_len
  821. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  822. else:
  823. pw_len = len(passwd)
  824. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  825. d = self.pw_info[self.pw_fmt]
  826. if d.valid_lens:
  827. if pw_len not in d.valid_lens:
  828. die(2,fs.format(l=pw_len,b=d.desc,c='not one of ',m=d.valid_lens,pw=passwd))
  829. elif pw_len > d.max_len:
  830. die(2,fs.format(l=pw_len,b=d.desc,c='>',m=d.max_len,pw=passwd))
  831. elif pw_len < d.min_len:
  832. die(2,fs.format(l=pw_len,b=d.desc,c='<',m=d.min_len,pw=passwd))
  833. def set_pw_len(self,pw_len):
  834. d = self.pw_info[self.pw_fmt]
  835. if pw_len is None:
  836. self.pw_len = d.dfl_len
  837. return
  838. if not is_int(pw_len):
  839. die(2,"'{}': invalid user-requested password length (not an integer)".format(pw_len,d.desc))
  840. self.pw_len = int(pw_len)
  841. self.chk_pw_len()
  842. def set_pw_len_vs_seed_len(self,pw_len,seed):
  843. pf = self.pw_fmt
  844. if pf == 'hex':
  845. pw_bytes = self.pw_len // 2
  846. good_pw_len = seed.byte_len * 2
  847. elif pf == 'bip39':
  848. from .bip39 import bip39
  849. pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
  850. good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
  851. elif pf == 'xmrseed':
  852. pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
  853. try:
  854. good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len]
  855. except:
  856. die(1,'{}: unsupported seed length for Monero new-style mnemonic'.format(seed.byte_len*8))
  857. elif pf in ('b32','b58'):
  858. pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
  859. pw_bytes = pw_int.bit_length() // 8
  860. good_pw_len = len(baseconv.frombytes(b'\xff'*seed.byte_len,wl_id=pf))
  861. else:
  862. raise NotImplementedError('{!r}: unknown password format'.format(pf))
  863. if pw_bytes > seed.byte_len:
  864. m1 = 'Cannot generate passwords with more entropy than underlying seed! ({} bits)'
  865. m2 = ( 'Re-run the command with --passwd-len={}' if pf in ('bip39','hex') else
  866. 'Re-run the command, specifying a password length of {} or less' )
  867. die(1,(m1+'\n'+m2).format(len(seed.data) * 8,good_pw_len))
  868. if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
  869. m1 = 'WARNING: requested {} length has less entropy than underlying seed!'
  870. m2 = 'Is this what you want?'
  871. if not keypress_confirm((m1+'\n'+m2).format(self.pw_info[pf].desc),default_yes=True):
  872. die(1,'Exiting at user request')
  873. def make_passwd(self,hex_sec):
  874. assert self.pw_fmt in self.pw_info
  875. if self.pw_fmt == 'hex':
  876. # take most significant part
  877. return hex_sec[:self.pw_len]
  878. elif self.pw_fmt == 'bip39':
  879. from .bip39 import bip39
  880. pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  881. # take most significant part
  882. return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
  883. elif self.pw_fmt == 'xmrseed':
  884. pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
  885. # take most significant part
  886. bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
  887. bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None)
  888. return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
  889. else:
  890. # take least significant part
  891. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  892. def check_format(self,pw):
  893. if not self.pw_info[self.pw_fmt].chk_func(pw):
  894. raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
  895. pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
  896. if pwlen != self.pw_len:
  897. raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
  898. return True
  899. def scramble_seed(self,seed):
  900. # Changing either pw_fmt or pw_len will cause a different, unrelated
  901. # set of passwords to be generated: this is what we want.
  902. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  903. scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
  904. if self.hex2bip39:
  905. from .bip39 import bip39
  906. pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  907. scramble_key = '{}:{}:{}'.format('hex',pwlen,self.pw_id_str)
  908. from .crypto import scramble_seed
  909. dmsg_sc('str',scramble_key)
  910. return scramble_seed(seed,scramble_key.encode())
  911. def get_line(self,lines):
  912. self.line_ctr += 1
  913. if self.pw_fmt in ('bip39','xmrseed'):
  914. ret = lines.pop(0).split(None,self.pw_len+1)
  915. if len(ret) > self.pw_len+1:
  916. m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1])
  917. m2 = '[bare comments not allowed in BIP39 password files]'
  918. m = m1+' '+m2
  919. elif len(ret) < self.pw_len+1:
  920. m = 'invalid password length {}'.format(len(ret)-1)
  921. else:
  922. return (ret[0],' '.join(ret[1:self.pw_len+1]),'')
  923. raise ValueError(m)
  924. else:
  925. ret = lines.pop(0).split(None,2)
  926. return ret if len(ret) == 3 else ret + ['']
  927. def make_label(self):
  928. return '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt_disp,self.pw_len)
  929. class AddrData(MMGenObject):
  930. msgs = {
  931. 'too_many_acct_addresses': """
  932. ERROR: More than one address found for account: '{{}}'.
  933. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  934. Please restore your tracking wallet from a backup or create a new one and
  935. re-import your addresses.
  936. """.strip().format(pnm=pnm)
  937. }
  938. def __new__(cls,proto,*args,**kwargs):
  939. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  940. def __init__(self,proto,*args,**kwargs):
  941. self.al_ids = {}
  942. self.proto = proto
  943. self.rpc = None
  944. def seed_ids(self):
  945. return list(self.al_ids.keys())
  946. def addrlist(self,al_id):
  947. # TODO: Validate al_id
  948. if al_id in self.al_ids:
  949. return self.al_ids[al_id]
  950. def mmaddr2coinaddr(self,mmaddr):
  951. al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
  952. coinaddr = ''
  953. if al_id in self.al_ids:
  954. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  955. return coinaddr or None
  956. def coinaddr2mmaddr(self,coinaddr):
  957. d = self.make_reverse_dict([coinaddr])
  958. return (list(d.values())[0][0]) if d else None
  959. def add(self,addrlist):
  960. if type(addrlist) == AddrList:
  961. self.al_ids[addrlist.al_id] = addrlist
  962. return True
  963. else:
  964. raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
  965. def make_reverse_dict(self,coinaddrs):
  966. d = MMGenDict()
  967. for al_id in self.al_ids:
  968. d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
  969. return d
  970. class TwAddrData(AddrData,metaclass=aInitMeta):
  971. def __new__(cls,proto,*args,**kwargs):
  972. return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
  973. def __init__(self,proto,*args,**kwargs):
  974. pass
  975. async def __ainit__(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))))