From 35ddbc029c29e0d04fb444b9e4481f269d84e34a Mon Sep 17 00:00:00 2001 From: philemon Date: Thu, 5 Dec 2013 00:10:05 +0400 Subject: [PATCH] Added transaction creation/signing scripts modified: MANIFEST new file: mmgen-txcreate new file: mmgen-txsign modified: mmgen/__init__.py modified: mmgen/bitcoin.py modified: mmgen/config.py modified: mmgen/license.py new file: mmgen/tx.py modified: mmgen/utils.py new file: scripts/bitcoind-walletunlock.py modified: setup.py --- MANIFEST | 3 + mmgen-txcreate | 106 ++++++++++++ mmgen-txsign | 118 +++++++++++++ mmgen/__init__.py | 1 + mmgen/bitcoin.py | 24 +++ mmgen/config.py | 1 + mmgen/license.py | 54 +++--- mmgen/tx.py | 285 +++++++++++++++++++++++++++++++ mmgen/utils.py | 118 ++++++++----- scripts/bitcoind-walletunlock.py | 61 +++++++ setup.py | 7 +- 11 files changed, 706 insertions(+), 72 deletions(-) create mode 100755 mmgen-txcreate create mode 100755 mmgen-txsign create mode 100755 mmgen/tx.py create mode 100755 scripts/bitcoind-walletunlock.py diff --git a/MANIFEST b/MANIFEST index 336746c6..8a9389a3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -2,6 +2,8 @@ mmgen-addrgen mmgen-keygen mmgen-passchg +mmgen-txcreate +mmgen-txsign mmgen-walletchk mmgen-walletgen setup.py @@ -14,5 +16,6 @@ mmgen/license.py mmgen/mn_electrum.py mmgen/mn_tirosh.py mmgen/mnemonic.py +mmgen/tx.py mmgen/utils.py mmgen/walletgen.py diff --git a/mmgen-txcreate b/mmgen-txcreate new file mode 100755 index 00000000..e24a7acb --- /dev/null +++ b/mmgen-txcreate @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C) 2013 by philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +mmgen-txcreate: Send BTC from specified outputs to specified addresses +""" + +import sys +#from hashlib import sha256 + +from mmgen.Opts import * +from mmgen.license import * +from mmgen.config import * +from mmgen.tx import * +from mmgen.utils import check_opts, msg, user_confirm +from decimal import Decimal + +prog_name = sys.argv[0].split("/")[-1] + +help_data = { + 'prog_name': prog_name, + 'desc': "Send BTC from specified outputs to specified addresses", + 'usage': "[opts] ", + 'options': """ +-h, --help Print this help message +-d, --outdir d Specify an alternate directory 'd' for output +-e, --echo-passphrase Print passphrase to screen when typing it +-q, --quiet Suppress warnings; overwrite files without asking +""" +} + +short_opts = "hd:eq" +long_opts = "help","outdir=","echo_passphrase","quiet" + +opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) + +# Exits on invalid input +check_opts(opts, ('outdir',)) + +if debug: + print "Processed options: %s" % repr(opts) + print "Cmd args: %s" % repr(cmd_args) + +if len(cmd_args) == 4: + rcpt_addr,send_amt,tx_fee,change_addr = cmd_args + check_address(change_addr) +elif len(cmd_args) == 3: + rcpt_addr,send_amt,tx_fee = cmd_args + change_addr = "" +else: usage(help_data) + +check_address(rcpt_addr) +send_amt = check_btc_amt(send_amt) +tx_fee = check_btc_amt(tx_fee) + +# Begin execution +c = connect_to_bitcoind() + +if not 'quiet' in opts: do_license_msg() + +unspent = sort_and_view(c.listunspent()) + +total = remove_exponent(sum([i.amount for i in unspent])) + +msg("Total unspent: %s BTC" % total) +msg("Amount to spend: %s BTC" % send_amt) +msg("%s unspent outputs total" % len(unspent)) + +sel_unspent = select_outputs(unspent,"Choose the outputs to spend: ") + +total_in = remove_exponent(sum([o.amount for o in sel_unspent])) +change = remove_exponent(total_in - (send_amt + tx_fee)) + +if change < 0: + msg(txmsg['not_enough_btc'] % change) + sys.exit(2) +elif change > 0 and not change_addr: + msg(txmsg['throwaway_change'] % (change, total_in-tx_fee)) + sys.exit(2) + +tx_in = [{"txid":i.txid, "vout":i.vout} for i in sel_unspent] +tx_out = {rcpt_addr:float(send_amt), change_addr:float(change)} +tx_hex = c.createrawtransaction(tx_in,tx_out) + +msg("Transaction successfully created\n") +prompt = "View decoded transaction?" +if user_confirm(prompt,default_yes=False): + view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex) + +prompt = "Save transaction?" +if user_confirm(prompt,default_yes=True): + print_tx_to_file(tx_hex,sel_unspent,send_amt,opts) diff --git a/mmgen-txsign b/mmgen-txsign new file mode 100755 index 00000000..732bcbab --- /dev/null +++ b/mmgen-txsign @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C) 2013 by philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +mmgen-txsign: Sign a Bitcoin transaction generated by mmgen-txcreate +""" + +import sys +#from hashlib import sha256 + +from mmgen.Opts import * +from mmgen.license import * +from mmgen.config import * +from mmgen.tx import * +from mmgen.utils import check_opts, msg, user_confirm, check_infile, get_lines_from_file, my_getpass, my_raw_input + +prog_name = sys.argv[0].split("/")[-1] + +help_data = { + 'prog_name': prog_name, + 'desc': "Sign a Bitcoin transaction generated by mmgen-txcreate", + 'usage': "[opts] ", + 'options': """ +-h, --help Print this help message +-d, --outdir d Specify an alternate directory 'd' for output +-e, --echo-passphrase Print passphrase to screen when typing it +-i, --info Just show info about the transaction and exit +-q, --quiet Suppress warnings; overwrite files without asking +""" +} + +short_opts = "hd:eiq" +long_opts = "help","outdir=","echo_passphrase","info","quiet" + +opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) + +# Exits on invalid input +check_opts(opts, ('outdir',)) + +if debug: + print "Processed options: %s" % repr(opts) + print "Cmd args: %s" % repr(cmd_args) + +if len(cmd_args) == 1: + infile = cmd_args[0] + check_infile(infile) +else: usage(help_data) + +# Begin execution + +c = connect_to_bitcoind() + +tx_data = get_lines_from_file(infile,"transaction data") + +timestamp,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data) + +if 'info' in opts: + view_tx_data(c,inputs_data,tx_hex,timestamp) + sys.exit(0) + +if not 'quiet' in opts and not 'info' in opts: do_license_msg() + +msg("Successfully opened transaction file '%s'" % infile) + +if user_confirm("View transaction data? ",default_yes=False): + view_tx_data(c,inputs_data,tx_hex,timestamp) + +prompt = "Enter bitcoind passphrase: " +if 'echo_passphrase' in opts: + password = my_raw_input(prompt) +else: + password = my_getpass(prompt) + +wallet_enc = True +from bitcoinrpc import exceptions + +try: + c.walletpassphrase(password, 9999) +except exceptions.WalletWrongEncState: + msg("Wallet is unencrypted") + wallet_enc = False +except exceptions.WalletPassphraseIncorrect: + msg("Passphrase incorrect") + sys.exit(3) +except exceptions.WalletAlreadyUnlocked: + msg("WARNING: Wallet already unlocked!") + +# signrawtransaction [{"txid":txid,"vout":n,"scriptPubKey":hex,"redeemScript":hex},...] [,...] [sighashtype="ALL"] +try: + sig_tx = c.signrawtransaction(tx_hex,sig_data) +except: + msg("Failed to sign transaction") + if wallet_enc: c.walletlock() + sys.exit(3) +finally: + if wallet_enc: c.walletlock() + +if not sig_tx['complete']: + msg("signrawtransaction() returned failure") + sys.exit(3) + +prompt = "Save signed transaction?" +if user_confirm(prompt,default_yes=True): + print_signed_tx_to_file(tx_hex,sig_tx['hex'],opts) diff --git a/mmgen/__init__.py b/mmgen/__init__.py index a8f8becc..76701e79 100755 --- a/mmgen/__init__.py +++ b/mmgen/__init__.py @@ -28,6 +28,7 @@ __all__ = [ 'mnemonic.py', 'mn_tirosh.py', 'Opts.py', + 'tx.py', 'utils.py', 'walletgen.py' ] diff --git a/mmgen/bitcoin.py b/mmgen/bitcoin.py index ec90aaf4..d2b3e8b5 100755 --- a/mmgen/bitcoin.py +++ b/mmgen/bitcoin.py @@ -66,6 +66,30 @@ def privnum2addr(numpriv): pubkey = hexlify(pko.get_verifying_key().to_string()) return _pubhex2addr('04'+pubkey) +def verify_addr(addr): + + if addr[0] != "1": + print "%s Invalid address" % addr + return False + + addr,lz = addr[1:],0 + while addr[0] == "1": addr = addr[1:]; lz += 1 + + addr_hex = lz * "00" + hex(_b58tonum(addr))[2:].rstrip("L") + + if len(addr_hex) != 48: + print "%s Invalid address" % addr + return False + + step1 = sha256(unhexlify('00'+addr_hex[:40])).digest() + step2 = sha256(step1).hexdigest() + + if step2[:8] != addr_hex[40:]: + print "Invalid checksum in address %s" % addr + return False + + return True + # Reworked code from here: def _numtob58(num): diff --git a/mmgen/config.py b/mmgen/config.py index 8f0bb00e..ba3666af 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -31,6 +31,7 @@ mnemonic_lens = [i / 32 * 3 for i in seed_lens] from os import getenv debug = True if getenv("MMGEN_DEBUG") else False +mins_per_block = 9 passwd_max_tries = 5 max_randlen,min_randlen = 80,5 usr_randlen = 20 diff --git a/mmgen/license.py b/mmgen/license.py index 67285068..41f51388 100755 --- a/mmgen/license.py +++ b/mmgen/license.py @@ -21,18 +21,16 @@ license.py: Show the license import sys from mmgen.config import proj_name -from mmgen.utils import msg, msg_r +from mmgen.utils import msg, msg_r, get_char gpl = { 'warning': """ -{} Copyright (C) 2013 by philemon -This program comes with ABSOLUTELY NO WARRANTY. -This is free software, and you are welcome to -redistribute it under certain conditions. +{} Copyright (C) 2013 by Philemon . This program +comes with ABSOLUTELY NO WARRANTY. This is free software, and you are +welcome to redistribute it under certain conditions. """.format(proj_name), 'prompt': """ -Press 'c' for conditions, 'w' for warranty info, -or ENTER to continue: +Press 'c' for conditions, 'w' for warranty info, or ENTER to continue: """, 'conditions': """ TERMS AND CONDITIONS @@ -620,30 +618,24 @@ copy of the Program in return for a fee. """ } +def do_pager(text): + import os + pager = os.environ['PAGER'] if 'PAGER' in os.environ else 'more' + + p = os.popen(pager, 'w') + p.write(text) + p.close() + msg_r("\r") + + def do_license_msg(): - ls = "\n " - msg(" " + ls.join(gpl['warning'].strip().split("\n"))) + msg("%s\n" % gpl['warning'].strip()) - try: - import os - os.system( - "stty -icanon min 1 time 0 -echo -echoe -echok -echonl -crterase noflsh" - ) - pager = os.environ['PAGER'] if 'PAGER' in os.environ else 'more' + while True: - while True: - msg_r(ls + ls.join(gpl['prompt'].strip().split("\n")) + " ") - reply = sys.stdin.read(1) - if reply == 'c': - m = gpl['conditions'] - elif reply == 'w': - m = gpl['warranty'] - else: break - p = os.popen(pager, 'w') - p.write(m) - p.close() - except: - msg("\nInterrupted by user") - sys.exit(1) - finally: - os.system("stty sane") + prompt = "%s " % gpl['prompt'].strip() + reply = get_char(prompt) + + if reply == 'c': do_pager(gpl['conditions']) + elif reply == 'w': do_pager(gpl['warranty']) + else: msg("\n"); break diff --git a/mmgen/tx.py b/mmgen/tx.py new file mode 100755 index 00000000..563b2d61 --- /dev/null +++ b/mmgen/tx.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C) 2013 by philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +tx.py: Bitcoin transaction routines +""" + +from binascii import unhexlify +from mmgen.utils import msg,msg_r,write_to_file,my_raw_input,get_char,make_chksum_8,make_timestamp +import sys, os +from bitcoinrpc.connection import * +from decimal import Decimal +from mmgen.config import * + +txmsg = { +'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)", +'throwaway_change': """ +ERROR: This transaction produces change (%s BTC); however, no change +address was specified. Total inputs - transaction fee = %s BTC. +To create a valid transaction with no change address, send this sum to the +specified recipient address. +""".strip() +} + +def connect_to_bitcoind(): + + host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword" + cfg = get_cfg_options((user,passwd)) + + try: + c = BitcoinConnection(cfg[user],cfg[passwd],host,port) + except: + msg("Unable to establish RPC connection with bitcoind") + sys.exit(2) + + return c + + +def remove_exponent(d): + '''Remove exponent and trailing zeros. + ''' + return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() + +def check_address(rcpt_address): + from mmgen.bitcoin import verify_addr + if not verify_addr(rcpt_address): + sys.exit(3) + +def check_btc_amt(send_amt): + + from decimal import Decimal + try: + retval = Decimal(send_amt) + except: + msg("%s: Invalid amount" % send_amt) + sys.exit(3) + + if retval.as_tuple()[-1] < -8: + msg("%s: Too many decimal places in amount" % send_amt) + sys.exit(3) + + return remove_exponent(retval) + + +def get_cfg_options(cfg_keys): + + cfg_file = "%s/%s" % (os.environ["HOME"], ".bitcoin/bitcoin.conf") + try: + f = open(cfg_file) + except: + msg("Unable to open file '%s' for reading" % cfg_file) + sys.exit(2) + + cfg = {} + + for line in f.readlines(): + s = line.translate(None,"\n\t ").split("=") + for k in cfg_keys: + if s[0] == k: cfg[k] = s[1] + + f.close() + + for k in cfg_keys: + if not k in cfg: + msg("Configuration option '%s' must be set in %s" % (k,cfg_file)) + sys.exit(2) + + return cfg + + +def print_tx_to_file(tx,sel_unspent,send_amt,opts): + sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey} + for i in sel_unspent] + tx_id = make_chksum_8(unhexlify(tx)) + outfile = "%s[%s].tx" % (tx_id,send_amt) + if 'outdir' in opts: + outfile = "%s/%s" % (opts['outdir'], outfile) + input_data = sel_unspent + data = "%s\n%s\n%s\n%s\n" % ( + make_timestamp(), tx, repr(sig_data), + repr([i.__dict__ for i in sel_unspent]) + ) + write_to_file(outfile,data,confirm=False) + msg("Transaction data saved to file '%s'" % outfile) + + +def print_signed_tx_to_file(tx,sig_tx,opts): + tx_id = make_chksum_8(unhexlify(tx)) + outfile = "%s.txsig" % tx_id + if 'outdir' in opts: + outfile = "%s/%s" % (opts['outdir'], outfile) + write_to_file(outfile,sig_tx+"\n",confirm=False) + msg("Signed transaction saved to file '%s'" % outfile) + + +def print_sent_tx_to_file(tx): + outfile = "tx.out" + write_to_file(outfile,tx+"\n",confirm=False) + msg("Transaction ID saved to file '%s'" % outfile) + +def sort_and_view(unspent): + + def s_amt(a,b): return cmp(a.amount,b.amount) + def s_txid(a,b): + return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout)) + def s_addr(a,b): return cmp(a.address,b.address) + def s_age(a,b): return cmp(b.confirmations,a.confirmations) + + fs = "%-4s %-11s %-2s %-34s %13s" + sort,group,reverse = "",False,False + + from copy import deepcopy + while True: + out = deepcopy(unspent) + for i in out: i.skip = "" + for n in range(len(out)): + if group and n < len(out)-1: + a,b = out[n],out[n+1] + if sort == "address" and a.address == b.address: + out[n+1].skip = "d" + elif sort == "txid" and a.txid == b.txid: + out[n+1].skip = "t" + + output = [] + output.append("Sort order: %s%s%s" % ( + "reverse " if reverse else "", + sort if sort else "None", + " (grouped)" if group and (sort == "address" or sort == "txid") else "" + )) + output.append(fs % ("Num","TX id","Vout","Address","Amount ")) + + for n,i in enumerate(out): + amt = str(remove_exponent(i.amount)) + fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9 + addr = " |" + "-"*32 if i.skip == "d" else i.address + txid = " |---" if i.skip == "t" else i.txid[:8]+"..." + + output.append(fs % (str(n+1)+")", txid,i.vout,addr,amt+(" "*fill))) + + while True: + reply = get_char("\n".join(output) + +"""\n +Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [g]roup +(Type 'q' to quit sorting): """).strip() + if reply == 'a': unspent.sort(s_amt); sort = "amount"; break + elif reply == 't': unspent.sort(s_txid); sort = "txid"; break + elif reply == 'd': unspent.sort(s_addr); sort = "address"; break + elif reply == 'A': unspent.sort(s_age); sort = "age"; break + elif reply == 'r': + reverse = False if reverse else True + unspent.reverse() + break + elif reply == 'g': group = False if group else True; break + elif reply == 'q': break + else: msg("Invalid input") + + msg("\n") + if reply == 'q': break + + return unspent + + +def view_tx_data(c,inputs_data,tx_hex,timestamp=""): + + td = c.decoderawtransaction(tx_hex) + + msg("TRANSACTION DATA:\n") + + if timestamp: msg("Timestamp: %s\n" % timestamp) + + msg("Inputs:") + total_in = 0 + for n,i in enumerate(td['vin']): + for j in inputs_data: + if j['txid'] == i['txid'] and j['vout'] == i['vout']: + days = j['confirmations'] * mins_per_block / (60*24) + total_in += j['amount'] + msg(""" +%-3s tx,vout: %s,%s + address: %s + amount: %s BTC + confirmations: %s (around %s days) +""".strip() % + (n+1,i['txid'],i['vout'],j['address'], + remove_exponent(j['amount']),j['confirmations'],days)+"\n") + break + + msg("Total input: %s BTC\n" % remove_exponent(total_in)) + + total_out = 0 + msg("Outputs:") + for n,i in enumerate(td['vout']): + total_out += i['value'] + msg(""" +%-3s address: %s + amount: %s BTC +""".strip() % ( + n, + i['scriptPubKey']['addresses'][0], + remove_exponent(i['value'])) + + "\n") + msg("Total output: %s BTC" % remove_exponent(total_out)) + msg("TX fee: %s BTC\n" % remove_exponent(total_in-total_out)) + + +def parse_tx_data(tx_data): + + if len(tx_data) != 4: + msg("'%s': not a transaction file" % infile) + sys.exit(2) + + try: unhexlify(tx_data[1]) + except: + msg("Transaction data is invalid") + sys.exit(2) + + try: + sig_data = eval(tx_data[2]) + except: + msg("Signature data is invalid") + sys.exit(2) + + try: + inputs_data = eval(tx_data[3]) + except: + msg("Inputs data is invalid") + sys.exit(2) + + return tx_data[0],tx_data[1],sig_data,inputs_data + + +def select_outputs(unspent,prompt): + + while True: + reply = my_raw_input(prompt).strip() + if reply: + selected = () + try: + selected = [int(i) - 1 for i in reply.split()] + except: pass + else: + for i in selected: + if i < 0 or i >= len(unspent): + msg( + "Input must be a number or numbers between 1 and %s" % len(unspent)) + break + else: break + + msg("'%s': Invalid input" % reply) + + return [unspent[i] for i in selected] diff --git a/mmgen/utils.py b/mmgen/utils.py index 478c6759..fae3909f 100755 --- a/mmgen/utils.py +++ b/mmgen/utils.py @@ -28,27 +28,44 @@ def msg_r(s): sys.stderr.write(s) def bail(): sys.exit(9) -def _my_getpass(prompt): +def my_getpass(prompt): from getpass import getpass # getpass prompts to stderr, so no trickery required as with raw_input() try: pw = getpass(prompt) except: - msg("\nInterrupted by user") + msg("\nUser interrupt") sys.exit(1) return pw +def get_char(prompt): -def _my_raw_input(prompt): + import os + msg_r(prompt) + os.system( +"stty -icanon min 1 time 0 -echo -echoe -echok -echonl -crterase noflsh" + ) + try: ch = sys.stdin.read(1) + except: + os.system("stty sane") + msg("\nUser interrupt") + sys.exit(1) + else: + os.system("stty sane") + + return ch + + +def my_raw_input(prompt): msg_r(prompt) - try: pw = raw_input() + try: reply = raw_input() except: - msg("\nInterrupted by user") + msg("\nUser interrupt") sys.exit(1) - return pw + return reply def _get_hash_params(hash_preset): @@ -175,7 +192,8 @@ future, you must continue using these same parameters def confirm_or_exit(message, question): - if message.strip(): msg(message.strip()) + m = message.strip() + if m: msg(m) msg("") conf_msg = "Type uppercase 'YES' to confirm: " @@ -185,13 +203,28 @@ def confirm_or_exit(message, question): else: prompt = "Are you sure you want to %s?\n%s" % (question,conf_msg) - if _my_raw_input(prompt).strip() != "YES": + if my_raw_input(prompt).strip() != "YES": msg("Program aborted by user") sys.exit(2) msg("") +def user_confirm(prompt,default_yes=False): + + q = "(Y/n)" if default_yes else "(y/N)" + + while True: + reply = get_char("%s %s: " % (prompt, q)).strip() + msg("") + + if not reply: + return True if default_yes else False + elif reply in 'yY': return True + elif reply in 'nN': return False + else: msg("Invalid reply") + + def set_if_unset_and_typeconvert(opts,item): for opt,var,dtype in item: @@ -282,10 +315,10 @@ just hit ENTER twice. for i in range(passwd_max_tries): if 'echo_passphrase' in opts: - return _my_raw_input("Enter %s: " % what) + return my_raw_input("Enter %s: " % what) else: - ret = _my_getpass("Enter %s: " % what) - if ret == _my_getpass("Repeat %s: " % what): + ret = my_getpass("Enter %s: " % what) + if ret == my_getpass("Repeat %s: " % what): s = " (empty)" if not len(ret) else "" msg("%ss match%s" % (what.capitalize(),s)) return ret @@ -359,6 +392,16 @@ def get_default_wordlist(): return wl.strip().split("\n") +def open_file_or_exit(filename,mode): + try: + f = open(filename, mode) + except: + what = "reading" if mode == 'r' else "writing" + msg("Unable to open file '%s' for %s" % (infile,what)) + sys.exit(2) + return f + + def write_to_file(outfile,data,confirm=False): if confirm: @@ -370,11 +413,7 @@ def write_to_file(outfile,data,confirm=False): else: confirm_or_exit("","File '%s' already exists\nOverwrite?" % outfile) - try: - f = open(outfile,'w') - except: - msg("Failed to open file '%s' for writing" % outfile) - sys.exit(2) + f = open_file_or_exit(outfile,'w') try: f.write(data) @@ -446,12 +485,12 @@ def col4(s): nondiv = 1 if len(s) % 4 else 0 return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)]) - -def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): - +def make_timestamp(): import time tv = time.gmtime(time.time())[:6] - ts_hdr = "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv) + return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv) + +def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): seed_id = make_chksum_8(seed) seed_len = str(len(seed)*8) @@ -470,7 +509,8 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): sf = b58encode_pad(salt) esf = b58encode_pad(enc_seed) - metadata = seed_id.lower(),key_id.lower(),seed_len,pw_status,ts_hdr + metadata = seed_id.lower(),key_id.lower(),\ + seed_len,pw_status,make_timestamp() lines = ( label, @@ -560,11 +600,7 @@ def get_data_from_wallet(infile,opts): msg("Getting {} wallet data from file: {}".format(proj_name,infile)) - try: - f = open(infile, 'r') - except: - msg("Unable to open file '" + infile + "' for reading") - sys.exit(2) + f = open_file_or_exit(infile, 'r') lines = [i.strip() for i in f.readlines()] f.close() @@ -602,23 +638,27 @@ def get_data_from_wallet(infile,opts): def _get_words_from_user(opts, prompt): # split() also strips if 'echo_passphrase' in opts: - return _my_raw_input(prompt).split() + return my_raw_input(prompt).split() else: - return _my_getpass(prompt).split() + return my_getpass(prompt).split() def _get_words_from_file(infile,what): - msg("Getting %s data from file '%s'" % (what,infile)) - try: - f = open(infile, 'r') - except: - msg("Unable to open file '%s' for reading" % infile) - sys.exit(2) - lines = f.readlines() - f.close() + msg("Getting %s from file '%s'" % (what,infile)) + f = open_file_or_exit(infile, 'r') + lines = f.readlines(); f.close() # split() also strips return [w for l in lines for w in l.split()] + +def get_lines_from_file(infile,what): + msg("Getting %s from file '%s'" % (what,infile)) + f = open_file_or_exit(infile,'r') + data = f.read(); f.close() + # split() doesn't strip final newline + return data.strip().split("\n") + + def get_words(infile,what,prompt,opts): if infile: words = _get_words_from_file(infile,what) @@ -709,7 +749,7 @@ def decrypt_seed(enc_seed, key, seed_id, key_id): def get_seed(infile,opts,no_wallet=False): if 'from_mnemonic' in opts: prompt = "Enter mnemonic: " - words = get_words(infile,"mnemonic",prompt,opts) + words = get_words(infile,"mnemonic data",prompt,opts) wl = get_default_wordlist() from mmgen.mnemonic import get_seed_from_mnemonic @@ -723,11 +763,11 @@ def get_seed(infile,opts,no_wallet=False): *_get_from_brain_opt_params(opts)), "continue") prompt = "Enter brainwallet passphrase: " - words = get_words(infile,"brainwallet",prompt,opts) + words = get_words(infile,"brainwallet data",prompt,opts) return _get_seed_from_brain_passphrase(words,opts) elif 'from_seed' in opts: prompt = "Enter seed in %s format: " % seed_ext - words = get_words(infile,"seed",prompt,opts) + words = get_words(infile,"seed data",prompt,opts) return get_seed_from_seed_data(words) elif no_wallet: return False diff --git a/scripts/bitcoind-walletunlock.py b/scripts/bitcoind-walletunlock.py new file mode 100755 index 00000000..3ecbcc48 --- /dev/null +++ b/scripts/bitcoind-walletunlock.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C) 2013 by philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +bitcoind-walletunlock.py: Unlock a Bitcoin wallet securely +""" + +import sys +from mmgen.Opts import * +from mmgen.tx import * +from mmgen.utils import msg, my_getpass, my_raw_input + +prog_name = sys.argv[0].split("/")[-1] + +help_data = { + 'prog_name': prog_name, + 'desc': "Unlock a Bitcoin wallet securely", + 'usage': "[opts]", + 'options': """ +-h, --help Print this help message +-e, --echo-passphrase Print passphrase to screen when typing it +""" +} + +short_opts = "he" +long_opts = "help","echo_passphrase" + +opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) + +c = connect_to_bitcoind() + +prompt = "Enter passphrase: " +if 'echo_passphrase' in opts: + password = my_raw_input(prompt) +else: + password = my_getpass(prompt) + +from bitcoinrpc import exceptions + +try: + c.walletpassphrase(password, 9999) +except exceptions.WalletWrongEncState: + msg("Wallet is unencrypted") +except exceptions.WalletPassphraseIncorrect: + msg("Passphrase incorrect") +except exceptions.WalletAlreadyUnlocked: + msg("WARNING: Wallet already unlocked!") diff --git a/setup.py b/setup.py index 57b94271..7870097c 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name = 'mmgen', - version = '0.6', + version = '0.6.1', author = 'Philemon', author_email = 'mmgen-py@yandex.com', url = 'https://github.com/mmgen/mmgen', @@ -17,6 +17,7 @@ setup( 'mmgen.mnemonic', 'mmgen.mn_tirosh', 'mmgen.Opts', + 'mmgen.tx', 'mmgen.utils', 'mmgen.walletgen' ], @@ -25,6 +26,8 @@ setup( 'mmgen-keygen', 'mmgen-passchg', 'mmgen-walletchk', - 'mmgen-walletgen' + 'mmgen-walletgen', + 'mmgen-txcreate', + 'mmgen-txsign' ])] )