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")