addr.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 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 binascii import hexlify,unhexlify
  23. from mmgen.common import *
  24. from mmgen.obj import *
  25. pnm = g.proj_name
  26. def dmsg_sc(desc,data):
  27. if g.debug_addrlist: Msg('sc_debug_{}: {}'.format(desc,data))
  28. class AddrGenerator(MMGenObject):
  29. def __new__(cls,addr_type):
  30. if type(addr_type) == str: # allow override w/o check
  31. gen_method = addr_type
  32. elif type(addr_type) == MMGenAddrType:
  33. assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin)
  34. gen_method = addr_type.gen_method
  35. else:
  36. raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__))
  37. gen_methods = {
  38. 'p2pkh': AddrGeneratorP2PKH,
  39. 'segwit': AddrGeneratorSegwit,
  40. 'bech32': AddrGeneratorBech32,
  41. 'ethereum': AddrGeneratorEthereum,
  42. 'zcash_z': AddrGeneratorZcashZ,
  43. 'monero': AddrGeneratorMonero}
  44. assert gen_method in gen_methods
  45. me = super(cls,cls).__new__(gen_methods[gen_method])
  46. me.desc = gen_methods
  47. return me
  48. class AddrGeneratorP2PKH(AddrGenerator):
  49. def to_addr(self,pubhex):
  50. from mmgen.protocol import hash160
  51. assert type(pubhex) == PubKey
  52. return CoinAddr(g.proto.pubhash2addr(hash160(pubhex),p2sh=False))
  53. def to_segwit_redeem_script(self,pubhex):
  54. raise NotImplementedError('Segwit redeem script not supported by this address type')
  55. class AddrGeneratorSegwit(AddrGenerator):
  56. def to_addr(self,pubhex):
  57. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  58. return CoinAddr(g.proto.pubhex2segwitaddr(pubhex))
  59. def to_segwit_redeem_script(self,pubhex):
  60. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  61. return HexBytes(g.proto.pubhex2redeem_script(pubhex))
  62. class AddrGeneratorBech32(AddrGenerator):
  63. def to_addr(self,pubhex):
  64. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  65. from mmgen.protocol import hash160
  66. return CoinAddr(g.proto.pubhash2bech32addr(hash160(pubhex)))
  67. def to_segwit_redeem_script(self,pubhex):
  68. raise NotImplementedError('Segwit redeem script not supported by this address type')
  69. class AddrGeneratorEthereum(AddrGenerator):
  70. def to_addr(self,pubhex):
  71. assert type(pubhex) == PubKey
  72. import sha3
  73. return CoinAddr(hexlify(sha3.keccak_256(unhexlify(pubhex[2:])).digest()[12:]).decode())
  74. def to_wallet_passwd(self,sk_hex):
  75. from mmgen.protocol import hash256
  76. return WalletPassword(hash256(sk_hex)[:32])
  77. def to_segwit_redeem_script(self,pubhex):
  78. raise NotImplementedError('Segwit redeem script not supported by this address type')
  79. # github.com/FiloSottile/zcash-mini/zcash/address.go
  80. class AddrGeneratorZcashZ(AddrGenerator):
  81. addr_width = 95
  82. vk_width = 97
  83. def zhash256(self,s,t):
  84. s = bytearray(s + bytes(32))
  85. s[0] |= 0xc0
  86. s[32] = t
  87. from mmgen.sha2 import Sha256
  88. return Sha256(s,preprocess=False).digest()
  89. def to_addr(self,pubhex): # pubhex is really privhex
  90. key = unhexlify(pubhex)
  91. assert len(key) == 32,'{}: incorrect privkey length'.format(len(key))
  92. if g.platform == 'win':
  93. ydie(1,'Zcash z-addresses not supported on Windows platform')
  94. from nacl.bindings import crypto_scalarmult_base
  95. p2 = crypto_scalarmult_base(self.zhash256(key,1))
  96. from mmgen.protocol import _b58chk_encode
  97. ret = _b58chk_encode(g.proto.addr_ver_num['zcash_z'][0] + hexlify(self.zhash256(key,0)+p2))
  98. assert len(ret) == self.addr_width,'Invalid Zcash z-address length'
  99. return CoinAddr(ret)
  100. def to_viewkey(self,pubhex): # pubhex is really privhex
  101. key = unhexlify(pubhex)
  102. assert len(key) == 32,'{}: incorrect privkey length'.format(len(key))
  103. vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
  104. vk[32] &= 0xf8
  105. vk[63] &= 0x7f
  106. vk[63] |= 0x40
  107. from mmgen.protocol import _b58chk_encode
  108. ret = _b58chk_encode(g.proto.addr_ver_num['viewkey'][0] + hexlify(vk))
  109. assert len(ret) == self.vk_width,'Invalid Zcash view key length'
  110. return ZcashViewKey(ret)
  111. def to_segwit_redeem_script(self,pubhex):
  112. raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
  113. class AddrGeneratorMonero(AddrGenerator):
  114. def b58enc(self,addr_str):
  115. enc,l = baseconv.fromhex,len(addr_str)
  116. a = ''.join([enc(hexlify(addr_str[i*8:i*8+8]),'b58',pad=11,tostr=True) for i in range(l//8)])
  117. b = enc(hexlify(addr_str[l-l%8:]),'b58',pad=7,tostr=True)
  118. return a + b
  119. def to_addr(self,sk_hex): # sk_hex instead of pubhex
  120. if opt.use_old_ed25519:
  121. from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
  122. else:
  123. from mmgen.ed25519ll_djbec import scalarmult
  124. from mmgen.ed25519 import edwards,encodepoint,B
  125. # Source and license for scalarmultbase function:
  126. # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
  127. # Copyright (c) 2014-2016, The Monero Project
  128. # All rights reserved.
  129. def scalarmultbase(e):
  130. if e == 0: return [0, 1]
  131. Q = scalarmult(B, e//2)
  132. Q = edwards(Q, Q)
  133. if e & 1: Q = edwards(Q, B)
  134. return Q
  135. def hex2int_le(hexstr):
  136. return int(hexlify(unhexlify(hexstr)[::-1]),16)
  137. vk_hex = self.to_viewkey(sk_hex)
  138. pk_str = encodepoint(scalarmultbase(hex2int_le(sk_hex)))
  139. pvk_str = encodepoint(scalarmultbase(hex2int_le(vk_hex)))
  140. addr_p1 = unhexlify(g.proto.addr_ver_num['monero'][0]) + pk_str + pvk_str
  141. import sha3
  142. return CoinAddr(self.b58enc(addr_p1 + sha3.keccak_256(addr_p1).digest()[:4]))
  143. def to_wallet_passwd(self,sk_hex):
  144. from mmgen.protocol import hash256
  145. return WalletPassword(hash256(sk_hex)[:32])
  146. def to_viewkey(self,sk_hex):
  147. assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex))
  148. import sha3
  149. return MoneroViewKey(g.proto.preprocess_key(sha3.keccak_256(unhexlify(sk_hex)).hexdigest(),None))
  150. def to_segwit_redeem_script(self,sk_hex):
  151. raise NotImplementedError('Monero addresses incompatible with Segwit')
  152. class KeyGenerator(MMGenObject):
  153. def __new__(cls,addr_type,generator=None,silent=False):
  154. if type(addr_type) == str: # allow override w/o check
  155. pubkey_type = addr_type
  156. elif type(addr_type) == MMGenAddrType:
  157. assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin)
  158. pubkey_type = addr_type.pubkey_type
  159. else:
  160. raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__))
  161. if pubkey_type == 'std':
  162. if cls.test_for_secp256k1(silent=silent) and generator != 1:
  163. if not opt.key_generator or opt.key_generator == 2 or generator == 2:
  164. return super(cls,cls).__new__(KeyGeneratorSecp256k1)
  165. else:
  166. msg('Using (slow) native Python ECDSA library for address generation')
  167. return super(cls,cls).__new__(KeyGeneratorPython)
  168. elif pubkey_type in ('zcash_z','monero'):
  169. me = super(cls,cls).__new__(KeyGeneratorDummy)
  170. me.desc = 'mmgen-'+pubkey_type
  171. return me
  172. else:
  173. raise ValueError('{}: invalid pubkey_type argument'.format(pubkey_type))
  174. @classmethod
  175. def test_for_secp256k1(self,silent=False):
  176. try:
  177. from mmgen.secp256k1 import priv2pub
  178. m = 'Unable to execute priv2pub() from secp256k1 extension module'
  179. assert priv2pub(unhexlify('deadbeef'*8),1),m
  180. return True
  181. except:
  182. return False
  183. import ecdsa
  184. class KeyGeneratorPython(KeyGenerator):
  185. desc = 'mmgen-python-ecdsa'
  186. def __init__(self,*args,**kwargs):
  187. # secp256k1: http://www.oid-info.com/get/1.3.132.0.10
  188. p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
  189. r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
  190. b = 0x0000000000000000000000000000000000000000000000000000000000000007
  191. a = 0x0000000000000000000000000000000000000000000000000000000000000000
  192. Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
  193. Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
  194. curve_fp = ecdsa.ellipticcurve.CurveFp(p,a,b)
  195. G = ecdsa.ellipticcurve.Point(curve_fp,Gx,Gy,r)
  196. oid = (1,3,132,0,10)
  197. self.secp256k1 = ecdsa.curves.Curve('secp256k1',curve_fp,G,oid)
  198. # devdoc/guide_wallets.md:
  199. # Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
  200. # 0x02 depending on whether they're greater or less than the midpoint of the curve.
  201. def privnum2pubhex(self,numpriv,compressed=False):
  202. pko = ecdsa.SigningKey.from_secret_exponent(numpriv,self.secp256k1)
  203. # pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
  204. pubkey = hexlify(pko.get_verifying_key().to_string())
  205. if compressed: # discard Y coord, replace with appropriate version byte
  206. # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
  207. return (b'03',b'02')[pubkey[-1] in b'02468ace'] + pubkey[:64]
  208. else:
  209. return b'04' + pubkey
  210. def to_pubhex(self,privhex):
  211. assert type(privhex) == PrivKey
  212. return PubKey(self.privnum2pubhex(
  213. int(privhex,16),compressed=privhex.compressed),compressed=privhex.compressed)
  214. class KeyGeneratorSecp256k1(KeyGenerator):
  215. desc = 'mmgen-secp256k1'
  216. def to_pubhex(self,privhex):
  217. assert type(privhex) == PrivKey
  218. from mmgen.secp256k1 import priv2pub
  219. return PubKey(hexlify(priv2pub(unhexlify(privhex),int(privhex.compressed))),compressed=privhex.compressed)
  220. class KeyGeneratorDummy(KeyGenerator):
  221. desc = 'mmgen-dummy'
  222. def to_pubhex(self,privhex):
  223. assert type(privhex) == PrivKey
  224. return PubKey(privhex.decode(),compressed=privhex.compressed)
  225. class AddrListEntry(MMGenListItem):
  226. addr = MMGenListItemAttr('addr','CoinAddr')
  227. idx = MMGenListItemAttr('idx','AddrIdx') # not present in flat addrlists
  228. label = MMGenListItemAttr('label','TwComment',reassign_ok=True)
  229. sec = MMGenListItemAttr('sec',PrivKey,typeconv=False)
  230. viewkey = MMGenListItemAttr('viewkey','ViewKey')
  231. wallet_passwd = MMGenListItemAttr('wallet_passwd','WalletPassword')
  232. class PasswordListEntry(MMGenListItem):
  233. passwd = MMGenImmutableAttr('passwd',str,typeconv=False) # TODO: create Password type
  234. idx = MMGenImmutableAttr('idx','AddrIdx')
  235. label = MMGenListItemAttr('label','TwComment',reassign_ok=True)
  236. sec = MMGenListItemAttr('sec',PrivKey,typeconv=False)
  237. class AddrListChksum(str,Hilite):
  238. color = 'pink'
  239. trunc_ok = False
  240. def __new__(cls,addrlist):
  241. ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
  242. lines = [' '.join(
  243. addrlist.chksum_rec_f(e) +
  244. tuple(getattr(e,a) for a in ea if getattr(e,a))
  245. ) for e in addrlist.data]
  246. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  247. class AddrListIDStr(str,Hilite):
  248. color = 'green'
  249. trunc_ok = False
  250. def __new__(cls,addrlist,fmt_str=None):
  251. idxs = [e.idx for e in addrlist.data]
  252. prev = idxs[0]
  253. ret = prev,
  254. for i in idxs[1:]:
  255. if i == prev + 1:
  256. if i == idxs[-1]: ret += '-', i
  257. else:
  258. if prev != ret[-1]: ret += '-', prev
  259. ret += ',', i
  260. prev = i
  261. s = ''.join(map(str,ret))
  262. if fmt_str:
  263. ret = fmt_str.format(s)
  264. else:
  265. bc = (g.proto.base_coin,g.coin)[g.proto.base_coin=='ETH']
  266. mt = addrlist.al_id.mmtype
  267. ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
  268. dmsg_sc('id_str',ret[8:].split('[')[0])
  269. return str.__new__(cls,ret)
  270. class AddrList(MMGenObject): # Address info for a single seed ID
  271. msgs = {
  272. 'file_header': """
  273. # {pnm} address file
  274. #
  275. # This file is editable.
  276. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  277. # A text label of {n} characters or less may be added to the right of each
  278. # address, and it will be appended to the tracking wallet label upon import.
  279. # The label may contain any printable ASCII symbol.
  280. """.strip().format(n=TwComment.max_len,pnm=pnm),
  281. 'record_chksum': """
  282. Record this checksum: it will be used to verify the address file in the future
  283. """.strip(),
  284. 'check_chksum': 'Check this value against your records',
  285. 'removed_dup_keys': """
  286. Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
  287. """.strip().format(pnm=pnm)
  288. }
  289. entry_type = AddrListEntry
  290. main_attr = 'addr'
  291. data_desc = 'address'
  292. file_desc = 'addresses'
  293. gen_desc = 'address'
  294. gen_desc_pl = 'es'
  295. gen_addrs = True
  296. gen_passwds = False
  297. gen_keys = False
  298. has_keys = False
  299. ext = 'addrs'
  300. scramble_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible
  301. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
  302. def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='',
  303. addrlist='',keylist='',mmtype=None):
  304. do_chksum = True
  305. self.update_msgs()
  306. mmtype = mmtype or g.proto.dfl_mmtype
  307. assert mmtype in MMGenAddrType.mmtypes,'{}: mmtype not in {}'.format(mmtype,repr(MMGenAddrType.mmtypes))
  308. if seed and addr_idxs: # data from seed + idxs
  309. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  310. adata = self.generate(seed,addr_idxs)
  311. elif addrfile: # data from MMGen address file
  312. adata = self.parse_file(addrfile) # sets self.al_id
  313. elif al_id and adata: # data from tracking wallet
  314. self.al_id = al_id
  315. do_chksum = False
  316. elif addrlist: # data from flat address list
  317. self.al_id = None
  318. adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)])
  319. elif keylist: # data from flat key list
  320. self.al_id = None
  321. adata = AddrListList([AddrListEntry(sec=PrivKey(wif=k)) for k in set(keylist)])
  322. elif seed or addr_idxs:
  323. die(3,'Must specify both seed and addr indexes')
  324. elif al_id or adata:
  325. die(3,'Must specify both al_id and adata')
  326. else:
  327. die(3,'Incorrect arguments for {}'.format(type(self).__name__))
  328. # al_id,adata now set
  329. self.data = adata
  330. self.num_addrs = len(adata)
  331. self.fmt_data = ''
  332. self.chksum = None
  333. if self.al_id == None: return
  334. self.id_str = AddrListIDStr(self)
  335. if type(self) == KeyList: return
  336. if do_chksum:
  337. self.chksum = AddrListChksum(self)
  338. qmsg('Checksum for {} data {}: {}'.format(
  339. self.data_desc,self.id_str.hl(),self.chksum.hl()))
  340. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  341. def update_msgs(self):
  342. self.msgs = AddrList.msgs
  343. self.msgs.update(type(self).msgs)
  344. def generate(self,seed,addrnums):
  345. assert type(addrnums) is AddrIdxList
  346. seed = seed.get_data()
  347. seed = self.scramble_seed(seed)
  348. dmsg_sc('seed',hexlify(seed[:8]).decode())
  349. compressed = self.al_id.mmtype.compressed
  350. pubkey_type = self.al_id.mmtype.pubkey_type
  351. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  352. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  353. if self.gen_addrs:
  354. kg = KeyGenerator(self.al_id.mmtype)
  355. ag = AddrGenerator(self.al_id.mmtype)
  356. t_addrs,num,pos,out = len(addrnums),0,0,AddrListList()
  357. le = self.entry_type
  358. while pos != t_addrs:
  359. seed = sha512(seed).digest()
  360. num += 1 # round
  361. if num != addrnums[pos]: continue
  362. pos += 1
  363. if not g.debug:
  364. qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs))
  365. e = le(idx=num)
  366. # Secret key is double sha256 of seed hash round /num/
  367. e.sec = PrivKey(sha256(sha256(seed).digest()).digest(),compressed=compressed,pubkey_type=pubkey_type)
  368. if self.gen_addrs:
  369. pubhex = kg.to_pubhex(e.sec)
  370. e.addr = ag.to_addr(pubhex)
  371. if gen_viewkey:
  372. e.viewkey = ag.to_viewkey(pubhex)
  373. if gen_wallet_passwd:
  374. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  375. if type(self) == PasswordList:
  376. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  377. dmsg('Key {:>03}: {}'.format(pos,e.passwd))
  378. out.append(e)
  379. if g.debug_addrlist: Msg('generate():\n{}'.format(e.pformat()))
  380. qmsg('\r{}: {} {}{} generated{}'.format(
  381. self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
  382. return out
  383. def check_format(self,addr): return True # format is checked when added to list entry object
  384. def scramble_seed(self,seed):
  385. is_btcfork = g.proto.base_coin == 'BTC'
  386. if is_btcfork and self.al_id.mmtype == 'L' and not g.proto.is_testnet():
  387. dmsg_sc('str','(none)')
  388. return seed
  389. if g.proto.base_coin == 'ETH':
  390. scramble_key = g.coin.lower()
  391. else:
  392. scramble_key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  393. from mmgen.crypto import scramble_seed
  394. if g.proto.is_testnet():
  395. scramble_key += ':testnet'
  396. dmsg_sc('str',scramble_key)
  397. return scramble_seed(seed,scramble_key.encode(),self.scramble_hash_rounds)
  398. def encrypt(self,desc='new key list'):
  399. from mmgen.crypto import mmgen_encrypt
  400. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  401. self.ext += '.'+g.mmenc_ext
  402. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  403. tn = ('','.testnet')[g.proto.is_testnet()]
  404. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  405. ask_tty = self.has_keys and not opt.quiet
  406. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  407. def idxs(self):
  408. return [e.idx for e in self.data]
  409. def addrs(self):
  410. return ['{}:{}'.format(self.al_id.sid,e.idx) for e in self.data]
  411. def addrpairs(self):
  412. return [(e.idx,e.addr) for e in self.data]
  413. def coinaddrs(self):
  414. return [e.addr for e in self.data]
  415. def comments(self):
  416. return [e.label for e in self.data]
  417. def entry(self,idx):
  418. for e in self.data:
  419. if idx == e.idx: return e
  420. def coinaddr(self,idx):
  421. for e in self.data:
  422. if idx == e.idx: return e.addr
  423. def comment(self,idx):
  424. for e in self.data:
  425. if idx == e.idx: return e.label
  426. def set_comment(self,idx,comment):
  427. for e in self.data:
  428. if idx == e.idx:
  429. e.label = comment
  430. def make_reverse_dict(self,coinaddrs):
  431. d,b = MMGenDict(),coinaddrs
  432. for e in self.data:
  433. try:
  434. d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label
  435. except: pass
  436. return d
  437. def remove_dup_keys(self,cmplist):
  438. assert self.has_keys
  439. pop_list = []
  440. for n,d in enumerate(self.data):
  441. for e in cmplist.data:
  442. if e.sec.wif == d.sec.wif:
  443. pop_list.append(n)
  444. for n in reversed(pop_list): self.data.pop(n)
  445. if pop_list:
  446. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed,'s')))
  447. def add_wifs(self,key_list):
  448. if not key_list: return
  449. for d in self.data:
  450. for e in key_list.data:
  451. if e.addr and e.sec and e.addr == d.addr:
  452. d.sec = e.sec
  453. def list_missing(self,key):
  454. return [d.addr for d in self.data if not getattr(d,key)]
  455. def generate_addrs_from_keys(self):
  456. # assume that the first listed mmtype is valid for flat key list
  457. t = MMGenAddrType(g.proto.mmtypes[0])
  458. kg = KeyGenerator(t.pubkey_type)
  459. ag = AddrGenerator(t.gen_method)
  460. d = self.data
  461. for n,e in enumerate(d,1):
  462. qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
  463. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  464. if g.debug_addrlist: Msg('generate_addrs_from_keys():\n{}'.format(e.pformat()))
  465. qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
  466. def format(self,enable_comments=False):
  467. out = [self.msgs['file_header']+'\n']
  468. if self.chksum:
  469. out.append('# {} data checksum for {}: {}'.format(
  470. capfirst(self.data_desc),self.id_str,self.chksum))
  471. out.append('# Record this value to a secure location.\n')
  472. if type(self) == PasswordList:
  473. lbl = '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
  474. else:
  475. bc,mt = g.proto.base_coin,self.al_id.mmtype
  476. l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
  477. l_type = [] if mt == 'E' or (mt == 'L' and not g.proto.is_testnet()) else [mt.name.upper()]
  478. l_tn = [] if not g.proto.is_testnet() else ['TESTNET']
  479. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  480. lbl = self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  481. dmsg_sc('lbl',lbl[9:])
  482. out.append('{} {{'.format(lbl))
  483. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  484. for e in self.data:
  485. c = ' '+e.label if enable_comments and e.label else ''
  486. if type(self) == KeyList:
  487. out.append(fs.format(e.idx,'{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  488. elif type(self) == PasswordList:
  489. out.append(fs.format(e.idx,e.passwd,c))
  490. else: # First line with idx
  491. out.append(fs.format(e.idx,e.addr,c))
  492. if self.has_keys:
  493. if opt.b16: out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c))
  494. out.append(fs.format('','{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  495. for k in ('viewkey','wallet_passwd'):
  496. v = getattr(e,k)
  497. if v: out.append(fs.format('','{}: {}'.format(k,v),c))
  498. out.append('}')
  499. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  500. def parse_file_body(self,lines):
  501. ret = AddrListList()
  502. le = self.entry_type
  503. def get_line():
  504. ret = lines.pop(0).split(None,2)
  505. if ret[0] == 'orig_hex:': # hacky
  506. return lines.pop(0).split(None,2)
  507. return ret
  508. while lines:
  509. d = get_line()
  510. assert is_mmgen_idx(d[0]),"'{}': invalid address num. in line: '{}'".format(d[0],' '.join(d))
  511. assert self.check_format(d[1]),"'{}': invalid {}".format(d[1],self.data_desc)
  512. if len(d) != 3: d.append('')
  513. a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]})
  514. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  515. d = get_line()
  516. assert d[0] == self.al_id.mmtype.wif_label,"Invalid line in file: '{}'".format(' '.join(d))
  517. a.sec = PrivKey(wif=d[1])
  518. for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)):
  519. if k in self.al_id.mmtype.extra_attrs:
  520. d = get_line()
  521. assert d[0] == k+':',"Invalid line in file: '{}'".format(' '.join(d))
  522. setattr(a,k,dtype(d[1]))
  523. ret.append(a)
  524. if self.has_keys:
  525. if (hasattr(opt,'yes') and opt.yes) or keypress_confirm('Check key-to-address validity?'):
  526. kg = KeyGenerator(self.al_id.mmtype)
  527. ag = AddrGenerator(self.al_id.mmtype)
  528. llen = len(ret)
  529. for n,e in enumerate(ret):
  530. qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen))
  531. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  532. "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr))
  533. qmsg(' - done')
  534. return ret
  535. def parse_file(self,fn,buf=[],exit_on_error=True):
  536. def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
  537. al_coin,al_mmtype = None,None
  538. tn = lbl[-8:] == ':TESTNET'
  539. if tn:
  540. assert g.proto.is_testnet(),'{} file is testnet but protocol is mainnet!'.format(self.data_desc)
  541. lbl = lbl[:-8]
  542. else:
  543. assert not g.proto.is_testnet(),'{} file is mainnet but protocol is testnet!'.format(self.data_desc)
  544. lbl = lbl.split(':',1)
  545. if len(lbl) == 2:
  546. al_coin,al_mmtype = lbl[0],lbl[1].lower()
  547. else:
  548. if lbl[0].lower() in MMGenAddrType.get_names():
  549. al_mmtype = lbl[0].lower()
  550. else:
  551. al_coin = lbl[0]
  552. # this block fails if al_mmtype is invalid for g.coin
  553. if not al_mmtype:
  554. mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise')
  555. else:
  556. mmtype = MMGenAddrType(al_mmtype,on_fail='raise')
  557. from mmgen.protocol import CoinProtocol
  558. base_coin = CoinProtocol(al_coin or 'BTC',testnet=False).base_coin
  559. return base_coin,mmtype
  560. def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
  561. m = '{} address file format, but base coin is {}!'
  562. assert base_coin == g.proto.base_coin, m.format(base_coin,g.proto.base_coin)
  563. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  564. try:
  565. assert len(lines) >= 3, 'Too few lines in address file ({})'.format(len(lines))
  566. ls = lines[0].split()
  567. assert 1 < len(ls) < 5, "Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])
  568. assert ls.pop() == '{', "'{}': invalid first line".format(ls)
  569. assert lines[-1] == '}', "'{}': invalid last line".format(lines[-1])
  570. sid = ls.pop(0)
  571. assert is_mmgen_seed_id(sid),"'{}': invalid Seed ID".format(ls[0])
  572. if type(self) == PasswordList and len(ls) == 2:
  573. ss = ls.pop().split(':')
  574. assert len(ss) == 2,"'{}': invalid password length specifier (must contain colon)".format(ls[2])
  575. self.set_pw_fmt(ss[0])
  576. self.set_pw_len(ss[1])
  577. self.pw_id_str = MMGenPWIDString(ls.pop())
  578. mmtype = MMGenPasswordType('P')
  579. elif len(ls) == 1:
  580. base_coin,mmtype = parse_addrfile_label(ls[0])
  581. check_coin_mismatch(base_coin)
  582. elif len(ls) == 0:
  583. base_coin,mmtype = 'BTC',MMGenAddrType('L')
  584. check_coin_mismatch(base_coin)
  585. else:
  586. raise ValueError("'{}': Invalid first line for {} file '{}'".format(lines[0],self.gen_desc,fn))
  587. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  588. data = self.parse_file_body(lines[1:-1])
  589. assert issubclass(type(data),list),'Invalid file body data'
  590. except Exception as e:
  591. m = 'Invalid address list file ({})'.format(e.args[0])
  592. if exit_on_error: die(3,m)
  593. msg(msg)
  594. return False
  595. return data
  596. class KeyAddrList(AddrList):
  597. data_desc = 'key-address'
  598. file_desc = 'secret keys'
  599. gen_desc = 'key/address pair'
  600. gen_desc_pl = 's'
  601. gen_addrs = True
  602. gen_keys = True
  603. has_keys = True
  604. ext = 'akeys'
  605. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  606. class KeyList(AddrList):
  607. msgs = {
  608. 'file_header': """
  609. # {pnm} key file
  610. #
  611. # This file is editable.
  612. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  613. """.strip().format(pnm=pnm)
  614. }
  615. data_desc = 'key'
  616. file_desc = 'secret keys'
  617. gen_desc = 'key'
  618. gen_desc_pl = 's'
  619. gen_addrs = False
  620. gen_keys = True
  621. has_keys = True
  622. ext = 'keys'
  623. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  624. class PasswordList(AddrList):
  625. msgs = {
  626. 'file_header': """
  627. # {pnm} password file
  628. #
  629. # This file is editable.
  630. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  631. # A text label of {n} characters or less may be added to the right of each
  632. # password. The label may contain any printable ASCII symbol.
  633. #
  634. """.strip().format(n=TwComment.max_len,pnm=pnm),
  635. 'record_chksum': """
  636. Record this checksum: it will be used to verify the password file in the future
  637. """.strip()
  638. }
  639. entry_type = PasswordListEntry
  640. main_attr = 'passwd'
  641. data_desc = 'password'
  642. file_desc = 'passwords'
  643. gen_desc = 'password'
  644. gen_desc_pl = 's'
  645. gen_addrs = False
  646. gen_keys = False
  647. gen_passwds = True
  648. has_keys = False
  649. ext = 'pws'
  650. pw_len = None
  651. pw_fmt = None
  652. pw_info = {
  653. 'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
  654. 'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' },
  655. 'hex': { 'min_len': 64 ,'max_len': 64 ,'dfl_len': 64, 'desc': 'raw hex password' }
  656. }
  657. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  658. def __init__( self,infile=None,seed=None,
  659. pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
  660. chk_params_only=False):
  661. self.update_msgs()
  662. if infile:
  663. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  664. else:
  665. for k in seed,pw_idxs: assert chk_params_only or k
  666. for k in (pw_id_str,pw_fmt): assert k
  667. self.pw_id_str = MMGenPWIDString(pw_id_str)
  668. self.set_pw_fmt(pw_fmt)
  669. self.set_pw_len(pw_len)
  670. if chk_params_only: return
  671. self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
  672. self.data = self.generate(seed,pw_idxs)
  673. self.num_addrs = len(self.data)
  674. self.fmt_data = ''
  675. self.chksum = AddrListChksum(self)
  676. fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
  677. self.id_str = AddrListIDStr(self,fs)
  678. qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
  679. qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
  680. def set_pw_fmt(self,pw_fmt):
  681. assert pw_fmt in self.pw_info
  682. self.pw_fmt = pw_fmt
  683. def chk_pw_len(self,passwd=None):
  684. if passwd is None:
  685. assert self.pw_len
  686. pw_len = self.pw_len
  687. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  688. else:
  689. pw_len = len(passwd)
  690. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  691. d = self.pw_info[self.pw_fmt]
  692. if pw_len > d['max_len']:
  693. die(2,fs.format(l=pw_len,b=d['desc'],c='>',m=d['max_len'],pw=passwd))
  694. elif pw_len < d['min_len']:
  695. die(2,fs.format(l=pw_len,b=d['desc'],c='<',m=d['min_len'],pw=passwd))
  696. def set_pw_len(self,pw_len):
  697. assert self.pw_fmt in self.pw_info
  698. d = self.pw_info[self.pw_fmt]
  699. if pw_len is None:
  700. self.pw_len = d['dfl_len']
  701. return
  702. if not is_int(pw_len):
  703. die(2,"'{}': invalid user-requested password length (not an integer)".format(pw_len,d['desc']))
  704. self.pw_len = int(pw_len)
  705. self.chk_pw_len()
  706. def make_passwd(self,hex_sec):
  707. assert self.pw_fmt in self.pw_info
  708. if self.pw_fmt == 'hex':
  709. return hex_sec.decode()
  710. else:
  711. # we take least significant part
  712. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  713. def check_format(self,pw):
  714. if not {'b58':is_b58_str,'b32':is_b32_str,'hex':is_hex_str}[self.pw_fmt](pw):
  715. msg('Password is not a valid {} string'.format(self.pw_fmt))
  716. return False
  717. if len(pw) != self.pw_len:
  718. msg('Password has incorrect length ({} != {})'.format(len(pw),self.pw_len))
  719. return False
  720. return True
  721. def scramble_seed(self,seed):
  722. # Changing either pw_fmt or pw_len will cause a different, unrelated
  723. # set of passwords to be generated: this is what we want.
  724. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  725. scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
  726. from mmgen.crypto import scramble_seed
  727. return scramble_seed(seed,scramble_key.encode(),self.scramble_hash_rounds)
  728. class AddrData(MMGenObject):
  729. msgs = {
  730. 'too_many_acct_addresses': """
  731. ERROR: More than one address found for account: '{{}}'.
  732. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  733. Please restore your tracking wallet from a backup or create a new one and
  734. re-import your addresses.
  735. """.strip().format(pnm=pnm)
  736. }
  737. def __new__(cls,*args,**kwargs):
  738. return MMGenObject.__new__(altcoin_subclass(cls,'tw','AddrData'))
  739. def __init__(self,source=None):
  740. self.al_ids = {}
  741. if source == 'tw': self.add_tw_data()
  742. def seed_ids(self):
  743. return list(self.al_ids.keys())
  744. def addrlist(self,al_id):
  745. # TODO: Validate al_id
  746. if al_id in self.al_ids:
  747. return self.al_ids[al_id]
  748. def mmaddr2coinaddr(self,mmaddr):
  749. al_id,idx = MMGenID(mmaddr).rsplit(':',1)
  750. coinaddr = ''
  751. if al_id in self.al_ids:
  752. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  753. return coinaddr or None
  754. def coinaddr2mmaddr(self,coinaddr):
  755. d = self.make_reverse_dict([coinaddr])
  756. return (list(d.values())[0][0]) if d else None
  757. @classmethod
  758. def get_tw_data(cls):
  759. vmsg('Getting address data from tracking wallet')
  760. if 'label_api' in g.rpch.caps:
  761. accts = g.rpch.listlabels()
  762. alists = [list(a.keys()) for a in g.rpch.getaddressesbylabel([[k] for k in accts],batch=True)]
  763. else:
  764. accts = g.rpch.listaccounts(0,True)
  765. alists = g.rpch.getaddressesbyaccount([[k] for k in accts],batch=True)
  766. return list(zip(accts,alists))
  767. def add_tw_data(self):
  768. d,out,i = self.get_tw_data(),{},0
  769. for acct,addr_array in d:
  770. l = TwLabel(acct,on_fail='silent')
  771. if l and l.mmid.type == 'mmgen':
  772. obj = l.mmid.obj
  773. i += 1
  774. if len(addr_array) != 1:
  775. die(2,self.msgs['too_many_acct_addresses'].format(acct))
  776. al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype))
  777. if al_id not in out:
  778. out[al_id] = []
  779. out[al_id].append(AddrListEntry(idx=obj.idx,addr=addr_array[0],label=l.comment))
  780. vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(d)))
  781. for al_id in out:
  782. self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(out[al_id],key=lambda a: a.idx))))
  783. def add(self,addrlist):
  784. if type(addrlist) == AddrList:
  785. self.al_ids[addrlist.al_id] = addrlist
  786. return True
  787. else:
  788. raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
  789. def make_reverse_dict(self,coinaddrs):
  790. d = MMGenDict()
  791. for al_id in self.al_ids:
  792. d.update(self.al_ids[al_id].make_reverse_dict(coinaddrs))
  793. return d