URI: 
       tProvide warnings about invalid BIP39 checksum in seed dialog - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit c6e09a60388325050410101e78fb72f43aaa525b
   DIR parent 8b194cd409a1a79e616578a5eaf91cba68b36dab
  HTML 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:
         M gui/qt/seed_dialog.py               |       9 ++++++---
         M lib/keystore.py                     |      32 +++++++++++++++++++++++++++++--
         M lib/mnemonic.py                     |      26 +++++++++++++++-----------
         M lib/tests/test_mnemonic.py          |       9 +++++++++
       
       4 files changed, 60 insertions(+), 16 deletions(-)
       ---
   DIR diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py
       t@@ -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/>',
       t@@ -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)
       t@@ -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())
       t@@ -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)
        
   DIR diff --git a/lib/keystore.py b/lib/keystore.py
       t@@ -24,6 +24,7 @@
        # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        # SOFTWARE.
        
       +import struct
        
        from unicodedata import normalize
        
       t@@ -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):
       t@@ -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
        
   DIR diff --git a/lib/mnemonic.py b/lib/mnemonic.py
       t@@ -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',
       t@@ -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
   DIR diff --git a/lib/tests/test_mnemonic.py b/lib/tests/test_mnemonic.py
       t@@ -1,4 +1,5 @@
        import unittest
       +from lib import keystore
        from lib import mnemonic
        from lib import old_mnemonic
        
       t@@ -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)