diff --git a/README.md b/README.md index 7ef232ce..1cbfa827 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Build an offline, airgapped Bitcoin signing device for less than $50! -![Image of SeedSigners in Open Pill Enclosures](docs/img/Open_Pill_Star.JPG)![Image of SeedSigner in an Orange Pill enclosure](docs/img/Orange_Pill.JPG) +![Image of SeedSigners in Mini Pill Enclosures](docs/img/Mini_Pill_Main_Photo.jpg) --------------- @@ -82,27 +82,27 @@ Notes: ## A Special Note On Minimizing Trust As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images. -However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made availabile. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). +However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made available. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). ## Downloading the Software -Download the current Version (0.6.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. +Download the current Version (0.7.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. | Board | Download Image Link/Name | | --------------------- | --------------------------------- | -|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | -|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | -|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | -|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.6.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi2.img) | -|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | -|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | -|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | +|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) | +|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) | +|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) | +|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.7.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi2.img) | +|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) | +|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) | +|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) | Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. **also download** these 2 signature verification files to the same folder -[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256) -[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256.sig) +[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt) +[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt.sig) Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)! @@ -131,7 +131,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore* Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.) ``` -gpg --verify seedsigner.0.6.*.sha256.sig +gpg --verify seedsigner.0.7.*.sha256.txt.sig ``` When the verify command completes successfully, it should display output like this: @@ -201,21 +201,21 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi **On Linux or OSX:** Run this command ``` -shasum -a 256 --ignore-missing --check seedsigner.0.6.*.sha256 +shasum -a 256 --ignore-missing --check seedsigner.0.7.*.sha256.txt ``` **On Windows (inside Powershell):** Run this command ``` -CertUtil -hashfile seedsigner_os.0.6.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256 +CertUtil -hashfile seedsigner_os.0.7.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256 ``` On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file.
Wait up to 30 seconds for the command to complete, and it should display: ``` -seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img: OK +seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img: OK ``` -**If you receive the "OK" message** for your **seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! +**If you receive the "OK" message** for your **seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! **All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card๐Ÿ˜„๐Ÿ˜„ !! If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above. diff --git a/docs/img/Mini_Pill_Main_Photo.jpg b/docs/img/Mini_Pill_Main_Photo.jpg new file mode 100644 index 00000000..bf52af86 Binary files /dev/null and b/docs/img/Mini_Pill_Main_Photo.jpg differ diff --git a/docs/manual_installation.md b/docs/manual_installation.md index a8c63da5..47b35a9a 100644 --- a/docs/manual_installation.md +++ b/docs/manual_installation.md @@ -1,10 +1,12 @@ # Manual Installation Instructions -Begin by acquiring a specific copy of the Raspberry Pi Lite operating system, dated 2021-05-28; this version can be found here: +Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here: -https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-05-28/ +https://downloads.raspberrypi.org/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2023-05-03/ -Best practice is to verify the downloaded .zip file containing the Raspberry Pi Lite OS matches the published SHA256 hash of the file; for additional reference that hash is: c5dad159a2775c687e9281b1a0e586f7471690ae28f2f2282c90e7d59f64273c. After verifying the file's data integrity, you can decompress the .zip file to obtain the operating system image that it contains. You can then use Balena's Etcher tool (https://www.balena.io/etcher/) to write the Raspberry Pi Lite software image to a memory card (4 GB or larger). It's important to note that an image authoring tool must be used (the operating system image cannot be simply copied into a file storage partition on the memory card). +SeedSigner does not work any of the more recent versions of Debian. This is a known limitation and there are open tickets to track the progress of this ([Debian 11 ticket](https://github.com/SeedSigner/seedsigner/issues/431), [Debian 12 ticket](https://github.com/SeedSigner/seedsigner/issues/430)). This guide does not work on the 64-bit versions of Buster, however pull requests to update it to be compatible are welcome. + +Best practice is to verify the downloaded file containing the Raspberry Pi Lite OS matches the published SHA256 hash of the file; for additional reference that hash is: 3d210e61b057de4de90eadb46e28837585a9b24247c221998f5bead04f88624c. After verifying the file's data integrity, you can decompress the .tar.xz file to obtain the operating system image that it contains. You can then use Balena's Etcher tool (https://www.balena.io/etcher/) to write the Raspberry Pi Lite software image to a memory card (4 GB or larger). It's important to note that an image authoring tool must be used (the operating system image cannot be simply copied into a file storage partition on the memory card). The manual SeedSigner installation and configuration process requires an internet connection on the Pi to download the necessary libraries and code. If your Pi does not have onboard wifi, you have two options: @@ -177,6 +179,20 @@ git clone https://github.com/SeedSigner/seedsigner cd seedsigner ``` +### Adding swap space +Compiling the dependencies requires more RAM than is available on a Raspberry +Pi 3B, let alone a Zero. Temporarily adding 1GB of additional swap space will +work around this limitation. The `/swapfile` can be deleted after you reboot. + +If building on a Raspberry Pi board with more than 1GB of RAM, this step can +be safely skipped. + +```bash +sudo dd if=/dev/zero of=/swapfile bs=4096 count=$((1024*256)) +sudo chmod 0600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +``` ### Install Python `pip` dependencies: ```bash diff --git a/requirements.txt b/requirements.txt index 58bdc59f..65c471ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ embit==0.7.0 -numpy==1.21.1 -Pillow==8.2.0 +numpy==1.25.2 +Pillow==9.4.0 pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03 qrcode==7.3.1 six==1.16.0 diff --git a/setup.py b/setup.py index fb6eff2c..2b58f46e 100644 --- a/setup.py +++ b/setup.py @@ -22,5 +22,5 @@ setuptools.setup( ], package_dir={"": "src"}, packages=setuptools.find_packages(where="src"), - python_requires=">=3.6", + python_requires=">=3.10", ) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index c21efe9c..30a50076 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -93,7 +93,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.7.0-rc1" + VERSION = "0.7.0" # Declare class member vars with type hints to enable richer IDE support throughout # the code. diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 26a53bae..13d3dcf7 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -174,36 +174,3 @@ class ScanScreen(BaseScreen): self.camera.stop_video_stream_mode() break - - -@dataclass -class SettingsUpdatedScreen(ButtonListScreen): - config_name: str = None - title: str = "Settings QR" - is_bottom_list: bool = True - - def __post_init__(self): - # Customize defaults - self.button_data = ["Home"] - self.show_back_button = False - - super().__post_init__() - - start_y = self.top_nav.height + 20 - if self.config_name: - self.config_name_textarea = TextArea( - text=f'"{self.config_name}"', - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - ) - self.components.append(self.config_name_textarea) - start_y = self.config_name_textarea.screen_y + 50 - - self.components.append(TextArea( - text="Settings imported successfully!", - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - )) - diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index d82567b3..b229857d 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -826,6 +826,7 @@ class LargeIconStatusScreen(ButtonListScreen): status_color: str = GUIConstants.SUCCESS_COLOR status_headline: str = "Success!" # The colored text under the large icon text: str = "" # The body text of the screen + text_edge_padding: int = GUIConstants.EDGE_PADDING button_data: list = None allow_text_overflow: bool = False @@ -861,7 +862,7 @@ class LargeIconStatusScreen(ButtonListScreen): height=self.buttons[0].screen_y - next_y, text=self.text, width=self.canvas_width, - edge_padding=GUIConstants.EDGE_PADDING, # Don't render all the way up to the far left/right edges + edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges screen_y=next_y, allow_text_overflow=self.allow_text_overflow, )) @@ -935,6 +936,7 @@ class WarningEdgesThread(BaseThread): @dataclass class WarningEdgesMixin: status_color: str = GUIConstants.WARNING_COLOR + text_edge_padding: int = 2 * GUIConstants.EDGE_PADDING def __post_init__(self): super().__post_init__() diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index c09f1e27..55fa5dd0 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -296,3 +296,36 @@ class DonateScreen(BaseTopNavScreen): supersampling_factor=1, screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING )) + + + +@dataclass +class SettingsQRConfirmationScreen(ButtonListScreen): + config_name: str = None + title: str = "Settings QR" + status_message: str = "Settings updated..." + is_bottom_list: bool = True + + def __post_init__(self): + # Customize defaults + self.button_data = ["Home"] + self.show_back_button = False + super().__post_init__() + + start_y = self.top_nav.height + 20 + if self.config_name: + self.config_name_textarea = TextArea( + text=f'"{self.config_name}"', + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + ) + self.components.append(self.config_name_textarea) + start_y = self.config_name_textarea.screen_y + 50 + + self.components.append(TextArea( + text=self.status_message, + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + )) diff --git a/src/seedsigner/helpers/ur2/bytewords.py b/src/seedsigner/helpers/ur2/bytewords.py index b7e8432a..a1204ed2 100644 --- a/src/seedsigner/helpers/ur2/bytewords.py +++ b/src/seedsigner/helpers/ur2/bytewords.py @@ -126,7 +126,7 @@ class Bytewords: elif style == Bytewords_Style_minimal: return encode_minimal(bytes) else: - assert(False) + assert False @staticmethod def decode(style, str): @@ -137,4 +137,4 @@ class Bytewords: elif style == Bytewords_Style_minimal: return decode(str, 0, 2) else: - assert(False) + assert False diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index c0b3f873..69eb64e3 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -255,7 +255,7 @@ class FountainDecoder: elif self.is_failure(): return 'Exception: {}'.format(self.result) else: - assert(False) + assert False def print_part(self, p): print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index d539e460..ac43bb62 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -84,7 +84,7 @@ class Part: class FountainEncoder: def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): - assert(len(message) <= MAX_UINT32) + assert len(message) <= MAX_UINT32 self.message_len = len(message) self.checksum = crc32_int(message) self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) @@ -93,9 +93,9 @@ class FountainEncoder: @staticmethod def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): - assert(message_len > 0) - assert(min_fragment_len > 0) - assert(max_fragment_len >= min_fragment_len) + assert message_len > 0 + assert min_fragment_len > 0 + assert max_fragment_len >= min_fragment_len max_fragment_count = message_len // min_fragment_len fragment_len = None @@ -104,7 +104,7 @@ class FountainEncoder: if fragment_len <= max_fragment_len: break - assert(fragment_len != None) + assert fragment_len != None return fragment_len diff --git a/src/seedsigner/helpers/ur2/random_sampler.py b/src/seedsigner/helpers/ur2/random_sampler.py index 087789e8..d1b80411 100644 --- a/src/seedsigner/helpers/ur2/random_sampler.py +++ b/src/seedsigner/helpers/ur2/random_sampler.py @@ -9,11 +9,11 @@ class RandomSampler: def __init__(self, probs): for p in probs: - assert(p > 0) + assert p > 0 # Normalize given probabilities total = sum(probs) - assert(total > 0) + assert total > 0 n = len(probs) diff --git a/src/seedsigner/helpers/ur2/utils.py b/src/seedsigner/helpers/ur2/utils.py index 4e88c363..31a0dfe1 100644 --- a/src/seedsigner/helpers/ur2/utils.py +++ b/src/seedsigner/helpers/ur2/utils.py @@ -57,7 +57,7 @@ def join_bytes(list_of_ba): def xor_into(target, source): count = len(target) - assert(count == len(source)) # Must be the same length + assert count == len(source) # Must be the same length for i in range(count): target[i] ^= source[i] diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 7b5f4602..f0b1c97f 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -87,6 +87,12 @@ class Settings(Singleton): values = value for v in values: if v not in [opt[0] for opt in settings_entry.selection_options]: + if settings_entry.attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and v == SettingsConstants.OPTION__ENABLED: + # Special case: trying to enable Persistent Settings when + # DISABLED is the only option allowed (because the SD card is not + # inserted. Explicitly set to DISABLED. + value = SettingsConstants.OPTION__DISABLED + break raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""") updated_settings[settings_entry.attr_name] = value diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 9c4e5990..17f71333 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -151,12 +151,15 @@ class ScanView(View): return Destination(NotYetImplementedView) elif self.decoder.is_invalid: + # For now, don't even try to re-do the attempted operation, just reset and + # start everything over. + self.controller.resume_main_flow = None 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), + button_text="Done", + next_destination=Destination(MainMenuView, clear_history=True), )) return Destination(MainMenuView) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index ca9c36dc..e2a742ca 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -1,5 +1,6 @@ import logging from seedsigner.gui.components import SeedSignerIconConstants +from seedsigner.hardware.microsd import MicroSD from .view import View, Destination, MainMenuView @@ -195,14 +196,21 @@ class SettingsIngestSettingsQRView(View): # May raise an Exception which will bubble up to the Controller to display to the # user. self.config_name, settings_update_dict = Settings.parse_settingsqr(data) + self.settings.update(settings_update_dict) - + + if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED: + self.status_message = "Persistent Settings enabled. Settings saved to SD card." + else: + self.status_message = "Settings updated in temporary memory" + def run(self): - from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen + from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen self.run_screen( - SettingsUpdatedScreen, - config_name=self.config_name + SettingsQRConfirmationScreen, + config_name=self.config_name, + status_message=self.status_message, ) # Only one exit point diff --git a/tests/base.py b/tests/base.py index fadf6ff7..496e1b86 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,6 +1,6 @@ import sys from dataclasses import dataclass -from mock import MagicMock, patch +from mock import MagicMock, Mock, patch from typing import Callable # Prevent importing modules w/Raspi hardware dependencies. @@ -11,17 +11,28 @@ sys.modules['seedsigner.gui.toast'] = MagicMock() sys.modules['seedsigner.views.screensaver'] = MagicMock() sys.modules['seedsigner.hardware.buttons'] = MagicMock() sys.modules['seedsigner.hardware.camera'] = MagicMock() -sys.modules['seedsigner.hardware.microsd'] = MagicMock() from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON +from seedsigner.hardware.microsd import MicroSD from seedsigner.models.settings import Settings from seedsigner.views.view import Destination, MainMenuView, View + class BaseTest: + class MockMicroSD(Mock): + """ + A test suite-friendly replacement for `MicroSD` that gives a test explicit + control over the reported state of the SD card. + """ + # Tests are free to directly manipulate this attribute as needed (it's reset to + # True before each test in `BaseTest.setup_method()`). + is_inserted: bool = True + + @classmethod def setup_class(cls): # Ensure there are no on-disk artifacts after running tests. @@ -30,6 +41,13 @@ class BaseTest: # Mock out the loading screen so it can't spawn. View classes must import locally! patch('seedsigner.gui.screens.screen.LoadingScreenThread').start() + # Instantiate the mocked MicroSD; hold on to the instance so tests can manipulate + # it later. + cls.mock_microsd = BaseTest.MockMicroSD() + + # And mock it over `MicroSD`'s instance + MicroSD.get_instance = Mock(return_value=cls.mock_microsd) + @classmethod def teardown_class(cls): @@ -62,11 +80,12 @@ class BaseTest: def setup_method(self): - """ Guarantee a clean/default Controller and Settings state for each test case """ + """ Guarantee a clean/default Controller, Settings, & MicroSD state for each test case """ BaseTest.reset_controller() BaseTest.reset_settings() self.controller = Controller.get_instance() self.settings = Settings.get_instance() + self.mock_microsd.is_inserted = True def teardown_method(self): diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 88a010ba..83a58135 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -31,7 +31,7 @@ from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, psbt_views, seed_views, settings_views, tools_views) -from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View +from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View from .utils import ScreenshotComplete, ScreenshotRenderer @@ -110,10 +110,11 @@ def test_generate_screenshots(target_locale): continue settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) - settings_views_list.append(settings_views.IOTestView) - settings_views_list.append(settings_views.DonateView) + settingsqr_data_persistent = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + settingsqr_data_not_persistent = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + screenshot_sections = { "Main Menu Views": [ MainMenuView, @@ -123,13 +124,6 @@ def test_generate_screenshots(target_locale): PowerOptionsView, RestartView, PowerOffView, - NotYetImplementedView, - (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), - (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")), - NetworkMismatchErrorView, - (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), - - ], "Seed Views": [ seed_views.SeedsMenuView, @@ -221,7 +215,24 @@ def test_generate_screenshots(target_locale): tools_views.ToolsAddressExplorerAddressListView, #tools_views.ToolsAddressExplorerAddressView, ], - "Settings Views": settings_views_list, + "Settings Views": settings_views_list + [ + settings_views.IOTestView, + settings_views.DonateView, + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"), + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"), + ], + "Misc Error Views": [ + NotYetImplementedView, + (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), + NetworkMismatchErrorView, + (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), + (ErrorView, dict( + title="Error", + status_headline="Unknown QR Type", + text="QRCode is invalid or is a data format not yet supported.", + button_text="Back", + )), + ] } readme = f"""# SeedSigner Screenshots\n""" diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index 1b7bfaa8..562d6aa2 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -331,12 +331,12 @@ def test_parse_derivation_path(): derivation_path = "m/84'/0'/0'/0/0" result = embit_utils.parse_derivation_path(derivation_path) - assert(result["script_type"] == SC.NATIVE_SEGWIT) - assert(result["network"] == SC.MAINNET) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET result = embit_utils.parse_derivation_path(derivation_path.replace("'", "h")) - assert(result["script_type"] == SC.NATIVE_SEGWIT) - assert(result["network"] == SC.MAINNET) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET # Now exhaustively test supported permutations vectors_args = { @@ -383,20 +383,20 @@ def test_parse_derivation_path(): actual_result = embit_utils.parse_derivation_path(derivation_path) if expected_result[0] == SC.MAINNET: - assert(actual_result["network"] == expected_result[0]) - assert(actual_result["clean_match"] is True) + assert actual_result["network"] == expected_result[0] + assert actual_result["clean_match"] is True elif expected_result[0] is None: - assert(actual_result["network"] is None) - assert(actual_result["clean_match"] is False) + assert actual_result["network"] is None + assert actual_result["clean_match"] is False else: # Testnet and regtest are returned as a list since the parser can't tell which is intended - assert(expected_result[0] in actual_result["network"]) - assert(actual_result["clean_match"] is True) + assert expected_result[0] in actual_result["network"] + assert actual_result["clean_match"] is True - assert(actual_result["script_type"] == expected_result[1]) - assert(actual_result["is_change"] == expected_result[2]) + assert actual_result["script_type"] == expected_result[1] + assert actual_result["is_change"] == expected_result[2] if len(expected_result) == 4: - assert(actual_result["index"] == expected_result[3]) + assert actual_result["index"] == expected_result[3] else: - assert(actual_result["index"] == int(derivation_path.split("/")[-1])) + assert actual_result["index"] == int(derivation_path.split("/")[-1]) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 521d77bc..3aedef94 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -8,7 +8,7 @@ from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDat from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.seed import Seed -from seedsigner.views.view import MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView +from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView from seedsigner.views import seed_views, scan_views, settings_views, tools_views @@ -477,3 +477,29 @@ class TestMessageSigningFlows(FlowTest): FlowStep(MainMenuView), ] ) + + + def test_sign_message_invalid_qr_flow(self): + """ + Should clear `Controller.resume_main_flow` and redirect to ErrorView if an + invalid signmessage QR is scanned. + + The error view should then forward to MainMenuView. + """ + # Ensure message signing is enabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) + + def load_invalid_signmessage_qr(view: scan_views.ScanView): + view.decoder.add_data("this text will not make sense to the decoder") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), + FlowStep(scan_views.ScanView, before_run=load_invalid_signmessage_qr), # simulate read message QR; ret val is ignored + FlowStep(ErrorView), + FlowStep(MainMenuView), + ]) + + assert self.controller.resume_main_flow is None diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index 1fb394a2..a6eb34d9 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -1,4 +1,7 @@ import os +from typing import Callable + +from mock import PropertyMock, patch # Must import test base before the Controller from base import FlowTest, FlowStep @@ -6,13 +9,13 @@ from base import FlowTest, FlowStep from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON +from seedsigner.hardware.microsd import MicroSD from seedsigner.views.view import MainMenuView -from seedsigner.views import settings_views +from seedsigner.views import scan_views, settings_views class TestSettingsFlows(FlowTest): - def test_persistent_settings(self): """ Basic flow from MainMenuView to enable/disable persistent settings """ # Which option are we testing? @@ -67,3 +70,69 @@ class TestSettingsFlows(FlowTest): FlowStep(settings_views.DonateView), FlowStep(settings_views.SettingsMenuView), ]) + + + def test_settingsqr(self): + """ + Scanning a SettingsQR should present the success screen and then return to + MainMenuView. + """ + def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_persistent) + + def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_not_persistent) + + def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str): + self.settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, initial_setting_state) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_settingsqr_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(settings_views.SettingsIngestSettingsQRView), # ret val is ignored + FlowStep(MainMenuView), + ]) + + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == expected_setting_state + + + # First load a SettingsQR that enables persistent settings + self.mock_microsd.is_inserted = True + assert MicroSD.get_instance().is_inserted is True + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__ENABLED + ) + + # Then one that disables it + _run_test( + initial_setting_state=SettingsConstants.OPTION__ENABLED, + load_settingsqr_into_decoder=load_not_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) + + # Now try to enable persistent settings when the SD card is not inserted + self.mock_microsd.is_inserted = False + assert MicroSD.get_instance().is_inserted is False + + # Have to jump through some hoops to completely simulate the SD card being + # removed; we need Settings to restrict Persistent Settings to only allow + # DISABLED. + with patch('seedsigner.models.settings.Settings.HOSTNAME', new_callable=PropertyMock) as mock_hostname: + # Must identify itself as SeedSigner OS to trigger the SD card removal logic + mock_hostname.return_value = Settings.SEEDSIGNER_OS + Settings.handle_microsd_state_change(MicroSD.ACTION__REMOVED) + + selection_options = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS).selection_options + assert len(selection_options) == 1 + assert selection_options[0][0] == SettingsConstants.OPTION__DISABLED + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) diff --git a/tests/test_settings.py b/tests/test_settings.py index 38ca68c1..94ac626a 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -8,6 +8,7 @@ from seedsigner.models.settings_definition import SettingsConstants class TestSettings(BaseTest): @classmethod def setup_class(cls): + super().setup_class() cls.settings = Settings.get_instance() @@ -37,17 +38,17 @@ class TestSettings(BaseTest): # Now parse the settingsqr_data config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert(config_name == settings_name) + assert config_name == settings_name self.settings.update(new_settings=settings_update_dict) # Now verify that the settings were updated correctly - assert(self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED) - assert(self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED) + assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED + assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) - assert(SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators) - assert(SettingsConstants.COORDINATOR__SPARROW in coordinators) - assert(SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators) + assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators + assert SettingsConstants.COORDINATOR__SPARROW in coordinators + assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators def test_settingsqr_version(self): @@ -61,7 +62,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v2 name=Foo" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) - assert("Unsupported SettingsQR version" in str(e.value)) + assert "Unsupported SettingsQR version" in str(e.value) # Should also fail if version omitted settingsqr_data = "settings name=Foo" @@ -79,8 +80,8 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert("favorite_food" not in settings_update_dict) - assert("xpub_export" in settings_update_dict) + assert "favorite_food" not in settings_update_dict + assert "xpub_export" in settings_update_dict # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) @@ -91,7 +92,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1 name=Foo xpub_export=Yep" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) - assert("xpub_export" in str(e.value)) + assert "xpub_export" in str(e.value) def test_settingsqr_parses_line_break_separators(self): @@ -99,7 +100,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert(len(settings_update_dict.keys()) == 3) + assert len(settings_update_dict.keys()) == 3 # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) diff --git a/tests/test_settingsqr_decoder.py b/tests/test_settingsqr_decoder.py index ac2720b3..bb124c4f 100644 --- a/tests/test_settingsqr_decoder.py +++ b/tests/test_settingsqr_decoder.py @@ -14,11 +14,11 @@ class TestSettingsQRDecoder: # Now parse the settings_qr_str decoder = DecodeQR() status = decoder.add_data(settings_qr_str) - assert(decoder.is_settings) - assert(status == DecodeQRStatus.COMPLETE) + assert decoder.is_settings + assert status == DecodeQRStatus.COMPLETE data = decoder.get_settings_data() - assert(data == settings_qr_str) + assert data == settings_qr_str def test_settingsqr_version(self): @@ -26,4 +26,4 @@ class TestSettingsQRDecoder: settings_qr_str = "name=Foo" decoder = DecodeQR() status = decoder.add_data(settings_qr_str) - assert(status == DecodeQRStatus.INVALID) + assert status == DecodeQRStatus.INVALID