Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Channel backup version #6236

Merged
merged 4 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions electrum/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/kivy/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def on_qr(self, data):
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:
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/kivy/uix/dialogs/lightning_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ def export_backup(self):
_("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()
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/channels_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def export_channel_backup(self, channel_id):
_("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():
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2617,7 +2617,7 @@ def read_tx_from_qrcode(self):
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)
Expand Down
6 changes: 6 additions & 0 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -179,6 +181,7 @@ def channel_id(self):

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)
Expand All @@ -198,6 +201,9 @@ def to_bytes(self):
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(),
Expand Down
14 changes: 8 additions & 6 deletions electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,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_with_version_and_mac, pw_decode_with_version_and_mac
from .lnutil import ChannelBackupStorage
from .lnchannel import ChannelBackup
from .channel_db import UpdateStatus
Expand Down Expand Up @@ -1396,9 +1396,9 @@ def export_channel_backup(self, channel_id):
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):
Expand Down Expand Up @@ -1449,9 +1449,11 @@ def stop(self):
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")
Expand Down