commit c6e09a60388325050410101e78fb72f43aaa525b
parent 8b194cd409a1a79e616578a5eaf91cba68b36dab
Author: Kacper Żuk <kacper.b.zuk@gmail.com>
Date: Sun, 22 Jan 2017 15:58:37 +0100
Provide warnings about invalid BIP39 checksum in seed dialog
Diffstat:
4 files changed, 60 insertions(+), 16 deletions(-)
diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py
@@ -64,6 +64,7 @@ class SeedLayout(QVBoxLayout):
def f(b):
self.is_seed = (lambda x: bool(x)) if b else self.saved_is_seed
self.on_edit()
+ self.is_bip39 = b
if b:
msg = ' '.join([
'<b>' + _('Warning') + ': BIP39 seeds are dangerous!' + '</b><br/><br/>',
@@ -76,7 +77,6 @@ class SeedLayout(QVBoxLayout):
else:
msg = ''
self.seed_warning.setText(msg)
-
cb_bip39 = QCheckBox(_('BIP39 seed'))
cb_bip39.toggled.connect(f)
cb_bip39.setChecked(self.is_bip39)
@@ -130,9 +130,9 @@ class SeedLayout(QVBoxLayout):
self.addLayout(hbox)
self.addStretch(1)
self.seed_warning = WWLabel('')
- self.addWidget(self.seed_warning)
if msg:
self.seed_warning.setText(seed_warning_msg(seed))
+ self.addWidget(self.seed_warning)
def get_seed(self):
text = unicode(self.seed_e.text())
@@ -146,7 +146,10 @@ class SeedLayout(QVBoxLayout):
t = seed_type(s)
label = _('Seed Type') + ': ' + t if t else ''
else:
- label = 'BIP39 (checksum disabled)'
+ from electrum.keystore import bip39_is_checksum_valid
+ is_checksum, is_wordlist = bip39_is_checksum_valid(s)
+ status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist'
+ label = 'BIP39' + ' (%s)'%status
self.seed_type_label.setText(label)
self.parent.next_button.setEnabled(b)
diff --git a/lib/keystore.py b/lib/keystore.py
@@ -24,6 +24,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+import struct
from unicodedata import normalize
@@ -35,7 +36,7 @@ from bitcoin import *
from bitcoin import is_old_seed, is_new_seed, is_seed
from util import PrintError, InvalidPassword
-from mnemonic import Mnemonic
+from mnemonic import Mnemonic, load_wordlist
class KeyStore(PrintError):
@@ -555,7 +556,34 @@ def bip39_to_seed(mnemonic, passphrase):
iterations = PBKDF2_ROUNDS, macmodule = hmac,
digestmodule = hashlib.sha512).read(64)
-
+# returns tuple (is_checksum_valid, is_wordlist_valid)
+def bip39_is_checksum_valid(mnemonic):
+ words = [ normalize('NFKD', word) for word in mnemonic.split() ]
+ words_len = len(words)
+ wordlist = load_wordlist("english.txt")
+ n = len(wordlist)
+ checksum_length = 11*words_len//33
+ entropy_length = 32*checksum_length
+ i = 0
+ words.reverse()
+ while words:
+ w = words.pop()
+ try:
+ k = wordlist.index(w)
+ except ValueError:
+ return False, False
+ i = i*n + k
+ if words_len not in [12, 15, 18, 21, 24]:
+ return False, True
+ entropy = i >> checksum_length
+ checksum = i % 2**checksum_length
+ h = '{:x}'.format(entropy)
+ while len(h) < entropy_length/4:
+ h = '0'+h
+ b = bytearray.fromhex(h)
+ hashed = int(hashlib.sha256(b).digest().encode('hex'), 16)
+ calculated_checksum = hashed >> (256 - checksum_length)
+ return checksum == calculated_checksum, True
# extended pubkeys
diff --git a/lib/mnemonic.py b/lib/mnemonic.py
@@ -91,6 +91,20 @@ def normalize_text(seed):
seed = u''.join([seed[i] for i in range(len(seed)) if not (seed[i] in string.whitespace and is_CJK(seed[i-1]) and is_CJK(seed[i+1]))])
return seed
+def load_wordlist(filename):
+ path = os.path.join(os.path.dirname(__file__), 'wordlist', filename)
+ s = open(path,'r').read().strip()
+ s = unicodedata.normalize('NFKD', s.decode('utf8'))
+ lines = s.split('\n')
+ wordlist = []
+ for line in lines:
+ line = line.split('#')[0]
+ line = line.strip(' \r')
+ assert ' ' not in line
+ if line:
+ wordlist.append(line)
+ return wordlist
+
filenames = {
'en':'english.txt',
@@ -110,17 +124,7 @@ class Mnemonic(object):
lang = lang or 'en'
print_error('language', lang)
filename = filenames.get(lang[0:2], 'english.txt')
- path = os.path.join(os.path.dirname(__file__), 'wordlist', filename)
- s = open(path,'r').read().strip()
- s = unicodedata.normalize('NFKD', s.decode('utf8'))
- lines = s.split('\n')
- self.wordlist = []
- for line in lines:
- line = line.split('#')[0]
- line = line.strip(' \r')
- assert ' ' not in line
- if line:
- self.wordlist.append(line)
+ self.wordlist = load_wordlist(filename)
print_error("wordlist has %d words"%len(self.wordlist))
@classmethod
diff --git a/lib/tests/test_mnemonic.py b/lib/tests/test_mnemonic.py
@@ -1,4 +1,5 @@
import unittest
+from lib import keystore
from lib import mnemonic
from lib import old_mnemonic
@@ -27,3 +28,11 @@ class Test_OldMnemonic(unittest.TestCase):
words = 'hardly point goal hallway patience key stone difference ready caught listen fact'
self.assertEquals(result, words.split())
self.assertEquals(old_mnemonic.mn_decode(result), seed)
+
+class Test_BIP39Checksum(unittest.TestCase):
+
+ def test(self):
+ mnemonic = u'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog'
+ is_checksum_valid, is_wordlist_valid = keystore.bip39_is_checksum_valid(mnemonic)
+ self.assertTrue(is_wordlist_valid)
+ self.assertTrue(is_checksum_valid)