add encryption version to channel backups

This commit is contained in:
ThomasV 2020-06-16 10:42:47 +02:00
parent cb4c8abe1c
commit 26ae6d68a3
6 changed files with 49 additions and 16 deletions

View file

@ -189,23 +189,21 @@ def _hash_password(password: Union[bytes, str], *, version: int) -> bytes:
raise UnexpectedPasswordHashVersion(version) raise UnexpectedPasswordHashVersion(version)
def pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str: def pw_encode_raw(data: bytes, password: Union[bytes, str], *, version: int) -> str:
"""plaintext bytes -> base64 ciphertext""" """bytes -> bytes"""
if version not in KNOWN_PW_HASH_VERSIONS: if version not in KNOWN_PW_HASH_VERSIONS:
raise UnexpectedPasswordHashVersion(version) raise UnexpectedPasswordHashVersion(version)
# derive key from password # derive key from password
secret = _hash_password(password, version=version) secret = _hash_password(password, version=version)
# encrypt given data # encrypt given data
ciphertext = EncodeAES_bytes(secret, data) ciphertext = EncodeAES_bytes(secret, data)
ciphertext_b64 = base64.b64encode(ciphertext) return ciphertext
return ciphertext_b64.decode('utf8')
def pw_decode_bytes(data: str, password: Union[bytes, str], *, version: int) -> bytes: def pw_decode_raw(data_bytes: bytes, password: Union[bytes, str], *, version: int) -> bytes:
"""base64 ciphertext -> plaintext bytes""" """bytes -> bytes"""
if version not in KNOWN_PW_HASH_VERSIONS: if version not in KNOWN_PW_HASH_VERSIONS:
raise UnexpectedPasswordHashVersion(version) raise UnexpectedPasswordHashVersion(version)
data_bytes = bytes(base64.b64decode(data))
# derive key from password # derive key from password
secret = _hash_password(password, version=version) secret = _hash_password(password, version=version)
# decrypt given data # decrypt given data
@ -216,6 +214,38 @@ def pw_decode_bytes(data: str, password: Union[bytes, str], *, version: int) ->
return d 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: def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
"""plaintext str -> base64 ciphertext""" """plaintext str -> base64 ciphertext"""
if not password: if not password:

View file

@ -415,7 +415,7 @@ class ElectrumWindow(App):
self.set_URI(data) self.set_URI(data)
return return
if data.startswith('channel_backup:'): if data.startswith('channel_backup:'):
self.import_channel_backup(data[15:]) self.import_channel_backup(data)
return return
bolt11_invoice = maybe_extract_bolt11_invoice(data) bolt11_invoice = maybe_extract_bolt11_invoice(data)
if bolt11_invoice is not None: if bolt11_invoice is not None:

View file

@ -379,7 +379,7 @@ class ChannelDetailsPopup(Popup):
_("Please note that channel backups cannot be used to restore your channels."), _("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."), _("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): def force_close(self):
Question(_('Force-close channel?'), self._force_close).open() Question(_('Force-close channel?'), self._force_close).open()

View file

@ -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."), _("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) 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 request_force_close(self, channel_id):
def task(): def task():

View file

@ -2617,7 +2617,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.pay_to_URI(data) self.pay_to_URI(data)
return return
if data.startswith('channel_backup:'): if data.startswith('channel_backup:'):
self.import_channel_backup(data[15:]) self.import_channel_backup(data)
return return
# else if the user scanned an offline signed tx # else if the user scanned an offline signed tx
tx = self.tx_from_text(data) tx = self.tx_from_text(data)

View file

@ -67,6 +67,7 @@ from .address_synchronizer import TX_HEIGHT_LOCAL
from . import lnsweep from . import lnsweep
from .lnwatcher import LNWalletWatcher from .lnwatcher import LNWalletWatcher
from .crypto import pw_encode_bytes, pw_decode_bytes, PW_HASH_VERSION_LATEST 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 .lnutil import ChannelBackupStorage
from .lnchannel import ChannelBackup from .lnchannel import ChannelBackup
from .channel_db import UpdateStatus from .channel_db import UpdateStatus
@ -1396,9 +1397,9 @@ class LNWallet(LNWorker):
xpub = self.wallet.get_fingerprint() xpub = self.wallet.get_fingerprint()
backup_bytes = self.create_channel_backup(channel_id).to_bytes() backup_bytes = self.create_channel_backup(channel_id).to_bytes()
assert backup_bytes == ChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), "roundtrip failed" assert backup_bytes == ChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), "roundtrip failed"
encrypted = pw_encode_bytes(backup_bytes, xpub, version=PW_HASH_VERSION_LATEST) encrypted = pw_encode_b64_with_version(backup_bytes, xpub)
assert backup_bytes == pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST), "encrypt failed" assert backup_bytes == pw_decode_b64_with_version(encrypted, xpub), "encrypt failed"
return encrypted return 'channel_backup:' + encrypted
class LNBackups(Logger): class LNBackups(Logger):
@ -1449,9 +1450,11 @@ class LNBackups(Logger):
self.lnwatcher.stop() self.lnwatcher.stop()
self.lnwatcher = None 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() 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) cb_storage = ChannelBackupStorage.from_bytes(decrypted)
channel_id = cb_storage.channel_id().hex() channel_id = cb_storage.channel_id().hex()
d = self.db.get_dict("channel_backups") d = self.db.get_dict("channel_backups")