addr.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
  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 mmgen.common import *
  23. from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex
  24. from mmgen.obj import *
  25. from mmgen.tx import *
  26. from mmgen.tw import *
  27. pnm = g.proj_name
  28. def _test_for_keyconv(silent=False):
  29. no_keyconv_errmsg = """
  30. Executable '{kconv}' unavailable. Please install '{kconv}' from the {vgen}
  31. package on your system or specify the secp256k1 library.
  32. """.format(kconv=g.keyconv_exec, vgen='vanitygen')
  33. from subprocess import check_output,STDOUT
  34. try:
  35. check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
  36. except:
  37. if not silent: msg(no_keyconv_errmsg.strip())
  38. return False
  39. return True
  40. def _test_for_secp256k1(silent=False):
  41. no_secp256k1_errmsg = """
  42. secp256k1 library unavailable. Will use '{kconv}', or failing that, the (slow)
  43. internal ECDSA library for address generation.
  44. """.format(kconv=g.keyconv_exec)
  45. try:
  46. from mmgen.secp256k1 import priv2pub
  47. assert priv2pub(os.urandom(32),1)
  48. except:
  49. if not silent: msg(no_secp256k1_errmsg.strip())
  50. return False
  51. return True
  52. def _wif2addr_python(wif):
  53. privhex = wif2hex(wif)
  54. if not privhex: return False
  55. return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet])
  56. def _wif2addr_keyconv(wif):
  57. if wif[0] == ('5','9')[g.testnet]:
  58. from subprocess import check_output
  59. return check_output(['keyconv', wif]).split()[1]
  60. else:
  61. return _wif2addr_python(wif)
  62. def _wif2addr_secp256k1(wif):
  63. return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet])
  64. def _privhex2addr_python(privhex,compressed=False):
  65. return privnum2addr(int(privhex,16),compressed)
  66. def _privhex2addr_keyconv(privhex,compressed=False):
  67. if compressed:
  68. return privnum2addr(int(privhex,16),compressed)
  69. else:
  70. from subprocess import check_output
  71. return check_output(['keyconv', hex2wif(privhex,compressed=False)]).split()[1]
  72. def _privhex2addr_secp256k1(privhex,compressed=False):
  73. from mmgen.secp256k1 import priv2pub
  74. from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr
  75. from binascii import hexlify,unhexlify
  76. pubkey = priv2pub(unhexlify(privhex),int(compressed))
  77. return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey)))
  78. def _keygen_selector(generator=None):
  79. if generator:
  80. if generator == 3 and _test_for_secp256k1(): return 2
  81. elif generator in (2,3) and _test_for_keyconv(): return 1
  82. else:
  83. if opt.key_generator == 3 and _test_for_secp256k1(): return 2
  84. elif opt.key_generator in (2,3) and _test_for_keyconv(): return 1
  85. msg('Using (slow) internal ECDSA library for address generation')
  86. return 0
  87. def get_wif2addr_f(generator=None):
  88. gen = _keygen_selector(generator=generator)
  89. return (_wif2addr_python,_wif2addr_keyconv,_wif2addr_secp256k1)[gen]
  90. def get_privhex2addr_f(generator=None):
  91. gen = _keygen_selector(generator=generator)
  92. return (_privhex2addr_python,_privhex2addr_keyconv,_privhex2addr_secp256k1)[gen]
  93. class AddrListEntry(MMGenListItem):
  94. attrs = 'idx','addr','label','wif','sec'
  95. label = MMGenListItemAttr('label','MMGenAddrLabel')
  96. idx = MMGenListItemAttr('idx','AddrIdx')
  97. class AddrListChksum(str,Hilite):
  98. color = 'pink'
  99. trunc_ok = False
  100. def __new__(cls,addrlist):
  101. lines=[' '.join([str(e.idx),e.addr]+([e.wif] if addrlist.has_keys else []))
  102. for e in addrlist.data]
  103. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  104. class AddrListID(str,Hilite):
  105. color = 'green'
  106. trunc_ok = False
  107. def __new__(cls,addrlist):
  108. try: int(addrlist.data[0].idx)
  109. except:
  110. s = '(no idxs)'
  111. else:
  112. idxs = [e.idx for e in addrlist.data]
  113. prev = idxs[0]
  114. ret = prev,
  115. for i in idxs[1:]:
  116. if i == prev + 1:
  117. if i == idxs[-1]: ret += '-', i
  118. else:
  119. if prev != ret[-1]: ret += '-', prev
  120. ret += ',', i
  121. prev = i
  122. s = ''.join([str(i) for i in ret])
  123. return str.__new__(cls,'%s[%s]' % (addrlist.seed_id,s))
  124. class AddrList(MMGenObject): # Address info for a single seed ID
  125. msgs = {
  126. 'file_header': """
  127. # {pnm} address file
  128. #
  129. # This file is editable.
  130. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  131. # A text label of {n} characters or less may be added to the right of each
  132. # address, and it will be appended to the bitcoind wallet label upon import.
  133. # The label may contain any printable ASCII symbol.
  134. """.strip().format(n=MMGenAddrLabel.max_len,pnm=pnm),
  135. 'record_chksum': """
  136. Record this checksum: it will be used to verify the address file in the future
  137. """.strip(),
  138. 'check_chksum': 'Check this value against your records',
  139. 'removed_dups': """
  140. Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
  141. """.strip().format(pnm=pnm)
  142. }
  143. data_desc = 'address'
  144. file_desc = 'addresses'
  145. gen_desc = 'address'
  146. gen_desc_pl = 'es'
  147. gen_addrs = True
  148. gen_keys = False
  149. has_keys = False
  150. ext = 'addrs'
  151. def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='',
  152. addrlist='',keylist='',do_chksum=True,chksum_only=False):
  153. self.update_msgs()
  154. if addrfile: # data from MMGen address file
  155. (sid,adata) = self.parse_file(addrfile)
  156. elif sid and adata: # data from tracking wallet
  157. do_chksum = False
  158. elif seed and addr_idxs: # data from seed + idxs
  159. sid,src = seed.sid,'gen'
  160. adata = self.generate(seed,addr_idxs)
  161. elif addrlist: # data from flat address list
  162. sid = None
  163. adata = [AddrListEntry(addr=a) for a in addrlist]
  164. elif keylist: # data from flat key list
  165. sid,do_chksum = None,False
  166. adata = [AddrListEntry(wif=k) for k in keylist]
  167. elif seed or addr_idxs:
  168. die(3,'Must specify both seed and addr indexes')
  169. elif sid or adata:
  170. die(3,'Must specify both seed_id and adata')
  171. else:
  172. die(3,'Incorrect arguments for %s' % type(self).__name__)
  173. # sid,adata now set
  174. self.seed_id = sid
  175. self.data = adata
  176. self.num_addrs = len(adata)
  177. self.fmt_data = ''
  178. self.id_str = None
  179. self.chksum = None
  180. if type(self) == KeyList:
  181. self.id_str = AddrListID(self)
  182. return
  183. if do_chksum:
  184. self.chksum = AddrListChksum(self)
  185. if chksum_only:
  186. Msg(self.chksum)
  187. else:
  188. self.id_str = AddrListID(self)
  189. qmsg('Checksum for %s data %s: %s' %
  190. (self.data_desc,self.id_str.hl(),self.chksum.hl()))
  191. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  192. def update_msgs(self):
  193. if type(self).msgs and type(self) != AddrList:
  194. for k in AddrList.msgs:
  195. if k not in self.msgs:
  196. self.msgs[k] = AddrList.msgs[k]
  197. def generate(self,seed,addrnums):
  198. assert type(addrnums) is AddrIdxList
  199. self.seed_id = SeedID(seed=seed)
  200. seed = seed.get_data()
  201. if self.gen_addrs:
  202. privhex2addr_f = get_privhex2addr_f()
  203. t_addrs,num,pos,out = len(addrnums),0,0,[]
  204. while pos != t_addrs:
  205. seed = sha512(seed).digest()
  206. num += 1 # round
  207. if num != addrnums[pos]: continue
  208. pos += 1
  209. qmsg_r('\rGenerating %s #%s (%s of %s)' % (self.gen_desc,num,pos,t_addrs))
  210. e = AddrListEntry(idx=num)
  211. # Secret key is double sha256 of seed hash round /num/
  212. sec = sha256(sha256(seed).digest()).hexdigest()
  213. if self.gen_addrs:
  214. e.addr = privhex2addr_f(sec,compressed=False)
  215. if self.gen_keys:
  216. e.wif = hex2wif(sec,compressed=False)
  217. if opt.b16: e.sec = sec
  218. out.append(e)
  219. qmsg('\r%s: %s %s%s generated%s' % (
  220. self.seed_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
  221. return out
  222. def encrypt(self):
  223. from mmgen.crypto import mmgen_encrypt
  224. self.fmt_data = mmgen_encrypt(self.fmt_data,'new key list','')
  225. self.ext += '.'+g.mmenc_ext
  226. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False):
  227. fn = '{}.{}'.format(self.id_str,self.ext)
  228. ask_tty = self.has_keys and not opt.quiet
  229. write_data_to_file(fn,self.fmt_data,self.file_desc,ask_tty=ask_tty,binary=binary)
  230. def idxs(self):
  231. return [e.idx for e in self.data]
  232. def addrs(self):
  233. return ['%s:%s'%(self.seed_id,e.idx) for e in self.data]
  234. def addrpairs(self):
  235. return [(e.idx,e.addr) for e in self.data]
  236. def btcaddrs(self):
  237. return [e.addr for e in self.data]
  238. def comments(self):
  239. return [e.label for e in self.data]
  240. def entry(self,idx):
  241. for e in self.data:
  242. if idx == e.idx: return e
  243. def btcaddr(self,idx):
  244. for e in self.data:
  245. if idx == e.idx: return e.addr
  246. def comment(self,idx):
  247. for e in self.data:
  248. if idx == e.idx: return e.label
  249. def set_comment(self,idx,comment):
  250. for e in self.data:
  251. if idx == e.idx:
  252. e.label = comment
  253. def make_reverse_dict(self,btcaddrs):
  254. d,b = {},btcaddrs
  255. for e in self.data:
  256. try:
  257. d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.label)
  258. except: pass
  259. return d
  260. def flat_list(self):
  261. class AddrListFlatEntry(AddrListEntry):
  262. attrs = 'mmid','addr','wif'
  263. return [AddrListFlatEntry(
  264. mmid='{}:{}'.format(self.seed_id,e.idx),
  265. addr=e.addr,
  266. wif=e.wif)
  267. for e in self.data]
  268. def remove_dups(self,cmplist,key='wif'):
  269. pop_list = []
  270. for n,d in enumerate(self.data):
  271. if getattr(d,key) == None: continue
  272. for e in cmplist.data:
  273. if getattr(e,key) and getattr(e,key) == getattr(d,key):
  274. pop_list.append(n)
  275. for n in reversed(pop_list): self.data.pop(n)
  276. if pop_list:
  277. vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'k')))
  278. def add_wifs(self,al_key):
  279. for d in self.data:
  280. for e in al_key.data:
  281. if e.addr and e.wif and e.addr == d.addr:
  282. d.wif = e.wif
  283. def list_missing(self,key):
  284. return [d for d in self.data if not getattr(d,key)]
  285. def get(self,key):
  286. return [getattr(d,key) for d in self.data if getattr(d,key)]
  287. def get_addrs(self): return self.get('addr')
  288. def get_wifs(self): return self.get('wif')
  289. def generate_addrs(self):
  290. wif2addr_f = get_wif2addr_f()
  291. d = self.data
  292. for n,e in enumerate(d,1):
  293. qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(d)))
  294. e.addr = wif2addr_f(e.wif)
  295. qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d)))
  296. def format(self,enable_comments=False):
  297. def check_attrs(key,desc):
  298. for e in self.data:
  299. if not getattr(e,key):
  300. die(3,'missing %s in addr data' % desc)
  301. if type(self) != KeyList: check_attrs('addr','addresses')
  302. if self.has_keys:
  303. if opt.b16: check_attrs('sec','hex keys')
  304. check_attrs('wif','wif keys')
  305. out = [self.msgs['file_header']+'\n']
  306. if self.chksum:
  307. out.append('# {} data checksum for {}: {}'.format(
  308. self.data_desc.capitalize(),self.id_str,self.chksum))
  309. out.append('# Record this value to a secure location.\n')
  310. out.append('%s {' % self.seed_id)
  311. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  312. for e in self.data:
  313. c = ' '+e.label if enable_comments and e.label else ''
  314. if type(self) == KeyList:
  315. out.append(fs.format(e.idx, 'wif: '+e.wif,c))
  316. else: # First line with idx
  317. out.append(fs.format(e.idx, e.addr,c))
  318. if self.has_keys:
  319. if opt.b16: out.append(fs.format('', 'hex: '+e.sec,c))
  320. out.append(fs.format('', 'wif: '+e.wif,c))
  321. out.append('}')
  322. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  323. def parse_file_body(self,lines):
  324. if self.has_keys and len(lines) % 2:
  325. return 'Key-address file has odd number of lines'
  326. ret = []
  327. while lines:
  328. l = lines.pop(0)
  329. d = l.split(None,2)
  330. if not is_mmgen_idx(d[0]):
  331. return "'%s': invalid address num. in line: '%s'" % (d[0],l)
  332. if not is_btc_addr(d[1]):
  333. return "'%s': invalid Bitcoin address" % d[1]
  334. if len(d) != 3: d.append('')
  335. a = AddrListEntry(idx=int(d[0]),addr=d[1],label=d[2])
  336. if self.has_keys:
  337. l = lines.pop(0)
  338. d = l.split(None,2)
  339. if d[0] != 'wif:':
  340. return "Invalid key line in file: '%s'" % l
  341. if not is_wif(d[1]):
  342. return "'%s': invalid Bitcoin key" % d[1]
  343. a.wif = d[1]
  344. ret.append(a)
  345. if self.has_keys and keypress_confirm('Check key-to-address validity?'):
  346. wif2addr_f = get_wif2addr_f()
  347. llen = len(ret)
  348. for n,e in enumerate(ret):
  349. msg_r('\rVerifying keys %s/%s' % (n+1,llen))
  350. if e.addr != wif2addr_f(e.wif):
  351. return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr)
  352. msg(' - done')
  353. return ret
  354. def parse_file(self,fn,buf=[],exit_on_error=True):
  355. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  356. try:
  357. sid,obrace = lines[0].split()
  358. except:
  359. errmsg = "Invalid first line: '%s'" % lines[0]
  360. else:
  361. cbrace = lines[-1]
  362. if obrace != '{':
  363. errmsg = "'%s': invalid first line" % lines[0]
  364. elif cbrace != '}':
  365. errmsg = "'%s': invalid last line" % cbrace
  366. elif not is_mmgen_seed_id(sid):
  367. errmsg = "'%s': invalid Seed ID" % sid
  368. else:
  369. ret = self.parse_file_body(lines[1:-1])
  370. if type(ret) == list:
  371. return sid,ret
  372. else:
  373. errmsg = ret
  374. if exit_on_error: die(3,errmsg)
  375. msg(errmsg)
  376. return False
  377. class KeyAddrList(AddrList):
  378. data_desc = 'key-address'
  379. file_desc = 'secret keys'
  380. gen_desc = 'key/address pair'
  381. gen_desc_pl = 's'
  382. gen_addrs = True
  383. gen_keys = True
  384. has_keys = True
  385. ext = 'akeys'
  386. class KeyList(AddrList):
  387. msgs = {
  388. 'file_header': """
  389. # {pnm} key file
  390. #
  391. # This file is editable.
  392. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  393. """.strip().format(pnm=pnm)
  394. }
  395. data_desc = 'key'
  396. file_desc = 'secret keys'
  397. gen_desc = 'key'
  398. gen_desc_pl = 's'
  399. gen_addrs = False
  400. gen_keys = True
  401. has_keys = True
  402. ext = 'keys'
  403. class AddrData(MMGenObject):
  404. msgs = {
  405. 'too_many_acct_addresses': """
  406. ERROR: More than one address found for account: '%s'.
  407. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  408. Please restore your tracking wallet from a backup or create a new one and
  409. re-import your addresses.
  410. """.strip().format(pnm=pnm)
  411. }
  412. def __init__(self,source=None):
  413. self.sids = {}
  414. if source == 'tw': self.add_tw_data()
  415. def seed_ids(self):
  416. return self.sids.keys()
  417. def addrlist(self,sid):
  418. # TODO: Validate sid
  419. if sid in self.sids:
  420. return self.sids[sid]
  421. def mmaddr2btcaddr(self,mmaddr):
  422. btcaddr = ''
  423. sid,idx = mmaddr.split(':')
  424. if sid in self.seed_ids():
  425. btcaddr = self.addrlist(sid).btcaddr(int(idx))
  426. return btcaddr
  427. def add_tw_data(self):
  428. vmsg_r('Getting address data from tracking wallet...')
  429. c = bitcoin_connection()
  430. accts = c.listaccounts(0,True)
  431. data,i = {},0
  432. alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
  433. for acct,addrlist in zip(accts,alists):
  434. maddr,label = parse_tw_acct_label(acct)
  435. if maddr:
  436. i += 1
  437. if len(addrlist) != 1:
  438. die(2,self.msgs['too_many_acct_addresses'] % acct)
  439. seed_id,idx = maddr.split(':')
  440. if seed_id not in data:
  441. data[seed_id] = []
  442. data[seed_id].append(AddrListEntry(idx=idx,addr=addrlist[0],label=label))
  443. vmsg('{n} {pnm} addresses found, {m} accounts total'.format(
  444. n=i,pnm=pnm,m=len(accts)))
  445. for sid in data:
  446. self.add(AddrList(sid=sid,adata=data[sid]))
  447. def add(self,addrlist):
  448. if type(addrlist) == AddrList:
  449. self.sids[addrlist.seed_id] = addrlist
  450. return True
  451. else:
  452. raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist)
  453. def make_reverse_dict(self,btcaddrs):
  454. d = {}
  455. for sid in self.sids:
  456. d.update(self.sids[sid].make_reverse_dict(btcaddrs))
  457. return d