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

112
jmclient/jmclient/wallet_utils.py

@ -353,31 +353,49 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False,
else: else:
return walletview return walletview
def cli_password_check(): def cli_get_wallet_passphrase_check():
password = get_password('Enter wallet encryption passphrase: ') password = get_password('Enter wallet file encryption passphrase: ')
password2 = get_password('Reenter wallet encryption passphrase: ') password2 = get_password('Reenter wallet file encryption passphrase: ')
if password != password2: if password != password2:
print('ERROR. Passwords did not match') print('ERROR. Passwords did not match')
return False, False return False
password_key = btc.bin_dbl_sha256(password) return password
return password, password_key
def cli_get_walletname(): def cli_get_wallet_file_name():
return raw_input('Input wallet file name (default: wallet.json): ') return raw_input('Input wallet file name (default: wallet.json): ')
def cli_user_words(words): def cli_display_user_words(words, mnemonic_extension):
print('Write down this wallet recovery seed\n\n' + words +'\n') text = 'Write down this wallet recovery mnemonic\n\n' + words +'\n'
if mnemonic_extension:
def cli_user_words_entry(): text += '\nAnd this mnemonic extension: ' + mnemonic_extension + '\n'
return raw_input("Input 12 word recovery seed: ") print(text)
def persist_walletfile(walletspath, default_wallet_name, encrypted_seed, def cli_user_mnemonic_entry():
callbacks=(cli_get_walletname,)): 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") timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
walletfile = json.dumps({'creator': 'joinmarket project', walletjson = {'creator': 'joinmarket project',
'creation_time': timestamp, 'creation_time': timestamp,
'encrypted_seed': encrypted_seed.encode('hex'), 'encrypted_entropy': encrypted_entropy.encode('hex'),
'network': get_network()}) 'network': get_network()}
if encrypted_mnemonic_extension:
walletjson['encrypted_mnemonic_extension'] = encrypted_mnemonic_extension.encode('hex')
walletfile = json.dumps(walletjson)
walletname = callbacks[0]() walletname = callbacks[0]()
if len(walletname) == 0: if len(walletname) == 0:
walletname = default_wallet_name walletname = default_wallet_name
@ -394,31 +412,55 @@ def persist_walletfile(walletspath, default_wallet_name, encrypted_seed,
return True return True
def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
callbacks=(cli_user_words, callbacks=(cli_display_user_words,
cli_user_words_entry, cli_user_mnemonic_entry,
cli_password_check, cli_get_wallet_passphrase_check,
cli_get_walletname)): cli_get_wallet_file_name,
cli_get_mnemonic_extension)):
"""Optionally provide callbacks: """Optionally provide callbacks:
0 - display seed 0 - display seed
1 - enter seed (for recovery) 1 - enter seed (for recovery)
2 - enter password 2 - enter wallet password
3 - enter wallet name 3 - enter wallet file name
4 - enter mnemonic extension
The defaults are for terminal entry. The defaults are for terminal entry.
""" """
#using 128 bit entropy, 12 words, mnemonic module #using 128 bit entropy, 12 words, mnemonic module
m = Mnemonic("english") m = Mnemonic("english")
if method == "generate": if method == "generate":
mnemonic_extension = callbacks[4]()
words = m.generate() words = m.generate()
callbacks[0](words) callbacks[0](words, mnemonic_extension)
elif method == 'recover': elif method == 'recover':
words = callbacks[1]() words, mnemonic_extension = callbacks[1]()
if not words:
return False
entropy = str(m.to_entropy(words)) entropy = str(m.to_entropy(words))
password, password_key = callbacks[2]() password = callbacks[2]()
if not password: if not password:
return False return False
password_key = btc.bin_dbl_sha256(password)
encrypted_entropy = encryptData(password_key, entropy) 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, return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy,
callbacks=(callbacks[3],)) encrypted_mnemonic_extension, callbacks=(callbacks[3],))
def wallet_generate_recover(method, walletspath, def wallet_generate_recover(method, walletspath,
default_wallet_name='wallet.json'): default_wallet_name='wallet.json'):
@ -439,9 +481,10 @@ def wallet_generate_recover(method, walletspath,
return False return False
seed = mn_decode(words) seed = mn_decode(words)
print(seed) print(seed)
password, password_key = cli_password_check() password = cli_get_wallet_passphrase_check()
if not password: if not password:
return False return False
password_key = btc.bin_dbl_sha256(password)
encrypted_seed = encryptData(password_key, seed.decode('hex')) encrypted_seed = encryptData(password_key, seed.decode('hex'))
return persist_walletfile(walletspath, default_wallet_name, encrypted_seed) return persist_walletfile(walletspath, default_wallet_name, encrypted_seed)
@ -653,15 +696,18 @@ def wallet_showseed(wallet):
if not wallet.entropy: if not wallet.entropy:
return "Entropy is not initialized." return "Entropy is not initialized."
m = Mnemonic("english") 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 hexseed = wallet.seed
print("hexseed = " + hexseed) print("hexseed = " + hexseed)
words = mn_encode(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): def wallet_importprivkey(wallet, mixdepth):
print('WARNING: This imported key will not be recoverable with your 12 ' + 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 ' print('WARNING: Handling of raw ECDSA bitcoin private keys can lead to '
'non-intuitive behaviour and loss of funds.\n Recommended instead ' 'non-intuitive behaviour and loss of funds.\n Recommended instead '
'is to use the \'sweep\' feature of sendpayment.py ') 'is to use the \'sweep\' feature of sendpayment.py ')

61
scripts/joinmarket-qt.py

@ -1281,11 +1281,22 @@ class JMMainWindow(QMainWindow):
def seedEntry(self): def seedEntry(self):
d = QDialog(self) d = QDialog(self)
d.setModal(1) d.setModal(1)
d.setWindowTitle('Recover from seed') d.setWindowTitle('Recover from mnemonic phrase')
layout = QGridLayout(d) layout = QGridLayout(d)
message_e = QTextEdit() message_e = QTextEdit()
layout.addWidget(QLabel('Enter 12 words'), 0, 0) layout.addWidget(QLabel('Enter 12 words'), 0, 0)
layout.addWidget(message_e, 1, 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() hbox = QHBoxLayout()
buttonBox = QDialogButtonBox(self) buttonBox = QDialogButtonBox(self)
buttonBox.setStandardButtons(QDialogButtonBox.Ok | buttonBox.setStandardButtons(QDialogButtonBox.Ok |
@ -1293,11 +1304,15 @@ class JMMainWindow(QMainWindow):
buttonBox.button(QDialogButtonBox.Ok).clicked.connect(d.accept) buttonBox.button(QDialogButtonBox.Ok).clicked.connect(d.accept)
buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(d.reject) buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(d.reject)
hbox.addWidget(buttonBox) hbox.addWidget(buttonBox)
layout.addLayout(hbox, 3, 0) layout.addLayout(hbox, 4, 0)
layout.addLayout(pp_hbox, 3, 0)
result = d.exec_() result = d.exec_()
if result != QDialog.Accepted: if result != QDialog.Accepted:
return None return None, None
return str(message_e.toPlainText()) 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): def restartForScan(self, msg):
JMQtMessageBox(self, msg, mbtype='info', JMQtMessageBox(self, msg, mbtype='info',
@ -1308,8 +1323,8 @@ class JMMainWindow(QMainWindow):
success = wallet_generate_recover_bip39("recover", "wallets", success = wallet_generate_recover_bip39("recover", "wallets",
"wallet.json", "wallet.json",
callbacks=(None, self.seedEntry, callbacks=(None, self.seedEntry,
self.getPasswordKey, self.getPassword,
self.getWalletName)) self.getWalletFileName))
if not success: if not success:
JMQtMessageBox(self, JMQtMessageBox(self,
"Failed to recover wallet.", "Failed to recover wallet.",
@ -1453,13 +1468,9 @@ class JMMainWindow(QMainWindow):
continue continue
break break
self.textpassword = str(pd.new_pw.text()) self.textpassword = str(pd.new_pw.text())
return self.textpassword
def getPasswordKey(self): def getWalletFileName(self):
self.getPassword()
password_key = btc.bin_dbl_sha256(self.textpassword)
return (self.textpassword, password_key)
def getWalletName(self):
walletname, ok = QInputDialog.getText(self, 'Choose wallet name', walletname, ok = QInputDialog.getText(self, 'Choose wallet name',
'Enter wallet file name:', 'Enter wallet file name:',
QLineEdit.Normal, "wallet.json") QLineEdit.Normal, "wallet.json")
@ -1469,7 +1480,7 @@ class JMMainWindow(QMainWindow):
self.walletname = str(walletname) self.walletname = str(walletname)
return self.walletname return self.walletname
def displayWords(self, words): def displayWords(self, words, mnemonic_extension):
mb = QMessageBox() mb = QMessageBox()
seed_recovery_warning = [ seed_recovery_warning = [
"WRITE DOWN THIS WALLET RECOVERY SEED.", "WRITE DOWN THIS WALLET RECOVERY SEED.",
@ -1477,10 +1488,27 @@ class JMMainWindow(QMainWindow):
"at risk. Do NOT ignore this step!!!" "at risk. Do NOT ignore this step!!!"
] ]
mb.setText("\n".join(seed_recovery_warning)) 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) mb.setStandardButtons(QMessageBox.Ok)
ret = mb.exec_() 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): def initWallet(self, seed=None, restart_cb=None):
'''Creates a new wallet if seed not provided. '''Creates a new wallet if seed not provided.
Initializes by syncing. Initializes by syncing.
@ -1491,8 +1519,9 @@ class JMMainWindow(QMainWindow):
"wallet.json", "wallet.json",
callbacks=(self.displayWords, callbacks=(self.displayWords,
None, None,
self.getPasswordKey, self.getPassword,
self.getWalletName)) self.getWalletFileName,
self.promptMnemonicExtension))
if not success: if not success:
JMQtMessageBox(self, "Failed to create new wallet file.", JMQtMessageBox(self, "Failed to create new wallet file.",
title="Error", mbtype="warn") title="Error", mbtype="warn")

Loading…
Cancel
Save