mmgen-autosign 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2017 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. mmgen-autosign: Auto-sign MMGen transactions
  20. """
  21. import sys,os,subprocess,time,signal,shutil
  22. from stat import *
  23. mountpoint = '/mnt/tx'
  24. tx_dir = '/mnt/tx/tx'
  25. part_label = 'MMGEN_TX'
  26. wallet_dir = '/dev/shm/autosign'
  27. key_fn = 'autosign.key'
  28. from mmgen.common import *
  29. prog_name = os.path.basename(sys.argv[0])
  30. opts_data = lambda: {
  31. 'desc': 'Auto-sign MMGen transactions',
  32. 'usage':'[opts] [command]',
  33. 'options': """
  34. -h, --help Print this help message
  35. --, --longhelp Print help message for long options (common options)
  36. -c, --coins=c Coins to sign for (comma-separated list)
  37. -l, --led Use status LED to signal standby, busy and error
  38. -m, --mountpoint=m Specify an alternate mountpoint (default: '{mp}')
  39. -s, --stealth-led Stealth LED mode - signal busy and error only, and only
  40. after successful authorization.
  41. -q, --quiet Produce quieter output
  42. -v, --verbose Produce more verbose output
  43. """.format(mp=mountpoint),
  44. 'notes': """
  45. COMMANDS
  46. gen_key - generate the wallet encryption key and copy it to '{td}'
  47. setup - generate the wallet encryption key and wallet
  48. wait - start in loop mode: wait-mount-sign-unmount-wait
  49. USAGE NOTES
  50. If invoked with no command, the program mounts a removable device containing
  51. MMGen transactions, signs any unsigned transactions, unmounts the removable
  52. device and exits.
  53. If invoked with 'wait', the program waits in a loop, mounting, signing and
  54. unmounting every time the removable device is inserted.
  55. On supported platforms (currently Orange Pi and Raspberry Pi boards), the
  56. status LED indicates whether the program is busy or in standby mode, i.e.
  57. ready for device insertion or removal.
  58. The removable device must have a partition labeled MMGEN_TX and a user-
  59. writable directory '/tx', where unsigned MMGen transactions are placed.
  60. On the signing machine the mount point '{mp}' must exist and /etc/fstab
  61. must contain the following entry:
  62. LABEL='MMGEN_TX' /mnt/tx auto noauto,user 0 0
  63. Transactions are signed with a wallet on the signing machine (in the directory
  64. '{wd}') encrypted with a 64-character hexadecimal password on the
  65. removable device.
  66. The password and wallet can be created in one operation by invoking the
  67. command with 'setup' with the removable device inserted. The user will be
  68. prompted for a seed mnemonic.
  69. Alternatively, the password and wallet can be created separately by first
  70. invoking the command with 'gen_key' and then creating and encrypting the
  71. wallet using the -P (--passwd-file) option:
  72. $ mmgen-walletconv -r0 -q -iwords -d{wd} -p1 -P{td}/{kf} -Llabel
  73. Note that the hash preset must be '1'. Multiple wallets are permissible.
  74. For good security, it's advisable to re-generate a new wallet and key for
  75. each signing session.
  76. This command is currently available only on Linux-based platforms.
  77. """.format(pnm=prog_name,wd=wallet_dir,td=tx_dir,kf=key_fn,mp=mountpoint)
  78. }
  79. cmd_args = opts.init(opts_data,add_opts=['mmgen_keys_from_file','in_fmt'])
  80. import mmgen.tx
  81. from mmgen.txsign import txsign
  82. from mmgen.protocol import CoinProtocol
  83. if opt.stealth_led: opt.led = True
  84. if opt.mountpoint: mountpoint = opt.mountpoint # TODO: make global
  85. opt.outdir = tx_dir = os.path.join(mountpoint,'tx')
  86. def check_daemons_running():
  87. if opt.coin:
  88. die(1,'--coin option not supported with this command. Use --coins instead')
  89. if opt.coins:
  90. coins = opt.coins.upper().split(',')
  91. else:
  92. ymsg('Warning: no coins specified, so defaulting to BTC only')
  93. coins = ['BTC']
  94. for coin in coins:
  95. g.proto = CoinProtocol(coin,g.testnet)
  96. vmsg('Checking {} daemon'.format(coin))
  97. try:
  98. rpc_init(reinit=True)
  99. g.rpch.getbalance()
  100. except SystemExit as e:
  101. if e[0] != 0:
  102. ydie(1,'{} daemon not running or not listening on port {}'.format(coin,g.proto.rpc_port))
  103. def get_wallet_files():
  104. wfs = [f for f in os.listdir(wallet_dir) if f[-6:] == '.mmdat']
  105. if not wfs:
  106. die(1,'No wallet files present!')
  107. return [os.path.join(wallet_dir,w) for w in wfs]
  108. def do_mount():
  109. if not os.path.ismount(mountpoint):
  110. if subprocess.Popen(['mount',mountpoint],stderr=subprocess.PIPE,stdout=subprocess.PIPE).wait() == 0:
  111. msg('Mounting '+mountpoint)
  112. try:
  113. ds = os.stat(tx_dir)
  114. assert S_ISDIR(ds.st_mode)
  115. assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR
  116. except:
  117. die(1,'{} missing, or not read/writable by user!'.format(tx_dir))
  118. def do_umount():
  119. if os.path.ismount(mountpoint):
  120. subprocess.call(['sync'])
  121. msg('Unmounting '+mountpoint)
  122. subprocess.call(['umount',mountpoint])
  123. def sign_tx_file(txfile):
  124. try:
  125. g.coin = mmgen.tx.MMGenTX(txfile,md_only=True).coin
  126. g.proto = CoinProtocol(g.coin,g.testnet)
  127. reload(sys.modules['mmgen.tx'])
  128. tx = mmgen.tx.MMGenTX(txfile)
  129. rpc_init(reinit=True)
  130. txsign(tx,wfs,None,None)
  131. tx.write_to_file(ask_write=False)
  132. return True
  133. except:
  134. return False
  135. def sign():
  136. dirlist = os.listdir(tx_dir)
  137. raw = [f for f in dirlist if f[-6:] == '.rawtx']
  138. signed = [f[:-6] for f in dirlist if f[-6:] == '.sigtx']
  139. unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed]
  140. if unsigned:
  141. fails = 0
  142. for txfile in unsigned:
  143. ret = sign_tx_file(txfile)
  144. if not ret:
  145. fails += 1
  146. qmsg('')
  147. time.sleep(0.3)
  148. n_ok = len(unsigned) - fails
  149. msg('{} transaction{} signed'.format(n_ok,suf(n_ok)))
  150. if fails:
  151. ymsg('{} transaction{} failed to sign'.format(fails,suf(fails)))
  152. return False if fails else True
  153. else:
  154. msg('No unsigned transactions')
  155. time.sleep(1)
  156. return True
  157. def decrypt_wallets():
  158. opt.hash_preset = '1'
  159. opt.set_by_user = ['hash_preset']
  160. opt.passwd_file = os.path.join(tx_dir,key_fn)
  161. # opt.passwd_file = '/tmp/key'
  162. from mmgen.seed import SeedSource
  163. msg("Unlocking wallet{} with key from '{}'".format(suf(wfs),opt.passwd_file))
  164. fails = 0
  165. for wf in wfs:
  166. try:
  167. SeedSource(wf)
  168. except SystemExit as e:
  169. if e[0] != 0:
  170. fails += 1
  171. return False if fails else True
  172. def do_sign():
  173. if not opt.stealth_led: set_led('busy')
  174. do_mount()
  175. key_ok = decrypt_wallets()
  176. if key_ok:
  177. if opt.stealth_led: set_led('busy')
  178. ret = sign()
  179. do_umount()
  180. set_led(('standby','off','error')[(not ret)*2 or bool(opt.stealth_led)])
  181. return ret
  182. else:
  183. msg('Password is incorrect!')
  184. do_umount()
  185. if not opt.stealth_led: set_led('error')
  186. return False
  187. def wipe_existing_key():
  188. fn = os.path.join(tx_dir,key_fn)
  189. try: os.stat(fn)
  190. except: pass
  191. else:
  192. msg('\nWiping existing key {}'.format(fn))
  193. subprocess.call(['wipe','-cf',fn])
  194. def create_key():
  195. from binascii import hexlify
  196. kdata = hexlify(os.urandom(32))
  197. fn = os.path.join(tx_dir,key_fn)
  198. desc = 'key file {}'.format(fn)
  199. msg('Creating ' + desc)
  200. try:
  201. with open(fn,'w') as f: f.write(kdata+'\n')
  202. os.chmod(fn,0400)
  203. msg('Wrote ' + desc)
  204. except:
  205. die(2,'Unable to write ' + desc)
  206. def gen_key(no_unmount=False):
  207. create_wallet_dir()
  208. if not get_insert_status():
  209. die(2,'Removable device not present!')
  210. do_mount()
  211. wipe_existing_key()
  212. create_key()
  213. if not no_unmount:
  214. do_umount()
  215. def remove_wallet_dir():
  216. msg("Deleting '{}'".format(wallet_dir))
  217. try: shutil.rmtree(wallet_dir)
  218. except: pass
  219. def create_wallet_dir():
  220. try: os.mkdir(wallet_dir)
  221. except: pass
  222. try: os.stat(wallet_dir)
  223. except: die(2,"Unable to create wallet directory '{}'".format(wallet_dir))
  224. def setup():
  225. remove_wallet_dir()
  226. gen_key(no_unmount=True)
  227. from mmgen.seed import SeedSource
  228. opt.hidden_incog_input_params = None
  229. opt.quiet = True
  230. opt.in_fmt = 'words'
  231. ss_in = SeedSource()
  232. opt.out_fmt = 'mmdat'
  233. opt.usr_randchars = 0
  234. opt.hash_preset = '1'
  235. opt.set_by_user = ['hash_preset']
  236. opt.passwd_file = os.path.join(tx_dir,key_fn)
  237. from mmgen.obj import MMGenWalletLabel
  238. opt.label = MMGenWalletLabel('Autosign Wallet')
  239. ss_out = SeedSource(ss=ss_in)
  240. ss_out.write_to_file(desc='autosign wallet',outdir=wallet_dir)
  241. def ev_sleep(secs):
  242. ev.wait(secs)
  243. return (False,True)[ev.isSet()]
  244. def do_led(on,off):
  245. if not on:
  246. with open(status_ctl,'w') as f: f.write('0\n')
  247. while True:
  248. if ev_sleep(3600): return
  249. while True:
  250. for s_time,val in ((on,255),(off,0)):
  251. with open(status_ctl,'w') as f: f.write('{}\n'.format(val))
  252. if ev_sleep(s_time): return
  253. def set_led(cmd):
  254. if not opt.led: return
  255. vmsg("Setting LED state to '{}'".format(cmd))
  256. timings = {
  257. 'off': ( 0, 0 ),
  258. 'standby': ( 2.2, 0.2 ),
  259. 'busy': ( 0.06, 0.06 ),
  260. 'error': ( 0.5, 0.5 )}[cmd]
  261. global led_thread
  262. if led_thread:
  263. ev.set(); led_thread.join(); ev.clear()
  264. led_thread = threading.Thread(target=do_led,name='LED loop',args=timings)
  265. led_thread.start()
  266. def get_insert_status():
  267. if os.getenv('MMGEN_TEST_SUITE'): return True
  268. try: os.stat(os.path.join('/dev/disk/by-label/',part_label))
  269. except: return False
  270. else: return True
  271. def do_loop():
  272. n,prev_status = 0,False
  273. if not opt.stealth_led:
  274. set_led('standby')
  275. while True:
  276. status = get_insert_status()
  277. if status and not prev_status:
  278. msg('Device insert detected')
  279. do_sign()
  280. prev_status = status
  281. if not n % 10:
  282. msg_r('\r{}\rWaiting'.format(' '*17))
  283. time.sleep(1)
  284. msg_r('.')
  285. n += 1
  286. def check_access(fn,desc='status LED control',init_val=None):
  287. try:
  288. with open(fn) as f: b = f.read().strip()
  289. with open(fn,'w') as f:
  290. f.write('{}\n'.format(init_val or b))
  291. return True
  292. except:
  293. m1 = "You do not have access to the {} file\n".format(desc)
  294. m2 = "To allow access, run 'sudo chmod 0666 {}'".format(fn)
  295. msg(m1+m2)
  296. return False
  297. def check_wipe_present():
  298. try:
  299. subprocess.Popen(['wipe','-v'],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
  300. except:
  301. die(2,"The 'wipe' utility must be installed before running this program")
  302. def init_led():
  303. sc = {
  304. 'opi': '/sys/class/leds/orangepi:red:status/brightness',
  305. 'rpi': '/sys/class/leds/led0/brightness'
  306. }
  307. tc = {
  308. 'rpi': '/sys/class/leds/led0/trigger', # mmc,none
  309. }
  310. for k in ('opi','rpi'):
  311. try: os.stat(sc[k])
  312. except: pass
  313. else:
  314. board = k
  315. break
  316. else:
  317. die(2,'Control files not found! LED option not supported')
  318. status_ctl = sc[board]
  319. trigger_ctl = tc[board] if board in tc else None
  320. if not check_access(status_ctl) or (
  321. trigger_ctl and not check_access(trigger_ctl,desc='LED trigger',init_val='none')
  322. ):
  323. sys.exit(1)
  324. if trigger_ctl:
  325. with open(trigger_ctl,'w') as f: f.write('none\n')
  326. return status_ctl,trigger_ctl
  327. def at_exit(exit_val,nl=True):
  328. if opt.led:
  329. set_led('off')
  330. ev.set()
  331. led_thread.join()
  332. if trigger_ctl:
  333. with open(trigger_ctl,'w') as f: f.write('mmc0\n')
  334. if nl: msg('')
  335. sys.exit(exit_val)
  336. def handler(a,b): at_exit(1)
  337. # main()
  338. if len(cmd_args) == 1:
  339. if cmd_args[0] in ('gen_key','setup'):
  340. globals()[cmd_args[0]]()
  341. sys.exit(0)
  342. elif cmd_args[0] == 'wait':
  343. pass
  344. else:
  345. die(2,"'{}': unrecognized command".format(cmd_args[0]))
  346. check_wipe_present()
  347. wfs = get_wallet_files()
  348. check_daemons_running()
  349. #sign()
  350. #sys.exit()
  351. if opt.led:
  352. import threading
  353. status_ctl,trigger_ctl = init_led()
  354. ev = threading.Event()
  355. led_thread = None
  356. signal.signal(signal.SIGTERM,handler)
  357. signal.signal(signal.SIGINT,handler)
  358. try:
  359. if len(cmd_args) == 1 and cmd_args[0] == 'wait':
  360. do_loop()
  361. elif len(cmd_args) == 0:
  362. ret = do_sign()
  363. at_exit((1,0)[ret],nl=False)
  364. else:
  365. msg('Invalid invocation')
  366. except IOError:
  367. at_exit(2)
  368. except KeyboardInterrupt:
  369. at_exit(1)