diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 0da76fcd7..4613ab89c 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -224,6 +224,10 @@ def get_seed_phrase(self): if self.is_seed: return self.decoder.get_seed_phrase() + def get_decrypted_text(self): + if self.is_encrypted_seedqr: + return self.decoder.get_decrypted_text() + def get_slip39_share(self): if self.is_slip39_share: return self.decoder.get_share() @@ -1399,6 +1403,7 @@ def __init__(self): super().__init__() self.public_data = None self.seed_phrase = [] + self.decrypted_text = None def add(self, segment, qr_type=QRType.SEED__ENCRYPTEDQR, encryption_key=None): @@ -1434,9 +1439,21 @@ def add(self, segment, qr_type=QRType.SEED__ENCRYPTEDQR, encryption_key=None): word_bytes = encrypted_qr.decrypt(encryption_key) if not word_bytes: return DecodeQRStatus.WRONG_KEY - self.seed_phrase = bip39.mnemonic_from_bytes(word_bytes).split() + self.decrypted_text = None + try: + self.seed_phrase = bip39.mnemonic_from_bytes(word_bytes).split() + except Exception: + self.seed_phrase = [] + try: + decoded_text = word_bytes.decode("utf-8") + except UnicodeDecodeError: + decoded_text = word_bytes.decode("utf-8", errors="replace") + self.decrypted_text = decoded_text + else: + self.decrypted_text = None else: self.seed_phrase = [] + self.decrypted_text = None self.complete = True self.collected_segments = 1 @@ -1456,6 +1473,9 @@ def get_seed_phrase(self): return self.seed_phrase[:] + def get_decrypted_text(self): + return self.decrypted_text + class TextQrDecoder(BaseSingleFrameQrDecoder): def __init__(self): diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index f54720018..9b9d60355 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -5,6 +5,7 @@ #from embit.descriptor import Descriptor +from seedsigner.gui.components import GUIConstants from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen from seedsigner.gui.screens.scan_screens import ScanEncryptedQRScreen, ScanTypeEncryptionKeyScreen, ScanReviewEncryptionKeyScreen from seedsigner.gui.screens import LargeIconStatusScreen @@ -638,15 +639,44 @@ def run(self): if status == DecodeQRStatus.COMPLETE: self.controller.storage2.clear_encryptedqr() - self.controller.storage.set_pending_seed( - Seed(mnemonic=decoder.get_seed_phrase(), wordlist_language_code=self.wordlist_language_code) - ) - if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED: - from seedsigner.views.seed_views import SeedAddPassphraseView - return Destination(SeedAddPassphraseView, skip_current_view=True) - else: - from .seed_views import SeedFinalizeView - return Destination(SeedFinalizeView, skip_current_view=True) + seed_phrase = decoder.get_seed_phrase() + + if seed_phrase: + try: + self.controller.storage.set_pending_seed( + Seed(mnemonic=seed_phrase, wordlist_language_code=self.wordlist_language_code) + ) + except InvalidSeedException: + seed_phrase = [] + else: + if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED: + from seedsigner.views.seed_views import SeedAddPassphraseView + return Destination(SeedAddPassphraseView, skip_current_view=True) + else: + from .seed_views import SeedFinalizeView + return Destination(SeedFinalizeView, skip_current_view=True) + + decrypted_text = decoder.get_decrypted_text() + if decrypted_text is not None: + self.run_screen( + LargeIconStatusScreen, + title=_("Decrypted Text"), + status_headline=None, + text=decrypted_text, + show_back_button=False, + button_data=[ButtonOption(_("Done"))], + status_icon_size=0, + status_color=GUIConstants.BODY_FONT_COLOR, + ) + return Destination(MainMenuView, skip_current_view=True) + + WarningScreen( + title="Error", + show_back_button=False, + status_headline="decryption failure", + text="Unknown error", + ).display() + return Destination(BackStackView) elif status == DecodeQRStatus.WRONG_KEY: WarningScreen( diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 06530286b..6b0a6ed5f 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -1,7 +1,10 @@ import os +from types import SimpleNamespace + from embit import bip39 -from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus +from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus, EncryptedQrDecoder from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder +from seedsigner.models.encryptedqr import EncryptedQR from seedsigner.models.qr_type import QRType from seedsigner.helpers.qr import QR @@ -161,3 +164,43 @@ def test_compact_seedqr_bytes_interpretable_as_str(): entropy_bytes.decode() # should not raise an exception mnemonic_length = 12 if len(entropy_bytes) == 16 else 24 run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR) + + +def test_encrypted_qr_falls_back_to_text(monkeypatch): + class DummyStorage: + def __init__(self): + self._encryptedqr = None + + @property + def encryptedqr(self): + return self._encryptedqr + + def set_encryptedqr(self, encryptedqr): + self._encryptedqr = encryptedqr + + def clear_encryptedqr(self): + self._encryptedqr = None + + dummy_storage = DummyStorage() + dummy_controller = SimpleNamespace(storage2=dummy_storage) + + monkeypatch.setattr( + "seedsigner.controller.Controller.get_instance", + lambda: dummy_controller, + ) + + class DummyEncryptedQRCode: + def decrypt(self, key): + assert key == "test-key" + return b"hello world" + + dummy_storage.set_encryptedqr(EncryptedQR(encrypted_qr=DummyEncryptedQRCode(), public_data="info")) + + decoder = EncryptedQrDecoder() + status = decoder.add(None, qr_type=QRType.SEED__ENCRYPTEDQR, encryption_key="test-key") + + assert status == DecodeQRStatus.COMPLETE + assert decoder.get_seed_phrase() == [] + assert decoder.get_decrypted_text() == "hello world" + + dummy_storage.clear_encryptedqr()