Skip to content

Commit

Permalink
Merge pull request #333 from kdmukai/scan_qrtype_checks
Browse files Browse the repository at this point in the history
[Enhancement] Specific `ScanView` child classes to limit acceptable QR types
  • Loading branch information
newtonick authored Aug 6, 2023
2 parents 2c56308 + 272cdc3 commit 93e90ce
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 27 deletions.
4 changes: 3 additions & 1 deletion src/seedsigner/gui/screens/scan_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ScanScreen(BaseScreen):
this should probably be refactored into the Controller.
"""
decoder: DecodeQR = None
instructions_text: str = "< back | Scan a QR code"
instructions_text: str = None
resolution: tuple[int,int] = (480, 480)
framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W?
render_rect: tuple[int,int,int,int] = None
Expand All @@ -53,6 +53,8 @@ def __post_init__(self):
# Initialize the base class
super().__post_init__()

self.instructions_text = "< back | " + self.instructions_text

self.camera = Camera.get_instance()
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")

Expand Down
4 changes: 2 additions & 2 deletions src/seedsigner/views/psbt_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def run(self):
self.controller.resume_main_flow = Controller.FLOW__PSBT

if self.button_data[selected_menu_num] == self.SCAN_SEED:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
from seedsigner.views.scan_views import ScanSeedQRView
return Destination(ScanSeedQRView)

elif self.button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
Expand Down
97 changes: 89 additions & 8 deletions src/seedsigner/views/scan_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,66 @@

from embit.descriptor import Descriptor

from seedsigner.gui.screens import scan_screens
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.seed import Seed
from seedsigner.models.settings import SettingsConstants
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
from seedsigner.views.view import MainMenuView, NotYetImplementedView, View, Destination
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination



class ScanView(View):
"""
The catch-all generic scanning View that will accept any of our supported QR
formats and will route to the most sensible next step.
Can also be used as a base class for more specific scanning flows with
dedicated errors when an unexpected QR type is scanned (e.g. Scan PSBT was
selected but a SeedQR was scanned).
"""
instructions_text = "Scan a QR code"
invalid_qr_type_message = "QRCode not recognized or not yet supported."


def __init__(self):
super().__init__()

# Set up the QR decoder here so we can inject data into it in the test suite's
# `before_run`.
# Define the decoder here to make it available to child classes' is_valid_qr_type
# checks and so we can inject data into it in the test suite's `before_run()`.
self.wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.decoder = DecodeQR(wordlist_language_code=self.wordlist_language_code)
self.decoder: DecodeQR = DecodeQR(wordlist_language_code=self.wordlist_language_code)


@property
def is_valid_qr_type(self):
return True


def run(self):
from seedsigner.gui.screens.scan_screens import ScanScreen

# Start the live preview and background QR reading
self.run_screen(scan_screens.ScanScreen, decoder=self.decoder)
self.run_screen(
ScanScreen,
instructions_text=self.instructions_text,
decoder=self.decoder
)

# Handle the results
if self.decoder.is_complete:
if not self.is_valid_qr_type:
# We recognized the QR type but it was not the type expected for the
# current flow.
# Report QR types in more human-readable text (e.g. QRType
# `seed__compactseedqr` as "seed: compactseedqr").
return Destination(ErrorView, view_args=dict(
title="Error",
status_headline="Wrong QR Type",
text=self.invalid_qr_type_message + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""",
button_text="Back",
next_destination=Destination(BackStackView, skip_current_view=True),
))

if self.decoder.is_seed:
seed_mnemonic = self.decoder.get_seed_phrase()

Expand Down Expand Up @@ -104,6 +139,52 @@ def run(self):
return Destination(NotYetImplementedView)

elif self.decoder.is_invalid:
raise Exception("QRCode not recognized or not yet supported.")
return Destination(ErrorView, view_args=dict(
title="Error",
status_headline="Unknown QR Type",
text="QRCode is invalid or is a data format not yet supported.",
button_text="Back",
next_destination=Destination(BackStackView, skip_current_view=True),
))

return Destination(MainMenuView)



class ScanPSBTView(ScanView):
instructions_text = "Scan PSBT"
invalid_qr_type_message = "Expected a PSBT"

@property
def is_valid_qr_type(self):
return self.decoder.is_psbt



class ScanSeedQRView(ScanView):
instructions_text = "Scan SeedQR"
invalid_qr_type_message = f"Expected a SeedQR"

@property
def is_valid_qr_type(self):
return self.decoder.is_seed



class ScanWalletDescriptorView(ScanView):
instructions_text = "Scan descriptor"
invalid_qr_type_message = "Expected a wallet descriptor QR"

@property
def is_valid_qr_type(self):
return self.decoder.is_wallet_descriptor



class ScanAddressView(ScanView):
instructions_text = "Scan address QR"
invalid_qr_type_message = "Expected an address QR"

@property
def is_valid_qr_type(self):
return self.decoder.is_address
18 changes: 9 additions & 9 deletions src/seedsigner/views/seed_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.views.psbt_views import PSBTChangeDetailsView
from seedsigner.views.scan_views import ScanView
from seedsigner.views.view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView


Expand Down Expand Up @@ -96,8 +94,8 @@ def run(self):
return Destination(BackStackView)

if button_data[selected_menu_num] == self.SEED_QR:
from .scan_views import ScanView
return Destination(ScanView)
from .scan_views import ScanSeedQRView
return Destination(ScanSeedQRView)

elif button_data[selected_menu_num] == self.TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
Expand Down Expand Up @@ -409,9 +407,9 @@ def run(self):
return Destination(MainMenuView)

if button_data[selected_menu_num] == self.SCAN_PSBT:
from seedsigner.views.scan_views import ScanView
from seedsigner.views.scan_views import ScanPSBTView
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
return Destination(ScanView)
return Destination(ScanPSBTView)

elif button_data[selected_menu_num] == self.VERIFY_ADDRESS:
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
Expand Down Expand Up @@ -1566,8 +1564,8 @@ def run(self):
self.controller.resume_main_flow = Controller.FLOW__VERIFY_SINGLESIG_ADDR

if button_data[selected_menu_num] == SCAN_SEED:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
from seedsigner.views.scan_views import ScanSeedQRView
return Destination(ScanSeedQRView)

elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
Expand Down Expand Up @@ -1792,7 +1790,8 @@ def run(self):
).display()

if button_data[selected_menu_num] == SCAN:
return Destination(ScanView)
from seedsigner.views.scan_views import ScanWalletDescriptorView
return Destination(ScanWalletDescriptorView)

elif button_data[selected_menu_num] == CANCEL:
if self.controller.resume_main_flow == Controller.FLOW__PSBT:
Expand Down Expand Up @@ -1840,6 +1839,7 @@ def run(self):

elif button_data[selected_menu_num] == RETURN:
# Jump straight back to PSBT change verification
from seedsigner.views.psbt_views import PSBTChangeDetailsView
self.controller.resume_main_flow = None
return Destination(PSBTChangeDetailsView, view_args=dict(change_address_num=0))

Expand Down
18 changes: 14 additions & 4 deletions src/seedsigner/views/tools_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ class ToolsMenuView(View):
DICE = ("New seed", FontAwesomeIconConstants.DICE)
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
EXPLORER = "Address Explorer"
ADDRESS = "Verify address"

def run(self):
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER]
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER, self.ADDRESS]

selected_menu_num = self.run_screen(
ButtonListScreen,
Expand All @@ -53,6 +54,11 @@ def run(self):
elif button_data[selected_menu_num] == self.EXPLORER:
return Destination(ToolsAddressExplorerSelectSourceView)

elif button_data[selected_menu_num] == self.ADDRESS:
from seedsigner.views.scan_views import ScanAddressView
return Destination(ScanAddressView)



"""****************************************************************************
Image entropy Views
Expand Down Expand Up @@ -465,9 +471,13 @@ def run(self):
)
)

elif button_data[selected_menu_num] in [self.SCAN_SEED, self.SCAN_DESCRIPTOR]:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == self.SCAN_SEED:
from seedsigner.views.scan_views import ScanSeedQRView
return Destination(ScanSeedQRView)

elif button_data[selected_menu_num] == self.SCAN_DESCRIPTOR:
from seedsigner.views.scan_views import ScanWalletDescriptorView
return Destination(ScanWalletDescriptorView)

elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
Expand Down
23 changes: 23 additions & 0 deletions src/seedsigner/views/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,29 @@ def run(self):



@dataclass
class ErrorView(View):
"""
"""
title: str = "Error"
status_headline: str = None
text: str = None
button_text: str = None
next_destination: Destination = Destination(MainMenuView, clear_history=True)

def run(self):
self.run_screen(
WarningScreen,
title=self.title,
status_headline=self.status_headline,
text=self.text,
button_data=[self.button_text],
)

return self.next_destination



class UnhandledExceptionView(View):
def __init__(self, error: list[str]):
self.error = error
Expand Down
2 changes: 1 addition & 1 deletion tests/test_flows_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load_seed_into_decoder(view: scan_views.ScanView):
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder),
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(psbt_views.PSBTOverviewView),
Expand Down
21 changes: 19 additions & 2 deletions tests/test_flows_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from seedsigner.controller import Controller
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from seedsigner.views.view import MainMenuView
from seedsigner.views.view import ErrorView, MainMenuView
from seedsigner.views import scan_views, seed_views, tools_views


Expand Down Expand Up @@ -48,7 +48,7 @@ def load_seed_into_decoder(view: scan_views.ScanView):
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), # simulate read SeedQR
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView),
Expand All @@ -70,3 +70,20 @@ def load_seed_into_decoder(view: scan_views.ScanView):
FlowStep(seed_views.SeedExportXpubScriptTypeView),
]
)


def test_addressexplorer_scan_wrong_qrtype(self):
"""
Scanning the wrong type of QR code when a SeedQR is expected should route to ErrorView
"""
def load_wrong_data_into_decoder(view: scan_views.ScanView):
view.decoder.add_data("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")

# Finalize the new seed w/out passphrase
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED),
FlowStep(scan_views.ScanSeedQRView, before_run=load_wrong_data_into_decoder), # simulate scanning the wrong QR type
FlowStep(ErrorView),
])

0 comments on commit 93e90ce

Please sign in to comment.