addr.py 32 KB

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