From 9a64bf42e2b5102bb343b8629c234e1f1bbbb937 Mon Sep 17 00:00:00 2001 From: philemon Date: Tue, 25 Mar 2014 12:18:26 +0400 Subject: [PATCH] Improved handling of user input; code cleanups --- README.md | 287 +++++++++++++++++++++++-------------- mmgen-addrgen | 15 +- mmgen-addrimport | 8 +- mmgen-keygen | 1 - mmgen-passchg | 8 +- mmgen-pywallet | 6 +- mmgen-txcreate | 22 +-- mmgen-txsend | 5 +- mmgen-txsign | 121 ++-------------- mmgen-walletchk | 5 +- mmgen-walletgen | 29 +--- mmgen/Opts.py | 117 +++++++++++++++ mmgen/addr.py | 6 +- mmgen/config.py | 7 +- mmgen/license.py | 6 +- mmgen/tx.py | 140 ++++++++++++++---- mmgen/utils.py | 344 +++++++++++++++++++-------------------------- mmgen/walletgen.py | 10 +- 18 files changed, 626 insertions(+), 511 deletions(-) delete mode 120000 mmgen-keygen diff --git a/README.md b/README.md index eb15c3ce..611ad32b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# mmgen = Multi-Mode GENerator +# MMGen = Multi-Mode GENerator ## a Bitcoin cold storage solution for the command line NOTE: Parts of this README are now **out of date**. In particular, the new transaction scripts automate the process of offline signing, so that -your private keys never touch the online machine. An updated README is +your private keys never touch your online machine. An updated README is on the way. For the time being, consult the `--help` option of the `mmgen-tx*` scripts. @@ -14,143 +14,218 @@ addresses is done at your own risk. ### Features: -> As with all deterministic wallets, mmgen can generate an unlimited number -> of address/key pairs from a single seed. You back up your wallet only once. +> Like all deterministic wallets, MMGen can generate a virtually +> unlimited number of address/key pairs from a single seed, allowing you +> to maintain and track a large number of addresses with balances. Your +> wallet never changes (unless you change the password), so you need +> back it up only once. -> With MMGen you can choose from four different ways to access your Bitcoins: +> The "master key", the seed providing access to all your Bitcoins, can +> be stored in four different ways: ->> 1) an encrypted wallet (the AES 256 key is generated from your +>> 1) in an encrypted wallet (the AES 256 key is generated from your >> password using the crack-resistant scrypt hash function. The >> wallet's password and hash strength can be changed); ->> 2) a short, human-readable seed file (unencrypted); +>> 2) in a one-line, human-readable seed file (unencrypted); ->> 3) an Electrum-like mnemonic of 12, 18 or 24 words; or +>> 3) as an Electrum-like mnemonic of 12, 18 or 24 words; or ->> 4) a brainwallet password (recommended for expert users only). +>> 4) as a brainwallet password (this option is recommended for expert +>> users only). > Furthermore, these methods can all be combined. If you forget your > mnemonic, for example, you can regenerate it and your keys from a > stored wallet or seed. Correspondingly, a lost wallet or seed can be > recovered from the mnemonic. -> The wallet and seed are short, simple text files suitable for printing -> or even writing out by hand. Built-in checksums are used to verify -> they've been correctly copied. The base-58-encoded seed is short -> enough to memorize, providing another brain storage alternative. +> The wallet and seed files are in a simple, ASCII-based format suitable +> for printing or even writing out by hand. Built-in checksums are used +> to verify they've been correctly copied. The base-58-encoded seed +> file is short enough to memorize, providing another brain storage +> alternative. -> Implemented as a suite of lightweight Python scripts with a -> command-line interface, MMGen demands practically no system resources. -> Yet in tandem with a bitcoind enabled for watch-only addresses -> (see below), it provides a complete solution for securely -> storing Bitcoins offline and tracking and spending them online. +> Transactions are signed offline: your private keys never touch an +> online computer. + +> Implemented as a suite of lightweight Python scripts for the console, +> MMGen requires only a bare minimum of system resources. Yet in tandem +> with a watch-only enabled bitcoind (see below), it provides a robust +> solution for securely storing, tracking, spending and receiving +> Bitcoins. + +> MMGen is currently supported on Windows and Linux. + +### Download/Install + +#### Debian/Ubuntu Linux: + +> **Perform the following steps on both your online and offline +> computers:** + +> Install the pip Python installer: + + sudo apt-get install python-pip + +> Install required Python modules: + + sudo pip install ecdsa scrypt pycrypto bitcoin-python + +> Install MMGen: + + git clone https://github.com/mmgen/mmgen.git + cd mmgen; sudo ./setup.py install + +> Install vanitygen (optional but recommended): + + git clone https://github.com/samr7/vanitygen.git + (build and put the 'keyconv' executable in your path) + +> At this point you may begin using MMgen to create wallets, generate +> keys and create raw transactions as described in **Using MMGen** +> below. But since you'd like to be able to track addresses and sign +> transactions too, you'll now need to install the bitcoin daemon, +> `bitcoind`, on both your online and offline machines. + +> **Bitcoind installation** + +> On the **offline machine**, the bitcoin daemon is used solely for +> signing transactions and can therefore be run without a blockchain. +> The version bundled with the prebuilt Bitcoin-QT is just fine for this +> purpose. It can be obtained here: + + https://bitcoin.org/en/download + +> After installation, locate the bitcoind executable, optionally place +> it in your path and start it with the arguments `-daemon -maxconnections=0`. + +> Note that in the absence of a blockchain the daemon starts very quickly +> and uses practically no CPU power once running. Thus you'll have no +> problem using a low-powered computer such as a netbook for your +> offline machine. + +> On the **online machine**, the bitcoin daemon is used for tracking +> addresses and must be run with the full blockchain. Thus a more +> powerful computer is required here. In addition, the precompiled +> bitcoin package we installed above lacks (for now) the watch-only +> address support we need, so we must get and compile Sipa's +> watch-only enabled version from github: + + git clone https://github.com/sipa/bitcoin + cd bitcoin + git branch wo origin/watchonly + git checkout wo + ./configure + make + (You may have to install the libboost-all-dev package for the build to succeed) +> With your online machine connected to the Internet, start your freshly +> compiled daemon and let it synchronize, making sure to move your old +> `wallet.dat` out of harm's way beforehand if you have an existing +> bitcoin installation. You'll use the new wallet created by the daemon +> on startup as your **tracking wallet**. + +> Your setup is now complete. -### Instructions for Linux/Unix: - -### Download/Install: -> Install required Python modules: - - sudo pip install ecdsa scrypt pycrypto bitcoin-python - -> Install mmgen: - - git clone https://github.com/mmgen/mmgen.git - cd mmgen; sudo ./setup.py install - -> Install vanitygen (optional but recommended): - - git clone https://github.com/samr7/vanitygen.git - (build and put the 'keyconv' executable in your path) - -### Getting Started: +### Using MMGen: > On your offline computer: > Generate a wallet with a random seed: - $ mmgen-walletgen - ... - Wallet saved to file '89ABCDEF-76543210[256,3].dat' + $ mmgen-walletgen + ... + Wallet saved to file '89ABCDEF-76543210[256,3].mmdat' -> "89ABCDEF" is the Seed ID; "76543210" is the Key ID. These are -> randomly generated, so your IDs will naturally be different than the -> fictitious ones used in this example. +> "89ABCDEF" is the Seed ID; "76543210" is the Key ID. These are +> randomly generated, so your IDs will of course be different than the +> fictitious ones used here. -> The Seed ID never changes and will be used to identify all -> keys/addresses generated by this wallet. The Key ID changes when the -> wallet's password or hash preset are changed. +> The Seed ID never changes and is used to identify all keys/addresses +> generated by this wallet. The Key ID changes when the wallet's +> password or hash preset are changed. -> "256" is the seed length; "3" is the scrypt hash preset. These are +> "256" is the seed length; "3" is the scrypt hash preset. These are > configurable. > Generate ten addresses with the wallet: - $ mmgen-addrgen 89ABCDEF-76543210[256,3].dat 1-10 - ... - Address data saved to file '89ABCDEF[1-10].addrs' + $ mmgen-addrgen 89ABCDEF-76543210[256,3].mmdat 1-10 + ... + Address data saved to file '89ABCDEF[1-10].addrs' -> Note that the address range, "1-10", is included in the resulting filename. +> Note that the address range, "1-10", is reflected in the resulting filename. - $ cat '89ABCDEF[1-10].addrs' - 89ABCDEF { - 1 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE - 2 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc - 3 1HgYCsfqYzIg7LVVfDTp7gYJocJEiDAy6N - 4 14Tu3z1tiexXDonNsFIkvzqutE5E3pTK8s - 5 1PeI55vtp2bX2uKDkAAR2c6ekHNYe4Hcq7 - 6 1FEqfEsSILwXPfMvVvVuUovzTaaST62Mnf - 7 1LTTzuhMqPLwQ4IGCwwugny6ZMtUQJSJ1 - 8 1F9495H8EJLb54wirgZkVgI47SP7M2RQWv - 9 1JbrCyt7BdxRE9GX1N7GiEct8UnIjPmpYd - 10 1H7vVTk4ejUbQXw45I6g5qvPBSe9bsjDqh + $ cat '89ABCDEF[1-10].addrs' + 89ABCDEF { + 1 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE + 2 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc + 3 1HgYCsfqYzIg7LVVfDTp7gYJocJEiDAy6N + 4 14Tu3z1tiexXDonNsFIkvzqutE5E3pTK8s + 5 1PeI55vtp2bX2uKDkAAR2c6ekHNYe4Hcq7 + 6 1FEqfEsSILwXPfMvVvVuUovzTaaST62Mnf + 7 1LTTzuhMqPLwQ4IGCwwugny6ZMtUQJSJ1 + 8 1F9495H8EJLb54wirgZkVgI47SP7M2RQWv + 9 1JbrCyt7BdxRE9GX1N7GiEct8UnIjPmpYd + 10 1H7vVTk4ejUbQXw45I6g5qvPBSe9bsjDqh + } + +> Let's label the first two addresses "Donations" and "Client 1" +> and import them into our tracking wallet. To do this, +> copy and edit the above file in a text editor such as vim: + + $ cat my.addrs + 89ABCDEF { + 1 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE Donations + 2 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc Client 1 } +> With the online bitcoind running, import the two addresses into the +> wallet: -> To store your Bitcoins, spend them into these addresses from whatever -> wallets/software you're currently using. If you have lots of BTC, -> generate many addresses so that each address will have only a -> relatively small balance. + $ mmgen-addrimport my.addrs + +### END rewrite ### Spending your stored coins: > Take address 1 out of cold storage by generating a key for it: - $ mmgen-keygen 89ABCDEF-76543210[256,3].dat 1 - ... - Key data saved to file '89ABCDEF[1].akeys' + $ mmgen-keygen 89ABCDEF-76543210[256,3].mmdat 1 + ... + Key data saved to file '89ABCDEF[1].akeys' - $ cat 89ABCDEF[1].akeys - 89ABCDEF { - 1 sec: 5JCAfK1pjRoJgmpmd2HEMNwHxAzprGIXeQt8dz5qt3iLvU2KCbS - addr: 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE - } + $ cat 89ABCDEF[1].akeys + 89ABCDEF { + 1 sec: 5JCAfK1pjRoJgmpmd2HEMNwHxAzprGIXeQt8dz5qt3iLvU2KCbS + addr: 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE + } > Save the .akeys file to a USB stick and transfer it to your online computer. > On your online computer, import the secret key into a running bitcoind > or bitcoin-qt: - $ bitcoind importprivkey 5JCAfK1pjRoJgmpmd2HEMNwHxAzprGIXeQt8dz5qt3iLvU2KCbS + $ bitcoind importprivkey 5JCAfK1pjRoJgmpmd2HEMNwHxAzprGIXeQt8dz5qt3iLvU2KCbS -> You're done! This address' balance can now be spent. +> You're done! This address' balance can now be spent. > OPTIONAL: To track balances without exposing secret keys on your > online computer, download and compile sipa's bitcoind patched for > watch-only addresses: - $ git clone https://github.com/sipa/bitcoin - $ git branch mywatchonly remotes/origin/watchonly - $ git checkout mywatchonly - (build, install) - (You may have to install libboost-all-dev for the build to succeed) + $ git clone https://github.com/sipa/bitcoin + $ git branch mywatchonly remotes/origin/watchonly + $ git checkout mywatchonly + (build, install) + (You may have to install libboost-all-dev for the build to succeed) > With your newly-compiled bitcoind running, import the addresses from > '89ABCDEF[1-10].addrs' to track their balances: - $ bitcoind importaddress 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE - $ bitcoind importaddress 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc - $ ... + $ bitcoind importaddress 16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE + $ bitcoind importaddress 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc + $ ... ### Using the mnemonic and seed features: @@ -158,51 +233,51 @@ addresses is done at your own risk. > Generate a mnemonic from the wallet: - $ mmgen-walletchk -m '89ABCDEF-76543210[256,3].dat' - ... - Mnemonic data saved to file '89ABCDEF.words' + $ mmgen-walletchk -m '89ABCDEF-76543210[256,3].mmdat' + ... + Mnemonic data saved to file '89ABCDEF.mmwords' - $ cat 89ABCDEF.words - pleasure tumble spider laughter many stumble secret bother - after search float absent path strong curtain savior - worst suspend bright touch away dirty measure thorn + $ cat 89ABCDEF.mmwords + pleasure tumble spider laughter many stumble secret bother + after search float absent path strong curtain savior + worst suspend bright touch away dirty measure thorn > Note: a 128- or 192-bit seed will generate a shorter mnemonic of 12 or > 18 words. You may generate a wallet with a these seed lengths by > using the `-l` option of `mmgen-walletgen`. Whether you consider -> 128 bits of entropy enough is your call. It's probably adequate for +> 128 bits of entropy enough is your call. It's probably adequate for > the foreseeable future. > Generate addresses 1-11 using the mnemonic instead of the wallet: - $ mmgen-addrgen -m 89ABCDEF.words 1-11 - ... - Address data saved to file '89ABCDEF[1-11].addrs' + $ mmgen-addrgen -m 89ABCDEF.mmwords 1-11 + ... + Address data saved to file '89ABCDEF[1-11].addrs' > Compare the first ten addresses with those earlier generated from the > wallet. You'll see they're the same. > Recover a lost wallet using the mnemonic: - $ mmgen-walletgen -m 89ABCDEF.words - ... - Wallet saved to file '89ABCDEF-01234567[256,3].dat' + $ mmgen-walletgen -m 89ABCDEF.mmwords + ... + Wallet saved to file '89ABCDEF-01234567[256,3].mmdat' > Note that the regenerated wallet has a different Key ID but > of course the same Seed ID. -> Seeds are generated and input the same way as mnemonics. Just change +> Seeds are generated and input the same way as mnemonics. Just change > the `-m` option to `-s` in the preceding commands. > A seed file for a 256-bit seed looks like this: - $ cat 8B7392ED.mmseed - f4c84b C5ZT wWpT Jsoi wRVw 2dm9 Aftd WLb8 FggQ eC8h Szjd da9L + $ cat 8B7392ED.mmseed + f4c84b C5ZT wWpT Jsoi wRVw 2dm9 Aftd WLb8 FggQ eC8h Szjd da9L > And for a 128-bit seed: - $ cat 8E0DFB78.mmseed - 0fe02f XnyC NfPH piuW dQ2d nM47 VU + $ cat 8E0DFB78.mmseed + 0fe02f XnyC NfPH piuW dQ2d nM47 VU > The latter is short enough to be memorized or written down. @@ -221,8 +296,8 @@ addresses is done at your own risk. > Mnemonic and seed files may be output to a directory of your choice > using the `-d` option of `mmgen-walletchk`. -> Bear in mind that mnemonic and seed data is unencrypted. If it's -> compromised, your Bitcoins can easily be stolen. Make sure no one's +> Bear in mind that mnemonic and seed data is unencrypted. If it's +> compromised, your Bitcoins can easily be stolen. Make sure no one's > looking over your shoulder when you print mnemonic or seed data to > screen. Securely delete your mnemonic and seed files. In Linux, you > can achieve additional security by writing the files to volatile @@ -235,7 +310,7 @@ addresses is done at your own risk. ### Test suite: > To see what tests are available, run the scripts in the 'tests' -> directory with no arguments. Among others, you might find the +> directory with no arguments. Among others, you might find the > following tests to be of interest: >> Compare 10 addresses generated by 'keyconv' with mmgen's diff --git a/mmgen-addrgen b/mmgen-addrgen index 3cb6c725..050baea1 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -122,23 +122,18 @@ opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts) if 'show_hash_presets' in opts: show_hash_presets() -# Sanity checking and processing of command-line arguments: - opts['gen_what'] = gen_what -set_if_unset_and_typeconvert(opts,( - ('hash_preset',hash_preset,'str'), - ('seed_len',seed_len,'int') -)) +check_opts(opts,long_opts) -# Exits on invalid input -check_opts(opts,('hash_preset','seed_len','outdir','from_brain')) +if debug: show_opts_and_cmd_args(opts,cmd_args) -if len(cmd_args) == 1 and ( +if len(cmd_args) == 1 and ( 'from_mnemonic' in opts or 'from_brain' in opts or 'from_seed' in opts - ): infile,addr_list_arg = "",cmd_args[0] + ): + infile,addr_list_arg = "",cmd_args[0] elif len(cmd_args) == 2: infile,addr_list_arg = cmd_args check_infile(infile) diff --git a/mmgen-addrimport b/mmgen-addrimport index 02251abe..a1cfbf47 100755 --- a/mmgen-addrimport +++ b/mmgen-addrimport @@ -48,6 +48,8 @@ if len(cmd_args) != 1 and not 'addrlist' in opts: msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)") sys.exit(1) +check_opts(opts,long_opts) + if cmd_args: check_infile(cmd_args[0]) seed_id,addr_data = parse_addrs_file(cmd_args[0]) @@ -55,15 +57,15 @@ else: seed_id,addr_data = "",[] if 'addrlist' in opts: - check_infile(opts['addrlist']) l = get_lines_from_file(opts['addrlist'],"non-mmgen addresses") addr_data += [(None,i) for i in l] msg_r("Validating addresses...") -for i in [i[1] for i in addr_data]: - if not verify_addr(i): +for i in addr_data: + if not verify_addr(i[1]): msg("%s: invalid address" % i) sys.exit(2) + msg("OK") import mmgen.config diff --git a/mmgen-keygen b/mmgen-keygen deleted file mode 120000 index 89090086..00000000 --- a/mmgen-keygen +++ /dev/null @@ -1 +0,0 @@ -mmgen-addrgen \ No newline at end of file diff --git a/mmgen-passchg b/mmgen-passchg index 57391fc0..f16e4ac9 100755 --- a/mmgen-passchg +++ b/mmgen-passchg @@ -21,8 +21,7 @@ mmgen-passchg: Change a mmgen deterministic wallet's passphrase, label or """ import sys -import mmgen.Opts as Opts - +from mmgen.Opts import * from mmgen.utils import * from mmgen.config import * @@ -51,12 +50,11 @@ short_opts = "hd:HkL:p:P:v" long_opts = "help","outdir=","show_hash_presets","keep_old_passphrase",\ "label=","hash_preset=","passwd_file=","verbose" -opts,cmd_args = Opts.process_opts(sys.argv,help_data,short_opts,long_opts) +opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'show_hash_presets' in opts: show_hash_presets() -set_if_unset_and_typeconvert(opts,(('hash_preset',hash_preset,'str'),)) -check_opts(opts,('hash_preset','outdir','label')) +check_opts(opts,long_opts) if len(cmd_args) != 1: msg("One input file must be specified") diff --git a/mmgen-pywallet b/mmgen-pywallet index 1ac4b8ab..fdd6fe99 100755 --- a/mmgen-pywallet +++ b/mmgen-pywallet @@ -92,15 +92,15 @@ long_opts = "help","outdir=","echo_passphrase","json","keys","addrs",\ opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) -from mmgen.utils import check_infile,check_opts +from mmgen.utils import check_infile + +check_opts(opts,long_opts) if len(cmd_args) == 1: check_infile(cmd_args[0]) else: usage(help_data) -check_opts(opts,('outdir',)) - if ('json' not in opts and 'keys' not in opts and 'addrs' not in opts and 'keysforaddrs' not in opts): usage(help_data) diff --git a/mmgen-txcreate b/mmgen-txcreate index 60c445e3..066dbba6 100755 --- a/mmgen-txcreate +++ b/mmgen-txcreate @@ -26,7 +26,7 @@ from mmgen.Opts import * from mmgen.license import * from mmgen.config import * from mmgen.tx import * -from mmgen.utils import check_opts, msg, msg_r, user_confirm +from mmgen.utils import msg, msg_r, user_confirm from decimal import Decimal prog_name = sys.argv[0].split("/")[-1] @@ -55,12 +55,9 @@ 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',)) +check_opts(opts,long_opts) -if debug: - print "Processed options: %s" % repr(opts) - print "Cmd args: %s" % repr(cmd_args) +if debug: show_opts_and_cmd_args(opts,cmd_args) if len(cmd_args) == 3: rcpt_arg,tx_fee,change_addr = cmd_args @@ -81,7 +78,7 @@ if not 'info' in opts: # Begin execution c = connect_to_bitcoind() -if not 'quiet' in opts and not 'info' in opts: do_license_msg() +if not 'quiet' in opts and not 'info' in opts: do_license_msg(immed=True) # Begin test # import mmgen.rpc @@ -145,10 +142,15 @@ for i in tx_out.keys(): tx_out[i] = float(tx_out[i]) if change: tx_out[change_addr] = float(change) tx_hex = c.createrawtransaction(tx_in,tx_out) -prompt = "Transaction successfully created\nView decoded transaction?" -if user_confirm(prompt,default_yes=False): - view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex) +msg("Transaction successfully created") +prompt = "View decoded transaction? (y)es, (N)o, (v)iew in pager" +reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True) +if reply and reply in "YyVv": + pager = True if reply in "Vv" else False + view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,pager=pager) prompt = "Save transaction?" if user_confirm(prompt,default_yes=True): print_tx_to_file(tx_hex,sel_unspent,send_amt,opts) +else: + msg("Transaction not saved") diff --git a/mmgen-txsend b/mmgen-txsend index f3ef0ed7..7b9f534a 100755 --- a/mmgen-txsend +++ b/mmgen-txsend @@ -25,7 +25,7 @@ from mmgen.Opts import * from mmgen.license import * from mmgen.config import * from mmgen.tx import * -from mmgen.utils import msg,check_opts,check_infile,get_lines_from_file,confirm_or_exit +from mmgen.utils import msg,check_infile,get_lines_from_file,confirm_or_exit prog_name = sys.argv[0].split("/")[-1] @@ -45,8 +45,7 @@ long_opts = "help","outdir=","quiet" opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) -# Exits on invalid input -check_opts(opts, ('outdir',)) +check_opts(opts,long_opts) if len(cmd_args) == 1: infile = cmd_args[0] diff --git a/mmgen-txsign b/mmgen-txsign index 3ce132b3..2a540e78 100755 --- a/mmgen-txsign +++ b/mmgen-txsign @@ -20,13 +20,12 @@ 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 * +from mmgen.utils import msg help_data = { 'prog_name': sys.argv[0].split("/")[-1], @@ -80,96 +79,12 @@ long_opts = "help","outdir=","echo_passphrase","info","tx_id",\ opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts) -# Exits on invalid input -check_opts(opts, ('outdir','from_brain')) -if 'keys_from_file' in opts: check_infile(opts['keys_from_file']) +check_opts(opts,long_opts) if not infiles: usage(help_data) for i in infiles: check_infile(i) - -def get_keys_for_mmgen_addrs(mmgen_addrs,infiles): - - seed_ids = list(set([i['account'][:8] for i in mmgen_addrs])) - seed_ids_save = seed_ids[0:] - keys = [] - - while seed_ids: - infile = False - if infiles: - infile = infiles.pop() - seed = get_seed(infile,opts) - elif "from_brain" in opts or "from_mnemonic" in opts or "from_seed" in opts: - msg("Need data for seed ID %s" % seed_ids[0]) - seed = get_seed_retry("",opts) - else: - b,p,v = ("A seed","","is") if len(seed_ids) == 1 else ("Seed","s","are") - msg("ERROR: %s source%s %s required for the following seed ID%s: %s" % - (b,p,v,p," ".join(seed_ids))) - sys.exit(2) - - seed_id = make_chksum_8(seed) - if seed_id in seed_ids: - seed_ids.remove(seed_id) - seed_id_addrs = [ - int(i['account'].split()[0][9:]) for i in mmgen_addrs - if i['account'][:8] == seed_id] - - from mmgen.addr import generate_keys - keys += [i['wif'] for i in generate_keys(seed, seed_id_addrs)] - else: - if seed_id in seed_ids_save: - msg_r("Ignoring duplicate seed source") - if infile: msg(" '%s'" % infile) - else: msg(" for ID %s" % seed_id) - else: - msg("Seed source produced an invalid seed ID (%s)" % seed_id) - if infile: - msg("Invalid input file: %s" % infile) - sys.exit(2) - - return keys - - -def sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys): - - try: - sig_tx = sign_transaction(c,tx_hex,sig_data,keys) - except: - from mmgen.rpc import exceptions - msg("Using keys in wallet.dat as per user request") - prompt = "Enter passphrase for bitcoind wallet: " - while True: - passwd = get_bitcoind_passphrase(prompt,opts) - - try: - c.walletpassphrase(passwd, 9999) - except exceptions.WalletPassphraseIncorrect: - msg("Passphrase incorrect") - else: - msg("Passphrase OK"); break - - sig_tx = sign_transaction(c,tx_hex,sig_data,keys) - - msg("Locking wallet") - try: - c.walletlock() - except: - msg("Failed to lock wallet") - - return sig_tx - - -def missing_keys_errormsg(other_addrs): - msg(""" -A key file (option '-f') or wallet.dat (option '-w') must be supplied -for the following non-mmgen address%s: %s""" % - ("" if len(other_addrs) == 1 else "es", - " ".join([i['address'] for i in other_addrs]) - )) - # Begin execution - c = connect_to_bitcoind() tx_file = infiles.pop(0) @@ -186,43 +101,37 @@ if 'info' in opts: view_tx_data(c,inputs_data,tx_hex,metadata) sys.exit(0) -if not 'quiet' in opts: do_license_msg() +if not 'quiet' in opts: do_license_msg(immed=True) msg("Successfully opened transaction file '%s'" % tx_file) -if user_confirm("View transaction data? ",default_yes=False): - view_tx_data(c,inputs_data,tx_hex,metadata) - +prompt = "View transaction data? (y)es, (N)o, (v)iew in pager" +reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True) +if reply and reply in "YyVv": + p = True if reply in "Vv" else False + view_tx_data(c,inputs_data,tx_hex,metadata,pager=p) # Are inputs mmgen addresses? -mmgen_addrs,other_addrs = [],[] +mmgen_addrs = [i for i in inputs_data if verify_mmgen_label(i['account'])] +other_addrs = [i for i in inputs_data if not verify_mmgen_label(i['account'])] -for i in inputs_data: - if verify_mmgen_label(i['account']): - mmgen_addrs.append(i) - else: - other_addrs.append(i) - - -if 'keys_from_file' in opts: - keys = get_lines_from_file(opts['keys_from_file'],"key data") -else: - keys = [] +keys = get_lines_from_file(opts['keys_from_file'],"key data") \ + if 'keys_from_file' in opts else [] if mmgen_addrs: if other_addrs and not keys and not 'use_wallet_dat' in opts: missing_keys_errormsg(other_addrs) sys.exit(2) - keys += get_keys_for_mmgen_addrs(mmgen_addrs,infiles) + keys += get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts) if 'use_wallet_dat' in opts: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys) + sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) else: sig_tx = sign_transaction(c,tx_hex,sig_data,keys) elif other_addrs: if 'use_wallet_dat' in opts: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys) + sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) else: if keys: sig_tx = sign_transaction(c,tx_hex,sig_data,keys) diff --git a/mmgen-walletchk b/mmgen-walletchk index 6e3c16cb..2565a5a2 100755 --- a/mmgen-walletchk +++ b/mmgen-walletchk @@ -49,7 +49,8 @@ long_opts = "help","outdir=","echo_passphrase","export_mnemonic",\ opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) -check_opts(opts, ('outdir',)) +# Argument sanity checks and processing: +check_opts(opts,long_opts) if len(cmd_args) != 1: usage(help_data) @@ -61,7 +62,7 @@ if 'export_seed' in opts: msg("Exporting seed data to file by user request") seed = get_seed_from_wallet(cmd_args[0], opts) -msg("Wallet is OK") +if seed: msg("Wallet is OK") if 'export_mnemonic' in opts: wl = get_default_wordlist() diff --git a/mmgen-walletgen b/mmgen-walletgen index 9bad153f..fb6154c9 100755 --- a/mmgen-walletgen +++ b/mmgen-walletgen @@ -101,30 +101,15 @@ opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'show_hash_presets' in opts: show_hash_presets() -# Argument sanity checks and processing: +check_opts(opts,long_opts) -set_if_unset_and_typeconvert(opts,( - ('usr_randlen',usr_randlen,'int'), - ('hash_preset',hash_preset,'str'), - ('seed_len',seed_len,'int') -)) +if debug: show_opts_and_cmd_args(opts,cmd_args) -# Exits on invalid input -check_opts(opts, - ('usr_randlen','hash_preset','seed_len','outdir','label','from_brain') -) - -if debug: - print "Processed options: %s" % repr(opts) - print "Cmd args: %s" % repr(cmd_args) - -if len(cmd_args) == 1 and ( - 'from_brain' in opts or - 'from_mnemonic' in opts or - 'from_seed' in opts): +if len(cmd_args) == 1: infile = cmd_args[0] check_infile(infile) -elif len(cmd_args) == 0: infile = "" +elif len(cmd_args) == 0: + infile = "" else: usage(help_data) # Begin execution @@ -134,7 +119,7 @@ if not 'quiet' in opts: do_license_msg() msg_r("Acquiring random data from your computer...") from time import sleep -sleep(1) +sleep(0.25) try: from Crypto import Random @@ -157,7 +142,7 @@ usr_rand_data = sha256(usr_keys).digest() + \ sha256("".join(key_timings)).digest() for i in 'from_mnemonic','from_brain','from_seed': - if i in opts: + if infile or (i in opts): seed = get_seed_retry(infile,opts); break else: # Truncate random data for smaller seed lengths diff --git a/mmgen/Opts.py b/mmgen/Opts.py index 94be3973..6c44d945 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -18,11 +18,13 @@ import sys, getopt from mmgen.config import * +from mmgen.utils import msg def usage(hd): print "USAGE: %s %s" % (hd['prog_name'], hd['usage']) sys.exit(2) + def print_help(progname,help_data): pn_len = str(len(progname)+2) print (" %-"+pn_len+"s %s") % (progname.upper()+":", help_data['desc']) @@ -66,3 +68,118 @@ def process_opts(argv,help_data,short_opts,long_opts): if debug: print "User-selected options: %s" % repr(opts) return opts,args + + +def check_opts(opts,long_opts): + + # These must be set to the default values in mmgen.config: + for i in cl_override_vars: + if i+"=" in long_opts: + set_if_unset_and_typeconvert(opts,i) + + for opt in opts.keys(): + + val = opts[opt] + what = "parameter for '--%s' option" % opt.replace("_","-") + + # Check for file existence and readability + for i in 'keys_from_file','addrlist','passwd_file','keysforaddrs': + if opt == i: + check_infile(val) + return + + if opt == 'outdir': + what = "output directory" + import re, os, stat + d = re.sub(r'/*$','', val) + opts[opt] = d + + try: mode = os.stat(d).st_mode + except: + msg("Unable to stat requested %s '%s'" % (what,d)) + sys.exit(1) + + if not stat.S_ISDIR(mode): + msg("Requested %s '%s' is not a directory" % (what,d)) + sys.exit(1) + + if not os.access(d, os.W_OK|os.X_OK): + msg("Requested %s '%s' is unwritable by you" % (what,d)) + sys.exit(1) + elif opt == 'label': + label = val.strip() + opts[opt] = label + + if len(label) > 32: + msg("Label must be 32 characters or less") + sys.exit(1) + + from string import ascii_letters, digits + label_chrs = list(ascii_letters + digits) + [".", "_", " "] + for ch in list(label): + if ch not in label_chrs: + msg("'%s': illegal character in label" % ch) + sys.exit(1) + elif opt == 'from_brain': + try: + l,p = val.split(",") + except: + msg("'%s': invalid %s" % (val,what)) + sys.exit(1) + + try: + int(l) + except: + msg("'%s': invalid 'l' %s (not an integer)" % (l,what)) + sys.exit(1) + + if int(l) not in seed_lens: + msg("'%s': invalid 'l' %s. Options: %s" % + (l, what, ", ".join([str(i) for i in seed_lens]))) + sys.exit(1) + + if p not in hash_presets: + hps = ", ".join([i for i in sorted(hash_presets.keys())]) + msg("'%s': invalid 'p' %s. Options: %s" % (p, what, hps)) + sys.exit(1) + elif opt == 'seed_len': + if val not in seed_lens: + msg("'%s': invalid %s. Options: %s" + % (val,what,", ".join([str(i) for i in seed_lens]))) + sys.exit(2) + elif opt == 'hash_preset': + if val not in hash_presets: + msg("'%s': invalid %s. Options: %s" + % (val,what,", ".join(sorted(hash_presets.keys())))) + sys.exit(2) + elif opt == 'usr_randlen': + if val > max_randlen or val < min_randlen: + msg("'%s': invalid %s (must be >= %s and <= %s)" + % (val,what,min_randlen,max_randlen)) + sys.exit(2) + else: + if debug: print "check_opts(): No test for opt '%s'" % opt + + +def show_opts_and_cmd_args(opts,cmd_args): + print "Processed options: %s" % repr(opts) + print "Cmd args: %s" % repr(cmd_args) + + +def set_if_unset_and_typeconvert(opts,opt): + + if opt in cl_override_vars: + if opt not in opts: + # Set to similarly named default value in mmgen.config + opts[opt] = eval(opt) + else: + vtype = type(eval(opt)) + if vtype == int: f,t = int,"an integer" + elif vtype == str: f,t = str,"a string" + + try: + opts[opt] = f(opts[opt]) + except: + msg("'%s': invalid parameter for '--%s' option (not %s)" % + (opts[opt],opt.replace("_","-"),t)) + sys.exit(1) diff --git a/mmgen/addr.py b/mmgen/addr.py index 295d0cc2..3085cd4c 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -157,9 +157,9 @@ def format_addr_data(addrlist, seed_chksum, opts): # address, and it will be appended to the bitcoind wallet label upon import. # The label may contain ASCII letters, numerals, and the symbols # '{}' and '{}'. -""".format(proj_name.capitalize(),max_wallet_addr_label_len, - "', '".join(wallet_addr_label_symbols[0:-1]), - wallet_addr_label_symbols[-1]).strip() +""".format(proj_name.capitalize(),max_addr_label_len, + "', '".join(addr_label_symbols[0:-1]), + addr_label_symbols[-1]).strip() data = [] if not 'stdout' in opts: data.append(header + "\n") data.append("%s {" % seed_chksum.upper()) diff --git a/mmgen/config.py b/mmgen/config.py index d0925d6d..5a93e99e 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -30,6 +30,8 @@ seed_exts = wallet_ext, seed_ext, mn_ext, brain_ext default_wl = "electrum" #default_wl = "tirosh" +cl_override_vars = 'seed_len','hash_preset','usr_randlen' + seed_lens = 128,192,256 seed_len = 256 @@ -56,6 +58,7 @@ hash_presets = { '3': [14, 8, 8], '4': [15, 8, 12], '5': [16, 8, 16], + '6': [17, 8, 20], } -wallet_addr_label_symbols = ".","_",",","-"," " -max_wallet_addr_label_len = 16 +addr_label_symbols = ".","_",",","-"," " +max_addr_label_len = 16 diff --git a/mmgen/license.py b/mmgen/license.py index e29df791..2d2fab2a 100755 --- a/mmgen/license.py +++ b/mmgen/license.py @@ -585,15 +585,15 @@ copy of the Program in return for a fee. """ } -def do_license_msg(): +def do_license_msg(immed=False): msg(gpl['warning']) prompt = "%s " % gpl['prompt'].strip() while True: - reply = get_char(prompt) + reply = get_char(prompt, immed_chars="wc" if immed else "") if reply == 'w': from mmgen.utils import do_pager - do_pager(gpl['conditions'],"END OF CONDITIONS AND WARRANTY") + do_pager(gpl['conditions']) elif reply == 'c': msg(""); break else: diff --git a/mmgen/tx.py b/mmgen/tx.py index 62970d82..c69c687b 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -206,7 +206,7 @@ def sort_and_view(unspent): for n,i in enumerate(out): if i.skip == "d": - addr = " |" + "-"*32 + addr = "|" + "." * 33 else: if show_mmaddr: if verify_mmgen_label(i.account): @@ -215,21 +215,26 @@ def sort_and_view(unspent): addr = i.address else: addr = i.address - txid = " |---" if i.skip == "t" else i.txid[:8]+"..." + txid = " |..." if i.skip == "t" else i.txid[:8]+"..." output.append(fs % (str(n+1)+")",txid,i.vout,addr,i.amt,i.days)) skip_body = False while True: - if skip_body: skip_body = False + if skip_body: + skip_body = False + immed_chars = "qpP" else: msg("\n".join(output)) msg(""" Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr View options: [g]roup, show [m]mgen addr""") + immed_chars = "qpPtadArMgm" reply = get_char( -"(Type 'q' to quit sorting, 'p' to print to file, 'P' to view in pager): ") +"(Type 'q' to quit sorting, 'p' to print to file, 'v' to view in pager): ", + immed_chars=immed_chars) + 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 @@ -268,7 +273,7 @@ View options: [g]roup, show [m]mgen addr""") write_to_file(outfile, outdata) skip_body = True msg("\nData written to '%s'" % outfile) - elif reply == 'P': do_pager("\n".join(output)) + elif reply == 'v': do_pager("\n".join(output)) elif reply == 'q': break else: msg("Invalid input") @@ -297,28 +302,28 @@ def verify_mmgen_label(s,return_str=False,check_label_len=False): if not i in "0123456789": return fail if check_label_len and comment: - check_wallet_addr_comment(comment) + check_addr_comment(comment) return success -def view_tx_data(c,inputs_data,tx_hex,metadata=[]): +def view_tx_data(c,inputs_data,tx_hex,metadata=[],pager=False): td = c.decoderawtransaction(tx_hex) - msg("TRANSACTION DATA:\n") + out = "TRANSACTION DATA\n\n" - if metadata: msg( - "Header: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n".format(*metadata)) + if metadata: + out += "Header: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n".format(*metadata) - msg("Inputs:") + out += "Inputs:\n\n" 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 = int(j['confirmations'] * mins_per_block / (60*24)) total_in += j['amount'] - msg(" " + """ + out += (" " + """ %-2s tx,vout: %s,%s address: %s ID/label: %s @@ -326,25 +331,29 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[]): confirmations: %s (around %s days) """.strip() % (n+1,i['txid'],i['vout'],j['address'],verify_mmgen_label(j['account'],True), - trim_exponent(j['amount']),j['confirmations'],days)+"\n") + trim_exponent(j['amount']),j['confirmations'],days)+"\n\n") break - msg("Total input: %s BTC\n" % trim_exponent(total_in)) + out += "Total input: %s BTC\n\n" % trim_exponent(total_in) total_out = 0 - msg("Outputs:") + out += "Outputs:\n\n" for n,i in enumerate(td['vout']): total_out += i['value'] - msg(" " + """ + out += (" " + """ %-2s address: %s amount: %s BTC """.strip() % ( n, i['scriptPubKey']['addresses'][0], trim_exponent(i['value'])) - + "\n") - msg("Total output: %s BTC" % trim_exponent(total_out)) - msg("TX fee: %s BTC\n" % trim_exponent(total_in-total_out)) + + "\n\n") + out += "Total output: %s BTC\n" % trim_exponent(total_out) + out += "TX fee: %s BTC\n" % trim_exponent(total_in-total_out) + + if pager: do_pager(out+"\n") + else: msg("\n"+out) + def parse_tx_data(tx_data,infile): @@ -418,20 +427,20 @@ def make_tx_out(rcpt_arg): return tx_out -def check_wallet_addr_comment(label): +def check_addr_comment(label): - if len(label) > max_wallet_addr_label_len: + if len(label) > max_addr_label_len: msg("'%s': overlong label (length must be <=%s)" % - (label,max_wallet_addr_label_len)) + (label,max_addr_label_len)) sys.exit(3) from string import ascii_letters, digits - chrs = tuple(ascii_letters + digits) + wallet_addr_label_symbols + chrs = tuple(ascii_letters + digits) + addr_label_symbols for ch in list(label): if ch not in chrs: msg("'%s': illegal character in label '%s'" % (ch,label)) msg("Permitted characters: A-Za-z0-9, plus '%s'" % - "', '".join(wallet_addr_label_symbols)) + "', '".join(addr_label_symbols)) sys.exit(3) @@ -474,7 +483,7 @@ def parse_addrs_file(f): msg("'%s': invalid address" % d[1]) sys.exit(3) - if len(d) == 3: check_wallet_addr_comment(d[2]) + if len(d) == 3: check_addr_comment(d[2]) ret.append(tuple(d)) @@ -501,3 +510,84 @@ def sign_transaction(c,tx_hex,sig_data,keys=None): # sys.exit(3) return sig_tx + + +def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts): + + seed_ids = list(set([i['account'][:8] for i in mmgen_addrs])) + seed_ids_save = seed_ids[0:] + keys = [] + + while seed_ids: + infile = False + if infiles: + infile = infiles.pop() + seed = get_seed(infile,opts) + elif "from_brain" in opts or "from_mnemonic" in opts or "from_seed" in opts: + msg("Need data for seed ID %s" % seed_ids[0]) + seed = get_seed_retry("",opts) + else: + b,p,v = ("A seed","","is") if len(seed_ids) == 1 else ("Seed","s","are") + msg("ERROR: %s source%s %s required for the following seed ID%s: %s" % + (b,p,v,p," ".join(seed_ids))) + sys.exit(2) + + seed_id = make_chksum_8(seed) + if seed_id in seed_ids: + seed_ids.remove(seed_id) + seed_id_addrs = [ + int(i['account'].split()[0][9:]) for i in mmgen_addrs + if i['account'][:8] == seed_id] + + from mmgen.addr import generate_keys + keys += [i['wif'] for i in generate_keys(seed, seed_id_addrs)] + else: + if seed_id in seed_ids_save: + msg_r("Ignoring duplicate seed source") + if infile: msg(" '%s'" % infile) + else: msg(" for ID %s" % seed_id) + else: + msg("Seed source produced an invalid seed ID (%s)" % seed_id) + if infile: + msg("Invalid input file: %s" % infile) + sys.exit(2) + + return keys + + +def sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts): + + try: + sig_tx = sign_transaction(c,tx_hex,sig_data,keys) + except: + from mmgen.rpc import exceptions + msg("Using keys in wallet.dat as per user request") + prompt = "Enter passphrase for bitcoind wallet: " + while True: + passwd = get_bitcoind_passphrase(prompt,opts) + + try: + c.walletpassphrase(passwd, 9999) + except exceptions.WalletPassphraseIncorrect: + msg("Passphrase incorrect") + else: + msg("Passphrase OK"); break + + sig_tx = sign_transaction(c,tx_hex,sig_data,keys) + + msg("Locking wallet") + try: + c.walletlock() + except: + msg("Failed to lock wallet") + + return sig_tx + + +def missing_keys_errormsg(other_addrs): + msg(""" +A key file (option '-f') or wallet.dat (option '-w') must be supplied +for the following non-mmgen address%s: %s""" % + ("" if len(other_addrs) == 1 else "es", + " ".join([i['address'] for i in other_addrs]) + )) diff --git a/mmgen/utils.py b/mmgen/utils.py index 09aa4f76..55079a94 100755 --- a/mmgen/utils.py +++ b/mmgen/utils.py @@ -26,70 +26,95 @@ from mmgen.bitcoin import b58decode_pad def msg(s): sys.stderr.write(s + "\n") def msg_r(s): sys.stderr.write(s) - def bail(): sys.exit(9) -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("\nUser interrupt") - sys.exit(1) - - return pw - -term = False - -def get_char(prompt=""): +def get_keypress_unix(prompt="",immed_chars=""): msg_r(prompt) + timeout = float(0.3) - global term - - if not term: - try: - import tty, termios - term = "unix" - except: - try: - import msvcrt - term = "mswin" - except: - msg("Unable to set terminal mode") - sys.exit(2) + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + tty.setcbreak(fd) try: - if term == "unix": - import tty, termios - fd = sys.stdin.fileno() - old = termios.tcgetattr(fd) - tty.setcbreak(fd) + while True: + select([sys.stdin], [], [], False) ch = sys.stdin.read(1) - elif term == "mswin": - import msvcrt - ch = msvcrt.getch() - if ord(ch) == 3: - raise KeyboardInterrupt + if immed_chars == "ALL" or ch in immed_chars: + return ch + if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r": + return ch + second_key = select([sys.stdin], [], [], timeout)[0] + if second_key: continue + else: return ch except: - msg("\nUser interrupt") + print "\nUser interrupt" sys.exit(1) finally: - if term == "unix": - termios.tcsetattr(fd, termios.TCSADRAIN, old) - - return ch + termios.tcsetattr(fd, termios.TCSADRAIN, old) -def my_raw_input(prompt): +def get_keypress_mswin(prompt="",immed_chars=""): msg_r(prompt) - try: reply = raw_input() + timeout = float(0.5) + + try: + while True: + if msvcrt.kbhit(): + ch = msvcrt.getch() + + if ord(ch) == 3: raise KeyboardInterrupt + + if immed_chars == "ALL" or ch in immed_chars: + return ch + if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r": + return ch + + hit_time = time.time() + + while True: + if msvcrt.kbhit(): break + if float(time.time() - hit_time) > timeout: + return ch except: msg("\nUser interrupt") sys.exit(1) + +try: + import tty, termios + from select import select + get_char = get_keypress_unix +except: + try: + import msvcrt, time + get_char = get_keypress_mswin + except: + if not sys.platform.startswith("linux") \ + and not sys.platform.startswith("win"): + msg("Unsupported platform: %s" % sys.platform) + msg("This program currently runs only on Linux and Windows") + else: + msg("Unable to set terminal mode") + sys.exit(2) + + +def my_raw_input(prompt,echo=True): + + msg_r(prompt) + reply = "" + + while True: + ch = get_char(immed_chars="ALL_EXCEPT_ENTER") + if echo: msg_r(ch) + if ch in "\n\r": + if not echo: msg("") + break + reply += ch + return reply @@ -101,6 +126,7 @@ def _get_hash_params(hash_preset): msg("%s: invalid 'hash_preset' value" % hash_preset) sys.exit(3) + def show_hash_presets(): fs = " {:<7} {:<6} {:<3} {}" msg("Available parameters for scrypt.hash():") @@ -111,87 +137,6 @@ def show_hash_presets(): sys.exit(0) -def check_opts(opts,keys): - - for key in keys: - if key not in opts: continue - - val = opts[key] - what = "parameter for '--%s' option" % key.replace("_","-") - - if key == 'outdir': - what = "output directory" - import re, os, stat - d = re.sub(r'/*$','', val) - opts[key] = d - - try: mode = os.stat(d).st_mode - except: - msg("Unable to stat requested %s '%s'" % (what,d)) - sys.exit(1) - - if not stat.S_ISDIR(mode): - msg("Requested %s '%s' is not a directory" % (what,d)) - sys.exit(1) - - if not os.access(d, os.W_OK|os.X_OK): - msg("Requested %s '%s' is unwritable by you" % (what,d)) - sys.exit(1) - - elif key == 'label': - label = val.strip() - opts[key] = label - - if len(label) > 32: - msg("Label must be 32 characters or less") - sys.exit(1) - - from string import ascii_letters, digits - label_chrs = list(ascii_letters + digits) + [".", "_", " "] - for ch in list(label): - if ch not in label_chrs: - msg("'%s': illegal character in label" % ch) - sys.exit(1) - - elif key == 'from_brain': - try: - l,p = val.split(",") - except: - msg("'%s': invalid %s" % (val,what)) - sys.exit(1) - - try: - int(l) - except: - msg("'%s': invalid 'l' %s (not an integer)" % (l,what)) - sys.exit(1) - - if int(l) not in seed_lens: - msg("'%s': invalid 'l' %s. Options: %s" % - (l, what, ", ".join([str(i) for i in seed_lens]))) - sys.exit(1) - - if p not in hash_presets: - hps = ", ".join([i for i in sorted(hash_presets.keys())]) - msg("'%s': invalid 'p' %s. Options: %s" % (p, what, hps)) - sys.exit(1) - elif key == 'seed_len': - if val not in seed_lens: - msg("'%s': invalid %s. Options: %s" - % (val,what,", ".join([str(i) for i in seed_lens]))) - sys.exit(2) - elif key == 'hash_preset': - if val not in hash_presets: - msg("'%s': invalid %s. Options: %s" - % (val,what,", ".join(sorted(hash_presets.keys())))) - sys.exit(2) - elif key == 'usr_randlen': - if val > max_randlen or val < min_randlen: - msg("'%s': invalid %s (must be >= %s and <= %s)" - % (val,what,min_randlen,max_randlen)) - sys.exit(2) - - cmessages = { 'null': "", 'unencrypted_secret_keys': """ @@ -215,6 +160,7 @@ future, you must continue using these same parameters """ } + def confirm_or_exit(message, question, expect="YES"): msg("") @@ -236,38 +182,34 @@ def confirm_or_exit(message, question, expect="YES"): msg("") -def user_confirm(prompt,default_yes=False): +def user_confirm(prompt,default_yes=False,verbose=False): q = "(Y/n)" if default_yes else "(y/N)" while True: - reply = get_char("%s %s: " % (prompt, q)).strip() - msg("") + reply = get_char("%s %s: " % (prompt, q)).strip("\n\r") 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: - if dtype == 'int': f,s = int,"an integer" - elif dtype == 'str': f,s = str,"a string" - - if opt in opts: - val = opts[opt] - what = "invalid parameter for '--%s' option" % opt.replace("_","-") - try: - f(val) - except: - msg("'%s': %s (not %s)" % (val,what,s)) - sys.exit(1) - opts[opt] = f(val) + if default_yes: msg(""); return True + else: msg(""); return False + elif reply in 'yY': msg(""); return True + elif reply in 'nN': msg(""); return False else: - opts[opt] = var + if verbose: msg("\nInvalid reply") + else: msg_r("\r") + + +def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): + + while True: + reply = get_char("%s: " % prompt).strip("\n\r") + + if reply in chars or (enter_ok and not reply): + msg("") + return reply + + if verbose: msg("\nInvalid reply") + else: msg_r("\r") def make_chksum_8(s): @@ -279,11 +221,6 @@ def make_chksum_6(s): return sha256(s).hexdigest()[:6] -def _get_from_brain_opt_params(opts): - l,p = opts['from_brain'].split(",") - return(int(l),p) - - def check_infile(f): import os, stat @@ -377,6 +314,11 @@ def _scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen) +def _get_from_brain_opt_params(opts): + l,p = opts['from_brain'].split(",") + return(int(l),p) + + def _get_seed_from_brain_passphrase(words,opts): bp = " ".join(words) if debug: print "Sanitized brain passphrase: %s" % bp @@ -541,6 +483,7 @@ def make_timestr(): def secs_to_hms(secs): return "{:02d}:{:02d}:{:02d}".format(secs/3600, (secs/60) % 60, secs % 60) + def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): seed_id = make_chksum_8(seed) @@ -701,10 +644,8 @@ def get_data_from_wallet(infile,opts,silent=False): def _get_words_from_user(prompt, opts): # split() also strips - if 'echo_passphrase' in opts: - words = my_raw_input(prompt).split() - else: - words = my_getpass(prompt).split() + words = my_raw_input(prompt, + echo=True if 'echo_passphrase' in opts else False).split() if debug: print "Sanitized input: [%s]" % " ".join(words) return words @@ -758,6 +699,7 @@ def _get_seed_from_seed_data(words): msg("Invalid checksum for {} seed".format(proj_name)) return False + passwd_file_used = False def mark_passwd_file_as_used(opts): @@ -781,10 +723,8 @@ def get_bitcoind_passphrase(prompt,opts): mark_passwd_file_as_used(opts) return get_data_from_file(opts['passwd_file'],"passphrase").strip("\r\n") else: - if 'echo_passphrase' in opts: - return my_raw_input(prompt) - else: - return my_getpass(prompt) + return my_raw_input(prompt, + echo=True if 'echo_passphrase' in opts else False) def get_seed_from_wallet( @@ -925,47 +865,45 @@ def remove_blanks_comments(lines): return ret -def do_pager(text,endmsg=""): - import os - if sys.platform.startswith("linux"): - if 'PAGER' in os.environ and os.environ['PAGER']: - try: - p = os.popen(os.environ['PAGER'], 'w') - except: - print text - else: - try: - p.write(text) - p.close() - except: - p.close() - msg_r("\r") - else: - print text - elif sys.platform.startswith("win"): - try: - import msvcrt - except: - print text - else: - try: - from subprocess import Popen, PIPE, STDOUT - p = Popen(["more","/C"], stdin=PIPE, shell=True) - if endmsg: - p.stdin.write("%s\n%s\n\n" % (text,endmsg)) - else: - p.stdin.write(text) - except: - msg("\nUser exit") - from time import sleep - # Flush stdin - while msvcrt.kbhit(): msvcrt.getch() - sleep(1) - while msvcrt.kbhit(): msvcrt.getch() - msg("") - else: - print text +def do_pager(text): + + pagers = ["less","more"] + shell = False + + from os import environ + +# Hack for MS Windows command line (i.e. non CygWin) environment +# When 'shell' is true, Windows aborts the calling program if executable +# not found. +# When 'shell' is false, an exception is raised, invoking the fallback +# 'print' instead of the pager. +# We risk assuming that "more" will always be available on a stock +# Windows installation. + if sys.platform.startswith("win") and 'HOME' not in environ: + shell = True + pagers = ["more"] + + if 'PAGER' in environ and environ['PAGER'] != pagers[0]: + pagers = [environ['PAGER']] + pagers + + for pager in pagers: + end = "" if pager == "less" else "\n(end of text)\n" + try: + from subprocess import Popen, PIPE, STDOUT + p = Popen([pager], stdin=PIPE, shell=shell) + except: pass + else: + try: + p.communicate(text+end+"\n") + except: + # Has no effect. Why? + if pager != "less": + msg("\n(Interrupted by user)\n") + finally: + msg_r("\r") + break + else: print text+end if __name__ == "__main__": diff --git a/mmgen/walletgen.py b/mmgen/walletgen.py index 3acd5c90..38c1c756 100755 --- a/mmgen/walletgen.py +++ b/mmgen/walletgen.py @@ -20,7 +20,7 @@ walletgen.py: Routines used for seed generation and wallet creation """ import sys -from mmgen.utils import msg, msg_r, get_char +from mmgen.utils import msg, msg_r, get_char, prompt_and_get_char from binascii import hexlify def get_random_data_from_user(opts): @@ -48,17 +48,19 @@ displayed on the screen. user_rand_data,intervals = "",[] for i in range(ulen): - user_rand_data += get_char() + user_rand_data += get_char(immed_chars="ALL") msg_r("\r" + prompt % (ulen - i - 1)) now = time.time() intervals.append(now - saved_time) saved_time = now + if 'quiet' in opts: msg_r("\r") else: msg_r("\rThank you. That's enough." + " "*15 + "\n\n") - time.sleep(0.5) - get_char("User random data successfully acquired. Press ENTER to continue: ") + + prompt = "User random data successfully acquired. Press ENTER to continue" + prompt_and_get_char(prompt,"",enter_ok=True) return user_rand_data, ["{:.22f}".format(i) for i in intervals]