tool.py 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  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. tool.py: Routines for the 'mmgen-tool' utility
  20. """
  21. from mmgen.protocol import hash160
  22. from mmgen.common import *
  23. from mmgen.crypto import *
  24. from mmgen.addr import *
  25. NL = ('\n','\r\n')[g.platform=='win']
  26. def _create_call_sig(cmd,parsed=False):
  27. m = getattr(MMGenToolCmd,cmd)
  28. if 'varargs_call_sig' in m.__code__.co_varnames: # hack
  29. flag = 'VAR_ARGS'
  30. va = m.__defaults__[0]
  31. args,dfls,ann = va['args'],va['dfls'],va['annots']
  32. else:
  33. flag = None
  34. args = m.__code__.co_varnames[1:m.__code__.co_argcount]
  35. dfls = m.__defaults__ or ()
  36. ann = m.__annotations__
  37. nargs = len(args) - len(dfls)
  38. def get_type_from_ann(arg):
  39. return ann[arg][1:] + (' or STDIN','')[parsed] if ann[arg] == 'sstr' else ann[arg].__name__
  40. if parsed:
  41. c_args = [(a,get_type_from_ann(a)) for a in args[:nargs]]
  42. c_kwargs = [(a,dfls[n]) for n,a in enumerate(args[nargs:])]
  43. return c_args,dict(c_kwargs),'STDIN_OK' if c_args and ann[args[0]] == 'sstr' else flag
  44. else:
  45. c_args = ['{} [{}]'.format(a,get_type_from_ann(a)) for a in args[:nargs]]
  46. c_kwargs = ['"{}" [{}={!r}{}]'.format(
  47. a, type(dfls[n]).__name__, dfls[n],
  48. (' ' + ann[a] if a in ann else ''))
  49. for n,a in enumerate(args[nargs:])]
  50. return ' '.join(c_args + c_kwargs)
  51. def _usage(cmd=None,exit_val=1):
  52. m1=('USAGE INFORMATION FOR MMGEN-TOOL COMMANDS:\n\n'
  53. ' Unquoted arguments are mandatory\n'
  54. ' Quoted arguments are optional, default values will be used\n'
  55. ' Argument types and default values are shown in square brackets\n')
  56. m2=(' To force a command to read from STDIN instead of file (for commands taking\n'
  57. ' a filename as their first argument), substitute "-" for the filename.\n\n'
  58. 'EXAMPLES:\n\n'
  59. ' Generate a random Bech32 public/private keypair for LTC:\n'
  60. ' $ mmgen-tool -r0 --coin=ltc --type=bech32 randpair\n\n'
  61. ' Generate a well-known burn address:\n'
  62. ' $ mmgen-tool hextob58chk 000000000000000000000000000000000000000000\n\n'
  63. ' Generate a random 12-word seed phrase:\n'
  64. ' $ mmgen-tool -r0 mn_rand128\n\n'
  65. ' Same as above, but get additional entropy from user:\n'
  66. ' $ mmgen-tool mn_rand128\n\n'
  67. ' Convert a string to base 58:\n'
  68. ' $ mmgen-tool bytestob58 /etc/timezone pad=20\n\n'
  69. ' Reverse a hex string:\n'
  70. ' $ mmgen-tool hexreverse "deadbeefcafe"\n\n'
  71. ' Same as above, but use a pipe:\n'
  72. ' $ echo "deadbeefcafe" | mmgen-tool hexreverse -')
  73. if not cmd:
  74. Msg(m1)
  75. for bc in MMGenToolCmd.__bases__:
  76. cls_info = bc.__doc__.strip().split('\n')[0]
  77. Msg(' {}{}\n'.format(cls_info[0].upper(),cls_info[1:]))
  78. ucmds = bc._user_commands()
  79. max_w = max(map(len,ucmds))
  80. for cmd in ucmds:
  81. if getattr(MMGenToolCmd,cmd).__doc__:
  82. Msg(' {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w))
  83. Msg('')
  84. Msg(m2)
  85. elif cmd in MMGenToolCmd._user_commands():
  86. docstr = getattr(MMGenToolCmd,cmd).__doc__.strip()
  87. msg('{}\n'.format(capfirst(docstr)))
  88. msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd)))
  89. else:
  90. die(1,"'{}': no such tool command".format(cmd))
  91. sys.exit(exit_val)
  92. def _process_args(cmd,cmd_args):
  93. c_args,c_kwargs,flag = _create_call_sig(cmd,parsed=True)
  94. have_stdin_input = False
  95. if flag != 'VAR_ARGS':
  96. if len(cmd_args) < len(c_args):
  97. m1 = 'Command requires exactly {} non-keyword argument{}'
  98. msg(m1.format(len(c_args),suf(c_args,'s')))
  99. _usage(cmd)
  100. u_args = cmd_args[:len(c_args)]
  101. # If we're reading from a pipe, replace '-' with output of previous command
  102. if flag == 'STDIN_OK' and u_args and u_args[0] == '-':
  103. if sys.stdin.isatty():
  104. raise BadFilename("Standard input is a TTY. Can't use '-' as a filename")
  105. else:
  106. max_dlen_spec = '10kB' # limit input to 10KB for now
  107. max_dlen = MMGenToolCmdUtil().bytespec(max_dlen_spec)
  108. u_args[0] = os.read(0,max_dlen)
  109. # try: u_args[0] = u_args[0].decode()
  110. # except: pass
  111. have_stdin_input = True
  112. if len(u_args[0]) >= max_dlen:
  113. die(2,'Maximum data input for this command is {}'.format(max_dlen_spec))
  114. if not u_args[0]:
  115. die(2,'{}: ERROR: no output from previous command in pipe'.format(cmd))
  116. u_nkwargs = len(cmd_args) - len(c_args)
  117. u_kwargs = {}
  118. if flag == 'VAR_ARGS':
  119. t = [a.split('=',1) for a in cmd_args if '=' in a]
  120. tk = [a[0] for a in t]
  121. tk_bad = [a for a in tk if a not in c_kwargs]
  122. if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '='
  123. die(1,"'{}': illegal keyword argument".format(tk_bad[-1]))
  124. u_kwargs = dict(t[len(tk_bad):])
  125. u_args = cmd_args[:-len(u_kwargs) or None]
  126. elif u_nkwargs > 0:
  127. u_kwargs = dict([a.split('=',1) for a in cmd_args[len(c_args):] if '=' in a])
  128. if len(u_kwargs) != u_nkwargs:
  129. msg('Command requires exactly {} non-keyword argument{}'.format(len(c_args),suf(c_args)))
  130. _usage(cmd)
  131. if len(u_kwargs) > len(c_kwargs):
  132. msg('Command accepts no more than {} keyword argument{}'.format(len(c_kwargs),suf(c_kwargs)))
  133. _usage(cmd)
  134. for k in u_kwargs:
  135. if k not in c_kwargs:
  136. msg("'{}': invalid keyword argument".format(k))
  137. _usage(cmd)
  138. def conv_type(arg,arg_name,arg_type):
  139. if arg_type == 'bytes' and type(arg) != bytes:
  140. die(1,"'Binary input data must be supplied via STDIN")
  141. if have_stdin_input and arg_type == 'str' and type(arg) == bytes:
  142. arg = arg.decode()
  143. if arg[-len(NL):] == NL: # rstrip one newline
  144. arg = arg[:-len(NL)]
  145. if arg_type == 'bool':
  146. if arg.lower() in ('true','yes','1','on'): arg = True
  147. elif arg.lower() in ('false','no','0','off'): arg = False
  148. else:
  149. msg("'{}': invalid boolean value for keyword argument".format(arg))
  150. _usage(cmd)
  151. try:
  152. return __builtins__[arg_type](arg)
  153. except:
  154. die(1,"'{}': Invalid argument for argument {} ('{}' required)".format(arg,arg_name,arg_type))
  155. if flag == 'VAR_ARGS':
  156. args = [conv_type(u_args[i],c_args[0][0],c_args[0][1]) for i in range(len(u_args))]
  157. else:
  158. args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
  159. kwargs = {k:conv_type(u_kwargs[k],k,type(c_kwargs[k]).__name__) for k in u_kwargs}
  160. return args,kwargs
  161. def _process_result(ret,pager=False,print_result=False):
  162. """
  163. Convert result to something suitable for output to screen and return it.
  164. If result is bytes and not convertible to utf8, output as binary using os.write().
  165. If 'print_result' is True, send the converted result directly to screen or
  166. pager instead of returning it.
  167. """
  168. def triage_result(o):
  169. return o if not print_result else do_pager(o) if pager else Msg(o)
  170. if ret == True:
  171. return True
  172. elif ret in (False,None):
  173. ydie(1,"tool command returned '{}'".format(ret))
  174. elif issubclass(type(ret),str):
  175. return triage_result(ret)
  176. elif issubclass(type(ret),int):
  177. return triage_result(str(ret))
  178. elif type(ret) == tuple:
  179. return triage_result('\n'.join([r.decode() if issubclass(type(r),bytes) else r for r in ret]))
  180. elif issubclass(type(ret),bytes):
  181. try:
  182. o = ret.decode()
  183. return o if not print_result else do_pager(o) if pager else Msg(o)
  184. except:
  185. # don't add NL to binary data if it can't be converted to utf8
  186. return ret if not print_result else os.write(1,ret)
  187. else:
  188. ydie(1,"tool.py: can't handle return value of type '{}'".format(type(ret).__name__))
  189. from mmgen.obj import MMGenAddrType
  190. def init_generators(arg=None):
  191. global at,kg,ag
  192. at = MMGenAddrType((hasattr(opt,'type') and opt.type) or g.proto.dfl_mmtype)
  193. if arg != 'at':
  194. kg = KeyGenerator(at)
  195. ag = AddrGenerator(at)
  196. wordlists = 'electrum','tirosh'
  197. dfl_wl_id = 'electrum'
  198. class MMGenToolCmdBase(object):
  199. @classmethod
  200. def _user_commands(cls):
  201. return [e for e in dir(cls) if e[0] != '_' and getattr(cls,e).__doc__]
  202. class MMGenToolCmdMisc(MMGenToolCmdBase):
  203. "miscellaneous commands"
  204. def help(self,command_name=''):
  205. "display usage information for a single command or all commands"
  206. _usage(command_name,exit_val=0)
  207. usage = help
  208. class MMGenToolCmdUtil(MMGenToolCmdBase):
  209. "general string conversion and hashing utilities"
  210. def bytespec(self,dd_style_byte_specifier:str):
  211. "convert a byte specifier such as '1GB' into an integer"
  212. return parse_bytespec(dd_style_byte_specifier)
  213. def randhex(self,nbytes='32'):
  214. "print 'n' bytes (default 32) of random data in hex format"
  215. return get_random(int(nbytes)).hex()
  216. def hexreverse(self,hexstr:'sstr'):
  217. "reverse bytes of a hexadecimal string"
  218. return bytes.fromhex(hexstr.strip())[::-1].hex()
  219. def hexlify(self,infile:str):
  220. "convert bytes in file to hexadecimal (use '-' for stdin)"
  221. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  222. return data.hex()
  223. def unhexlify(self,hexstr:'sstr'):
  224. "convert hexadecimal value to bytes (warning: outputs binary data)"
  225. return bytes.fromhex(hexstr)
  226. def hexdump(self,infile:str,cols=8,line_nums=True):
  227. "create hexdump of data from file (use '-' for stdin)"
  228. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  229. return pretty_hexdump(data,cols=cols,line_nums=line_nums).rstrip()
  230. def unhexdump(self,infile:str):
  231. "decode hexdump from file (use '-' for stdin) (warning: outputs binary data)"
  232. if g.platform == 'win':
  233. import msvcrt
  234. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  235. hexdata = get_data_from_file(infile,dash=True,quiet=True)
  236. return decode_pretty_hexdump(hexdata)
  237. def hash160(self,hexstr:'sstr'):
  238. "compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
  239. return hash160(hexstr)
  240. def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
  241. "compute sha256(sha256(data)) (double sha256)"
  242. from hashlib import sha256
  243. if file_input: b = get_data_from_file(string_or_bytes,binary=True)
  244. elif hex_input: b = decode_pretty_hexdump(string_or_bytes)
  245. else: b = string_or_bytes
  246. return sha256(sha256(b.encode()).digest()).hexdigest()
  247. def id6(self,infile:str):
  248. "generate 6-character MMGen ID for a file (use '-' for stdin)"
  249. return make_chksum_6(
  250. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  251. def str2id6(self,string:'sstr'): # retain ignoring of space for backwards compat
  252. "generate 6-character MMGen ID for a string, ignoring spaces"
  253. return make_chksum_6(''.join(string.split()))
  254. def id8(self,infile:str):
  255. "generate 8-character MMGen ID for a file (use '-' for stdin)"
  256. return make_chksum_8(
  257. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  258. def randb58(self,nbytes=32,pad=True):
  259. "generate random data (default: 32 bytes) and convert it to base 58"
  260. return baseconv.b58encode(get_random(nbytes),pad=pad)
  261. def bytestob58(self,infile:str,pad=0):
  262. "convert bytes to base 58 (supply data via STDIN)"
  263. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  264. return baseconv.fromhex(data.hex(),'b58',pad=pad,tostr=True)
  265. def b58tobytes(self,b58num:'sstr',pad=0):
  266. "convert a base 58 number to bytes (warning: outputs binary data)"
  267. return bytes.fromhex(baseconv.tohex(b58num,'b58',pad=pad))
  268. def hextob58(self,hexstr:'sstr',pad=0):
  269. "convert a hexadecimal number to base 58"
  270. return baseconv.fromhex(hexstr,'b58',pad=pad,tostr=True)
  271. def b58tohex(self,b58num:'sstr',pad=0):
  272. "convert a base 58 number to hexadecimal"
  273. return baseconv.tohex(b58num,'b58',pad=pad)
  274. def hextob58chk(self,hexstr:'sstr'):
  275. "convert a hexadecimal number to base58-check encoding"
  276. from mmgen.protocol import _b58chk_encode
  277. return _b58chk_encode(hexstr)
  278. def b58chktohex(self,b58chk_num:'sstr'):
  279. "convert a base58-check encoded number to hexadecimal"
  280. from mmgen.protocol import _b58chk_decode
  281. return _b58chk_decode(b58chk_num)
  282. def hextob32(self,hexstr:'sstr',pad=0):
  283. "convert a hexadecimal number to MMGen's flavor of base 32"
  284. return baseconv.fromhex(hexstr,'b32',pad,tostr=True)
  285. def b32tohex(self,b32num:'sstr',pad=0):
  286. "convert an MMGen-flavor base 32 number to hexadecimal"
  287. return baseconv.tohex(b32num.upper(),'b32',pad)
  288. class MMGenToolCmdCoin(MMGenToolCmdBase):
  289. """
  290. cryptocoin key/address utilities
  291. May require use of the '--coin', '--type' and/or '--testnet' options
  292. Examples:
  293. mmgen-tool --coin=ltc --type=bech32 wif2addr <wif key>
  294. mmgen-tool --coin=zec --type=zcash_z randpair
  295. """
  296. def randwif(self):
  297. "generate a random private key in WIF format"
  298. init_generators('at')
  299. return PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed).wif
  300. def randpair(self):
  301. "generate a random private key/address pair"
  302. init_generators()
  303. privhex = PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed)
  304. addr = ag.to_addr(kg.to_pubhex(privhex))
  305. return (privhex.wif,addr)
  306. def wif2hex(self,wifkey:'sstr'):
  307. "convert a private key from WIF to hex format"
  308. return PrivKey(wif=wifkey)
  309. def hex2wif(self,privhex:'sstr'):
  310. "convert a private key from hex to WIF format"
  311. init_generators('at')
  312. return g.proto.hex2wif(privhex,pubkey_type=at.pubkey_type,compressed=at.compressed)
  313. def wif2addr(self,wifkey:'sstr'):
  314. "generate a coin address from a key in WIF format"
  315. init_generators()
  316. privhex = PrivKey(wif=wifkey)
  317. addr = ag.to_addr(kg.to_pubhex(privhex))
  318. return addr
  319. def wif2redeem_script(self,wifkey:'sstr'): # new
  320. "convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
  321. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  322. init_generators()
  323. privhex = PrivKey(wif=wifkey)
  324. return ag.to_segwit_redeem_script(kg.to_pubhex(privhex))
  325. def wif2segwit_pair(self,wifkey:'sstr'):
  326. "generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
  327. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  328. init_generators()
  329. pubhex = kg.to_pubhex(PrivKey(wif=wifkey))
  330. addr = ag.to_addr(pubhex)
  331. rs = ag.to_segwit_redeem_script(pubhex)
  332. return (rs,addr)
  333. def privhex2addr(self,privhex:'sstr',output_pubhex=False):
  334. "generate coin address from private key in hex format"
  335. init_generators()
  336. pk = PrivKey(bytes.fromhex(privhex),compressed=at.compressed,pubkey_type=at.pubkey_type)
  337. ph = kg.to_pubhex(pk)
  338. return ph if output_pubhex else ag.to_addr(ph)
  339. def privhex2pubhex(self,privhex:'sstr'): # new
  340. "generate a hex public key from a hex private key"
  341. return self.privhex2addr(privhex,output_pubhex=True)
  342. def pubhex2addr(self,pubkeyhex:'sstr'):
  343. "convert a hex pubkey to an address"
  344. if opt.type == 'segwit':
  345. return g.proto.pubhex2segwitaddr(pubkeyhex)
  346. else:
  347. return self.pubhash2addr(hash160(pubkeyhex))
  348. def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
  349. "convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
  350. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  351. return g.proto.pubhex2redeem_script(pubkeyhex)
  352. def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
  353. "convert a Segwit P2SH-P2WPKH redeem script to an address"
  354. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  355. assert redeem_scripthex[:4] == '0014','{!r}: invalid redeem script'.format(redeem_scripthex)
  356. assert len(redeem_scripthex) == 44,'{} bytes: invalid redeem script length'.format(len(redeem_scripthex)//2)
  357. return self.pubhash2addr(self.hash160(redeem_scripthex))
  358. def pubhash2addr(self,pubhashhex:'sstr'):
  359. "convert public key hash to address"
  360. if opt.type == 'bech32':
  361. return g.proto.pubhash2bech32addr(pubhashhex)
  362. else:
  363. init_generators('at')
  364. return g.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh')
  365. def addr2pubhash(self,addr:'sstr'):
  366. "convert coin address to public key hash"
  367. from mmgen.tx import addr2pubhash
  368. return addr2pubhash(CoinAddr(addr))
  369. def addr2scriptpubkey(self,addr:'sstr'):
  370. "convert coin address to scriptPubKey"
  371. from mmgen.tx import addr2scriptPubKey
  372. return addr2scriptPubKey(CoinAddr(addr))
  373. def scriptpubkey2addr(self,hexstr:'sstr'):
  374. "convert scriptPubKey to coin address"
  375. from mmgen.tx import scriptPubKey2addr
  376. return scriptPubKey2addr(hexstr)[0]
  377. class MMGenToolCmdMnemonic(MMGenToolCmdBase):
  378. """
  379. seed mnemonic utilities (wordlist: choose 'electrum' (default) or 'tirosh')
  380. IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
  381. computed using a different algorithm and are NOT Electrum-compatible!
  382. """
  383. def _do_random_mn(self,nbytes:int,wordlist_id:str):
  384. assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
  385. hexrand = get_random(nbytes).hex()
  386. Vmsg('Seed: {}'.format(hexrand))
  387. return self.hex2mn(hexrand,wordlist_id=wordlist_id)
  388. def mn_rand128(self,wordlist=dfl_wl_id):
  389. "generate random 128-bit mnemonic"
  390. return self._do_random_mn(16,wordlist)
  391. def mn_rand192(self,wordlist=dfl_wl_id):
  392. "generate random 192-bit mnemonic"
  393. return self._do_random_mn(24,wordlist)
  394. def mn_rand256(self,wordlist=dfl_wl_id):
  395. "generate random 256-bit mnemonic"
  396. return self._do_random_mn(32,wordlist)
  397. def hex2mn(self,hexstr:'sstr',wordlist_id=dfl_wl_id):
  398. "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic"
  399. opt.out_fmt = 'words'
  400. from mmgen.seed import SeedSource
  401. s = SeedSource(seed=bytes.fromhex(hexstr))
  402. s._format()
  403. return ' '.join(s.ssdata.mnemonic)
  404. def mn2hex(self,seed_mnemonic:'sstr',wordlist=dfl_wl_id):
  405. "convert a 12, 18 or 24-word mnemonic to a hexadecimal number"
  406. opt.quiet = True
  407. from mmgen.seed import SeedSource
  408. return SeedSource(in_data=seed_mnemonic,in_fmt='words').seed.hexdata
  409. def mn_stats(self,wordlist=dfl_wl_id):
  410. "show stats for mnemonic wordlist"
  411. wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
  412. baseconv.check_wordlist(wordlist)
  413. return True
  414. def mn_printlist(self,wordlist=dfl_wl_id):
  415. "print mnemonic wordlist"
  416. wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
  417. return '\n'.join(baseconv.digits[wordlist])
  418. class MMGenToolCmdFile(MMGenToolCmdBase):
  419. "utilities for viewing/checking MMGen address and transaction files"
  420. def addrfile_chksum(self,mmgen_addrfile:str):
  421. "compute checksum for MMGen address file"
  422. opt.yes = True
  423. opt.quiet = True
  424. from mmgen.addr import AddrList
  425. return AddrList(mmgen_addrfile).chksum
  426. def keyaddrfile_chksum(self,mmgen_keyaddrfile:str):
  427. "compute checksum for MMGen key-address file"
  428. opt.yes = True
  429. opt.quiet = True
  430. from mmgen.addr import KeyAddrList
  431. return KeyAddrList(mmgen_keyaddrfile).chksum
  432. def passwdfile_chksum(self,mmgen_passwdfile:str):
  433. "compute checksum for MMGen password file"
  434. from mmgen.addr import PasswordList
  435. return PasswordList(infile=mmgen_passwdfile).chksum
  436. def txview( varargs_call_sig = { # hack to allow for multiple filenames
  437. 'args': (
  438. 'mmgen_tx_file(s)',
  439. 'pager',
  440. 'terse',
  441. 'sort',
  442. 'filesort' ),
  443. 'dfls': ( False, False, 'addr', 'mtime' ),
  444. 'annots': {
  445. 'mmgen_tx_file(s)': str,
  446. 'sort': '(valid options: addr,raw)',
  447. 'filesort': '(valid options: mtime,ctime,atime)'
  448. } },
  449. *infiles,**kwargs):
  450. "show raw/signed MMGen transaction in human-readable form"
  451. terse = bool(kwargs.get('terse'))
  452. tx_sort = kwargs.get('sort') or 'addr'
  453. file_sort = kwargs.get('filesort') or 'mtime'
  454. from mmgen.filename import MMGenFileList
  455. from mmgen.tx import MMGenTX
  456. flist = MMGenFileList(infiles,ftype=MMGenTX)
  457. flist.sort_by_age(key=file_sort) # in-place sort
  458. sep = '—'*77+'\n'
  459. return sep.join([MMGenTX(fn).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]).rstrip()
  460. class MMGenToolCmdFileCrypt(MMGenToolCmdBase):
  461. """
  462. file encryption and decryption
  463. MMGen encryption suite:
  464. * Key: Scrypt (user-configurable hash parameters, 32-byte salt)
  465. * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
  466. * The encrypted file is indistinguishable from random data
  467. """
  468. def encrypt(self,infile:str,outfile='',hash_preset=''):
  469. "encrypt a file"
  470. data = get_data_from_file(infile,'data for encryption',binary=True)
  471. enc_d = mmgen_encrypt(data,'user data',hash_preset)
  472. if not outfile:
  473. outfile = '{}.{}'.format(os.path.basename(infile),g.mmenc_ext)
  474. write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
  475. return True
  476. def decrypt(self,infile:str,outfile='',hash_preset=''):
  477. "decrypt a file"
  478. enc_d = get_data_from_file(infile,'encrypted data',binary=True)
  479. while True:
  480. dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
  481. if dec_d: break
  482. msg('Trying again...')
  483. if not outfile:
  484. o = os.path.basename(infile)
  485. outfile = remove_extension(o,g.mmenc_ext)
  486. if outfile == o: outfile += '.dec'
  487. write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
  488. return True
  489. class MMGenToolCmdFileUtil(MMGenToolCmdBase):
  490. "file utilities"
  491. def find_incog_data(self,filename:str,incog_id:str,keep_searching=False):
  492. "Use an Incog ID to find hidden incognito wallet data"
  493. ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
  494. n,carry = 0,b' '*ivsize
  495. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  496. f = os.open(filename,flgs)
  497. for ch in incog_id:
  498. if ch not in '0123456789ABCDEF':
  499. die(2,"'{}': invalid Incog ID".format(incog_id))
  500. while True:
  501. d = os.read(f,bsize)
  502. if not d: break
  503. d = carry + d
  504. for i in range(bsize):
  505. if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == incog_id:
  506. if n+i < ivsize: continue
  507. msg('\rIncog data for ID {} found at offset {}'.format(incog_id,n+i-ivsize))
  508. if not keep_searching: sys.exit(0)
  509. carry = d[len(d)-ivsize:]
  510. n += bsize
  511. if not n % mod:
  512. msg_r('\rSearched: {} bytes'.format(n))
  513. msg('')
  514. os.close(f)
  515. return True
  516. def rand2file(self,outfile:str,nbytes:str,threads=4,silent=False):
  517. "write 'n' bytes of random data to specified file"
  518. from threading import Thread
  519. from queue import Queue
  520. from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  521. from cryptography.hazmat.backends import default_backend
  522. def encrypt_worker(wid):
  523. ctr_init_val = os.urandom(g.aesctr_iv_len)
  524. c = Cipher(algorithms.AES(key),modes.CTR(ctr_init_val),backend=default_backend())
  525. encryptor = c.encryptor()
  526. while True:
  527. q2.put(encryptor.update(q1.get()))
  528. q1.task_done()
  529. def output_worker():
  530. while True:
  531. f.write(q2.get())
  532. q2.task_done()
  533. nbytes = parse_bytespec(nbytes)
  534. if opt.outdir:
  535. outfile = make_full_path(opt.outdir,outfile)
  536. f = open(outfile,'wb')
  537. key = get_random(32)
  538. q1,q2 = Queue(),Queue()
  539. for i in range(max(1,threads-2)):
  540. t = Thread(target=encrypt_worker,args=[i])
  541. t.daemon = True
  542. t.start()
  543. t = Thread(target=output_worker)
  544. t.daemon = True
  545. t.start()
  546. blk_size = 1024 * 1024
  547. for i in range(nbytes // blk_size):
  548. if not i % 4:
  549. msg_r('\rRead: {} bytes'.format(i * blk_size))
  550. q1.put(os.urandom(blk_size))
  551. if nbytes % blk_size:
  552. q1.put(os.urandom(nbytes % blk_size))
  553. q1.join()
  554. q2.join()
  555. f.close()
  556. fsize = os.stat(outfile).st_size
  557. if fsize != nbytes:
  558. die(3,'{}: incorrect random file size (should be {})'.format(fsize,nbytes))
  559. if not silent:
  560. msg('\rRead: {} bytes'.format(nbytes))
  561. qmsg("\r{} byte{} of random data written to file '{}'".format(nbytes,suf(nbytes),outfile))
  562. return True
  563. class MMGenToolCmdWallet(MMGenToolCmdBase):
  564. "key, address or subseed generation from an MMGen wallet"
  565. def get_subseed(self,subseed_idx:str,wallet=''):
  566. "get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
  567. opt.quiet = True
  568. sf = get_seed_file([wallet] if wallet else [],1)
  569. from mmgen.seed import SeedSource
  570. return SeedSource(sf).seed.subseed(subseed_idx).sid
  571. def get_subseed_by_seed_id(self,seed_id:str,wallet='',last_idx=g.subseeds):
  572. "get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
  573. opt.quiet = True
  574. sf = get_seed_file([wallet] if wallet else [],1)
  575. from mmgen.seed import SeedSource
  576. ret = SeedSource(sf).seed.subseed_by_seed_id(seed_id,last_idx)
  577. return ret.ss_idx if ret else None
  578. def list_subseeds(self,subseed_idx_range:str,wallet=''):
  579. "list a range of subseed Seed IDs for default or specified wallet"
  580. opt.quiet = True
  581. sf = get_seed_file([wallet] if wallet else [],1)
  582. from mmgen.seed import SeedSource
  583. return SeedSource(sf).seed.fmt_subseeds(*SubSeedIdxRange(subseed_idx_range))
  584. def gen_key(self,mmgen_addr:str,wallet=''):
  585. "generate a single MMGen WIF key from default or specified wallet"
  586. return self.gen_addr(mmgen_addr,wallet,target='wif')
  587. def gen_addr(self,mmgen_addr:str,wallet='',target='addr'):
  588. "generate a single MMGen address from default or specified wallet"
  589. addr = MMGenID(mmgen_addr)
  590. opt.quiet = True
  591. sf = get_seed_file([wallet] if wallet else [],1)
  592. from mmgen.seed import SeedSource
  593. ss = SeedSource(sf)
  594. if ss.seed.sid != addr.sid:
  595. m = 'Seed ID of requested address ({}) does not match wallet ({})'
  596. die(1,m.format(addr.sid,ss.seed.sid))
  597. al = AddrList(seed=ss.seed,addr_idxs=AddrIdxList(str(addr.idx)),mmtype=addr.mmtype)
  598. d = al.data[0]
  599. ret = d.sec.wif if target=='wif' else d.addr
  600. return ret
  601. class MMGenToolCmdRPC(MMGenToolCmdBase):
  602. "tracking wallet commands using the JSON-RPC interface"
  603. def getbalance(self,minconf=1,quiet=False,pager=False):
  604. "list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
  605. from mmgen.tw import TwGetBalance
  606. return TwGetBalance(minconf,quiet).format()
  607. def listaddress(self,
  608. mmgen_addr:str,
  609. minconf = 1,
  610. pager = False,
  611. showempty = True,
  612. showbtcaddr = True,
  613. age_fmt:'(valid options: days,confs)' = ''):
  614. "list the specified MMGen address and its balance"
  615. return self.listaddresses( mmgen_addrs = mmgen_addr,
  616. minconf = minconf,
  617. pager = pager,
  618. showempty = showempty,
  619. showbtcaddrs = showbtcaddr,
  620. age_fmt = age_fmt)
  621. def listaddresses( self,
  622. mmgen_addrs:'(range or list)' = '',
  623. minconf = 1,
  624. showempty = False,
  625. pager = False,
  626. showbtcaddrs = True,
  627. all_labels = False,
  628. sort:'(valid options: reverse,age)' = '',
  629. age_fmt:'(valid options: days,confs)' = ''):
  630. "list MMGen addresses and their balances"
  631. show_age = bool(age_fmt)
  632. if sort:
  633. sort = set(sort.split(','))
  634. sort_params = {'reverse','age'}
  635. if not sort.issubset(sort_params):
  636. die(1,"The sort option takes the following parameters: '{}'".format("','".join(sort_params)))
  637. usr_addr_list = []
  638. if mmgen_addrs:
  639. a = mmgen_addrs.rsplit(':',1)
  640. if len(a) != 2:
  641. m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
  642. die(1,m.format(mmgen_addrs))
  643. usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
  644. from mmgen.tw import TwAddrList
  645. al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
  646. if not al:
  647. die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
  648. return al.format(showbtcaddrs,sort,show_age,age_fmt or 'days')
  649. def twview( self,
  650. pager = False,
  651. reverse = False,
  652. wide = False,
  653. minconf = 1,
  654. sort = 'age',
  655. age_fmt:'(valid options: days,confs)' = 'days',
  656. show_mmid = True):
  657. "view tracking wallet"
  658. rpc_init()
  659. from mmgen.tw import TwUnspentOutputs
  660. tw = TwUnspentOutputs(minconf=minconf)
  661. tw.do_sort(sort,reverse=reverse)
  662. tw.age_fmt = age_fmt
  663. tw.show_mmid = show_mmid
  664. return tw.format_for_printing(color=True) if wide else tw.format_for_display()
  665. def add_label(self,mmgen_or_coin_addr:str,label:str):
  666. "add descriptive label for address in tracking wallet"
  667. rpc_init()
  668. from mmgen.tw import TrackingWallet
  669. TrackingWallet(mode='w').add_label(mmgen_or_coin_addr,label,on_fail='raise')
  670. return True
  671. def remove_label(self,mmgen_or_coin_addr:str):
  672. "remove descriptive label for address in tracking wallet"
  673. self.add_label(mmgen_or_coin_addr,'')
  674. return True
  675. def remove_address(self,mmgen_or_coin_addr:str):
  676. "remove an address from tracking wallet"
  677. from mmgen.tw import TrackingWallet
  678. tw = TrackingWallet(mode='w')
  679. ret = tw.remove_address(mmgen_or_coin_addr) # returns None on failure
  680. if ret:
  681. msg("Address '{}' deleted from tracking wallet".format(ret))
  682. return ret
  683. class MMGenToolCmdMonero(MMGenToolCmdBase):
  684. "Monero wallet utilities"
  685. def keyaddrlist2monerowallets( self,
  686. xmr_keyaddrfile:str,
  687. blockheight:'(default: current height)' = 0,
  688. addrs:'(integer range or list)' = ''):
  689. "create Monero wallets from key-address list"
  690. return self.monero_wallet_ops( infile = xmr_keyaddrfile,
  691. op = 'create',
  692. blockheight = blockheight,
  693. addrs = addrs)
  694. def syncmonerowallets(self,xmr_keyaddrfile:str,addrs:'(integer range or list)'=''):
  695. "sync Monero wallets from key-address list"
  696. return self.monero_wallet_ops(infile=xmr_keyaddrfile,op='sync',addrs=addrs)
  697. def monero_wallet_ops(self,infile:str,op:str,blockheight=0,addrs=''):
  698. exit_if_mswin('Monero wallet operations')
  699. def run_cmd(cmd):
  700. import subprocess as sp
  701. p = sp.Popen(cmd,stdin=sp.PIPE,stdout=sp.PIPE,stderr=sp.PIPE)
  702. return p
  703. def test_rpc():
  704. p = run_cmd(['monero-wallet-cli','--version'])
  705. if not b'Monero' in p.stdout.read():
  706. die(1,"Unable to run 'monero-wallet-cli'!")
  707. p = run_cmd(['monerod','status'])
  708. import re
  709. m = re.search(r'Height: (\d+)/\d+ ',p.stdout.read().decode())
  710. if not m:
  711. die(1,'Unable to connect to monerod!')
  712. return int(m.group(1))
  713. def my_expect(p,m,s,regex=False):
  714. if m: msg_r(' {}...'.format(m))
  715. ret = (p.expect_exact,p.expect)[regex](s)
  716. vmsg("\nexpect: '{}' => {}".format(s,ret))
  717. if not (ret == 0 or (type(s) == list and ret in (0,1))):
  718. die(2,"Expect failed: '{}' (return value: {})".format(s,ret))
  719. if m: msg('OK')
  720. return ret
  721. def my_sendline(p,m,s,usr_ret):
  722. if m: msg_r(' {}...'.format(m))
  723. ret = p.sendline(s)
  724. if ret != usr_ret:
  725. die(2,"Unable to send line '{}' (return value {})".format(s,ret))
  726. if m: msg('OK')
  727. vmsg("sendline: '{}' => {}".format(s,ret))
  728. def create(n,d,fn):
  729. try: os.stat(fn)
  730. except: pass
  731. else: die(1,"Wallet '{}' already exists!".format(fn))
  732. p = pexpect.spawn('monero-wallet-cli --generate-from-spend-key {}'.format(fn))
  733. if g.debug: p.logfile = sys.stdout
  734. my_expect(p,'Awaiting initial prompt','Secret spend key: ')
  735. my_sendline(p,'',d.sec,65)
  736. my_expect(p,'','Enter.* new.* password.*: ',regex=True)
  737. my_sendline(p,'Sending password',d.wallet_passwd,33)
  738. my_expect(p,'','Confirm password: ')
  739. my_sendline(p,'Sending password again',d.wallet_passwd,33)
  740. my_expect(p,'','of your choice: ')
  741. my_sendline(p,'','1',2)
  742. my_expect(p,'monerod generating wallet','Generated new wallet: ')
  743. my_expect(p,'','\n')
  744. if d.addr not in p.before.decode():
  745. die(3,'Addresses do not match!\n MMGen: {}\n Monero: {}'.format(d.addr,p.before.decode()))
  746. my_expect(p,'','View key: ')
  747. my_expect(p,'','\n')
  748. if d.viewkey not in p.before.decode():
  749. die(3,'View keys do not match!\n MMGen: {}\n Monero: {}'.format(d.viewkey,p.before.decode()))
  750. my_expect(p,'','(YYYY-MM-DD): ')
  751. h = str(blockheight or cur_height-1)
  752. my_sendline(p,'',h,len(h)+1)
  753. ret = my_expect(p,'',['Starting refresh','Still apply restore height? (Y/Yes/N/No): '])
  754. if ret == 1:
  755. my_sendline(p,'','Y',2)
  756. m = ' Warning: {}: blockheight argument is higher than current blockheight'
  757. ymsg(m.format(blockheight))
  758. elif blockheight:
  759. p.logfile = sys.stderr
  760. my_expect(p,'Syncing wallet','\[wallet.*$',regex=True)
  761. p.logfile = None
  762. my_sendline(p,'Exiting','exit',5)
  763. p.read()
  764. def sync(n,d,fn):
  765. try: os.stat(fn)
  766. except: die(1,"Wallet '{}' does not exist!".format(fn))
  767. p = pexpect.spawn('monero-wallet-cli --wallet-file={}'.format(fn))
  768. if g.debug: p.logfile = sys.stdout
  769. my_expect(p,'Awaiting password prompt','Wallet password: ')
  770. my_sendline(p,'Sending password',d.wallet_passwd,33)
  771. msg(' Starting refresh...')
  772. height = None
  773. while True:
  774. ret = p.expect([r' / .*',r'\[wallet.*:.*'])
  775. if ret == 0: # TODO: coverage
  776. cur_block = p.before.decode().split()[-1]
  777. height = p.after.decode()
  778. msg_r('\r Block {}{}'.format(cur_block,height))
  779. elif ret == 1:
  780. if height:
  781. height = height.split()[-1]
  782. msg('\r Block {h} / {h}'.format(h=height))
  783. else:
  784. msg(' Wallet in sync')
  785. b = [l for l in p.before.decode().splitlines() if len(l) > 7 and l[:8] == 'Balance:'][0].split()
  786. msg(' Balance: {} Unlocked balance: {}'.format(b[1],b[4]))
  787. from mmgen.obj import XMRAmt
  788. bals[fn] = ( XMRAmt(b[1][:-1]), XMRAmt(b[4]) )
  789. my_sendline(p,'Exiting','exit',5)
  790. p.read()
  791. break
  792. else:
  793. die(2,"\nExpect failed: (return value: {})".format(ret))
  794. def process_wallets():
  795. m = { 'create': ('Creat','Generat',create,False),
  796. 'sync': ('Sync', 'Sync', sync, True) }
  797. opt.accept_defaults = opt.accept_defaults or m[op][3]
  798. from mmgen.protocol import init_coin
  799. init_coin('xmr')
  800. from mmgen.addr import AddrList
  801. al = KeyAddrList(infile)
  802. data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)]
  803. dl = len(data)
  804. assert dl,"No addresses in addrfile within range '{}'".format(addrs)
  805. gmsg('\n{}ing {} wallet{}'.format(m[op][0],dl,suf(dl)))
  806. for n,d in enumerate(data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
  807. fn = os.path.join(
  808. opt.outdir or '','{}-{}-MoneroWallet{}'.format(
  809. al.al_id.sid,
  810. d.idx,
  811. '-α' if g.debug_utf8 else ''))
  812. gmsg('\n{}ing wallet {}/{} ({})'.format(m[op][1],n+1,dl,fn))
  813. m[op][2](n,d,fn)
  814. gmsg('\n{} wallet{} {}ed'.format(dl,suf(dl),m[op][0].lower()))
  815. if op == 'sync':
  816. col1_w = max(map(len,bals)) + 1
  817. fs = '{:%s} {} {}' % col1_w
  818. msg('\n'+fs.format('Wallet','Balance ','Unlocked Balance '))
  819. from mmgen.obj import XMRAmt
  820. tbals = [XMRAmt('0'),XMRAmt('0')]
  821. for bal in bals:
  822. for i in (0,1): tbals[i] += bals[bal][i]
  823. msg(fs.format(bal+':',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in bals[bal]]))
  824. msg(fs.format('-'*col1_w,'-'*18,'-'*18))
  825. msg(fs.format('TOTAL:',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in tbals]))
  826. os.environ['LANG'] = 'C'
  827. import pexpect
  828. if blockheight < 0:
  829. blockheight = 0 # TODO: handle the non-zero case
  830. cur_height = test_rpc() # empty blockchain returns 1
  831. from collections import OrderedDict
  832. bals = OrderedDict() # locked,unlocked
  833. try:
  834. process_wallets()
  835. except KeyboardInterrupt:
  836. rdie(1,'\nUser interrupt\n')
  837. except EOFError:
  838. rdie(2,'\nEnd of file\n')
  839. except Exception as e:
  840. try:
  841. die(1,'Error: {}'.format(e.args[0]))
  842. except:
  843. rdie(1,'Error: {!r}'.format(e.args[0]))
  844. return True
  845. class MMGenToolCmd(
  846. MMGenToolCmdMisc,
  847. MMGenToolCmdUtil,
  848. MMGenToolCmdCoin,
  849. MMGenToolCmdMnemonic,
  850. MMGenToolCmdFile,
  851. MMGenToolCmdFileCrypt,
  852. MMGenToolCmdFileUtil,
  853. MMGenToolCmdWallet,
  854. MMGenToolCmdRPC,
  855. MMGenToolCmdMonero,
  856. ): pass