Browse Source

mmgen-passgen: generate passwords from your MMGen seed

   - frees you from having to back up or remember passwords
   - generate passwords in either base32 or base58 format
   - user-configurable password length
   - password generation is keyed to an ID string, allowing you to generate
     separate sets of passwords for each online account

   EXAMPLE:
     mmgen-passgen yourname@nowhere.com 1-10
     (generates ten passwords for your email account using your default wallet)

   For more examples and detailed usage information, type 'mmgen-passgen --help'

   This is the new implementation using hmac.  It supersedes commit 85cf5b3

Mnemonic entry mode:
   - Word-by-word mnemonic entry at the prompt
   - Each word is checked individually, user is re-prompted in case of error
   - Activated automatically if stdin is a terminal
philemon 7 years ago
parent
commit
fde885f55b

+ 6 - 2
doc/wiki/install-linux/Install-MMGen-on-Debian-or-Ubuntu-Linux.md

@@ -2,11 +2,11 @@
 
 Install required Debian/Ubuntu packages:
 
-		$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool wipe
+		$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool wipe python-setuptools
 
 Install the Python Cryptography Toolkit:
 
-		$ sudo pip install pycrypto
+		$ sudo -H pip install pycrypto
 
 Install the secp256k1 library:
 
@@ -16,6 +16,8 @@ Install the secp256k1 library:
 		$ ./configure
 		$ make
 		$ sudo make install
+		$ sudo ldconfig
+		$ cd ..
 
 Install MMGen:
 
@@ -23,6 +25,7 @@ Install MMGen:
 		$ cd mmgen
 		$ git checkout -b stable stable_linux
 		$ sudo ./setup.py install
+		$ cd ..
 
 Install vanitygen (optional):
 
@@ -30,6 +33,7 @@ Install vanitygen (optional):
 		$ git clone https://github.com/samr7/vanitygen.git
 		$ cd vanitygen; make
 		(copy the "keyconv" executable to your execution path)
+		$ cd ..
 
 Install bitcoind:
 

+ 4 - 0
doc/wiki/install-mswin/Install-MMGen-on-Microsoft-Windows.md

@@ -3,6 +3,10 @@ it still requires patience, and the user experience is less than optimal.
 You're urged to use the prebuilt [MMGenLive][20] USB image instead.  It's now
 the preferred way for all non-Linux users to run MMGen.***
 
+***Windows XP warning: MMGen is no longer officially supported on Windows XP due
+to incompatibilities with the Crypto.Random module.  The scripts run, but the
+security of your random numbers cannot be guaranteed.  Use at your own risk.***
+
 Install MMGen on Windows by completing the following three steps:
 
 >> 1. Install MinGW and MSYS ([WinXP][03]|[>=Win7][01]), if you haven't already;

+ 361 - 103
doc/wiki/using-mmgen/Getting-Started-with-MMGen.md

@@ -1,5 +1,11 @@
 ## Table of Contents
 
+#### <a href='#a_i'>Preliminaries</a>
+* <a href='#a_iv'>Invocation</a>
+* <a href='#a_cf'>Configuration file</a>
+* <a href='#a_ts'>Test setup and testnet</a>
+* <a href='#a_bb'>Before you begin</a>
+
 #### <a href='#a_bo'>Basic Operations</a>
 * <a href='#a_gw'>Generate a wallet</a>
 * <a href='#a_ga'>Generate addresses</a>
@@ -14,47 +20,104 @@
 * <a href='#a_ic'>Incognito wallets</a>
 	* <a href='#a_hi'>Hidden incognito wallets</a>
 
-The following primer presupposes you have MMGen installed on two computers, one
-offline and one online.  However, if you have an online computer and a few
-Bitcoin addresses with small balances, it’s perfectly possible to perform the
-operations described below on a single online machine.
-
-For those who just want to experiment with MMGen: all wallet generation, wallet
-format conversion, address and key generation, and address import operations can
-be performed on either an online or offline computer with an empty blockchain
-and no Bitcoin balance.
-
-*NOTE: Beginning with v0.8.7a, MMGen supports testnet, allowing you to perform
-the entire set of MMGen operations without risking real funds (free testnet
-coins may be obtained at [https://tpfaucet.appspot.com/][02]). To use this
-feature, start bitcoind with the -testnet option and sync the testnet blockchain
-(about 9GB at this time of writing).  To make MMGen use testnet instead of
-mainnet, supply the `--testnet=1` option as the first argument to all MMGen
-commands you run.  To save typing, the option may also be set in the MMGen
-configuration file.*
-
-Note that all the filenames, seed IDs, Bitcoin addresses and so forth used in
-this primer are fake and for purposes of illustration only.  The up arrow (for
-repeating commands) and tab key (or Ctrl-I) (for completing commands and
-filenames) will speed up your work at the command line greatly.
+#### <a href='#a_at'>Advanced Topics</a>
+* <a href='#a_hw'>Hot wallets and key-address files</a>
+* <a href='#a_fee'>Transaction Fees</a>
+* <a href='#a_rbf'>BIP 125 replace-by-fee (RBF) transactions</a>
+	* <a href='#a_rbf_onl'>With an online (hot) wallet</a>
+	* <a href='#a_rbf_onf'>With an offline (cold storage) wallet</a>
+
+### <a name='a_i'>Preliminaries</a>
+
+#### <a name='a_iv'>Invocation</a>
+
+The MMGen wallet system is not a single program but a suite of lightweight
+commands run from the command line.  MMGen's commands all begin, not
+surprisingly, with 'mmgen'.  To see a list of available commands, type 'mmgen'
+followed by the TAB key.  Every mmgen command has a help screen displaying
+detailed usage and options information.  To view it, type the command name
+followed by `--help`.  Note that most command options have long and short
+versions.  For example, the `--help` option may be abbreviated to `-h`.
+Exceptions are the options listed by `--longhelp`, which have no short versions.
+
+MMGen commands are generally interactive, informing you at every step and
+prompting you for input.  The `--verbose` or `-v` option requests commands to be
+more wordy, while the `--quiet` or `-q` option suppresses all but the most
+essential information.  These options are available for all MMGen commands.  The
+`--yes` option (available only for certain commands) suppresses even more
+information and can be used to make some commands non-interactive or scriptable.
+
+Certain options require parameters, such as the `--seed-len` option, for
+instance, which takes a parameter of '128', '192' or '256'.  Commands may also
+take optional or required arguments.  For example, `mmgen-addrgen` requires an
+address or range of addresses as an argument.  Arguments must always follow
+options on the command line.
+
+Sample MMGen command invocations:
+
+		$ mmgen-txcreate --help
+		$ mmgen-addrgen --verbose 1-10
+		$ mmgen-walletgen
+		$ mmgen-walletgen --quiet --seed-len 128
+
+#### <a name='a_cf'>Configuration file</a>
+
+Just like Bitcoin Core, MMGen has its own data directory and configuration file.
+The data directory is '.mmgen' in the user's home directory and the config
+file is 'mmgen.cfg'.  The config file contains global settings which you may
+wish to edit at some point to customize MMGen to your needs.  These settings
+include the maximum transaction fee; the user name, password and hostname
+used for communicating with bitcoind; and a number of others.
+
+#### <a name='a_ts'>Test setup and testnet</a>
+
+If you just want to quickly try out MMGen, it's possible to perform all wallet
+generation, wallet format conversion, address and key generation, and address
+import operations on an offline computer with no blockchain and no bitcoin
+balance.
+
+If you want to practice creating, signing and sending transactions, however,
+as well as tracking balances, you'll need a fully synced blockchain and some
+actual coins to play with.  To avoid risking real funds, it's *highly
+recommended* to practice transaction operations on testnet until you feel
+confident you know what you're doing.  Testnet is just like the real Bitcoin
+network, but testnet coins have no monetary value.  Free testnet coins may be
+obtained at [https://tpfaucet.appspot.com][02].
+
+To use MMGen with testnet, you must first start bitcoind with the `-testnet`
+option and sync the testnet blockchain (about 12GB at the time of writing).  To
+force any MMGen command to use testnet just add the `--testnet=1` option after
+the command name.  Or just set the `testnet` option to `true` in 'mmgen.cfg' to
+make *all* commands use testnet.
+With testnet you can safely practice all the operations below,
+including the offline ones, on an online computer.
+
+#### <a name='a_bb'>Before you begin</a>
+
+Before you begin, note that the filenames, seed IDs and Bitcoin addresses used
+in this primer are intentionally invalid and are for purposes of illustration
+only.  As you perform the exercises, you'll naturally substitute real ones in
+their place.
+
+The up arrow (for repeating commands) and tab key (or Ctrl-I) (for completing
+commands and filenames) will speed up your work at the command line greatly.
 
 ### <a name='a_bo'>Basic Operations</a>
 
-#### <a name='a_gw'>Generate a wallet (offline computer):</a>
+#### <a name='a_gw'>Generate a wallet (offline computer)</a>
 
-*NOTE: Beginning with v0.8.8, MMGen supports a “default wallet” feature.  After
-creating your wallet, MMGen will prompt you to make it your default.  If you
-answer 'y', the wallet will be stored in your MMGen data directory and used for
-all future commands that require a wallet or other seed source.*
+*NOTE: MMGen supports a “default wallet” feature.  After generating your wallet,
+you'll be prompted to make it your default.  If you answer 'y', the wallet will
+be stored in your MMGen data directory and used for all future commands that
+require a wallet or other seed source.*
 
-*You may not want this feature if you plan to store your MMGen wallet in another
-location than your MMGen data directory.  Otherwise, it's recommended, as it
-frees you from having to type your wallet on the command line.*
+*You may not want this feature if you plan to store your MMGen wallet in a
+location other than your MMGen data directory.  Otherwise, it’s recommended,
+as it frees you from having to type your wallet file on the command line.*
 
-*The following examples suppose that you've chosen to use a default wallet.
-Bear in mind that in the absence of a default wallet, the path to a wallet file
-or other seed source needs to be included in all commands where it's
-applicable.*
+*The following examples suppose that you’ve chosen to use a default wallet.
+If you haven't, then you must include the path to a wallet file or other seed
+source in all commands where a seed source is required.*
 
 On your offline computer, generate a wallet:
 
@@ -73,18 +136,21 @@ wallet’s password or hash preset are changed and is less important.
 configurable: type `mmgen-walletgen --help` for details.
 
 Before moving any funds into your MMGen wallet, you should back it up in several
-places and preferably on several media such as paper, flash memory or a CD-ROM.
-You’re advised to use a passphrase with your wallet.  Otherwise, anyone who
-gains physical access to one of your backups can easily steal your coins.  Don’t
-forget your passphrase.  If you do, the coins in your MMGen wallet are gone
-forever.
+places and preferably on several media such as paper, flash memory or optical
+disk.  You’re advised to use a passphrase with your wallet.  Otherwise, anyone
+who gains physical access to one of your backups can easily steal your coins.
+Don’t forget your passphrase.  If you do, the coins in your MMGen wallet are
+gone forever.
 
 Since the wallet is a small, humanly readable ASCII file, it can easily be
-printed out on paper.  It can also be exported to more compact forms, the seed
-file and mnemonic (discussed below).  These formats are short enough to be
-written out by hand or memorized.
+printed out on paper.
 
-#### <a name='a_ga'>Generate addresses (offline computer):</a>
+Another highly recommended way to back up your wallet is to generate a mnemonic
+or seed file <a href='#a_ms'>as described below </a> and memorize it.  If you
+have an average or better memory, you'll find memorizing mnemonics to be
+surprisingly easy.
+
+#### <a name='a_ga'>Generate addresses (offline computer)</a>
 
 Now generate ten addresses with your just-created wallet:
 
@@ -111,15 +177,16 @@ the resulting filename.  MMGen addresses consist of the Seed ID followed by ‘:
 and an index.  In this example, ‘89ABCDEF:1’ represents the Bitcoin address
 ‘16bNmy...’, ‘89ABCDEF:2’ represents ‘1AmkUx...’ and so forth.
 
-To begin moving your Bitcoin holdings into your MMGen wallet, just spend into
-any of these addresses.  If you run out of addresses, generate more.  To
-generate a hundred addresses, for example, you’d specify an address range of ‘1-100’.
+To fund your MMGen wallet, first import the addresses into your tracking wallet
+and then spend some bitcoin into any of them.  If you run out of addresses,
+generate more.  To generate a hundred addresses, for example, specify an address
+range of ‘1-100’.
 
 Let’s say you’ve decided to spend some BTC into the first four addresses above.
-Before doing so, you must import these addresses into the tracking wallet on
-your online machine so their balances will be visible.  For convenience
-of reference, provide the addresses with labels.  We’ll use the labels
-‘Donations’, ‘Storage 1’, ‘Storage 2’ and ‘Storage 3’.
+Begin by importing these addresses into the tracking wallet on your online
+machine so their balances will be visible.  For convenience of reference,
+provide the addresses with labels.  We’ll use the labels ‘Donations’, ‘Storage
+1’, ‘Storage 2’ and ‘Storage 3’.
 
 Make a copy of the address file
 
@@ -150,7 +217,7 @@ of lines as well.
 
 Save the file, copy it onto a USB stick and transfer it to your online computer.
 
-#### <a name='a_ia'>Import addresses (online computer):</a>
+#### <a name='a_ia'>Import addresses (online computer)</a>
 
 On your online computer, go to your bitcoind data directory and move any
 existing 'wallet.dat' file out of harm’s way.  Start bitcoind and let it
@@ -177,9 +244,9 @@ also).
 
 *While not covered in this introduction, note that it’s also possible to [import
 ordinary Bitcoin addresses into your tracking wallet][01].  This allows you to
-move funds from another wallet directly to MMGen without having to go through
-the network.  To use it, you must save the keys corresponding to the addresses
-where the funds are stored in a separate file for use during signing.*
+track and spend funds from another wallet using MMGen without having to go
+through the network.  To use it, you must save the keys corresponding to the
+addresses where the funds are stored in a separate file to use during signing.*
 
 Now that your addresses are being tracked, you may go ahead and send some BTC to
 them over the Bitcoin network.  If you send 0.1, 0.2, 0.3 and 0.4 BTC
@@ -194,14 +261,13 @@ after the transactions have been confirmed:
 		89ABCDEF:4  Storage 3    0.4
 		TOTAL: 1 BTC
 
-#### <a name='a_ct'>Create a transaction (online computer):</a>
+#### <a name='a_ct'>Create a transaction (online computer)</a>
 
 Now that you have some BTC under MMGen’s control, you’re ready to create a
 transaction.  Note that transactions are harmless until they’re signed and
 broadcast to the network, so feel free to experiment and create transactions
 with different combinations of inputs and outputs.  If you're using testnet,
-then even broadcast transactions are harmless, so it's highly recommended you
-do so if you want to practice sending transactions.
+then you risk nothing even when broadcasting transactions, of course.
 
 To send 0.1 BTC to the a third-party address 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc,
 for example, and send the change back to yourself at address 89ABCDEF:5, you’d
@@ -247,23 +313,20 @@ After quitting the menu with 'q', you’ll see the following prompt:
 		Enter a range or space-separated list of outputs to spend:
 
 Here you must choose unspent outputs of sufficient value to cover the send
-amount of 0.1 BTC, plus the transaction fee.  By the way, MMGen calculates fees
-automatically using bitcoind’s 'estimatefee' RPC call, which makes things very
-convenient.  If you want to increase the fee a bit for speedier confirmation,
-use the `--tx-fee-adj` option.  Type `mmgen-txcreate --help` for details.
-
-Output #2 is worth 0.2 BTC, which is sufficient, so let’s choose it.  After
-several more prompts and confirmations, your transaction will be saved:
+amount of 0.1 BTC, plus the transaction fee (for more on fees, see 'Transaction
+Fees' under 'Advanced Topics' below).  Output #2 is worth 0.2 BTC, which is
+sufficient, so we’ll choose that.  After several more prompts and confirmations,
+your transaction will be saved:
 
 		Transaction written to file 'FEDCBA[0.1].rawtx'
 
-Note that the transaction filename consists of a unique ID plus the spend
-amount.
+Note that the transaction filename consists of a unique MMGen Transaction ID
+plus the non-change spend amount.
 
 As you can see, MMGen gives you complete control over your transaction inputs
 and change addresses.  This feature will be appreciated by privacy-conscious users.
 
-#### <a name='a_sg'>Sign a transaction (offline computer):</a>
+#### <a name='a_sg'>Sign a transaction (offline computer)</a>
 
 Now transfer the the raw transaction file to your offline computer and sign it
 using your default wallet:
@@ -274,16 +337,21 @@ using your default wallet:
 
 Note that the signed transaction file has a new extension, '.sigtx'.
 
-#### <a name='a_st'>Send a transaction (online computer):</a>
+#### <a name='a_st'>Send a transaction (online computer)</a>
 
 Now you’re ready for the final step: broadcasting the transaction to the
 network.  Copy the signed transaction file to your online computer, start
-bitcoind if necessary, and issue the command:
+bitcoind if necessary, make sure your blockchain is up-to-date and issue the
+command:
 
 		$ mmgen-txsend FEDCBA[0.1].sigtx
+		...
+		Transaction sent: abcd1234....
 
 Like all MMGen commands, 'mmgen-txsend' is interactive, so you’ll be prompted
-before the transaction is actually sent.
+before the transaction is actually broadcast.  If the send was successful, a
+64-character hexadecimal Bitcoin Transaction ID will be displayed ('abcd1234...'
+in our case).
 
 Once the transaction is broadcast to the network and confirmed, your address
 listing should look something like this:
@@ -300,15 +368,12 @@ Since you’ve sent 0.1 BTC to a third party, your balance has declined by 0.1 B
 plus the tx fee of 0.0001 BTC.  To verify that your transaction’s received its
 second, third and so on confirmations, increase `minconf` accordingly.
 
-Congratulations!  You’ve now mastered the basics of MMGen!
-
-Some of MMGen’s more advanced features are discussed below.  Others are
-documented in the help screens of the individual MMGen commands: display these
-by invoking the desired command with the `-h` or `--help` switch.
+Congratulations!  You’ve now mastered the basics of MMGen!  To learn about some
+of MMGen’s more advanced features, continue reading.
 
 ### <a name='a_af'>Additional Features</a>
 
-#### <a name='a_ms'>Using the mnemonic, seed and hexseed formats:</a>
+#### <a name='a_ms'>Using the mnemonic, seed and hexseed formats</a>
 
 Continuing our example above, generate a mnemonic from the default wallet:
 
@@ -321,29 +386,33 @@ Continuing our example above, generate a mnemonic from the default wallet:
 		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 these seed lengths using the `-l`
-option to 'mmgen-walletgen'.
+Since our seed is 256 bits long, the mnemonic contains 24 words.  128-bit and
+192-bit seeds generate shorter mnemonics of 12 and 18 words, respectively.
+Wallets with these seed lengths can be generated using the `--seed-len` option
+to 'mmgen-walletgen'.
 
 Though some consider 128 bits of entropy to provide adequate security for the
-foreseeable future, it’s advisable to stick to the default 256-bit seed length
-if you’re not planning to use the mnemonic feature.
+foreseeable future, it’s advisable to stick to the default 256-bit seed length.
+You'll find that even a 24-word mnemonic is not difficult to memorize.
 
 NOTE: MMGen mnemonics are generated from the Electrum wordlist, but using
 ordinary base conversion instead of Electrum’s more complicated algorithm.
 
-The mnemonic file may be used any place you’d use a MMGen wallet with the same
-Seed ID.  You can generate ten addresses with it just as you did with the
-wallet, for example:
+The mnemonic is a complete representation of your seed and may be used anywhere
+where you’d use an MMGen wallet.  You can generate addresses with it just as you
+do with a wallet:
 
 		$ mmgen-addrgen 89ABCDEF.mmwords 1-10
 		...
 		Address data written to file '89ABCDEF[1-10].addrs'
 
-The resulting address file will be identical to one generated by any wallet with
-Seed ID '89ABCDEF'.
+You can use it to sign transactions:
 
-The mnemonic can be used to regenerate a lost wallet:
+		$ mmgen-txsign FEDCBA[0.1].rawtx 89ABCDEF.mmwords
+		...
+		Signed transaction written to file 'FEDCBA[0.1].sigtx'
+
+The mnemonic can also be used to regenerate a lost wallet:
 
 		$ mmgen-walletconv 89ABCDEF.mmwords
 		...
@@ -352,32 +421,33 @@ The mnemonic can be used to regenerate a lost wallet:
 Note that the regenerated wallet has a different Key ID but of course the same
 Seed ID.
 
-Seed files bear the extension '.mmseed' and are generated and used exactly
-the same way as mnemonic files:
+An alternative to mnemonics, seed files provide yet another way of representing
+your seed.  They bear the extension '.mmseed' and are generated exactly the same
+way as mnemonic files:
 
 		$ mmgen-walletconv -o seed
 		...
 		Seed data written to file '89ABCDEF.mmseed'
 
-And they can also be used to regenerate a wallet:
+They can be used just like mnemonics to regenerate a wallet:
 
 		$ mmgen-walletconv 89ABCDEF.mmseed
 		...
 		MMGen wallet written to file '89ABCDEF-23456701[256,3].mmdat'
 
-Here’s a sample seed file for a 256-bit wallet:
+Here’s a sample seed file for a 256-bit seed:
 
 		$ cat 8B7392ED.mmseed
 		f4c84b C5ZT wWpT Jsoi wRVw 2dm9 Aftd WLb8 FggQ eC8h Szjd da9L
 
-And for a 128-bit wallet:
+And for a 128-bit seed:
 
 		$ cat 8E0DFB78.mmseed
 		0fe02f XnyC NfPH piuW dQ2d nM47 VU
 
 As you can see, seed files are short enough to be easily written out by hand or
-even memorized.  And their built-in checksum makes it easy to test your memory
-using a simple Unix shell command:
+memorized.  And their built-in checksum makes it easy to test your memory using
+a simple Unix shell command:
 
 		$ echo -n XnyC NfPH piuW dQ2d nM47 VU | tr -d ' '| sha256sum | cut -c 1-6
 		0fe02f
@@ -387,10 +457,9 @@ Or you can do the same thing with 'mmgen-tool':
 		$ mmgen-tool str2id6 'XnyC NfPH piuW dQ2d nM47 VU'
 		0fe02f
 
-Beginning with version 0.9.0, export to and generation from hexadecimal
-(hexseed) format is also supported.  Hexseed files are identical to seed files
-but encoded in hexadecimal rather than base 58.  They bear the extension
-'.mmhex':
+Beginning with version 0.9.0, MMGen also supports seed files in hexadecimal
+(hexseed) format.  Hexseed files are identical to seed files but encoded in
+hexadecimal rather than base 58.  They bear the extension '.mmhex':
 
 		$ cat FE3C6545.mmhex
 		afc3fe 456d 7f5f 1c4b fe3b c916 b875 60ae 6a3e
@@ -401,19 +470,22 @@ standard command-line tools:
 		$ echo 456d 7f5f 1c4b fe3b c916 b875 60ae 6a3e | tr -d ' ' | xxd -r -p | sha256sum -b | xxd -r -p | sha256sum -b | cut -c 1-8
 		fe3c6545
 
-A hexseed can be used to easily generate keys even without the MMGen software,
-as explained in [this tutorial][03].
+A hexseed can be used to generate keys even without the MMGen software,
+using basic command-line utilities, as explained in [this tutorial][03].
 
 #### <a name='a_ai'>Mnemonics, seeds and hexseeds: additional information</a>
 
-MMGen commands that take mnemonic, seed or hexseed data may receive the data
+All MMGen commands that take mnemonic, seed or hexseed data may receive the data
 from a prompt instead of a file.  Just omit the file name and specify the input
 format:
 
-		$ mmgen-walletconv -i words
+		$ mmgen-addrgen -i words 1-10
 		...
 		Enter mnemonic data: <type or paste your mnemonic here>
 
+This means that you may keep your seed entirely in your head, as either a
+mnemonic, seed or hexseed, and never record it anywhere on any medium.
+
 With the `-S` option, MMGen commands may be requested to print wallet data to
 screen instead of a file.  To safeguard against over-the-shoulder, Van Eck
 phreaking and other side-channel attacks, you’ll be prompted before this
@@ -549,6 +621,192 @@ Transaction signing uses the same syntax:
 		...
 		Signed transaction written to file 'ABCDEF[0.1].sigtx'
 
+### <a name='a_at'>Advanced Topics</a>
+
+#### <a name='a_hw'>Hot wallets and key-address files</a>
+
+Chances are you'll want to use MMGen not only for cold storage but for
+day-to-day transactions too.  For this you'll need to place a portion of your
+funds in a “hot wallet” on your online computer.  With hot wallet funds you
+can use the command `mmgen-txdo` to quickly create, sign and send transactions
+in one operation.
+
+There are two hot wallet strategies you can use.  The first is to generate a
+separate MMGen wallet on your online computer for use as the hot wallet.  The
+advantage of this is convenience: you won't have to specify a wallet or seed
+source on the command line.  In addition, your hot wallet and cold wallet funds
+will be easily distinguishable in your tracking wallet by their different Seed
+IDs.  The drawback of this strategy is that you now have two seeds that need
+backing up or memorizing.
+
+The other strategy, which avoids this drawback, is to partition your cold wallet
+by mentally setting aside “hot” and “cold” address ranges.  For example, you
+might choose to reserve all addresses in the range 1-1000 for cold storage and
+everything above that for your hot wallet.
+
+The next step is to create a key-address file for a sufficient number of “hot”
+addresses to cover your day-to-day transaction needs for the foreseeable future.
+A key-address file is just like an address file except that it contains keys as
+well as addresses, thus functioning as a hot wallet for a range of addresses.
+Assuming your hot address range begins at 1001, you could start by creating a
+key-address file for a hundred hot addresses like this:
+
+		$ mmgen-keygen 1001-1100
+		...
+		Secret keys written to file '89ABCDEF[1001-1100].akeys.mmenc'
+
+`mmgen-keygen` prompts you for a password to encrypt the key-address file with.
+This is a wise precaution, as it provides at least some security for keys that
+will be stored on an online machine.
+
+Now copy the key-address file to your online machine and import the addresses
+into your tracking wallet:
+
+		$ mmgen-addrimport --batch --keyaddr-file '89ABCDEF[1001-1100].akeys.mmenc'
+
+After funding your hot wallet by spending into some addresses in this range you
+can do quickie transactions with these funds using the `mmgen-txdo` command:
+
+		$ mmgen-txdo -M '89ABCDEF[1001-1100].akeys.mmenc' 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc,0.1 89ABCDEF:1010
+		...
+		Transaction sent: dcea1357....
+
+The `--mmgen-keys-from-file` or `-M` option is required when using a key-address
+file in place of a default wallet.  Note that your change address 89ABCDEF:1010
+is within the range covered by the key-address file, so your change funds will
+remain “hot spendable”.
+
+Using `mmgen-txdo` with a default online hot wallet is even simpler.  For a hot
+wallet with Seed ID 0FDE89AB, for instance, creating and sending a transaction
+looks like this:
+
+		$ mmgen-txdo 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc,0.1 0FDE89AB:10
+
+
+#### <a name='a_fee'>Transaction Fees</a>
+
+MMGen gives you several options for dealing with transaction fees.
+
+Firstly, and most simply, you may do nothing, in which case MMGen will calculate
+the fee automatically using bitcoind’s 'estimatefee' RPC call.  You can adjust
+the estimated fee by any factor using the `--tx-fee-adj` option, a handy feature
+when you need transactions to confirm a bit more quickly.  MMGen has no default
+fee, so if network fee estimation fails for any reason, you'll be prompted to
+enter the fee manually.
+
+Secondly, you may specify the fee as an absolute BTC amount (a decimal number).
+This can be done either on the command line or at the interactive prompt when
+creating transactions with `mmgen-txcreate`, `mmgen-txdo` or `mmgen-txbump`.
+
+Thirdly, instead of using an absolute BTC amount, you may specify the fee in
+satoshis per byte and let MMGen calculate the fee based on the transaction size.
+This also works both on the command line and at the interactive prompt.  The
+satoshis-per-byte specification is an integer followed by the letter 's'.  A fee
+of 90 satoshis per byte is thus represented as '90s'.
+
+MMGen has a hard maximum fee (currently 0.01 BTC) which is alterable only in the
+config file.  Thus MMGen will never create or broadcast any transaction with a
+mistakenly or dangerously high fee unless you expressly permit it to.
+
+#### <a name='a_rbf'>BIP 125 replace-by-fee (RBF) transactions</a>
+
+As of version 0.9.1, MMGen supports creating replaceable and replacement
+transactions in accordance with the BIP 125 replace-by-fee (RBF) specification.
+
+To make your transactions replaceable, just specify the `--rbf` option when
+creating them with `mmgen-txcreate` or `mmgen-txdo`.
+
+Version 0.9.1 also introduces `mmgen-txbump`, a convenient command for quickly
+creating replacement transactions from existing replaceable ones.
+`mmgen-txbump` can create, sign and send transactions in a single operation if
+desired.
+
+Continuing the examples from our primer above, we'll examine two RBF scenarios,
+one for a hot wallet and one for a cold storage wallet.  In the first scenario,
+initial and replacement transactions will be created, signed and sent in one
+operation.  In the second, a batch of replacement transactions with
+incrementally increasing fees will created online and then signed offline.
+
+#### <a name='a_rbf_onl'>With an online (hot) wallet</a>
+
+Create, sign and send a BIP 125 replaceable transaction with a fee of 50
+satoshis per byte:
+
+		$ mmgen-txdo --rbf --tx-fee 50s 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc,0.1 0FDE89AB:5
+		...
+		Signed transaction written to file 'FEDCBB[0.1,50].sigtx'
+		...
+		Transaction sent: dcba4321....
+
+Here you've sent 0.1 BTC to a third-party address and the change back to
+yourself at address #5 of your default hot wallet with Seed ID 0FDE89AB.
+
+Note that the fee is shown in the filename after the send amount.  The presence
+of the fee in the filename identifies the transaction as replaceable.
+
+If the transaction fails to confirm in your desired timeframe, then create, sign
+and send a replacement transaction with a higher fee, say 100 satoshis per byte:
+
+		$ mmgen-txbump --send --tx-fee 100s --output-to-reduce c 'FEDCBB[0.1,50].sigtx'
+		...
+		Signed transaction written to file 'DAE123[0.1,100].sigtx'
+		...
+		Transaction sent: eef01357....
+
+The `--send` switch instructs `mmgen-txbump` to sign and send the transaction
+after creating it.  The `--output-to-reduce` switch with an argument of 'c'
+requests that the increased fee be deducted from the change ('c') output, which
+is usually what is desired.  If you want it taken from some other output,
+identify the output by number.  Note that the resulting replacement transaction
+has a different identifier, since it's a new transaction.
+
+If this transaction also fails to confirm, then repeat the above step as many
+times as necessary to get a confirmation, increasing the fee each time.  The
+only thing you have to modify with each iteration is the argument to `--tx-fee`.
+To reduce your typing even further, use the `--yes` switch to skip all
+non-essential prompts.
+
+Note that if you're using a key-address file instead of a default hot wallet,
+you'll need to supply it on the command line as a parameter to the `-M` option.
+
+#### <a name='a_rbf_onf'>With an offline (cold storage) wallet</a>
+
+To achieve the same result as in the above example using a cold wallet, just
+create the initial transaction with `mmgen-txcreate` instead of `mmgen-txdo`:
+
+		$ mmgen-txcreate --rbf --tx-fee 50s 1AmkUxrfy5dMrfmeYwTxLxfIswUCcpeysc,0.1 89ABCDEF:5
+		...
+		Transaction written to file 'FEDCBC[0.1,50].rawtx'
+
+Now create a series of transactions with incrementally increasing fees for
+offline signing:
+
+		$ mmgen-txbump --tx-fee 100s --output-to-reduce c 'FEDCBC[0.1,50].rawtx'
+		$ mmgen-txbump --tx-fee 150s --output-to-reduce c 'FEDCBC[0.1,50].rawtx'
+		$ mmgen-txbump --tx-fee 200s --output-to-reduce c 'FEDCBC[0.1,50].rawtx'
+
+To speed things up, add the `--yes` switch to make `mmgen-txbump` completely
+non-interactive.
+
+The result will be four raw transaction files with increasing fees, like this:
+
+		FEDCBC[0.1,50].rawtx
+		3EBB00[0.1,100].rawtx
+		124FFF[0.1,150].rawtx
+		73DABB[0.1,200].rawtx
+
+Copy the files to an empty folder, transfer the folder to your offline machine and batch sign them:
+
+		$ mmgen-txsign -d my_folder --yes my_folder/*.rawtx
+
+Then copy the signed transaction files back to your online machine and broadcast
+them in turn until you get a confirmation:
+
+		$ mmgen-txsend FEDCBC[0.1,50].sigtx   # ...if this doesn't confirm, then
+		$ mmgen-txsend 3EBB00[0.1,100].sigtx  # ...if this doesn't confirm, then
+		$ mmgen-txsend 124FFF[0.1,150].sigtx  # ...if this doesn't confirm, then
+		$ mmgen-txsend 73DABB[0.1,200].sigtx
+
 [01]: https://github.com/mmgen/mmgen/wiki/Tracking-and-spending-ordinary-Bitcoin-addresses
 [02]: https://tpfaucet.appspot.com
 [03]: Recovering-Keys-Without-MMGen

+ 20 - 26
mmgen/addr.py

@@ -20,7 +20,8 @@
 addr.py:  Address generation/display routines for the MMGen suite
 """
 
-from hashlib import sha256, sha512
+from hashlib import sha256,sha512
+from binascii import hexlify,unhexlify
 from mmgen.common import *
 from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex
 from mmgen.obj import *
@@ -83,7 +84,6 @@ def _privhex2addr_keyconv(privhex,compressed=False):
 def _privhex2addr_secp256k1(privhex,compressed=False):
 	from mmgen.secp256k1 import priv2pub
 	from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr
-	from binascii import hexlify,unhexlify
 	pubkey = priv2pub(unhexlify(privhex),int(compressed))
 	return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey)))
 
@@ -275,10 +275,10 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 		self.fmt_data = mmgen_encrypt(self.fmt_data.encode('utf8'),desc,'')
 		self.ext += '.'+g.mmenc_ext
 
-	def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False):
+	def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
 		fn = u'{}.{}'.format(self.id_str,self.ext)
 		ask_tty = self.has_keys and not opt.quiet
-		write_data_to_file(fn,self.fmt_data,self.file_desc,ask_tty=ask_tty,binary=binary)
+		write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
 
 	def idxs(self):
 		return [e.idx for e in self.data]
@@ -543,35 +543,27 @@ Record this checksum: it will be used to verify the password file in the future
 	pw_len      = None
 	pw_fmt      = None
 	pw_info     = {
-		'base58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
-		'base32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }
+		'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
+		'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }
 		}
 	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 
-	def __init__(self,
-				seed=None,
-				addr_idxs=None,
-				pw_id_str=None,
-				pw_len=None,
-				infile=None,
-				chksum_only=False,
-				pw_fmt=None,
-				chk_params_only=False
-				):
+	def __init__(self,infile=None,seed=None,pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
+				chksum_only=False,chk_params_only=False):
 
 		self.update_msgs()
 
 		if infile:
 			(self.seed_id,self.data) = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
 		else:
-			for k in seed,addr_idxs: assert chk_params_only or k
+			for k in seed,pw_idxs: assert chk_params_only or k
 			for k in pw_id_str,pw_fmt: assert k
 			self.pw_id_str = MMGenPWIDString(pw_id_str)
 			self.set_pw_fmt(pw_fmt)
 			self.set_pw_len(pw_len)
 			if chk_params_only: return
 			self.seed_id = seed.sid
-			self.data = self.generate(seed,addr_idxs)
+			self.data = self.generate(seed,pw_idxs)
 
 		self.num_addrs = len(self.data)
 		self.fmt_data = ''
@@ -618,13 +610,11 @@ Record this checksum: it will be used to verify the password file in the future
 
 	def make_passwd(self,hex_sec):
 		assert self.pw_fmt in self.pw_info
-		from mmgen.bitcoin import b58a
-		alpha,base = ((b58a,58),(b32a,32))[self.pw_fmt=='base32']
 		# we take least significant part
-		return ''.join(baseconv.fromhex(base,hex_sec,alpha,pad=self.pw_len))[-self.pw_len:]
+		return ''.join(baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len))[-self.pw_len:]
 
 	def chk_addr_or_pw(self,pw):
-		if not (is_b58_str,is_b32_str)[self.pw_fmt=='base32'](pw):
+		if not (is_b58_str,is_b32_str)[self.pw_fmt=='b32'](pw):
 			msg('Password is not a valid {} string'.format(self.pw_fmt))
 			return False
 		if len(pw) != self.pw_len:
@@ -634,10 +624,14 @@ Record this checksum: it will be used to verify the password file in the future
 
 	def cook_seed(self,seed):
 		from mmgen.crypto import sha256_rounds
-		# Changing either pw_fmt or pw_len will cause a different, unrelated set of passwords to
-		# be generated: this is what we want
-		cseed = '{}{}:{}:{}'.format(seed,self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
-		dmsg('Cooked seed: {}\nSeed len: {}'.format(repr(cseed),len(cseed)))
+		# Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of
+		# passwords to be generated: this is what we want
+		fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
+		dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8')))
+		# Original implementation was 'cseed = seed + fid_str'; hmac was not used
+		import hmac
+		cseed = hmac.new(seed,fid_str,sha256).digest()
+		dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
 		return sha256_rounds(cseed,self.cook_hash_rounds)
 
 

+ 1 - 1
mmgen/globalvars.py

@@ -50,6 +50,7 @@ class g(object):
 	user_entropy   = ''
 	hash_preset    = '3'
 	usr_randchars  = 30
+	stdin_tty      = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))
 
 	max_tx_fee   = BTCAmt('0.01')
 	tx_fee_adj   = 1.0
@@ -144,7 +145,6 @@ class g(object):
 	min_urandchars = 10
 
 	seed_lens = 128,192,256
-	mn_lens = [i / 32 * 3 for i in seed_lens]
 
 	mmenc_ext      = 'mmenc'
 	salt_len       = 16

+ 1 - 1
mmgen/main_addrgen.py

@@ -125,6 +125,6 @@ if al.gen_addrs and opt.print_checksum:
 
 if al.gen_keys and keypress_confirm('Encrypt key list?'):
 	al.encrypt()
-	al.write_to_file(binary=True)
+	al.write_to_file(binary=True,desc='encrypted '+al.file_desc)
 else:
 	al.write_to_file()

+ 21 - 17
mmgen/main_passgen.py

@@ -27,6 +27,11 @@ from mmgen.addr import PasswordList,AddrIdxList
 from mmgen.seed import SeedSource
 from mmgen.obj import MMGenPWIDString
 
+dfl_len = {
+	'b58': PasswordList.pw_info['b58']['dfl_len'],
+	'b32': PasswordList.pw_info['b32']['dfl_len']
+}
+
 opts_data = {
 	'sets': [('print_checksum',True,'quiet',True)],
 	'desc': """Generate a range or list of passwords from an {pnm} wallet,
@@ -43,7 +48,9 @@ opts_data = {
                       'f' at offset 'o' (comma-separated)
 -O, --old-incog-fmt   Specify old-format incognito input
 -L, --passwd-len=  l  Specify length of generated passwords
-                      (default: {p} chars [base58], {q} chars [base32])
+                      (default: {d58} chars [base58], {d32} chars [base32]).
+                      An argument of 'h' will generate passwords of half
+                      the default length.
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
                       with non-standard (< {g.seed_len}-bit) seed lengths
@@ -58,11 +65,8 @@ opts_data = {
 -v, --verbose         Produce more verbose output
 """.format(
 	seed_lens=', '.join([str(i) for i in g.seed_lens]),
-	pnm=g.proj_name,
-	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
-	g=g,
-	p=PasswordList.pw_info['base58']['dfl_len'],
-	q=PasswordList.pw_info['base32']['dfl_len']
+	g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'],
+	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
 ),
 	'notes': """
 
@@ -79,13 +83,13 @@ Changing either the password format (base32,base58) or length alters the seed
 and thus generates a completely new set of passwords.
 
 EXAMPLE:
-  Generate ten base58 passwords of length {dfl58} for Alice's email account:
+  Generate ten base58 passwords of length {d58} for Alice's email account:
   {g.prog_name} alice@nowhere.com 1-10
 
   Generate ten base58 passwords of length 16 for Alice's email account:
   {g.prog_name} -L16 alice@nowhere.com 1-10
 
-  Generate ten base32 passwords of length {dfl32} for Alice's email account:
+  Generate ten base32 passwords of length {d32} for Alice's email account:
   {g.prog_name} -b alice@nowhere.com 1-10
 
   The three sets of passwords are completely unrelated to each other, so
@@ -102,10 +106,8 @@ FMT CODES:
   {f}
 """.format(
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-		o=opts,g=g,
+		o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
 		ml=MMGenPWIDString.max_len,
-		dfl58=PasswordList.pw_info['base58']['dfl_len'],
-		dfl32=PasswordList.pw_info['base32']['dfl_len'],
 		fs="', '".join(MMGenPWIDString.forbidden)
 	)
 }
@@ -114,25 +116,27 @@ cmd_args = opts.init(opts_data,add_opts=['b16'])
 
 if len(cmd_args) < 2: opts.usage()
 
-idxs = AddrIdxList(fmt_str=cmd_args.pop())
+pw_idxs = AddrIdxList(fmt_str=cmd_args.pop())
 
 pw_id_str = cmd_args.pop()
 
 sf = get_seed_file(cmd_args,1)
 
-pw_fmt = ('base58','base32')[bool(opt.base32)]
+pw_fmt = ('b58','b32')[bool(opt.base32)]
+
+pw_len = (opt.passwd_len,dfl_len[pw_fmt]/2)[opt.passwd_len in ('h','H')]
 
-PasswordList(pw_id_str=pw_id_str,pw_len=opt.passwd_len,pw_fmt=pw_fmt,chk_params_only=True)
+PasswordList(pw_id_str=pw_id_str,pw_len=pw_len,pw_fmt=pw_fmt,chk_params_only=True)
 do_license_msg()
 
 ss = SeedSource(sf)
 
-al = PasswordList(seed=ss.seed,addr_idxs=idxs,pw_id_str=pw_id_str,pw_len=opt.passwd_len,pw_fmt=pw_fmt)
+al = PasswordList(seed=ss.seed,pw_idxs=pw_idxs,pw_id_str=pw_id_str,pw_len=pw_len,pw_fmt=pw_fmt)
 
 al.format()
 
 if keypress_confirm('Encrypt password list?'):
 	al.encrypt(desc='password list')
-	al.write_to_file(binary=True)
+	al.write_to_file(binary=True,desc='encrypted password list')
 else:
-	al.write_to_file()
+	al.write_to_file(desc='password list')

+ 77 - 60
mmgen/seed.py

@@ -117,7 +117,7 @@ class SeedSource(MMGenObject):
 		if hasattr(self,'seed'):
 			self._encrypt()
 			return
-		elif hasattr(self,'infile'):
+		elif hasattr(self,'infile') or not g.stdin_tty:
 			self._deformat_once()
 			self._decrypt_retry()
 		else:
@@ -135,7 +135,10 @@ class SeedSource(MMGenObject):
 			self.fmt_data = get_data_from_file(self.infile.name,self.desc,
 								binary=self.file_mode=='binary')
 		else:
-			self.fmt_data = get_data_from_user(self.desc)
+			self.fmt_data = self._get_data_from_user(self.desc)
+
+	def _get_data_from_user(self,desc):
+		return get_data_from_user(desc)
 
 	def _deformat_once(self):
 		self._get_data()
@@ -355,67 +358,80 @@ class Mnemonic (SeedSourceUnenc):
 	fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m'
 	desc = 'mnemonic data'
 	ext = 'mmwords'
-	wl_checksums = {
-		'electrum': '5ca31424',
-		'tirosh':   '1a5faeff'
-	}
-	mn_base = 1626
-	wordlists = sorted(wl_checksums)
-	dfl_wordlist = 'electrum'
-	# dfl_wordlist = 'tirosh'
+	mn_lens = [i / 32 * 3 for i in g.seed_lens]
+	wl_id = 'electrum' # or 'tirosh'
 
-	@staticmethod
-	def _mn2hex_pad(mn): return len(mn) * 8 / 3
+	def _get_data_from_user(self,desc):
 
-	@staticmethod
-	def _hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
+		if not g.stdin_tty:
+			return get_data_from_user(desc)
 
-	@classmethod
-	def hex2mn(cls,hexnum,wordlist):
-		wl = cls.get_wordlist(wordlist)
-		return baseconv.fromhex(cls.mn_base,hexnum,wl,cls._hex2mn_pad(hexnum))
+		from mmgen.term import get_char_raw,get_char
 
-	@classmethod
-	def mn2hex(cls,mn,wordlist):
-		wl = cls.get_wordlist(wordlist)
-		return baseconv.tohex(cls.mn_base,mn,wl,cls._mn2hex_pad(mn))
+		def choose_mn_len():
+			prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
+			urange = [str(i+1) for i in range(len(self.mn_lens))]
+			while True:
+				r = get_char('\r'+prompt)
+				if r in urange: break
+			msg_r('\r' + ' '*len(prompt) + '\r')
+			return self.mn_lens[int(r)-1]
 
-	@classmethod
-	def get_wordlist(cls,wordlist=None):
-		wordlist = wordlist or cls.dfl_wordlist
-		if wordlist not in cls.wordlists:
-			die(1,"'%s': invalid wordlist.  Valid choices: '%s'" %
-				(wordlist,"' '".join(cls.wordlists)))
-
-		return __import__('mmgen.mn_'+wordlist,fromlist=['words']).words.split()
+		while True:
+			mn_len = choose_mn_len()
+			prompt = 'Mnemonic length of {} words chosen. OK?'.format(mn_len)
+			if keypress_confirm(prompt,default_yes=True,no_nl=True): break
+
+		m = 'Enter your {}-word mnemonic, hitting RETURN or SPACE after each word:'
+		msg(m.format(mn_len))
+
+		def get_word():
+			s = ''
+			while True:
+				ch = get_char_raw('')
+				if ch in '\b\x7f':
+					if s: s = s[:-1]
+				elif ch in '\n ':
+					if s: break
+				else: s += ch
+			return s
+
+		wl = baseconv.digits[self.wl_id]
+
+		def in_list(w):
+			from bisect import bisect_left
+			idx = bisect_left(wl,w)
+			return(True,False)[idx == len(wl) or w != wl[idx]]
+
+		words,i,p = [],0,('Enter word #{}: ','Incorrect entry. Repeat word #{}: ')
+		while len(words) < mn_len:
+			msg_r('{r}{s}{r}'.format(r='\r',s=' '*40))
+			if i == 1: time.sleep(0.1)
+			msg_r(p[i].format(len(words)+1))
+			s = get_word()
+			if in_list(s):
+				words.append(s); i = 0
+			else:
+				i = 1
+		msg('')
+		qmsg('Mnemonic successfully entered')
+		return ' '.join(words)
 
-	@classmethod
-	def check_wordlist(cls,wlname):
+	@staticmethod
+	def _mn2hex_pad(mn): return len(mn) * 8 / 3
 
-		wl = cls.get_wordlist(wlname)
-		Msg('Wordlist: %s\nLength: %i words' % (capfirst(wlname),len(wl)))
-		new_chksum = sha256(' '.join(wl)).hexdigest()[:8]
+	@staticmethod
+	def _hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
 
-		if (sorted(wl) == wl):
-			Msg('List is sorted')
-		else:
-			die(3,'ERROR: List is not sorted!')
+	def _format(self):
 
-		compare_chksums(
-			new_chksum,'generated checksum',
-			cls.wl_checksums[wlname],'saved checksum',
-			die_on_fail=True)
-		Msg('Checksum %s matches' % new_chksum)
+		hexseed = self.seed.hexdata
 
-	def _format(self):
-		wl = self.get_wordlist()
-		seed_hex = self.seed.hexdata
-		mn = baseconv.fromhex(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
+		mn  = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
+		ret = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
 
-		ret = baseconv.tohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
 		# Internal error, so just die on fail
-		compare_or_die(ret,'recomputed seed',
-						seed_hex,'original',e='Internal error')
+		compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error')
 
 		self.ssdata.mnemonic = mn
 		self.fmt_data = ' '.join(mn) + '\n'
@@ -423,27 +439,28 @@ class Mnemonic (SeedSourceUnenc):
 	def _deformat(self):
 
 		mn = self.fmt_data.split()
-		wl = self.get_wordlist()
 
-		if len(mn) not in g.mn_lens:
+		if len(mn) not in self.mn_lens:
 			msg('Invalid mnemonic (%i words).  Allowed numbers of words: %s' %
-					(len(mn),', '.join([str(i) for i in g.mn_lens])))
+					(len(mn),', '.join([str(i) for i in self.mn_lens])))
 			return False
 
 		for n,w in enumerate(mn,1):
-			if w not in wl:
+			if w not in baseconv.digits[self.wl_id]:
 				msg('Invalid mnemonic: word #%s is not in the wordlist' % n)
 				return False
 
-		seed_hex = baseconv.tohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
+		hexseed = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
+		ret     = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
 
-		ret = baseconv.fromhex(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
+		if len(hexseed) * 4 not in g.seed_lens:
+			msg('Invalid mnemonic (produces too large a number)')
+			return False
 
 		# Internal error, so just die
-		compare_or_die(' '.join(ret),'recomputed mnemonic',
-						' '.join(mn),'original',e='Internal error')
+		compare_or_die(' '.join(ret),'recomputed mnemonic',' '.join(mn),'original',e='Internal error')
 
-		self.seed = Seed(unhexlify(seed_hex))
+		self.seed = Seed(unhexlify(hexseed))
 		self.ssdata.mnemonic = mn
 
 		check_usr_seed_len(self.seed.length)

+ 3 - 1
mmgen/term.py

@@ -196,14 +196,16 @@ def _get_terminal_size_mswin():
 		return 80,25
 
 def set_terminal_vars():
-	global get_char,kb_hold_protect,get_terminal_size
+	global get_char,get_char_raw,kb_hold_protect,get_terminal_size
 	if _platform == 'linux':
+		get_char_raw = _get_keypress_unix_raw
 		get_char = (_get_keypress_unix_raw,_get_keypress_unix)[g.hold_protect]
 		kb_hold_protect = (_kb_hold_protect_unix_raw,_kb_hold_protect_unix)[g.hold_protect]
 		if not sys.stdin.isatty():
 			get_char,kb_hold_protect = _get_keypress_unix_stub,_kb_hold_protect_unix_raw
 		get_terminal_size = _get_terminal_size_linux
 	else:
+		get_char_raw = _get_keypress_mswin_raw
 		get_char = (_get_keypress_mswin_raw,_get_keypress_mswin)[g.hold_protect]
 		kb_hold_protect = (_kb_hold_protect_mswin_raw,_kb_hold_protect_mswin)[g.hold_protect]
 		if not sys.stdin.isatty():

+ 22 - 36
mmgen/tool.py

@@ -300,52 +300,38 @@ def wif2addr(wif,compressed=False):
 	Vmsg_r('Addr: '); Msg(addr)
 
 wordlists = 'electrum','tirosh'
-dfl_wordlist = 'electrum'
+dfl_wl_id = 'electrum'
 
-from mmgen.seed import Mnemonic
 def do_random_mn(nbytes,wordlist):
 	hexrand = ba.hexlify(get_random(nbytes))
 	Vmsg('Seed: %s' % hexrand)
-	for wlname in ([wordlist],wordlists)[wordlist=='all']:
+	for wl_id in ([wordlist],wordlists)[wordlist=='all']:
 		if wordlist == 'all':
-			Msg('%s mnemonic:' % (capfirst(wlname)))
-		mn = Mnemonic.hex2mn(hexrand,wordlist=wlname)
+			Msg('%s mnemonic:' % (capfirst(wl_id)))
+		mn = baseconv.fromhex(hexrand,wl_id)
 		Msg(' '.join(mn))
 
-def mn_rand128(wordlist=dfl_wordlist): do_random_mn(16,wordlist)
-def mn_rand192(wordlist=dfl_wordlist): do_random_mn(24,wordlist)
-def mn_rand256(wordlist=dfl_wordlist): do_random_mn(32,wordlist)
-
-def hex2mn(s,wordlist=dfl_wordlist):
-	Msg(' '.join(Mnemonic.hex2mn(s,wordlist)))
-
-def mn2hex(s,wordlist=dfl_wordlist):
-	Msg(Mnemonic.mn2hex(s.split(),wordlist))
-
-def strtob58(s,pad=None):
-	Msg(''.join(baseconv.fromhex(58,ba.hexlify(s),mmb.b58a,pad)))
-
-def b58tostr(s):
-	Msg(ba.unhexlify(baseconv.tohex(58,s,mmb.b58a)))
+def mn_rand128(wordlist=dfl_wl_id): do_random_mn(16,wordlist)
+def mn_rand192(wordlist=dfl_wl_id): do_random_mn(24,wordlist)
+def mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist)
 
-def b58tohex(s,pad=None):
-	Msg(baseconv.tohex(58,s,mmb.b58a,pad))
+def hex2mn(s,wordlist=dfl_wl_id): Msg(' '.join(baseconv.fromhex(s,wordlist)))
+def mn2hex(s,wordlist=dfl_wl_id): Msg(baseconv.tohex(s.split(),wordlist))
 
-def hextob58(s,pad=None):
-	Msg(''.join(baseconv.fromhex(58,s,mmb.b58a,pad)))
+def strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(ba.hexlify(s),'b58',pad)))
+def hextob58(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b58',pad)))
+def hextob32(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b32',pad)))
+def b58tostr(s):          Msg(ba.unhexlify(baseconv.tohex(s,'b58')))
+def b58tohex(s,pad=None): Msg(baseconv.tohex(s,'b58',pad))
+def b32tohex(s,pad=None): Msg(baseconv.tohex(s.upper(),'b32',pad))
 
-def b32tohex(s,pad=None):
-	Msg(baseconv.tohex(32,s.upper(),b32a,pad))
-
-def hextob32(s,pad=None):
-	Msg(''.join(baseconv.fromhex(32,s,b32a,pad)))
-
-def mn_stats(wordlist=dfl_wordlist):
-	Mnemonic.check_wordlist(wordlist)
-
-def mn_printlist(wordlist=dfl_wordlist):
-	wl = Mnemonic.get_wordlist(wordlist)
-	Msg('\n'.join(wl))
+from mmgen.seed import Mnemonic
+def mn_stats(wordlist=dfl_wl_id):
+	wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
+	baseconv.check_wordlist(wordlist)
+def mn_printlist(wordlist=dfl_wl_id):
+	wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
+	Msg('\n'.join(baseconv.digits[wordlist]))
 
 def id8(infile):
 	Msg(make_chksum_8(

+ 72 - 26
mmgen/util.py

@@ -213,14 +213,11 @@ def is_int(s):
 
 # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
 # https://tools.ietf.org/html/rfc4648
-b32a = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
-def is_b32_str(s):    return set(list(s))         <= set(list(b32a))
 def is_hex_str(s):    return set(list(s.lower())) <= set(list(hexdigits.lower()))
 def is_hex_str_lc(s): return set(list(s))         <= set(list(hexdigits.lower()))
 def is_hex_str_uc(s): return set(list(s))         <= set(list(hexdigits.upper()))
-def is_b58_str(s):
-	from mmgen.bitcoin import b58a
-	return set(list(s)) <= set(b58a)
+def is_b58_str(s):    return set(list(s))         <= set(baseconv.digits['b58'])
+def is_b32_str(s):    return set(list(s))         <= set(baseconv.digits['b32'])
 
 def is_ascii(s,enc='ascii'):
 	try:    s.decode(enc)
@@ -231,29 +228,76 @@ def is_utf8(s): return is_ascii(s,enc='utf8')
 
 class baseconv(object):
 
-	@staticmethod
-	def tohex(base,words,wl,pad=None): # accepts both string and list input
-		if type(words) not in (list,tuple):
-			words = tuple(words.strip())
-		if not set(words).issubset(set(wl)):
-			die(2,'{} is not in base-{} format'.format(repr(words_arg),base))
-		deconv =  [wl.index(words[::-1][i])*(base**i)
-					for i in range(len(words))]
+	mn_base = 1626 # tirosh list is 1633 words long!
+	digits = {
+		'electrum': tuple(__import__('mmgen.mn_electrum',fromlist=['words']).words.split()),
+		'tirosh': tuple(__import__('mmgen.mn_tirosh',fromlist=['words']).words.split()[:mn_base]),
+		'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'),
+		'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
+		'b16': tuple('0123456789abcdef'),
+		'b10': tuple('0123456789'),
+		'b8':  tuple('01234567'),
+	}
+	wl_chksums = {
+		'electrum': '5ca31424',
+		'tirosh':   '48f05e1f', # tirosh truncated to mn_base (1626)
+		# 'tirosh1633': '1a5faeff'
+	}
+
+	@classmethod
+	def get_wordlist_chksum(cls,wl_id):
+		return sha256(' '.join(cls.digits[wl_id])).hexdigest()[:8]
+
+	@classmethod
+	def check_wordlists(cls):
+		for k,v in cls.wl_chksums.items(): assert cls.get_wordlist_chksum(k) == v
+
+	@classmethod
+	def check_wordlist(cls,wl_id):
+
+		wl = baseconv.digits[wl_id]
+		Msg('Wordlist: %s\nLength: %i words' % (capfirst(wl_id),len(wl)))
+		new_chksum = cls.get_wordlist_chksum(wl_id)
+
+		a,b = 'generated checksum','saved checksum'
+		compare_chksums(new_chksum,a,cls.wl_chksums[wl_id],b,die_on_fail=True)
+
+		Msg('Checksum %s matches' % new_chksum)
+		Msg('List is sorted') if tuple(sorted(wl)) == wl else die(3,'ERROR: List is not sorted!')
+
+
+	@classmethod
+	def tohex(cls,words_arg,wl_id,pad=None):
+
+		words = words_arg if type(words_arg) in (list,tuple) else tuple(words_arg.strip())
+
+		wl = cls.digits[wl_id]
+		base = len(wl)
+
+		if not set(words) <= set(wl):
+			die(2,'{} is not in {} (base{}) format'.format(repr(words_arg),wl_id,base))
+
+		deconv =  [wl.index(words[::-1][i])*(base**i) for i in range(len(words))]
 		ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0))
 		return ('','0')[len(ret) % 2] + ret
 
-	@staticmethod
-	def fromhex(base,hexnum,wl,pad=None):
-		assert len(wl) == base
+	@classmethod
+	def fromhex(cls,hexnum,wl_id,pad=None):
+
 		hexnum = hexnum.strip()
 		if not is_hex_str(hexnum):
 			die(2,"'%s': not a hexadecimal number" % hexnum)
+
+		wl = cls.digits[wl_id]
+		base = len(wl)
 		num,ret = int(hexnum,16),[]
 		while num:
 			ret.append(num % base)
 			num /= base
 		return [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
 
+baseconv.check_wordlists()
+
 def match_ext(addr,ext):
 	return addr.split('.')[-1] == ext
 
@@ -464,7 +508,7 @@ def write_data_to_file(
 		if sys.stdout.isatty():
 			if no_tty:
 				die(2,'Printing %s to screen is not allowed' % desc)
-			if ask_tty and not opt.quiet:
+			if (ask_tty and not opt.quiet) or binary:
 				confirm_or_exit('','output %s to screen' % desc)
 		else:
 			try:    of = os.readlink('/proc/%d/fd/1' % os.getpid()) # Linux
@@ -567,7 +611,8 @@ def get_lines_from_file(fn,desc='',trim_comments=False):
 	return ret
 
 def get_data_from_user(desc='data',silent=False):
-	data = my_raw_input('Enter %s: ' % desc, echo=opt.echo_passphrase)
+	p = ('','Enter {}: '.format(desc))[g.stdin_tty]
+	data = my_raw_input(p,echo=opt.echo_passphrase)
 	dmsg('User input: [%s]' % data)
 	return data
 
@@ -618,20 +663,21 @@ def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
 
 	return reply.strip()
 
-def keypress_confirm(prompt,default_yes=False,verbose=False):
+def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False):
 
 	from mmgen.term import get_char
 
 	q = ('(y/N)','(Y/n)')[bool(default_yes)]
+	p = '{} {}: '.format(prompt,q)
+	nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl]
 
 	while True:
-		reply = get_char('%s %s: ' % (prompt, q)).strip('\n\r')
-
+		reply = get_char(p).strip('\n\r')
 		if not reply:
-			if default_yes: msg(''); return True
-			else:           msg(''); return False
-		elif reply in 'yY': msg(''); return True
-		elif reply in 'nN': msg(''); return False
+			if default_yes: msg_r(nl); return True
+			else:           msg_r(nl); return False
+		elif reply in 'yY': msg_r(nl); return True
+		elif reply in 'nN': msg_r(nl); return False
 		else:
 			if verbose: msg('\nInvalid reply')
 			else: msg_r('\r')
@@ -674,7 +720,7 @@ def do_pager(text):
 
 def do_license_msg(immed=False):
 
-	if opt.quiet or g.no_license or opt.yes: return
+	if opt.quiet or g.no_license or opt.yes or not g.stdin_tty: return
 
 	import mmgen.license as gpl
 

+ 17 - 0
test/ref/98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws

@@ -0,0 +1,17 @@
+# MMGen password file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by MMGen.
+# A text label of 32 characters or less may be added to the right of each
+# password.  The label may contain any printable ASCII symbol.
+#
+# Password data checksum for 98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100]: A983 DAB9 5514 27FB
+# Record this value to a secure location.
+98831F3A фубар@crypto.org b58:20 {
+  1     MZ5eTt6xjpK1h6YeFuzJ
+  4     E5gXkqxwpLPRzCVSHNfv
+  9     qaoShe24QgvDXA19Ue5r
+  10    kSMEeWLRH29Fthd6pDE7
+  11    5JXuV3oRdG2KQcejAYhE
+  1100  n3VQMkfGutW49yZDUny1
+}

+ 12 - 9
test/test.py

@@ -280,8 +280,8 @@ cfgs = {
 		'ref_bw_seed_id':  '33F10310',
 		'addrfile_chk':    ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
 		'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
-		'passfile_chk':    '3EA0 A3C9 DA28 5126',
-		'passfile32_chk':  'EF67 D0BE 4B24 9B4F',
+		'passfile_chk':    'EB29 DC4F 924B 289F',
+		'passfile32_chk':  '37B6 C218 2ABC 7508',
 		'wpasswd':         'reference password',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -309,8 +309,8 @@ cfgs = {
 		'ref_bw_seed_id':  'CE918388',
 		'addrfile_chk':    ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
 		'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
-		'passfile_chk':    '000C 7711 CD45 C5BE',
-		'passfile32_chk':  'AFEC 54A1 7D79 1866',
+		'passfile_chk':    'ADEA 0083 094D 489A',
+		'passfile32_chk':  '2A28 C5C7 36EC 217A',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -338,16 +338,16 @@ cfgs = {
 		'ref_bw_seed_id':  'B48CD7FC',
 		'addrfile_chk':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
 		'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
-		'passfile_chk':    '54B1 A5BE 9F07 1FDD',
-		'passfile32_chk':  '072A 4A13 FB64 B64B',
+		'passfile_chk':    '2D6D 8FBA 422E 1315',
+		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
 		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
 		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
-		'ref_passwdfile':  '98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100].pws',
+		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
 		'ref_addrfile_chksum':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
 		'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
-		'ref_passwdfile_chksum':  '7723 735B 2CBB 2571',
+		'ref_passwdfile_chksum':  'A983 DAB9 5514 27FB',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 		'ref_tx_file':     'FFB367[1.234]{}.rawtx'.format(tn_desc),
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
@@ -631,6 +631,9 @@ os.environ['MMGEN_NO_LICENSE'] = '1'
 os.environ['MMGEN_MIN_URANDCHARS'] = '3'
 os.environ['MMGEN_BOGUS_SEND'] = '1'
 
+# Tell spawned programs they're running in the test suite
+os.environ['MMGEN_TEST_SUITE'] = '1'
+
 if opt.debug_scripts: os.environ['MMGEN_DEBUG'] = '1'
 
 if opt.buf_keypress:
@@ -1713,7 +1716,7 @@ class MMGenTestSuite(object):
 		t.hash_preset('new key list','1')
 #		t.passphrase_new('new key list','kafile password')
 		t.passphrase_new('new key list',cfg['kapasswd'])
-		t.written_to_file('Secret keys',oo=True)
+		t.written_to_file('Encrypted secret keys',oo=True)
 		t.ok()
 
 	def refkeyaddrgen(self,name,wf,pf):