util.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  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. util.py: Low-level routines imported by other modules in the MMGen suite
  20. """
  21. import sys,os,time,stat,re
  22. from hashlib import sha256
  23. from string import hexdigits,digits
  24. from mmgen.color import *
  25. from mmgen.exception import *
  26. from mmgen.globalvars import *
  27. if g.platform == 'win':
  28. def msg_r(s):
  29. try:
  30. g.stderr.write(s)
  31. g.stderr.flush()
  32. except:
  33. os.write(2,s.encode())
  34. def Msg_r(s):
  35. try:
  36. g.stdout.write(s)
  37. g.stdout.flush()
  38. except:
  39. os.write(1,s.encode())
  40. def msg(s): msg_r(s + '\n')
  41. def Msg(s): Msg_r(s + '\n')
  42. else:
  43. def msg_r(s):
  44. g.stderr.write(s)
  45. g.stderr.flush()
  46. def Msg_r(s):
  47. g.stdout.write(s)
  48. g.stdout.flush()
  49. def msg(s): g.stderr.write(s + '\n')
  50. def Msg(s): g.stdout.write(s + '\n')
  51. def msgred(s): msg(red(s))
  52. def rmsg(s): msg(red(s))
  53. def rmsg_r(s): msg_r(red(s))
  54. def ymsg(s): msg(yellow(s))
  55. def ymsg_r(s): msg_r(yellow(s))
  56. def gmsg(s): msg(green(s))
  57. def gmsg_r(s): msg_r(green(s))
  58. def bmsg(s): msg(blue(s))
  59. def bmsg_r(s): msg_r(blue(s))
  60. def mmsg(*args):
  61. for d in args: Msg(repr(d))
  62. def mdie(*args):
  63. mmsg(*args); sys.exit(0)
  64. def die_wait(delay,ev=0,s=''):
  65. assert isinstance(delay,int)
  66. assert isinstance(ev,int)
  67. if s: msg(s)
  68. time.sleep(delay)
  69. sys.exit(ev)
  70. def die_pause(ev=0,s=''):
  71. assert isinstance(ev,int)
  72. if s: msg(s)
  73. input('Press ENTER to exit')
  74. sys.exit(ev)
  75. def die(ev=0,s=''):
  76. assert isinstance(ev,int)
  77. if s: msg(s)
  78. sys.exit(ev)
  79. def Die(ev=0,s=''):
  80. assert isinstance(ev,int)
  81. if s: Msg(s)
  82. sys.exit(ev)
  83. def rdie(ev=0,s=''): die(ev,red(s))
  84. def ydie(ev=0,s=''): die(ev,yellow(s))
  85. def pp_fmt(d):
  86. import pprint
  87. return pprint.PrettyPrinter(indent=4,compact=True).pformat(d)
  88. def pp_msg(d):
  89. msg(pp_fmt(d))
  90. def set_for_type(val,refval,desc,invert_bool=False,src=None):
  91. src_str = (''," in '{}'".format(src))[bool(src)]
  92. if type(refval) == bool:
  93. v = str(val).lower()
  94. if v in ('true','yes','1'): ret = True
  95. elif v in ('false','no','none','0'): ret = False
  96. else: die(1,"'{}': invalid value for '{}'{} (must be of type '{}')".format(
  97. val,desc,src_str,'bool'))
  98. if invert_bool: ret = not ret
  99. else:
  100. try:
  101. ret = type(refval)((val,not val)[invert_bool])
  102. except:
  103. die(1,"'{}': invalid value for '{}'{} (must be of type '{}')".format(
  104. val,desc,src_str,type(refval).__name__))
  105. return ret
  106. # From 'man dd':
  107. # c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
  108. # GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y.
  109. def parse_bytespec(nbytes):
  110. smap = (('c', 1),
  111. ('w', 2),
  112. ('b', 512),
  113. ('kB', 1000),
  114. ('K', 1024),
  115. ('MB', 1000*1000),
  116. ('M', 1024*1024),
  117. ('GB', 1000*1000*1000),
  118. ('G', 1024*1024*1024),
  119. ('TB', 1000*1000*1000*1000),
  120. ('T', 1024*1024*1024*1024))
  121. import re
  122. m = re.match(r'([0123456789.]+)(.*)',nbytes)
  123. if m:
  124. if m.group(2):
  125. for k,v in smap:
  126. if k == m.group(2):
  127. from decimal import Decimal
  128. return int(Decimal(m.group(1)) * v)
  129. else:
  130. msg("Valid byte specifiers: '{}'".format("' '".join([i[0] for i in smap])))
  131. elif '.' in nbytes:
  132. raise ValueError('fractional bytes not allowed')
  133. else:
  134. return int(nbytes)
  135. die(1,"'{}': invalid byte specifier".format(nbytes))
  136. def check_or_create_dir(path):
  137. try:
  138. os.listdir(path)
  139. except:
  140. try:
  141. os.makedirs(path,0o700)
  142. except:
  143. die(2,"ERROR: unable to read or create path '{}'".format(path))
  144. from mmgen.opts import opt
  145. def qmsg(s,alt=None):
  146. if opt.quiet:
  147. if alt != None: msg(alt)
  148. else: msg(s)
  149. def qmsg_r(s,alt=None):
  150. if opt.quiet:
  151. if alt != None: msg_r(alt)
  152. else: msg_r(s)
  153. def vmsg(s,force=False):
  154. if opt.verbose or force: msg(s)
  155. def vmsg_r(s,force=False):
  156. if opt.verbose or force: msg_r(s)
  157. def Vmsg(s,force=False):
  158. if opt.verbose or force: Msg(s)
  159. def Vmsg_r(s,force=False):
  160. if opt.verbose or force: Msg_r(s)
  161. def dmsg(s):
  162. if opt.debug: msg(s)
  163. def suf(arg,suf_type='s'):
  164. suf_types = { 's': '', 'es': '', 'ies': 'y' }
  165. assert suf_type in suf_types,'invalid suffix type'
  166. if isinstance(arg,int):
  167. n = arg
  168. elif isinstance(arg,(list,tuple,set,dict)):
  169. n = len(arg)
  170. else:
  171. die(2,'{}: invalid parameter for suf()'.format(arg))
  172. return suf_types[suf_type] if n == 1 else suf_type
  173. def get_extension(f):
  174. a,b = os.path.splitext(f)
  175. return ('',b[1:])[len(b) > 1]
  176. def remove_extension(f,e):
  177. a,b = os.path.splitext(f)
  178. return (f,a)[len(b)>1 and b[1:]==e]
  179. def make_chksum_N(s,nchars,sep=False):
  180. if isinstance(s,str): s = s.encode()
  181. if nchars%4 or not (4 <= nchars <= 64): return False
  182. s = sha256(sha256(s).digest()).hexdigest().upper()
  183. sep = ('',' ')[bool(sep)]
  184. return sep.join([s[i*4:i*4+4] for i in range(nchars//4)])
  185. def make_chksum_8(s,sep=False):
  186. from mmgen.obj import HexStr
  187. s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
  188. return '{} {}'.format(s[:4],s[4:]) if sep else s
  189. def make_chksum_6(s):
  190. from mmgen.obj import HexStr
  191. if isinstance(s,str): s = s.encode()
  192. return HexStr(sha256(s).hexdigest()[:6])
  193. def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
  194. def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
  195. def splitN(s,n,sep=None): # always return an n-element list
  196. ret = s.split(sep,n-1)
  197. return ret + ['' for i in range(n-len(ret))]
  198. def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
  199. def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
  200. def split_into_cols(col_wid,s):
  201. return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip()
  202. def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
  203. return s if len(s) == 0 else s[0].upper() + s[1:]
  204. def decode_timestamp(s):
  205. # tz_save = open('/etc/timezone').read().rstrip()
  206. os.environ['TZ'] = 'UTC'
  207. ts = time.strptime(s,'%Y%m%d_%H%M%S')
  208. t = time.mktime(ts)
  209. # os.environ['TZ'] = tz_save
  210. return int(t)
  211. def make_timestamp(secs=None):
  212. t = int(secs) if secs else time.time()
  213. tv = time.gmtime(t)[:6]
  214. return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*tv)
  215. def make_timestr(secs=None):
  216. t = int(secs) if secs else time.time()
  217. tv = time.gmtime(t)[:6]
  218. return '{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}'.format(*tv)
  219. def secs_to_dhms(secs):
  220. dsecs = secs//3600
  221. return '{}{:02d}:{:02d}:{:02d}'.format(
  222. ('','{} day{}, '.format(dsecs//24,suf(dsecs//24)))[dsecs > 24],
  223. dsecs % 24, (secs//60) % 60, secs % 60)
  224. def secs_to_hms(secs):
  225. return '{:02d}:{:02d}:{:02d}'.format(secs//3600, (secs//60) % 60, secs % 60)
  226. def secs_to_ms(secs):
  227. return '{:02d}:{:02d}'.format(secs//60, secs % 60)
  228. def is_digits(s): return set(list(s)) <= set(list(digits))
  229. def is_int(s):
  230. try:
  231. int(str(s))
  232. return True
  233. except:
  234. return False
  235. # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
  236. # https://tools.ietf.org/html/rfc4648
  237. def is_hex_str(s): return set(list(s.lower())) <= set(list(hexdigits.lower()))
  238. def is_hex_str_lc(s): return set(list(s)) <= set(list(hexdigits.lower()))
  239. def is_hex_str_uc(s): return set(list(s)) <= set(list(hexdigits.upper()))
  240. def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58'])
  241. def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32'])
  242. def is_ascii(s,enc='ascii'):
  243. try: s.decode(enc)
  244. except: return False
  245. else: return True
  246. def is_utf8(s): return is_ascii(s,enc='utf8')
  247. class baseconv(object):
  248. desc = {
  249. 'b58': ('base58', 'base58-encoded data'),
  250. 'b32': ('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'),
  251. 'b16': ('hexadecimal string', 'base16 (hexadecimal) string data'),
  252. 'b10': ('base10 string', 'base10 (decimal) string data'),
  253. 'b8': ('base8 string', 'base8 (octal) string data'),
  254. 'b6d': ('base6d (die roll)', 'base6 data using the digits from one to six'),
  255. 'mmgen': ('MMGen native mnemonic',
  256. 'MMGen native mnemonic seed phrase data created using old Electrum wordlist and simple base conversion'),
  257. }
  258. digits = {
  259. 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'),
  260. 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
  261. 'b16': tuple('0123456789abcdef'),
  262. 'b10': tuple('0123456789'),
  263. 'b8': tuple('01234567'),
  264. 'b6d': tuple('123456'),
  265. }
  266. mn_base = 1626 # tirosh list is 1633 words long!
  267. mn_ids = ('mmgen','tirosh')
  268. wl_chksums = {
  269. 'mmgen': '5ca31424',
  270. 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626)
  271. # 'tirosh1633': '1a5faeff'
  272. }
  273. seed_pad_lens = {
  274. 'b58': { 16:22, 24:33, 32:44 },
  275. 'b6d': { 16:50, 24:75, 32:100 },
  276. }
  277. seed_pad_lens_rev = {
  278. 'b58': { 22:16, 33:24, 44:32 },
  279. 'b6d': { 50:16, 75:24, 100:32 },
  280. }
  281. @classmethod
  282. def init_mn(cls,mn_id):
  283. assert mn_id in cls.mn_ids
  284. if mn_id == 'mmgen':
  285. from mmgen.mn_electrum import words
  286. cls.digits[mn_id] = words
  287. elif mn_id == 'tirosh':
  288. from mmgen.mn_tirosh import words
  289. cls.digits[mn_id] = words[:cls.mn_base]
  290. else: # bip39
  291. cls.digits[mn_id] = cls.words
  292. @classmethod
  293. def get_wordlist(cls,wl_id):
  294. cls.init_mn(wl_id)
  295. return cls.digits[wl_id]
  296. @classmethod
  297. def get_wordlist_chksum(cls,wl_id):
  298. cls.init_mn(wl_id)
  299. return sha256(' '.join(cls.digits[wl_id]).encode()).hexdigest()[:8]
  300. @classmethod
  301. def check_wordlists(cls):
  302. for k,v in list(cls.wl_chksums.items()):
  303. res = cls.get_wordlist_chksum(k)
  304. assert res == v,'{}: checksum mismatch for {} (should be {})'.format(res,k,v)
  305. @classmethod
  306. def check_wordlist(cls,wl_id):
  307. cls.init_mn(wl_id)
  308. wl = cls.digits[wl_id]
  309. qmsg('Wordlist: {}\nLength: {} words'.format(wl_id,len(wl)))
  310. new_chksum = cls.get_wordlist_chksum(wl_id)
  311. a,b = 'generated','saved'
  312. compare_chksums(new_chksum,a,cls.wl_chksums[wl_id],b,die_on_fail=True)
  313. qmsg('List is sorted') if tuple(sorted(wl)) == wl else die(3,'ERROR: List is not sorted!')
  314. @classmethod
  315. def get_pad(cls,pad,seed_pad_func):
  316. """
  317. 'pad' argument to baseconv conversion methods must be either None, 'seed' or an integer.
  318. If None, output of minimum (but never zero) length will be produced.
  319. If 'seed', output length will be mapped from input length using data in seed_pad_lens.
  320. If an integer, the string, hex string or byte output will be padded to this length.
  321. """
  322. if pad == None:
  323. return 0
  324. elif type(pad) == int:
  325. return pad
  326. elif pad == 'seed':
  327. return seed_pad_func()
  328. else:
  329. m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)"
  330. raise BaseConversionPadError(m.format(pad))
  331. @classmethod
  332. def tohex(cls,words_arg,wl_id,pad=None):
  333. "convert string or list data of base 'wl_id' to hex string"
  334. return cls.tobytes(words_arg,wl_id,pad//2 if type(pad)==int else pad).hex()
  335. @classmethod
  336. def tobytes(cls,words_arg,wl_id,pad=None):
  337. "convert string or list data of base 'wl_id' to byte string"
  338. words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip())
  339. desc = cls.desc[wl_id][0]
  340. if len(words) == 0:
  341. raise BaseConversionError('empty {} data'.format(desc))
  342. def get_seed_pad():
  343. assert wl_id in cls.seed_pad_lens_rev,'seed padding not supported for base {!r}'.format(wl_id)
  344. d = cls.seed_pad_lens_rev[wl_id]
  345. if not len(words) in d:
  346. m = '{}: invalid length for seed-padded {} data in base conversion'
  347. raise BaseConversionError(m.format(len(words),desc))
  348. return d[len(words)]
  349. pad_val = max(cls.get_pad(pad,get_seed_pad),1)
  350. wl = cls.digits[wl_id]
  351. base = len(wl)
  352. if not set(words) <= set(wl):
  353. m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format'
  354. raise BaseConversionError(m.format(w=words_arg,d=desc))
  355. ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))])
  356. bl = ret.bit_length()
  357. return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big')
  358. @classmethod
  359. def fromhex(cls,hexstr,wl_id,pad=None,tostr=False):
  360. "convert hex string to list or string data of base 'wl_id'"
  361. if not is_hex_str(hexstr):
  362. m = ('{h!r}:','seed data')[pad=='seed'] + ' not a hexadecimal string'
  363. raise HexadecimalStringError(m.format(h=hexstr))
  364. return cls.frombytes(bytes.fromhex(hexstr),wl_id,pad,tostr)
  365. @classmethod
  366. def frombytes(cls,bytestr,wl_id,pad=None,tostr=False):
  367. "convert byte string to list or string data of base 'wl_id'"
  368. if not bytestr:
  369. raise BaseConversionError('empty data not allowed in base conversion')
  370. def get_seed_pad():
  371. assert wl_id in cls.seed_pad_lens,'seed padding not supported for base {!r}'.format(wl_id)
  372. d = cls.seed_pad_lens[wl_id]
  373. if not len(bytestr) in d:
  374. m = '{}: invalid byte length for seed data in seed-padded base conversion'
  375. raise SeedLengthError(m.format(len(bytestr)))
  376. return d[len(bytestr)]
  377. pad = max(cls.get_pad(pad,get_seed_pad),1)
  378. wl = cls.digits[wl_id]
  379. base = len(wl)
  380. num = int.from_bytes(bytestr,'big')
  381. ret = []
  382. while num:
  383. ret.append(num % base)
  384. num //= base
  385. o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
  386. return ''.join(o) if tostr else o
  387. def match_ext(addr,ext):
  388. return addr.split('.')[-1] == ext
  389. def file_exists(f):
  390. try:
  391. os.stat(f)
  392. return True
  393. except:
  394. return False
  395. def file_is_readable(f):
  396. from stat import S_IREAD
  397. try:
  398. assert os.stat(f).st_mode & S_IREAD
  399. except:
  400. return False
  401. else:
  402. return True
  403. def get_from_brain_opt_params():
  404. l,p = opt.from_brain.split(',')
  405. return(int(l),p)
  406. def pretty_format(s,width=80,pfx=''):
  407. out = []
  408. while(s):
  409. if len(s) <= width:
  410. out.append(s)
  411. break
  412. i = s[:width].rfind(' ')
  413. out.append(s[:i])
  414. s = s[i+1:]
  415. return pfx + ('\n'+pfx).join(out)
  416. def block_format(data,gw=2,cols=8,line_nums=None,data_is_hex=False):
  417. assert line_nums in (None,'hex','dec'),"'line_nums' must be one of None, 'hex' or 'dec'"
  418. ln_fs = '{:06x}: ' if line_nums == 'hex' else '{:06}: '
  419. bytes_per_chunk = gw
  420. if data_is_hex:
  421. gw *= 2
  422. nchunks = len(data)//gw + bool(len(data)%gw)
  423. return ''.join(
  424. ('' if (line_nums == None or i % cols) else ln_fs.format(i*bytes_per_chunk))
  425. + data[i*gw:i*gw+gw]
  426. + (' ' if (i+1) % cols else '\n')
  427. for i in range(nchunks)
  428. ).rstrip() + '\n'
  429. def pretty_hexdump(data,gw=2,cols=8,line_nums=None):
  430. return block_format(data.hex(),gw,cols,line_nums,data_is_hex=True)
  431. def decode_pretty_hexdump(data):
  432. from string import hexdigits
  433. pat = r'^[{}]+:\s+'.format(hexdigits)
  434. lines = [re.sub(pat,'',l) for l in data.splitlines()]
  435. try:
  436. return bytes.fromhex(''.join((''.join(lines).split())))
  437. except:
  438. msg('Data not in hexdump format')
  439. return False
  440. def strip_comments(line):
  441. return re.sub(r'\s+$','',re.sub(r'#.*','',line,1))
  442. def remove_comments(lines):
  443. return [m for m in [strip_comments(l) for l in lines] if m != '']
  444. def get_hash_params(hash_preset):
  445. if hash_preset in g.hash_presets:
  446. return g.hash_presets[hash_preset] # N,p,r,buflen
  447. else: # Shouldn't be here
  448. die(3,"{}: invalid 'hash_preset' value".format(hash_preset))
  449. def compare_chksums(chk1,desc1,chk2,desc2,hdr='',die_on_fail=False,verbose=False):
  450. if not chk1 == chk2:
  451. fs = "{} ERROR: {} checksum ({}) doesn't match {} checksum ({})"
  452. m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'),desc2,chk2,desc1,chk1)
  453. if die_on_fail:
  454. die(3,m)
  455. else:
  456. vmsg(m,force=verbose)
  457. return False
  458. vmsg('{} checksum OK ({})'.format(capfirst(desc1),chk1))
  459. return True
  460. def compare_or_die(val1, desc1, val2, desc2, e='Error'):
  461. if val1 != val2:
  462. die(3,"{}: {} ({}) doesn't match {} ({})".format(e,desc2,val2,desc1,val1))
  463. dmsg('{} OK ({})'.format(capfirst(desc2),val2))
  464. return True
  465. def open_file_or_exit(filename,mode,silent=False):
  466. try:
  467. return open(filename, mode)
  468. except:
  469. op = ('writing','reading')['r' in mode]
  470. die(2,("Unable to open file '{}' for {}".format(filename,op),'')[silent])
  471. def check_file_type_and_access(fname,ftype,blkdev_ok=False):
  472. a = ((os.R_OK,'read'),(os.W_OK,'writ'))
  473. access,m = a[ftype in ('output file','output directory')]
  474. ok_types = [
  475. (stat.S_ISREG,'regular file'),
  476. (stat.S_ISLNK,'symbolic link')
  477. ]
  478. if blkdev_ok: ok_types.append((stat.S_ISBLK,'block device'))
  479. if ftype == 'output directory': ok_types = [(stat.S_ISDIR, 'output directory')]
  480. try: mode = os.stat(fname).st_mode
  481. except:
  482. raise FileNotFound("Requested {} '{}' not found".format(ftype,fname))
  483. for t in ok_types:
  484. if t[0](mode): break
  485. else:
  486. die(1,"Requested {} '{}' is not a {}".format(ftype,fname,' or '.join([t[1] for t in ok_types])))
  487. if not os.access(fname, access):
  488. die(1,"Requested {} '{}' is not {}able by you".format(ftype,fname,m))
  489. return True
  490. def check_infile(f,blkdev_ok=False):
  491. return check_file_type_and_access(f,'input file',blkdev_ok=blkdev_ok)
  492. def check_outfile(f,blkdev_ok=False):
  493. return check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok)
  494. def check_outdir(f):
  495. return check_file_type_and_access(f,'output directory')
  496. def check_wallet_extension(fn):
  497. from mmgen.seed import SeedSource
  498. if not SeedSource.ext_to_type(get_extension(fn)):
  499. raise BadFileExtension("'{}': unrecognized seed source file extension".format(fn))
  500. def make_full_path(outdir,outfile):
  501. return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
  502. def get_seed_file(cmd_args,nargs,invoked_as=None):
  503. from mmgen.filename import find_file_in_dir
  504. from mmgen.seed import Wallet
  505. wf = find_file_in_dir(Wallet,g.data_dir)
  506. wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
  507. import mmgen.opts as opts
  508. if len(cmd_args) + (wd_from_opt or bool(wf)) < nargs:
  509. if not wf:
  510. msg('No default wallet found, and no other seed source was specified')
  511. opts.usage()
  512. elif len(cmd_args) > nargs:
  513. opts.usage()
  514. elif len(cmd_args) == nargs and wf and invoked_as != 'gen':
  515. qmsg('Warning: overriding default wallet with user-supplied wallet')
  516. if cmd_args or wf:
  517. check_infile(cmd_args[0] if cmd_args else wf)
  518. return cmd_args[0] if cmd_args else (wf,None)[wd_from_opt]
  519. def get_new_passphrase(desc,passchg=False):
  520. w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
  521. if opt.passwd_file:
  522. pw = ' '.join(get_words_from_file(opt.passwd_file,w))
  523. elif opt.echo_passphrase:
  524. pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
  525. else:
  526. from mmgen.common import mswin_pw_warning
  527. mswin_pw_warning()
  528. for i in range(g.passwd_max_tries):
  529. pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
  530. pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
  531. dmsg('Passphrases: [{}] [{}]'.format(pw,pw2))
  532. if pw == pw2:
  533. vmsg('Passphrases match'); break
  534. else: msg('Passphrases do not match. Try again.')
  535. else:
  536. die(2,'User failed to duplicate passphrase in {} attempts'.format(g.passwd_max_tries))
  537. if pw == '': qmsg('WARNING: Empty passphrase')
  538. return pw
  539. def confirm_or_raise(message,q,expect='YES',exit_msg='Exiting at user request'):
  540. m = message.strip()
  541. if m: msg(m)
  542. a = q+' ' if q[0].isupper() else 'Are you sure you want to {}?\n'.format(q)
  543. b = "Type uppercase '{}' to confirm: ".format(expect)
  544. if my_raw_input(a+b).strip() != expect:
  545. raise UserNonConfirmation(exit_msg)
  546. def write_data_to_file( outfile,data,desc='data',
  547. ask_write=False,
  548. ask_write_prompt='',
  549. ask_write_default_yes=True,
  550. ask_overwrite=True,
  551. ask_tty=True,
  552. no_tty=False,
  553. quiet=False,
  554. binary=False,
  555. ignore_opt_outdir=False,
  556. check_data=False,
  557. cmp_data=None):
  558. if quiet: ask_tty = ask_overwrite = False
  559. if opt.quiet: ask_overwrite = False
  560. if ask_write_default_yes == False or ask_write_prompt:
  561. ask_write = True
  562. def do_stdout():
  563. qmsg('Output to STDOUT requested')
  564. if g.stdin_tty:
  565. if no_tty:
  566. die(2,'Printing {} to screen is not allowed'.format(desc))
  567. if (ask_tty and not opt.quiet) or binary:
  568. confirm_or_raise('','output {} to screen'.format(desc))
  569. else:
  570. try: of = os.readlink('/proc/{}/fd/1'.format(os.getpid())) # Linux
  571. except: of = None # Windows
  572. if of:
  573. if of[:5] == 'pipe:':
  574. if no_tty:
  575. die(2,'Writing {} to pipe is not allowed'.format(desc))
  576. if ask_tty and not opt.quiet:
  577. confirm_or_raise('','output {} to pipe'.format(desc))
  578. msg('')
  579. of2,pd = os.path.relpath(of),os.path.pardir
  580. msg("Redirecting output to file '{}'".format((of2,of)[of2[:len(pd)] == pd]))
  581. else:
  582. msg('Redirecting output to file')
  583. if binary and g.platform == 'win':
  584. import msvcrt
  585. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  586. sys.stdout.write(data.decode() if isinstance(data,bytes) else data)
  587. def do_file(outfile,ask_write_prompt):
  588. if opt.outdir and not ignore_opt_outdir and not os.path.isabs(outfile):
  589. outfile = make_full_path(opt.outdir,outfile)
  590. if ask_write:
  591. if not ask_write_prompt: ask_write_prompt = 'Save {}?'.format(desc)
  592. if not keypress_confirm(ask_write_prompt,
  593. default_yes=ask_write_default_yes):
  594. die(1,'{} not saved'.format(capfirst(desc)))
  595. hush = False
  596. if file_exists(outfile) and ask_overwrite:
  597. q = "File '{}' already exists\nOverwrite?".format(outfile)
  598. confirm_or_raise('',q)
  599. msg("Overwriting file '{}'".format(outfile))
  600. hush = True
  601. # not atomic, but better than nothing
  602. # if cmp_data is empty, file can be either empty or non-existent
  603. if check_data:
  604. try:
  605. d = open(outfile,('r','rb')[bool(binary)]).read()
  606. except:
  607. d = ''
  608. finally:
  609. if d != cmp_data:
  610. m = "{} in file '{}' has been altered by some other program! Aborting file write"
  611. die(3,m.format(desc,outfile))
  612. # To maintain portability, always open files in binary mode
  613. # If 'binary' option not set, encode/decode data before writing and after reading
  614. f = open_file_or_exit(outfile,'wb')
  615. try:
  616. f.write(data if binary else data.encode())
  617. except:
  618. die(2,"Failed to write {} to file '{}'".format(desc,outfile))
  619. f.close
  620. if not (hush or quiet):
  621. msg("{} written to file '{}'".format(capfirst(desc),outfile))
  622. return True
  623. if opt.stdout or outfile in ('','-'):
  624. do_stdout()
  625. elif sys.stdin.isatty() and not sys.stdout.isatty():
  626. do_stdout()
  627. else:
  628. do_file(outfile,ask_write_prompt)
  629. def get_words_from_user(prompt):
  630. words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
  631. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  632. return words
  633. def get_words_from_file(infile,desc,quiet=False):
  634. if not quiet:
  635. qmsg("Getting {} from file '{}'".format(desc,infile))
  636. f = open_file_or_exit(infile, 'rb')
  637. try: words = f.read().decode().split()
  638. except: die(1,'{} data must be UTF-8 encoded.'.format(capfirst(desc)))
  639. f.close()
  640. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  641. return words
  642. def get_words(infile,desc,prompt):
  643. if infile:
  644. return get_words_from_file(infile,desc)
  645. else:
  646. return get_words_from_user(prompt)
  647. def mmgen_decrypt_file_maybe(fn,desc='',quiet=False,silent=False):
  648. d = get_data_from_file(fn,desc,binary=True,quiet=quiet,silent=silent)
  649. have_enc_ext = get_extension(fn) == g.mmenc_ext
  650. if have_enc_ext or not is_utf8(d):
  651. m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
  652. qmsg("{} {} '{}'".format(m,desc,fn))
  653. from mmgen.crypto import mmgen_decrypt_retry
  654. d = mmgen_decrypt_retry(d,desc)
  655. return d
  656. def get_lines_from_file(fn,desc='',trim_comments=False,quiet=False,silent=False):
  657. dec = mmgen_decrypt_file_maybe(fn,desc,quiet=quiet,silent=silent)
  658. ret = dec.decode().splitlines()
  659. if trim_comments: ret = remove_comments(ret)
  660. dmsg("Got {} lines from file '{}'".format(len(ret),fn))
  661. return ret
  662. def get_data_from_user(desc='data'): # user input MUST be UTF-8
  663. p = ('','Enter {}: '.format(desc))[g.stdin_tty]
  664. data = my_raw_input(p,echo=opt.echo_passphrase)
  665. dmsg('User input: [{}]'.format(data))
  666. return data
  667. def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False,quiet=False):
  668. if not opt.quiet and not silent and not quiet and desc:
  669. qmsg("Getting {} from file '{}'".format(desc,infile))
  670. if dash and infile == '-':
  671. data = os.fdopen(0,'rb').read(g.max_input_size+1)
  672. else:
  673. data = open_file_or_exit(infile,'rb',silent=silent).read(g.max_input_size+1)
  674. if not binary:
  675. data = data.decode()
  676. if len(data) == g.max_input_size + 1:
  677. raise MaxInputSizeExceeded('Too much input data! Max input data size: {} bytes'.format(g.max_input_size))
  678. return data
  679. def pwfile_reuse_warning():
  680. if 'passwd_file_used' in globals():
  681. qmsg("Reusing passphrase from file '{}' at user request".format(opt.passwd_file))
  682. return True
  683. globals()['passwd_file_used'] = True
  684. return False
  685. def get_mmgen_passphrase(desc,passchg=False):
  686. prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
  687. if opt.passwd_file:
  688. pwfile_reuse_warning()
  689. return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
  690. else:
  691. from mmgen.common import mswin_pw_warning
  692. mswin_pw_warning()
  693. return ' '.join(get_words_from_user(prompt))
  694. def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
  695. try: import readline
  696. except: use_readline = False # Windows
  697. if use_readline and sys.stdout.isatty():
  698. def st_hook(): readline.insert_text(insert_txt)
  699. readline.set_startup_hook(st_hook)
  700. else:
  701. msg_r(prompt)
  702. prompt = ''
  703. from mmgen.term import kb_hold_protect
  704. kb_hold_protect()
  705. if g.test_suite_popen_spawn:
  706. msg(prompt)
  707. sys.stderr.flush()
  708. reply = os.read(0,4096).decode()
  709. elif echo or not sys.stdin.isatty():
  710. reply = input(prompt)
  711. else:
  712. from getpass import getpass
  713. if g.platform == 'win':
  714. # MSWin hack - getpass('foo') doesn't flush stderr
  715. msg_r(prompt.strip()) # getpass('') adds a space
  716. sys.stderr.flush()
  717. reply = getpass('')
  718. else:
  719. reply = getpass(prompt)
  720. kb_hold_protect()
  721. try:
  722. return reply.strip()
  723. except:
  724. die(1,'User input must be UTF-8 encoded.')
  725. def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete_prompt=False):
  726. from mmgen.term import get_char
  727. q = ('(y/N)','(Y/n)')[bool(default_yes)]
  728. p = prompt if complete_prompt else '{} {}: '.format(prompt,q)
  729. nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl]
  730. if opt.accept_defaults:
  731. msg(p)
  732. return default_yes
  733. while True:
  734. r = get_char(p).strip(b'\n\r')
  735. if not r:
  736. if default_yes: msg_r(nl); return True
  737. else: msg_r(nl); return False
  738. elif r in b'yY': msg_r(nl); return True
  739. elif r in b'nN': msg_r(nl); return False
  740. else:
  741. if verbose: msg('\nInvalid reply')
  742. else: msg_r('\r')
  743. def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
  744. from mmgen.term import get_char
  745. while True:
  746. reply = get_char('{}: '.format(prompt)).strip(b'\n\r')
  747. if reply in chars.encode() or (enter_ok and not reply):
  748. msg('')
  749. return reply.decode()
  750. if verbose: msg('\nInvalid reply')
  751. else: msg_r('\r')
  752. def do_pager(text):
  753. pagers = ['less','more']
  754. end_msg = '\n(end of text)\n\n'
  755. # --- Non-MSYS Windows code deleted ---
  756. # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
  757. os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
  758. if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
  759. pagers = [os.environ['PAGER']] + pagers
  760. for pager in pagers:
  761. try:
  762. from subprocess import run
  763. m = text + ('' if pager == 'less' else end_msg)
  764. p = run([pager],input=m.encode(),check=True)
  765. msg_r('\r')
  766. except:
  767. pass
  768. else:
  769. break
  770. else:
  771. Msg(text+end_msg)
  772. def do_license_msg(immed=False):
  773. if opt.quiet or g.no_license or opt.yes or not g.stdin_tty: return
  774. import mmgen.license as gpl
  775. p = "Press 'w' for conditions and warranty info, or 'c' to continue:"
  776. msg(gpl.warning)
  777. prompt = '{} '.format(p.strip())
  778. from mmgen.term import get_char
  779. while True:
  780. reply = get_char(prompt, immed_chars=('','wc')[bool(immed)])
  781. if reply == b'w':
  782. do_pager(gpl.conditions)
  783. elif reply == b'c':
  784. msg(''); break
  785. else:
  786. msg_r('\r')
  787. msg('')
  788. def get_daemon_cfg_options(cfg_keys):
  789. cfg_file = os.path.join(g.proto.daemon_data_dir,g.proto.name+'.conf')
  790. try:
  791. lines = get_lines_from_file(cfg_file,'',silent=bool(opt.quiet))
  792. kv_pairs = [l.split('=') for l in lines]
  793. cfg = {k:v for k,v in kv_pairs if k in cfg_keys}
  794. except:
  795. vmsg("Warning: '{}' does not exist or is unreadable".format(cfg_file))
  796. cfg = {}
  797. for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
  798. return cfg
  799. def get_coin_daemon_auth_cookie():
  800. f = os.path.join(g.proto.daemon_data_dir,g.proto.daemon_data_subdir,'.cookie')
  801. return get_lines_from_file(f,'')[0] if file_is_readable(f) else ''
  802. def rpc_init(reinit=False):
  803. if not 'rpc' in g.proto.mmcaps:
  804. die(1,'Coin daemon operations not supported for coin {}!'.format(g.coin))
  805. if g.rpch != None and not reinit: return g.rpch
  806. from mmgen.rpc import init_daemon
  807. g.rpch = init_daemon(g.proto.daemon_family)
  808. return g.rpch
  809. def format_par(s,indent=0,width=80,as_list=False):
  810. words,lines = s.split(),[]
  811. assert width >= indent + 4,'width must be >= indent + 4'
  812. while words:
  813. line = ''
  814. while len(line) <= (width-indent) and words:
  815. if line and len(line) + len(words[0]) + 1 > width-indent: break
  816. line += ('',' ')[bool(line)] + words.pop(0)
  817. lines.append(' '*indent + line)
  818. return lines if as_list else '\n'.join(lines) + '\n'
  819. # module loading magic for tx.py and tw.py
  820. def altcoin_subclass(cls,mod_id,cls_name):
  821. if cls.__name__ != cls_name: return cls
  822. mod_dir = g.proto.base_coin.lower()
  823. pname = g.proto.class_pfx if hasattr(g.proto,'class_pfx') else capfirst(g.proto.name)
  824. tname = 'Token' if g.token else ''
  825. import importlib
  826. modname = 'mmgen.altcoins.{}.{}'.format(mod_dir,mod_id)
  827. clsname = '{}{}{}'.format(pname,tname,cls_name)
  828. try:
  829. return getattr(importlib.import_module(modname),clsname)
  830. except ImportError:
  831. return cls
  832. # decorator for TrackingWallet
  833. def write_mode(orig_func):
  834. def f(self,*args,**kwargs):
  835. if self.mode != 'w':
  836. m = '{} opened in read-only mode: cannot execute method {}()'
  837. die(1,m.format(type(self).__name__,locals()['orig_func'].__name__))
  838. return orig_func(self,*args,**kwargs)
  839. return f