URI: 
       tMerge pull request #6236 from spesmilo/channel_backup_version - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit eb910ba14f2a5493b85ef65efcc46cdeacfa787a
   DIR parent cb4c8abe1c121ee8f7c39de6b4b1824821542b7e
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu, 18 Jun 2020 15:17:13 +0200
       
       Merge pull request #6236 from spesmilo/channel_backup_version
       
       Channel backup version
       Diffstat:
         M electrum/crypto.py                  |      50 ++++++++++++++++++++++++++-----
         M electrum/gui/kivy/main_window.py    |       2 +-
         M electrum/gui/kivy/uix/dialogs/ligh… |       2 +-
         M electrum/gui/qt/channels_list.py    |       2 +-
         M electrum/gui/qt/main_window.py      |       2 +-
         M electrum/lnutil.py                  |       6 ++++++
         M electrum/lnworker.py                |      14 ++++++++------
       
       7 files changed, 61 insertions(+), 17 deletions(-)
       ---
   DIR diff --git a/electrum/crypto.py b/electrum/crypto.py
       t@@ -189,23 +189,19 @@ def _hash_password(password: Union[bytes, str], *, version: int) -> bytes:
                raise UnexpectedPasswordHashVersion(version)
        
        
       -def pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str:
       -    """plaintext bytes -> base64 ciphertext"""
       +def _pw_encode_raw(data: bytes, password: Union[bytes, str], *, version: int) -> bytes:
            if version not in KNOWN_PW_HASH_VERSIONS:
                raise UnexpectedPasswordHashVersion(version)
            # derive key from password
            secret = _hash_password(password, version=version)
            # encrypt given data
            ciphertext = EncodeAES_bytes(secret, data)
       -    ciphertext_b64 = base64.b64encode(ciphertext)
       -    return ciphertext_b64.decode('utf8')
       +    return ciphertext
        
        
       -def pw_decode_bytes(data: str, password: Union[bytes, str], *, version: int) -> bytes:
       -    """base64 ciphertext -> plaintext bytes"""
       +def _pw_decode_raw(data_bytes: bytes, password: Union[bytes, str], *, version: int) -> bytes:
            if version not in KNOWN_PW_HASH_VERSIONS:
                raise UnexpectedPasswordHashVersion(version)
       -    data_bytes = bytes(base64.b64decode(data))
            # derive key from password
            secret = _hash_password(password, version=version)
            # decrypt given data
       t@@ -216,6 +212,46 @@ def pw_decode_bytes(data: str, password: Union[bytes, str], *, version: int) -> 
            return d
        
        
       +def pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str:
       +    """plaintext bytes -> base64 ciphertext"""
       +    ciphertext = _pw_encode_raw(data, password, version=version)
       +    ciphertext_b64 = base64.b64encode(ciphertext)
       +    return ciphertext_b64.decode('utf8')
       +
       +
       +def pw_decode_bytes(data: str, password: Union[bytes, str], *, version:int) -> bytes:
       +    """base64 ciphertext -> plaintext bytes"""
       +    if version not in KNOWN_PW_HASH_VERSIONS:
       +        raise UnexpectedPasswordHashVersion(version)
       +    data_bytes = bytes(base64.b64decode(data))
       +    return _pw_decode_raw(data_bytes, password, version=version)
       +
       +
       +def pw_encode_with_version_and_mac(data: bytes, password: Union[bytes, str]) -> str:
       +    """plaintext bytes -> base64 ciphertext"""
       +    # https://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac
       +    # Encrypt-and-MAC. The MAC will be used to detect invalid passwords
       +    version = PW_HASH_VERSION_LATEST
       +    mac = sha256(data)[0:4]
       +    ciphertext = _pw_encode_raw(data, password, version=version)
       +    ciphertext_b64 = base64.b64encode(bytes([version]) + ciphertext + mac)
       +    return ciphertext_b64.decode('utf8')
       +
       +
       +def pw_decode_with_version_and_mac(data: str, password: Union[bytes, str]) -> bytes:
       +    """base64 ciphertext -> plaintext bytes"""
       +    data_bytes = bytes(base64.b64decode(data))
       +    version = int(data_bytes[0])
       +    encrypted = data_bytes[1:-4]
       +    mac = data_bytes[-4:]
       +    if version not in KNOWN_PW_HASH_VERSIONS:
       +        raise UnexpectedPasswordHashVersion(version)
       +    decrypted = _pw_decode_raw(encrypted, password, version=version)
       +    if sha256(decrypted)[0:4] != mac:
       +        raise InvalidPassword()
       +    return decrypted
       +
       +
        def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
            """plaintext str -> base64 ciphertext"""
            if not password:
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -415,7 +415,7 @@ class ElectrumWindow(App):
                    self.set_URI(data)
                    return
                if data.startswith('channel_backup:'):
       -            self.import_channel_backup(data[15:])
       +            self.import_channel_backup(data)
                    return
                bolt11_invoice = maybe_extract_bolt11_invoice(data)
                if bolt11_invoice is not None:
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py
       t@@ -379,7 +379,7 @@ class ChannelDetailsPopup(Popup):
                    _("Please note that channel backups cannot be used to restore your channels."),
                    _("If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain."),
                ])
       -        self.app.qr_dialog(_("Channel Backup " + self.chan.short_id_for_GUI()), 'channel_backup:'+text, help_text=help_text)
       +        self.app.qr_dialog(_("Channel Backup " + self.chan.short_id_for_GUI()), text, help_text=help_text)
        
            def force_close(self):
                Question(_('Force-close channel?'), self._force_close).open()
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -132,7 +132,7 @@ class ChannelsList(MyTreeView):
                    _("If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain."),
                ])
                data = self.lnworker.export_channel_backup(channel_id)
       -        self.main_window.show_qrcode('channel_backup:' + data, 'channel backup', help_text=msg)
       +        self.main_window.show_qrcode(data, 'channel backup', help_text=msg)
        
            def request_force_close(self, channel_id):
                def task():
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -2617,7 +2617,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.pay_to_URI(data)
                    return
                if data.startswith('channel_backup:'):
       -            self.import_channel_backup(data[15:])
       +            self.import_channel_backup(data)
                    return
                # else if the user scanned an offline signed tx
                tx = self.tx_from_text(data)
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -154,6 +154,8 @@ class ChannelConstraints(StoredObject):
            is_initiator = attr.ib(type=bool)  # note: sometimes also called "funder"
            funding_txn_minimum_depth = attr.ib(type=int)
        
       +
       +CHANNEL_BACKUP_VERSION = 0
        @attr.s
        class ChannelBackupStorage(StoredObject):
            node_id = attr.ib(type=bytes, converter=hex_to_bytes)
       t@@ -179,6 +181,7 @@ class ChannelBackupStorage(StoredObject):
        
            def to_bytes(self):
                vds = BCDataStream()
       +        vds.write_int16(CHANNEL_BACKUP_VERSION)
                vds.write_boolean(self.is_initiator)
                vds.write_bytes(self.privkey, 32)
                vds.write_bytes(self.channel_seed, 32)
       t@@ -198,6 +201,9 @@ class ChannelBackupStorage(StoredObject):
            def from_bytes(s):
                vds = BCDataStream()
                vds.write(s)
       +        version = vds.read_int16()
       +        if version != CHANNEL_BACKUP_VERSION:
       +            raise Exception(f"unknown version for channel backup: {version}")
                return ChannelBackupStorage(
                    is_initiator = bool(vds.read_bytes(1)),
                    privkey = vds.read_bytes(32).hex(),
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -66,7 +66,7 @@ from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_t
        from .address_synchronizer import TX_HEIGHT_LOCAL
        from . import lnsweep
        from .lnwatcher import LNWalletWatcher
       -from .crypto import pw_encode_bytes, pw_decode_bytes, PW_HASH_VERSION_LATEST
       +from .crypto import pw_encode_with_version_and_mac, pw_decode_with_version_and_mac
        from .lnutil import ChannelBackupStorage
        from .lnchannel import ChannelBackup
        from .channel_db import UpdateStatus
       t@@ -1396,9 +1396,9 @@ class LNWallet(LNWorker):
                xpub = self.wallet.get_fingerprint()
                backup_bytes = self.create_channel_backup(channel_id).to_bytes()
                assert backup_bytes == ChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), "roundtrip failed"
       -        encrypted = pw_encode_bytes(backup_bytes, xpub, version=PW_HASH_VERSION_LATEST)
       -        assert backup_bytes == pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST), "encrypt failed"
       -        return encrypted
       +        encrypted = pw_encode_with_version_and_mac(backup_bytes, xpub)
       +        assert backup_bytes == pw_decode_with_version_and_mac(encrypted, xpub), "encrypt failed"
       +        return 'channel_backup:' + encrypted
        
        
        class LNBackups(Logger):
       t@@ -1449,9 +1449,11 @@ class LNBackups(Logger):
                self.lnwatcher.stop()
                self.lnwatcher = None
        
       -    def import_channel_backup(self, encrypted):
       +    def import_channel_backup(self, data):
       +        assert data.startswith('channel_backup:')
       +        encrypted = data[15:]
                xpub = self.wallet.get_fingerprint()
       -        decrypted = pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST)
       +        decrypted = pw_decode_with_version_and_mac(encrypted, xpub)
                cb_storage = ChannelBackupStorage.from_bytes(decrypted)
                channel_id = cb_storage.channel_id().hex()
                d = self.db.get_dict("channel_backups")