Skip to content
Open
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
22 changes: 21 additions & 1 deletion src/seedsigner/models/decode_qr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
48 changes: 39 additions & 9 deletions src/seedsigner/views/scan_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
45 changes: 44 additions & 1 deletion tests/test_seedqr.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
Loading