From 26ae6d68a3df8eae26b2d66c6dc41ea8ed25b637 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 16 Jun 2020 10:42:47 +0200 Subject: [PATCH] add encryption version to channel backups --- electrum/crypto.py | 44 ++++++++++++++++--- electrum/gui/kivy/main_window.py | 2 +- .../kivy/uix/dialogs/lightning_channels.py | 2 +- electrum/gui/qt/channels_list.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/lnworker.py | 13 +++--- 6 files changed, 49 insertions(+), 16 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index fd09f67e3..70192eaf0 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -189,23 +189,21 @@ 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) -> str: + """bytes -> 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: + """bytes -> 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 @@ -216,6 +214,38 @@ 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_b64_with_version(data: bytes, password: Union[bytes, str]) -> str: + """plaintext bytes -> base64 ciphertext""" + version = PW_HASH_VERSION_LATEST + ciphertext = pw_encode_raw(data, password, version=version) + ciphertext_b64 = base64.b64encode(bytes([version]) + ciphertext) + return ciphertext_b64.decode('utf8') + + +def pw_decode_b64_with_version(data: str, password: Union[bytes, str]) -> bytes: + """base64 ciphertext -> plaintext bytes""" + data_bytes = bytes(base64.b64decode(data)) + version = int(data_bytes[0]) + if version not in KNOWN_PW_HASH_VERSIONS: + raise UnexpectedPasswordHashVersion(version) + return pw_decode_raw(data_bytes[1:], password, version=version) + + def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str: """plaintext str -> base64 ciphertext""" if not password: diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index d0e8ebf60..6dd364c67 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -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: diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py index 20cfaf25e..78b81cc79 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_channels.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_channels.py @@ -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() diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index c7306e29d..e5450736d 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -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(): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index ed735eecb..be348aae9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 0381d3dfb..99f01f21a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -67,6 +67,7 @@ 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_b64_with_version, pw_decode_b64_with_version from .lnutil import ChannelBackupStorage from .lnchannel import ChannelBackup from .channel_db import UpdateStatus @@ -1396,9 +1397,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_b64_with_version(backup_bytes, xpub) + assert backup_bytes == pw_decode_b64_with_version(encrypted, xpub), "encrypt failed" + return 'channel_backup:' + encrypted class LNBackups(Logger): @@ -1449,9 +1450,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_b64_with_version(encrypted, xpub) cb_storage = ChannelBackupStorage.from_bytes(decrypted) channel_id = cb_storage.channel_id().hex() d = self.db.get_dict("channel_backups")