Browse Source

Merge #88: added optional feature to use mnemonic passphrases to create two factor recovery phrases

74c019b added optional feature to use mnemonic passphrases to create two factor recovery phrases (chris-belcher)
master
AdamISZ 8 years ago
parent
commit
ba913c3528
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 52
      jmclient/jmclient/wallet.py
  2. 112
      jmclient/jmclient/wallet_utils.py
  3. 61
      scripts/joinmarket-qt.py

52
jmclient/jmclient/wallet.py

@ -153,7 +153,7 @@ class Wallet(AbstractWallet):
self.unspent = {}
self.spent_utxos = []
self.imported_privkeys = {}
self.seed = self.entropy_to_seed(
self.seed = self.wallet_data_to_seed(
self.read_wallet_file_data(seedarg, pwd, wallet_dir=wallet_dir))
if not self.seed:
raise WalletError("Failed to decrypt wallet")
@ -189,7 +189,7 @@ class Wallet(AbstractWallet):
def get_root_path(self):
return "m/0"
def entropy_to_seed(self, entropy):
def wallet_data_to_seed(self, entropy):
"""for base/legacy wallet type, this is a passthrough.
for bip39 style wallets, this will convert from one to the other
"""
@ -255,19 +255,41 @@ class Wallet(AbstractWallet):
self.index_cache += [[0,0]] * (
self.max_mix_depth - len(self.index_cache))
password_key = btc.bin_dbl_sha256(pwd)
encrypted_seed = walletdata['encrypted_seed']
if 'encrypted_seed' in walletdata: #accept old field name
encrypted_entropy = walletdata['encrypted_seed']
elif 'encrypted_entropy' in walletdata:
encrypted_entropy = walletdata['encrypted_entropy']
try:
decrypted_seed = decryptData(
decrypted_entropy = decryptData(
password_key,
encrypted_seed.decode('hex')).encode('hex')
encrypted_entropy.decode('hex')).encode('hex')
# there is a small probability of getting a valid PKCS7
# padding by chance from a wrong password; sanity check the
# seed length
if len(decrypted_seed) != 32:
if len(decrypted_entropy) != 32:
raise ValueError
except ValueError:
log.info('Incorrect password')
return None
if 'encrypted_mnemonic_extension' in walletdata:
try:
cleartext = decryptData(password_key,
walletdata['encrypted_mnemonic_extension'].decode('hex'))
#theres a small chance of not getting a ValueError from the wrong
# password so also check the sum
if cleartext[-9] != '\xff':
raise ValueError
chunks = cleartext.split('\xff')
if len(chunks) < 3 or cleartext[-8:] != btc.dbl_sha256(chunks[1])[:8]:
raise ValueError
mnemonic_extension = chunks[1]
except ValueError:
log.info('incorrect password')
return None
else:
mnemonic_extension = None
if self.storepassword:
self.password_key = password_key
self.walletdata = walletdata
@ -288,7 +310,11 @@ class Wallet(AbstractWallet):
privkey, magicbyte=get_p2pk_vbyte())] = (epk_m['mixdepth'], -1,
len(self.imported_privkeys[epk_m['mixdepth']]))
self.imported_privkeys[epk_m['mixdepth']].append(privkey)
return decrypted_seed
if mnemonic_extension:
return (decrypted_entropy, mnemonic_extension)
else:
return decrypted_entropy
def update_cache_index(self):
if not self.path:
@ -398,15 +424,21 @@ class Bip39Wallet(Wallet):
BIP39, English only:
https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
"""
def entropy_to_seed(self, entropy):
if entropy is None:
def wallet_data_to_seed(self, data):
if data is None:
return None
self.mnemonic_extension = None
if isinstance(data, tuple):
entropy, self.mnemonic_extension = data
else:
entropy = data
if get_network() == "testnet":
if entropy.startswith("FAKESEED"):
return entropy[8:]
self.entropy = entropy.decode('hex')
m = Mnemonic("english")
return m.to_seed(m.to_mnemonic(self.entropy)).encode('hex')
return m.to_seed(m.to_mnemonic(self.entropy),
'' if not self.mnemonic_extension else self.mnemonic_extension).encode('hex')
class SegwitWallet(Bip39Wallet):

112
jmclient/jmclient/wallet_utils.py

@ -353,31 +353,49 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False,
else:
return walletview
def cli_password_check():
password = get_password('Enter wallet encryption passphrase: ')
password2 = get_password('Reenter wallet encryption passphrase: ')
def cli_get_wallet_passphrase_check():
password = get_password('Enter wallet file encryption passphrase: ')
password2 = get_password('Reenter wallet file encryption passphrase: ')
if password != password2:
print('ERROR. Passwords did not match')
return False, False
password_key = btc.bin_dbl_sha256(password)
return password, password_key
return False
return password
def cli_get_walletname():
def cli_get_wallet_file_name():
return raw_input('Input wallet file name (default: wallet.json): ')
def cli_user_words(words):
print('Write down this wallet recovery seed\n\n' + words +'\n')
def cli_user_words_entry():
return raw_input("Input 12 word recovery seed: ")
def persist_walletfile(walletspath, default_wallet_name, encrypted_seed,
callbacks=(cli_get_walletname,)):
def cli_display_user_words(words, mnemonic_extension):
text = 'Write down this wallet recovery mnemonic\n\n' + words +'\n'
if mnemonic_extension:
text += '\nAnd this mnemonic extension: ' + mnemonic_extension + '\n'
print(text)
def cli_user_mnemonic_entry():
mnemonic_phrase = raw_input("Input 12 word mnemonic recovery phrase: ")
mnemonic_extension = raw_input("Input mnemonic extension, leave blank if there isnt one: ")
if len(mnemonic_extension.strip()) == 0:
mnemonic_extension = None
return (mnemonic_phrase, mnemonic_extension)
def cli_get_mnemonic_extension():
uin = raw_input('Would you like to use a two-factor mnemonic recovery'
+ ' phrase? write \'n\' if you don\'t know what this is (y/n): ')
if len(uin) == 0 or uin[0] != 'y':
print('Not using mnemonic extension')
return None #no mnemonic extension
return raw_input('Enter mnemonic extension: ')
def persist_walletfile(walletspath, default_wallet_name, encrypted_entropy,
encrypted_mnemonic_extension=None,
callbacks=(cli_get_wallet_file_name,)):
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
walletfile = json.dumps({'creator': 'joinmarket project',
'creation_time': timestamp,
'encrypted_seed': encrypted_seed.encode('hex'),
'network': get_network()})
walletjson = {'creator': 'joinmarket project',
'creation_time': timestamp,
'encrypted_entropy': encrypted_entropy.encode('hex'),
'network': get_network()}
if encrypted_mnemonic_extension:
walletjson['encrypted_mnemonic_extension'] = encrypted_mnemonic_extension.encode('hex')
walletfile = json.dumps(walletjson)
walletname = callbacks[0]()
if len(walletname) == 0:
walletname = default_wallet_name
@ -394,31 +412,55 @@ def persist_walletfile(walletspath, default_wallet_name, encrypted_seed,
return True
def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
callbacks=(cli_user_words,
cli_user_words_entry,
cli_password_check,
cli_get_walletname)):
callbacks=(cli_display_user_words,
cli_user_mnemonic_entry,
cli_get_wallet_passphrase_check,
cli_get_wallet_file_name,
cli_get_mnemonic_extension)):
"""Optionally provide callbacks:
0 - display seed
1 - enter seed (for recovery)
2 - enter password
3 - enter wallet name
2 - enter wallet password
3 - enter wallet file name
4 - enter mnemonic extension
The defaults are for terminal entry.
"""
#using 128 bit entropy, 12 words, mnemonic module
m = Mnemonic("english")
if method == "generate":
mnemonic_extension = callbacks[4]()
words = m.generate()
callbacks[0](words)
callbacks[0](words, mnemonic_extension)
elif method == 'recover':
words = callbacks[1]()
words, mnemonic_extension = callbacks[1]()
if not words:
return False
entropy = str(m.to_entropy(words))
password, password_key = callbacks[2]()
password = callbacks[2]()
if not password:
return False
password_key = btc.bin_dbl_sha256(password)
encrypted_entropy = encryptData(password_key, entropy)
encrypted_mnemonic_extension = None
if mnemonic_extension:
mnemonic_extension = mnemonic_extension.strip()
#check all ascii printable
if not all([a > '\x19' and a < '\x7f' for a in mnemonic_extension]):
return False
#padding to stop an adversary easily telling how long the mn extension is
#padding at the start because of how aes blocks are combined
#checksum in order to tell whether the decryption was successful
cleartext_length = 79
padding_length = cleartext_length - 10 - len(mnemonic_extension)
if padding_length > 0:
padding = os.urandom(padding_length).replace('\xff', '\xfe')
else:
padding = ''
cleartext = (padding + '\xff' + mnemonic_extension + '\xff'
+ btc.dbl_sha256(mnemonic_extension)[:8])
encrypted_mnemonic_extension = encryptData(password_key, cleartext)
return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy,
callbacks=(callbacks[3],))
encrypted_mnemonic_extension, callbacks=(callbacks[3],))
def wallet_generate_recover(method, walletspath,
default_wallet_name='wallet.json'):
@ -439,9 +481,10 @@ def wallet_generate_recover(method, walletspath,
return False
seed = mn_decode(words)
print(seed)
password, password_key = cli_password_check()
password = cli_get_wallet_passphrase_check()
if not password:
return False
password_key = btc.bin_dbl_sha256(password)
encrypted_seed = encryptData(password_key, seed.decode('hex'))
return persist_walletfile(walletspath, default_wallet_name, encrypted_seed)
@ -653,15 +696,18 @@ def wallet_showseed(wallet):
if not wallet.entropy:
return "Entropy is not initialized."
m = Mnemonic("english")
return "Wallet recovery seed\n\n" + m.to_mnemonic(wallet.entropy) + "\n"
text = "Wallet mnemonic recovery phrase:\n\n" + m.to_mnemonic(wallet.entropy) + "\n"
if wallet.mnemonic_extension:
text += '\nWallet mnemonic extension: ' + wallet.mnemonic_extension + '\n'
return text
hexseed = wallet.seed
print("hexseed = " + hexseed)
words = mn_encode(hexseed)
return "Wallet recovery seed\n\n" + " ".join(words) + "\n"
return "Wallet mnemonic seed phrase:\n\n" + " ".join(words) + "\n"
def wallet_importprivkey(wallet, mixdepth):
print('WARNING: This imported key will not be recoverable with your 12 ' +
'word mnemonic seed. Make sure you have backups.')
'word mnemonic phrase. Make sure you have backups.')
print('WARNING: Handling of raw ECDSA bitcoin private keys can lead to '
'non-intuitive behaviour and loss of funds.\n Recommended instead '
'is to use the \'sweep\' feature of sendpayment.py ')

61
scripts/joinmarket-qt.py

@ -1281,11 +1281,22 @@ class JMMainWindow(QMainWindow):
def seedEntry(self):
d = QDialog(self)
d.setModal(1)
d.setWindowTitle('Recover from seed')
d.setWindowTitle('Recover from mnemonic phrase')
layout = QGridLayout(d)
message_e = QTextEdit()
layout.addWidget(QLabel('Enter 12 words'), 0, 0)
layout.addWidget(message_e, 1, 0)
pp_hbox = QHBoxLayout()
pp_field = QLineEdit()
pp_field.setEnabled(False)
use_pp = QCheckBox('Input Mnemonic Extension', self)
use_pp.setCheckState(False)
use_pp.stateChanged.connect(lambda state: pp_field.setEnabled(state
== QtCore.Qt.Checked))
pp_hbox.addWidget(use_pp)
pp_hbox.addWidget(pp_field)
hbox = QHBoxLayout()
buttonBox = QDialogButtonBox(self)
buttonBox.setStandardButtons(QDialogButtonBox.Ok |
@ -1293,11 +1304,15 @@ class JMMainWindow(QMainWindow):
buttonBox.button(QDialogButtonBox.Ok).clicked.connect(d.accept)
buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(d.reject)
hbox.addWidget(buttonBox)
layout.addLayout(hbox, 3, 0)
layout.addLayout(hbox, 4, 0)
layout.addLayout(pp_hbox, 3, 0)
result = d.exec_()
if result != QDialog.Accepted:
return None
return str(message_e.toPlainText())
return None, None
mn_extension = None
if use_pp.checkState() == QtCore.Qt.Checked:
mn_extension = str(pp_field.text())
return str(message_e.toPlainText()), mn_extension
def restartForScan(self, msg):
JMQtMessageBox(self, msg, mbtype='info',
@ -1308,8 +1323,8 @@ class JMMainWindow(QMainWindow):
success = wallet_generate_recover_bip39("recover", "wallets",
"wallet.json",
callbacks=(None, self.seedEntry,
self.getPasswordKey,
self.getWalletName))
self.getPassword,
self.getWalletFileName))
if not success:
JMQtMessageBox(self,
"Failed to recover wallet.",
@ -1453,13 +1468,9 @@ class JMMainWindow(QMainWindow):
continue
break
self.textpassword = str(pd.new_pw.text())
return self.textpassword
def getPasswordKey(self):
self.getPassword()
password_key = btc.bin_dbl_sha256(self.textpassword)
return (self.textpassword, password_key)
def getWalletName(self):
def getWalletFileName(self):
walletname, ok = QInputDialog.getText(self, 'Choose wallet name',
'Enter wallet file name:',
QLineEdit.Normal, "wallet.json")
@ -1469,7 +1480,7 @@ class JMMainWindow(QMainWindow):
self.walletname = str(walletname)
return self.walletname
def displayWords(self, words):
def displayWords(self, words, mnemonic_extension):
mb = QMessageBox()
seed_recovery_warning = [
"WRITE DOWN THIS WALLET RECOVERY SEED.",
@ -1477,10 +1488,27 @@ class JMMainWindow(QMainWindow):
"at risk. Do NOT ignore this step!!!"
]
mb.setText("\n".join(seed_recovery_warning))
mb.setInformativeText(words)
text = words
if mnemonic_extension:
text += '\n\nMnemonic extension: ' + mnemonic_extension
mb.setInformativeText(text)
mb.setStandardButtons(QMessageBox.Ok)
ret = mb.exec_()
def promptMnemonicExtension(self):
msg = "Would you like to use a two-factor mnemonic recovery phrase?\nIf you don\'t know what this is press No."
reply = QMessageBox.question(self, 'Use mnemonic extension?',
msg, QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.No:
return None
mnemonic_extension, ok = QInputDialog.getText(self,
'Input Mnemonic Extension',
'Enter mnemonic Extension:',
QLineEdit.Normal, "")
if not ok:
return None
return str(mnemonic_extension)
def initWallet(self, seed=None, restart_cb=None):
'''Creates a new wallet if seed not provided.
Initializes by syncing.
@ -1491,8 +1519,9 @@ class JMMainWindow(QMainWindow):
"wallet.json",
callbacks=(self.displayWords,
None,
self.getPasswordKey,
self.getWalletName))
self.getPassword,
self.getWalletFileName,
self.promptMnemonicExtension))
if not success:
JMQtMessageBox(self, "Failed to create new wallet file.",
title="Error", mbtype="warn")

Loading…
Cancel
Save