mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-29 16:31:29 +00:00
Merge pull request #6236 from spesmilo/channel_backup_version
Channel backup version
This commit is contained in:
commit
eb910ba14f
7 changed files with 61 additions and 17 deletions
|
@ -189,23 +189,19 @@ 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) -> bytes:
|
||||||
"""plaintext bytes -> base64 ciphertext"""
|
|
||||||
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"""
|
|
||||||
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 +212,46 @@ 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_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:
|
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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -154,6 +154,8 @@ class ChannelConstraints(StoredObject):
|
||||||
is_initiator = attr.ib(type=bool) # note: sometimes also called "funder"
|
is_initiator = attr.ib(type=bool) # note: sometimes also called "funder"
|
||||||
funding_txn_minimum_depth = attr.ib(type=int)
|
funding_txn_minimum_depth = attr.ib(type=int)
|
||||||
|
|
||||||
|
|
||||||
|
CHANNEL_BACKUP_VERSION = 0
|
||||||
@attr.s
|
@attr.s
|
||||||
class ChannelBackupStorage(StoredObject):
|
class ChannelBackupStorage(StoredObject):
|
||||||
node_id = attr.ib(type=bytes, converter=hex_to_bytes)
|
node_id = attr.ib(type=bytes, converter=hex_to_bytes)
|
||||||
|
@ -179,6 +181,7 @@ class ChannelBackupStorage(StoredObject):
|
||||||
|
|
||||||
def to_bytes(self):
|
def to_bytes(self):
|
||||||
vds = BCDataStream()
|
vds = BCDataStream()
|
||||||
|
vds.write_int16(CHANNEL_BACKUP_VERSION)
|
||||||
vds.write_boolean(self.is_initiator)
|
vds.write_boolean(self.is_initiator)
|
||||||
vds.write_bytes(self.privkey, 32)
|
vds.write_bytes(self.privkey, 32)
|
||||||
vds.write_bytes(self.channel_seed, 32)
|
vds.write_bytes(self.channel_seed, 32)
|
||||||
|
@ -198,6 +201,9 @@ class ChannelBackupStorage(StoredObject):
|
||||||
def from_bytes(s):
|
def from_bytes(s):
|
||||||
vds = BCDataStream()
|
vds = BCDataStream()
|
||||||
vds.write(s)
|
vds.write(s)
|
||||||
|
version = vds.read_int16()
|
||||||
|
if version != CHANNEL_BACKUP_VERSION:
|
||||||
|
raise Exception(f"unknown version for channel backup: {version}")
|
||||||
return ChannelBackupStorage(
|
return ChannelBackupStorage(
|
||||||
is_initiator = bool(vds.read_bytes(1)),
|
is_initiator = bool(vds.read_bytes(1)),
|
||||||
privkey = vds.read_bytes(32).hex(),
|
privkey = vds.read_bytes(32).hex(),
|
||||||
|
|
|
@ -66,7 +66,7 @@ from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_t
|
||||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
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_with_version_and_mac, pw_decode_with_version_and_mac
|
||||||
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 +1396,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_with_version_and_mac(backup_bytes, xpub)
|
||||||
assert backup_bytes == pw_decode_bytes(encrypted, xpub, version=PW_HASH_VERSION_LATEST), "encrypt failed"
|
assert backup_bytes == pw_decode_with_version_and_mac(encrypted, xpub), "encrypt failed"
|
||||||
return encrypted
|
return 'channel_backup:' + encrypted
|
||||||
|
|
||||||
|
|
||||||
class LNBackups(Logger):
|
class LNBackups(Logger):
|
||||||
|
@ -1449,9 +1449,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_with_version_and_mac(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")
|
||||||
|
|
Loading…
Add table
Reference in a new issue