Skip to content

Commit

Permalink
Merge 330b482 into b9db163
Browse files Browse the repository at this point in the history
  • Loading branch information
ecdsa committed Jun 17, 2020
2 parents b9db163 + 330b482 commit 167387f
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 17 deletions.
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 @@ -2606,7 +2606,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 @@ -1380,9 +1380,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 @@ -1433,9 +1433,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

0 comments on commit 167387f

Please sign in to comment.