Merge branch 'dev' into docker-dev-env

This commit is contained in:
Nick Klockenga
2023-09-13 23:02:44 -04:00
committed by GitHub
24 changed files with 282 additions and 121 deletions
+17 -17
View File
@@ -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.
<BR>
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+19 -3
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -22,5 +22,5 @@ setuptools.setup(
],
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
python_requires=">=3.6",
python_requires=">=3.10",
)
+1 -1
View File
@@ -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.
@@ -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
))
+3 -1
View File
@@ -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__()
@@ -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
))
+2 -2
View File
@@ -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
@@ -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)))
@@ -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
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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]
+6
View File
@@ -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
+5 -2
View File
@@ -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)
+12 -4
View File
@@ -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
+22 -3
View File
@@ -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):
+22 -11
View File
@@ -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"""
+14 -14
View File
@@ -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])
+27 -1
View File
@@ -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
+71 -2
View File
@@ -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
)
+12 -11
View File
@@ -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)
+4 -4
View File
@@ -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