addr.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2015 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. import sys
  22. from hashlib import sha256, sha512
  23. from hashlib import new as hashlib_new
  24. from binascii import hexlify, unhexlify
  25. from mmgen.bitcoin import numtowif
  26. # from mmgen.util import msg,qmsg,qmsg_r,make_chksum_N,get_lines_from_file,get_data_from_file,get_extension
  27. from mmgen.util import *
  28. from mmgen.tx import *
  29. from mmgen.obj import *
  30. import mmgen.config as g
  31. import mmgen.opt as opt
  32. pnm = g.proj_name
  33. addrmsgs = {
  34. 'addrfile_header': """
  35. # {pnm} address file
  36. #
  37. # This file is editable.
  38. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  39. # A text label of {n} characters or less may be added to the right of each
  40. # address, and it will be appended to the bitcoind wallet label upon import.
  41. # The label may contain any printable ASCII symbol.
  42. """.strip().format(n=g.max_addr_label_len,pnm=pnm),
  43. 'no_keyconv_msg': """
  44. Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library.
  45. Please install '{kconv}' from the {vgen} package on your system for much
  46. faster address generation.
  47. """.format(kconv=g.keyconv_exec, vgen="vanitygen")
  48. }
  49. def test_for_keyconv(silent=False):
  50. from subprocess import check_output,STDOUT
  51. try:
  52. check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
  53. except:
  54. if not silent: msg(addrmsgs['no_keyconv_msg'])
  55. return False
  56. return True
  57. def generate_addrs(seed, addrnums, source="addrgen"):
  58. from util import make_chksum_8
  59. seed_id = make_chksum_8(seed) # Must do this before seed gets clobbered
  60. if 'a' in opt.gen_what:
  61. if opt.no_keyconv or test_for_keyconv() == False:
  62. msg("Using (slow) internal ECDSA library for address generation")
  63. from mmgen.bitcoin import privnum2addr
  64. keyconv = False
  65. else:
  66. from subprocess import check_output
  67. keyconv = "keyconv"
  68. addrnums = sorted(set(addrnums)) # don't trust the calling function
  69. t_addrs,num,pos,out = len(addrnums),0,0,[]
  70. w = {
  71. 'ka': ('key/address pair','s'),
  72. 'k': ('key','s'),
  73. 'a': ('address','es')
  74. }[opt.gen_what]
  75. from mmgen.addr import AddrInfoEntry,AddrInfo
  76. while pos != t_addrs:
  77. seed = sha512(seed).digest()
  78. num += 1 # round
  79. if num != addrnums[pos]: continue
  80. pos += 1
  81. qmsg_r("\rGenerating %s #%s (%s of %s)" % (w[0],num,pos,t_addrs))
  82. e = AddrInfoEntry()
  83. e.idx = num
  84. # Secret key is double sha256 of seed hash round /num/
  85. sec = sha256(sha256(seed).digest()).hexdigest()
  86. wif = numtowif(int(sec,16))
  87. if 'a' in opt.gen_what:
  88. if keyconv:
  89. e.addr = check_output([keyconv, wif]).split()[1]
  90. else:
  91. e.addr = privnum2addr(int(sec,16))
  92. if 'k' in opt.gen_what: e.wif = wif
  93. if opt.b16: e.sec = sec
  94. out.append(e)
  95. m = w[0] if t_addrs == 1 else w[0]+w[1]
  96. qmsg("\r%s: %s %s generated%s" % (seed_id,t_addrs,m," "*15))
  97. a = AddrInfo(has_keys='k' in opt.gen_what, source=source)
  98. a.initialize(seed_id,out)
  99. return a
  100. def _parse_addrfile_body(lines,has_keys=False,check=False):
  101. if has_keys and len(lines) % 2:
  102. return "Key-address file has odd number of lines"
  103. ret = []
  104. while lines:
  105. a = AddrInfoEntry()
  106. l = lines.pop(0)
  107. d = l.split(None,2)
  108. if not is_mmgen_idx(d[0]):
  109. return "'%s': invalid address num. in line: '%s'" % (d[0],l)
  110. if not is_btc_addr(d[1]):
  111. return "'%s': invalid Bitcoin address" % d[1]
  112. if len(d) == 3: check_addr_label(d[2])
  113. else: d.append("")
  114. a.idx,a.addr,a.comment = int(d[0]),unicode(d[1]),unicode(d[2])
  115. if has_keys:
  116. l = lines.pop(0)
  117. d = l.split(None,2)
  118. if d[0] != "wif:":
  119. return "Invalid key line in file: '%s'" % l
  120. if not is_wif(d[1]):
  121. return "'%s': invalid Bitcoin key" % d[1]
  122. a.wif = unicode(d[1])
  123. ret.append(a)
  124. if has_keys and keypress_confirm("Check key-to-address validity?"):
  125. wif2addr_f = get_wif2addr_f()
  126. llen = len(ret)
  127. for n,e in enumerate(ret):
  128. msg_r("\rVerifying keys %s/%s" % (n+1,llen))
  129. if e.addr != wif2addr_f(e.wif):
  130. return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr)
  131. msg(" - done")
  132. return ret
  133. def _parse_addrfile(fn,buf=[],has_keys=False,exit_on_error=True):
  134. if buf: lines = remove_comments(buf.split("\n"))
  135. else: lines = get_lines_from_file(fn,"address data",trim_comments=True)
  136. try:
  137. sid,obrace = lines[0].split()
  138. except:
  139. errmsg = "Invalid first line: '%s'" % lines[0]
  140. else:
  141. cbrace = lines[-1]
  142. if obrace != '{':
  143. errmsg = "'%s': invalid first line" % lines[0]
  144. elif cbrace != '}':
  145. errmsg = "'%s': invalid last line" % cbrace
  146. elif not is_mmgen_seed_id(sid):
  147. errmsg = "'%s': invalid seed ID" % sid
  148. else:
  149. ret = _parse_addrfile_body(lines[1:-1],has_keys)
  150. if type(ret) == list: return sid,ret
  151. else: errmsg = ret
  152. if exit_on_error:
  153. msg(errmsg)
  154. sys.exit(3)
  155. else:
  156. return False
  157. def _parse_keyaddr_file(infile):
  158. d = get_data_from_file(infile,"{pnm} key-address file data".format(pnm=pnm))
  159. enc_ext = get_extension(infile) == g.mmenc_ext
  160. if enc_ext or not is_utf8(d):
  161. m = "Decrypting" if enc_ext else "Attempting to decrypt"
  162. msg("%s key-address file %s" % (m,infile))
  163. from crypto import mmgen_decrypt_retry
  164. d = mmgen_decrypt_retry(d,"key-address file")
  165. return _parse_addrfile("",buf=d,has_keys=True,exit_on_error=False)
  166. class AddrInfoList(MMGenObject):
  167. def __init__(self,addrinfo=None,bitcoind_connection=None):
  168. self.data = {}
  169. if bitcoind_connection:
  170. self.add_wallet_data(bitcoind_connection)
  171. def seed_ids(self):
  172. return self.data.keys()
  173. def addrinfo(self,sid):
  174. # TODO: Validate sid
  175. if sid in self.data:
  176. return self.data[sid]
  177. def add_wallet_data(self,c):
  178. vmsg_r("Getting account data from wallet...")
  179. data,accts,i = {},c.listaccounts(minconf=0,includeWatchonly=True),0
  180. for acct in accts:
  181. ma,comment = parse_mmgen_label(acct)
  182. if ma:
  183. i += 1
  184. addrlist = c.getaddressesbyaccount(acct)
  185. if len(addrlist) != 1:
  186. msg(wmsg['too_many_acct_addresses'] % acct)
  187. sys.exit(2)
  188. seed_id,idx = ma.split(":")
  189. if seed_id not in data:
  190. data[seed_id] = []
  191. a = AddrInfoEntry()
  192. a.idx,a.addr,a.comment = \
  193. int(idx),unicode(addrlist[0]),unicode(comment)
  194. data[seed_id].append(a)
  195. vmsg("{n} {pnm} addresses found, {m} accounts total".format(
  196. n=i,pnm=pnm,m=len(accts)))
  197. for sid in data:
  198. self.add(AddrInfo(sid=sid,adata=data[sid]))
  199. def add(self,addrinfo):
  200. if type(addrinfo) == AddrInfo:
  201. self.data[addrinfo.seed_id] = addrinfo
  202. return True
  203. else:
  204. msg("Error: object %s is not of type AddrInfo" % repr(addrinfo))
  205. sys.exit(1)
  206. def make_reverse_dict(self,btcaddrs):
  207. d = {}
  208. for k in self.data.keys():
  209. d.update(self.data[k].make_reverse_dict(btcaddrs))
  210. return d
  211. class AddrInfoEntry(MMGenObject):
  212. def __init__(self): pass
  213. class AddrInfo(MMGenObject):
  214. def __init__(self,addrfile="",has_keys=False,sid="",adata=[], source=""):
  215. self.has_keys = has_keys
  216. do_chksum = True
  217. if addrfile:
  218. f = _parse_keyaddr_file if has_keys else _parse_addrfile
  219. sid,adata = f(addrfile)
  220. self.source = "addrfile"
  221. elif sid and adata: # data from wallet
  222. self.source = "wallet"
  223. elif sid or adata:
  224. die(3,"Must specify address file, or seed_id + adata")
  225. else:
  226. self.source = source if source else "unknown"
  227. return
  228. self.initialize(sid,adata)
  229. def initialize(self,seed_id,addrdata):
  230. if seed_id in self.__dict__:
  231. msg("Seed ID already set for object %s" % self)
  232. return False
  233. self.seed_id = seed_id
  234. self.addrdata = addrdata
  235. self.num_addrs = len(addrdata)
  236. if self.source in ("wallet","txsign") or \
  237. (self.source == "addrgen" and opt.gen_what == "k"):
  238. self.checksum = None
  239. self.idxs_fmt = None
  240. else: # self.source in addrfile, addrgen
  241. self.make_addrdata_chksum()
  242. self.fmt_addr_idxs()
  243. w = "key-address" if self.has_keys else "address"
  244. qmsg("Checksum for %s data %s[%s]: %s" %
  245. (w,self.seed_id,self.idxs_fmt,self.checksum))
  246. if self.source == "addrgen":
  247. qmsg(
  248. "This checksum will be used to verify the address file in the future")
  249. elif self.source == "addrfile":
  250. qmsg("Check this value against your records")
  251. def idxs(self):
  252. return [e.idx for e in self.addrdata]
  253. def addrs(self):
  254. return ["%s:%s"%(self.seed_id,e.idx) for e in self.addrdata]
  255. def addrpairs(self):
  256. return [(e.idx,e.addr) for e in self.addrdata]
  257. def btcaddrs(self):
  258. return [e.addr for e in self.addrdata]
  259. def comments(self):
  260. return [e.comment for e in self.addrdata]
  261. def entry(self,idx):
  262. for e in self.addrdata:
  263. if idx == e.idx: return e
  264. def btcaddr(self,idx):
  265. for e in self.addrdata:
  266. if idx == e.idx: return e.addr
  267. def comment(self,idx):
  268. for e in self.addrdata:
  269. if idx == e.idx: return e.comment
  270. def set_comment(self,idx,comment):
  271. for e in self.addrdata:
  272. if idx == e.idx:
  273. if is_valid_tx_comment(comment):
  274. e.comment = comment
  275. else:
  276. sys.exit(2)
  277. def make_reverse_dict(self,btcaddrs):
  278. d,b = {},btcaddrs
  279. for e in self.addrdata:
  280. try:
  281. d[b[b.index(e.addr)]] = ("%s:%s"%(self.seed_id,e.idx),e.comment)
  282. except: pass
  283. return d
  284. def make_addrdata_chksum(self):
  285. nchars = 24
  286. lines=[" ".join([str(e.idx),e.addr]+([e.wif] if self.has_keys else []))
  287. for e in self.addrdata]
  288. self.checksum = make_chksum_N(" ".join(lines), nchars, sep=True)
  289. def fmt_data(self,enable_comments=False):
  290. # Check data integrity - either all or none must exist for each attr
  291. attrs = ['addr','wif','sec']
  292. status = [0,0,0]
  293. for i in range(self.num_addrs):
  294. for j,attr in enumerate(attrs):
  295. try:
  296. getattr(self.addrdata[i],attr)
  297. status[j] += 1
  298. except: pass
  299. for i,s in enumerate(status):
  300. if s != 0 and s != self.num_addrs:
  301. msg("%s missing %s in addr data"% (self.num_addrs-s,attrs[i]))
  302. sys.exit(3)
  303. if status[0] == None and status[1] == None:
  304. msg("Addr data contains neither addresses nor keys")
  305. sys.exit(3)
  306. # Header
  307. out = []
  308. from mmgen.addr import addrmsgs
  309. out.append(addrmsgs['addrfile_header'] + "\n")
  310. w = "Key-address" if status[1] else "Address"
  311. out.append("# {} data checksum for {}[{}]: {}".format(
  312. w, self.seed_id, self.idxs_fmt, self.checksum))
  313. out.append("# Record this value to a secure location\n")
  314. out.append("%s {" % self.seed_id)
  315. # Body
  316. fs = " {:<%s} {:<34}{}" % len(str(self.addrdata[-1].idx))
  317. for e in self.addrdata:
  318. c = ""
  319. if enable_comments:
  320. try: c = " "+e.comment
  321. except: pass
  322. if status[0]: # First line with idx
  323. out.append(fs.format(e.idx, e.addr,c))
  324. else:
  325. out.append(fs.format(e.idx, "wif: "+e.wif,c))
  326. if status[1]: # Subsequent lines
  327. if status[2]:
  328. out.append(fs.format("", "hex: "+e.sec,c))
  329. if status[0]:
  330. out.append(fs.format("", "wif: "+e.wif,c))
  331. out.append("}")
  332. return "\n".join([l.rstrip() for l in out])
  333. def fmt_addr_idxs(self):
  334. try: int(self.addrdata[0].idx)
  335. except:
  336. self.idxs_fmt = "(no idxs)"
  337. return
  338. addr_idxs = [e.idx for e in self.addrdata]
  339. prev = addr_idxs[0]
  340. ret = prev,
  341. for i in addr_idxs[1:]:
  342. if i == prev + 1:
  343. if i == addr_idxs[-1]: ret += "-", i
  344. else:
  345. if prev != ret[-1]: ret += "-", prev
  346. ret += ",", i
  347. prev = i
  348. self.idxs_fmt = "".join([str(i) for i in ret])