mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch 'dev' into docker-dev-env
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Build an offline, airgapped Bitcoin signing device for less than $50!
|
||||
|
||||

|
||||

|
||||
|
||||
---------------
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
|
||||
|
||||
@@ -22,5 +22,5 @@ setuptools.setup(
|
||||
],
|
||||
package_dir={"": "src"},
|
||||
packages=setuptools.find_packages(where="src"),
|
||||
python_requires=">=3.6",
|
||||
python_requires=">=3.10",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user