Merge remote-tracking branch 'upstream/0_4_6' into ui_overhaul

This commit is contained in:
kdmukai
2022-02-14 10:36:08 -06:00
28 changed files with 1032 additions and 141 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "urtypes"]
path = src/vendor/urtypes
url = https://github.com/jreesun/urtypes.git
+203 -75
View File
@@ -1,90 +1,175 @@
# Build an offline, airgapped Bitcoin signing device for less than $50!
![Image of SeedSigner in an Orange Pill enclosure](docs/img/Orange_Pill.JPG)
![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)
---------------
## Contributors Welcome!
# Project Summary
* Now that the overall code structure has been largely established, we welcome potential feature additions/improvements
* We are also actively seeking contributors to create walkthroughs/tutorials on building and using SeedSigner
* We are seeking a smaller group of more technical users to test new features and upcoming releases
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multi-signature wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
Please contact @SeedSigner on Twitter/Telegram to inquire with any questions about contributing!
Additional information about the project can be found at [seedsigner.com](https://seedsigner.com).
You can follow [@SeedSigner](https://twitter.com/SeedSigner) on Twitter for the latest project news and developments.
If you have specific questions about the project, our [Telegram Group](https://t.me/joinchat/GHNuc_nhNQjLPWsS) is a great place to ask them.
### Feature Highlights:
* Calculate word 12/24 of a BIP39 seed phrase
* Create a 24-word BIP39 seed phrase with 99 dice rolls
* Create a 24-word BIP39 seed phrase by taking a digital photo
* Temporarily store up to 3 seed phrases while device is powered
* Guided interface to manually create a SeedQR for instant input [(demo video here)](https://youtu.be/c1-PqTNx1vc)
* BIP39 passphrase / word 25 support
* Native Segwit Multisig XPUB generation w/ QR display
* Scan and parse transaction data from animated QR codes
* Sign transactions & transfer XPUB data using animated QR codes [(demo video here)](https://youtu.be/LPqvdQ2gSzs)
* Live preview during photo-to-seed and QR scanning UX
* Optimized seed word entry interface
* Support for Bitcoin Mainnet & Testnet
* Support for custom user-defined derivation paths
* On-demand receive address verification
* User-configurable QR code display density
* Responsive, event-driven user interface
### Considerations:
* Built for compatibility with Specter-desktop, Sparrow and BlueWallet Vaults
* Device takes up to 60 seconds to boot before menu appears (be patient!)
* Always test your setup before transfering larger amounts of bitcoin (try testnet first!)
* Taproot not quite yet supported
* Slightly rotating the screen clockwise or counter-clockwise should resolve lighting/glare issues
* If you think SeedSigner adds value to the Bitcoin ecosystem, please help us spread the word! (tweets, pics, videos, etc.)
### Planned Upcoming Improvements / Functionality:
* Single-sig and multi-sig change address verification
* Re-imagined, graphically-focused user interface
* Multi-language support
* Customized Linux live-boot OS to allow MicroSD card removal
* Other optimizations based on user feedback!
### Known Bug:
* Dice-to-Seed module incorrectly processes "6" inputs, making resulting seeds containing that number inconsistent with Coldcard / Ian Coleman routines; this error has been corrected in the main branch for those building for source, and will be corrected in the next pre-assembled release
---------------
## Shopping List
# Shopping List
To build a SeedSigner, you will need:
* Raspberry Pi Zero (version 1.3 with no wireless capability)
* Waveshare 1.3" 240x240 pxl LCD (more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
* Pi Zero-conpatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work)
* Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
Notes:
* You will need to solder the 40 GPIO pins (20 pins per row) to the Raspberry Pi Zero board. If you don't want to solder, purchase "GPIO Hammer Headers" for a solderless experience.
* Other cameras with the above sensor module should work, but may not fit in the Orange Pill enclosure
* Choose the Waveshare screen carefully; make sure to purchase the model that has a resolution of 240x240 pixels
The easiest way to install the software is to download the "seedsigner_X_X_X.zip" file in the current release, extract the seedsigner .img file, and write it to a MicroSD card (at least 4GB in size or larger). Then install the MicroSD in the assembled hardware and off you go.
---------------
# Important Note on Software Installation
The quickest and easiest way to install the software is to download the most recent "seedsigner_X_X_X.zip" file in the [software releases](https://github.com/SeedSigner/seedsigner/releases) section of this repository. After downloading the .zip file, extract the seedsigner .img file, and write it to a MicroSD card (at least 4GB in size or larger). Then install the MicroSD in the assembled hardware and off you go. If your goal is a more trustless installation, you can follow the manual software installation instructions below.
---------------
## Now make your own "Orange Pill" enclosure too!
# Important Note on Software Authenticity Verification
The Orange Pill enclosure design has been open-sourced! Check out the "Orange_Pill" folder in this repo. You'll need the following additional hardware to assemble it:
You can verify the data integrity and authenticity of the latest release with as little as three commands (though moving forward you will have to replace the version in the following commands with the version number you are attempting to validate). This process assumes that you have navigated to a folder where you have these four relevant files present:
* seedsigner_pubkey.gpg (from the main folder of this repo)
* seedsigner_0_4_5.img.zip (from the software release)
* seedsigner_0_4_5.img.zip.sha256 (from the software release)
* seedsigner_0_4_5.img.zip.sha256.sig (from the software release)
This process also assumes you are running the commands from a system where both GPG and shasum are installed and working.
First make sure that the public key is present in your keychain:
```
gpg --import seedsigner_pubkey.gpg
```
This command will import the public key, or return:
```
key <...> not changed
```
Now you can verify the authenticity of the small text file containing the release's SHA256 hash with the command:
```
gpg --verify seedsigner_0_4_5.img.zip.sha256.sig
```
The reponse to this command should include the text:
```
Good signature from "seedsigner <btc.hardware.solutions@gmail.com>" [unknown]
```
The previous command validates that aforementioned small text file was signed using the private key that matches the published public key associated with the project (an early timestamped record of this public/private key's creation can be found in this [tweet](https://twitter.com/SeedSigner/status/1389617642286329856?s=20)).
The last step is to make sure the .zip file that you've downloaded, and that contains the released software, is a perfect match to the software that was published by the holder of the private key in the last step. The command for this step is:
```
shasum -a 256 -c seedsigner_0_4_5.img.zip.sha256
```
The reponse to this command should include the text:
```
seedsigner_0_4_5.img.zip: OK
```
There are other steps you can take to verify the software, including examining the hash value in the .sha256 text file, but this one has been documented here because it seems the simplest for most people to follow. Please recognize that this process can only validate the software to the extent that the entity that first published the key is an honest actor, and assumes the private key has remained uncompromised and is not being used by a malicious actor.
---------------
# Enclosure Designs
### Open Pill
The Open Pill enclosure design is all about quick, simple and inexpensive depoloyment of a SeedSigner device. The design does not require any additional hardware and can be printed using a standard FDM 3D printer in about 2 hours, no supports necessary. A video demonstrating the assembly process can be found [here](https://youtu.be/gXPFJygZobEa). To access the design file and printable model, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/open_pill).
### Orange Pill
The Orange Pill enclosure design offers a more finished look that includes button covers and a joystick topper. You'll also need the following additional hardware to assemble it:
* 4 x F-F M2.5 spacers, 10mm length
* 4 x M2.5 pan head screws, 6mm length
* 4 x M2.5 pan head screws, 12mm length
The upper and lower portions of the enclosure can be printed with a conventional FDM 3D printer, 0.2mm layer height is fine, no supports necessary. The buttons and joystick nub should be produced with a SLA/resin printer (orient the square hole on the joystick nub away from the build plate). An overview of the entire assembly process can be found here:
https://youtu.be/aIIc2DiZYcI
The upper and lower portions of the enclosure can be printed using a standard FDM 3D printer, no supports necessary. The buttons and joystick nub should ideally be produced with a SLA/resin printer. An overview of the entire assembly process can be found [here](https://youtu.be/aIIc2DiZYcI). To access the design files and printable models, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/orange_pill).
---------------
### Feature Highlights:
* Calculate word 12/24 of a BIP39 seed phrase
* Create 24-word BIP39 seed phrase with 99 dice rolls
* Temporarily store up to 3 seed phrases while device is powered
* Native Segwit Multisig XPUB generation w/ QR display
* Scan and parse transaction data from animated QR codes
* Sign transactions & transfer XPUB data using animated QR codes
* Support for Bitcoin Mainnet & Testnet
* Responsive, event-driven user interface
* Only valid input letters appear during seed word entry (time-saver!)
# Manual Software Installation Instructions:
### Considerations:
* Built for compatibility with Specter-desktop, Sparrow and BlueWallet Vaults
* Current release takes ~50 seconds to boot before menu appears (be patient!)
* Always test your setup before transfering larger amounts of bitcoin (try testnet first!)
* Currently ONLY generating Native Segwit Multisig Xpubs
* (Holding the screen upside-down or at a slight angle can significantly reduce glare)
* Check out our "seedsigner" telegram group for community help / feedback: (https://t.me/joinchat/GHNuc_nhNQjLPWsS)
* If you think SeedSigner adds value to the Bitcoin ecosystem, please help me spread the word! (tweets, pics, videos, etc.)
Begin by acquiring a specific copy of the Raspberry Pi Lite operating system, dated 2021-05-28; this version can be found here:
### Planned Upcoming Improvements / Functionality:
* Support for single-signature XPUB generation / signing
* Support for BIP39 passphrases
* User-guided manual QR transcription to quickly input seed phrases
* Low/Med/High customizable QR density
* Other optimizations based on user feedback!
https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-05-28/
---------------
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).
## MANUAL BUILD INSTRUCTIONS:
Begin by preparing a copy of the Raspberry Pi Lite operating system (https://www.raspberrypi.org/software/operating-systems/) on a MicroSD card. Their [Raspberry Pi Imager](https://www.raspberrypi.org/software/) tool makes this easy.
SeedSigner installation and configuration requires an internet connection on the device to download the necessary libraries and code. But because the Pi Zero 1.3 does not have onboard wifi, you have two options:
The manual SeedSigner installation and configuration process requires an internet connection on the device to download the necessary libraries and code. But because the Pi Zero 1.3 does not have onboard wifi, you have two options:
1. Run these steps on a separate Raspberry Pi 2/3/4 or Zero W which can connect to the internet and then transfer the SD card to the Pi Zero 1.3 when complete.
2. OR configure the Pi Zero 1.3 directly by relaying through your computer's internet connection over USB. See instructions [here](docs/usb_relay.md).
Connect a keyboard & monitor to the device or SSH into the Pi if you're familiar with that process.
For the following steps you'll need to either connect a keyboard & monitor to the network-connected Raspberry Pi you are working with, or SSH into the Pi if you're familiar with that process.
### Configure the Pi
On the Pi bring up its system config:
First things first, verify that you are using the correct version of the Raspberry Pi Lite operating system by typing the command:
```
cat /etc/os-release
```
The output of this command should match the following text:
```
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
```
Now launch the Raspberry Pi's System Configuration tool using the command:
```
sudo raspi-config
```
@@ -95,14 +180,15 @@ Set the following:
* `SPI`: enable
* `Localisation Options`:
* `Locale`: arrow up and down through the list and select or deselect languages with the spacebar.
* `en_US.UTF-8 UTF-8` for US English
* WiFi settings (only necessary for the option #1 setup above)
* Deselect the default language option that is selected
* Select `en_US.UTF-8 UTF-8` for US English
* You will also need to configure the WiFi settings if you are using the #1 option above to connect to the internet
Exit and reboot when prompted within the raspi-config interface.
When you exit the System Configuration tool, you will be prompted to reboot the system; allow the system to reboot and continue with these instructions.
Install these dependencies:
Install these dependencies (you can use this entire text string to install them all at once):
```
sudo apt-get update && sudo apt-get install -y wiringpi python3-pip python3-numpy python-pil libopenjp2-7 ttf-mscorefonts-installer git python3-opencv libzbar0 python3-picamera libatlas-base-dev qrencode
sudo apt-get update && sudo apt-get install -y wiringpi python3-pip python3-numpy python-pil libopenjp2-7 git python3-opencv libzbar0 python3-picamera libatlas-base-dev qrencode
```
Install the [C library for Broadcom BCM 2835](http://www.airspayce.com/mikem/bcm2835/):
@@ -116,12 +202,12 @@ cd ..
rm bcm2835-1.60.tar.gz
```
Set up virtualenv
Install the "virtualenvwrapper" tool:
```
pip3 install virtualenvwrapper
```
Edit your bash profile with `nano ~/.profile` and add the following to the end:
Edit your bash profile with the command `nano ~/.profile` and add the following to the end:
```
export WORKON_HOME=$HOME/.envs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
@@ -129,20 +215,29 @@ source /home/pi/.local/bin/virtualenvwrapper.sh
```
Then `CTRL-X` and `y` to exit and save changes.
Now create the python virtualenv for SeedSigner:
Now create the python virtualenv for SeedSigner with these two commands:
```
source ~/.profile
mkvirtualenv --python=python3 seedsigner-env
```
Download SeedSigner
Now download the SeedSigner code:
```
git clone https://github.com/SeedSigner/seedsigner
git clone --recursive https://github.com/SeedSigner/seedsigner
```
Install python dependencies:
If you have already cloned in the past and just need to init/pull submodules:
```
cd seedsigner
git submodule update --init
cd ..
```
Install the necessary Python dependencies:
```
cd seedsigner
pip3 install -r requirements.txt
cd ..
```
Modify the systemd to run SeedSigner at boot:
@@ -150,7 +245,7 @@ Modify the systemd to run SeedSigner at boot:
sudo nano /etc/systemd/system/seedsigner.service
```
Add the following contents to the file:
Add the following contents to the text file that was created:
```
[Unit]
Description=Seedsigner
@@ -158,36 +253,69 @@ Description=Seedsigner
[Service]
User=pi
WorkingDirectory=/home/pi/seedsigner/src/
ExecStart=/usr/bin/python3 main.py
ExecStart=/home/pi/.envs/seedsigner-env/bin/python main.py
Restart=always
[Install]
WantedBy=multi-user.target
```
Use Control + O, then [enter], to write the file.
Then use Control + X, to exit the program.
Use `CTRL-X` and `y` to exit and save changes.
Run `sudo systemctl enable seedsigner.service` to enable service on boot. (This will restart the seedsigner code automatically at startup and if it crashes.)
(Optional) Modify the system swap configuration to disable virtual memory:
```sudo nano /etc/dphys-swapfile```
Change `CONF_SWAPSIZE=100` to `CONF_SWAPSIZE=0`
Use Control + O, then [enter], to write the file.
Then use Control + X, to exit the program.
If you completed these steps on a separate Pi (option #1), shut down the pi with `sudo shutdown --poweroff now` and then transfer the SD card to the Pi Zero 1.3 (_note: the LCD Hat will also run fine on a Raspberry Pi 2/3/4 or Zero W; just remember to disable networking if you want to run the software in isolation)_.
OR if you're working directly on the Pi Zero 1.3 (option #2), just reboot it:
Now reboot the Raspberry Pi with the command:
```
sudo reboot
```
It will take about a minute after the Pi is powered on for the GUI to launch -- be patient!
After the Raspberry Pi reboots, you should see the SeedSigner splash screen and the SeedSigner menu subsequently appear on the LCD screen (note that it can take up to 60 seconds for the menu to appear).
_Reminder: If you used option #2, [return the guide](docs/usb_relay.md) to remove the internet access over USB configuration._
Next, disable and remove the system's virtual memory / swap file with the commands:
```
sudo apt remove dphys-swapfile
sudo rm /var/swap
```
For those who will use the SeedSigner installation for testing/development, it can be helpful to change the system's host name so it doesn't potentially conflict with other Raspberry Pi's that may already be present on your network. (For those who don't plan to use the installation for testing or development, you can skip this portion of the process.) To change the host name first edit the "hostname" with the command:
```
sudo nano /etc/hostname
```
and change "raspberrypi" to "seedsigner" (or another name). Use `CTRL-X` and `y` to exit and save changes. You'll also need to edit the "hosts" file with the command:
```
sudo nano /etc/hosts
```
and change "raspberrypi" to "seedsigner" (or the other name you previously chose). Use `CTRL-X` and `y` to exit and save changes.
## Run the tests
It's also a good idea to change the system's password from the default "raspberry". Do do this, use the command:
```
passwd
```
You will be prompted to enter the current password (raspberry) and then to enter a new password twice. In our prepared release image, the password used is "AirG@pped!" (without quotes).
If you plan to use your installation on a Raspberry Pi that is not a Zero version 1.3, but rather on a Raspberry Pi that has WiFi and Bluetooth capabilities, it is a good idea to disable the following WiFi & Bluetooth, as well as other relevant services (assuming you are not creating this installation for testing/development purposes). Enter the followiing commands to disable WiFi, Bluetooth, & other relevant services:
```
sudo systemctl disable bluetooth.service
sudo systemctl disable wpa_supplicant.service
sudo systemctl disable dhcpcd.service
sudo systemctl disable sshd.service
sudo systemctl disable networking.service
sudo systemctl disable dphys-swapfile.service
sudo ifconfig wlan0 down
```
Please note that if you are using WiFi to connect/interact with your Raspberry Pi, the last command will sever that connection.
You can now safely power the Raspberry Pi off from the SeedSigner main menu.
If you do not plan to use your installation for testing/development, it is also a good idea to disable WiFi and Bluetooth by editing the config.txt file found in the installation's "boot" partition. You can add the following text to the end of that file with any simple text editor (Windows: Notepad, Mac: TextEdit, Linux: nano):
```
dtoverlay=disable-bt
dtoverlay=pi3-disable-wifi
```
If you used option #2 above and don't plan to continue to access your SeedSigner via SSH over USB, it is a good idea to reverse the steps you took to enable it -- those instructions can be found near the end of [this guide](docs/usb_relay.md).
Please remember that it can take up to a minute for the GUI to appear when subsequently powering your SeedSigner on.
### Optional: Run the tests
see: [tests/README.md](tests/README.md)
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
## Verifying dice seed generation
It is possible to do a 'dry run' to verify that seed generation has not been tempered.
This will ensure that the derivation algorithm has not been tempered and is still same as well known algorithm used bu coldcard and ian coleman bip39 webpage.
For example an 'evil maid' attack would be someone accesing your sdcard and change the code to only take in account 5 or 6 dice, so the 24 words would still feel random but the attacker will need to brute force only 5 or 6 dice (very easy).
This is part of the "do not trust, verify" crypto mottos.
### Verifying with Ian coleman webpage
Go to https://iancoleman.io/bip39 and check show entropy detail:
<img src="img/dice_entr.png">
And then make sure to check 'Hex' or 'base 10' (1) and 24 words as mnemonic length (2).
Do not use 'dice' format because dice 6 will be replaced by 0.
And then enter the 99 dices numbers in (3) :
<img src="img/dice_type.png">
When the 99 dice number has been entered in the seedsigner and ian coleman page, you will be able to verify that the 24 words are the same.
#### Here in real life:
We start with first 3 roll, it is important to always read dice roll from left to right to avoid human bias:
<img src="img/dice_pic1.png">
Then we arrive at the 99th dice:
<img src="img/dice_pic2.png">
Then same 24 words! :
<img src="img/dice_pic3.png">
Now we are sure that the dice derivation is correct and we can unplug everything and do a new dice roll only on the seedsigner.
### Verifying with Coldcard
There is nothing specific, the algorithm are completely the same. Coldcard has a verification script in python and all explanations here:
https://coldcard.com/docs/verifying-dice-roll-math
### Epilogue
You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner!
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -1,4 +1,4 @@
embit==0.4.8
embit==0.4.10
numpy==1.21.1
picamera==1.13
Pillow==8.2.0
@@ -7,3 +7,4 @@ qrcode==6.1
RPi.GPIO==0.7.0
six==1.16.0
spidev==3.5
-e src/vendor/urtypes
+1 -1
View File
@@ -5,7 +5,7 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
qr_background_color = 555555
camera_rotation = 0
[wallet]
+267 -7
View File
@@ -1,8 +1,14 @@
import os
import sys
import time
import re
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip32, script, ec
from embit.networks import NETWORKS
from embit.descriptor import Descriptor
from binascii import hexlify
from threading import Thread
from embit.psbt import PSBT
@@ -68,8 +74,10 @@ class Controller(Singleton):
controller.psbt_parser = None
# settings
# TODO: Remove these
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
controller.current_bg_qr_color = controller.settings.qr_background_color
# Configure the Renderer
Renderer.configure_instance({"text_color": controller.color})
@@ -476,9 +484,9 @@ class Controller(Singleton):
self.storage.save_passphrase(passphrase, slot_num)
if r in (0,1):
self.renderer.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
self.menu_view.draw_passphrase("Passphrase Added", passphrase, "Right to Continue")
elif r == 2:
self.renderer.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
self.menu_view.draw_passphrase("Passphrase Updated", passphrase, "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
@@ -603,6 +611,208 @@ class Controller(Singleton):
else:
return Path.MAIN_MENU
elif decoder.isComplete() and decoder.isAddress():
address = decoder.getAddress()
address_type = decoder.getAddressType()
self.menu_view.draw_address(address)
self.buttons.wait_for([B.KEY_RIGHT])
validate_network = NETWORKS[self.settings.network]
validate_network_text = self.settings.network
if "main" in address_type:
validate_network = NETWORKS["main"]
validate_network_text = "main"
elif "test" in address_type:
validate_network = NETWORKS["test"]
validate_network_text = "test"
r = 0
if address_type in ("Bech32-main", "Bech32-test") and len(address) == 62:
r = 2
else:
# check single sig vs multi sig address
r = self.menu_view.display_generic_selection_menu(["Single Sig Seed", "JSON Wallet Desc"], "Validate Bitcoin Address")
if r == 1:
# validate single sig using seed
# No valid seed yet, If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
return Path.MAIN_MENU
seed = self.storage.get_seed(slot_num)
used_saved_seed = True
if not seed:
# no valid seed yet, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Cancel ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.MAIN_MENU
if not seed:
return Path.MAIN_MENU
# check if seed phrase is valid
self.menu_view.draw_modal(["Validating Seed ..."])
if not seed:
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if len(seed.passphrase) == 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
if len(seed.passphrase) == 0:
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.MAIN_MENU
# Ask to save seed
if self.storage.slot_avaliable() and used_saved_seed == False:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
#
# Validate if address from seed
#
# choose derivation standard
r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
if r == 1:
script_type = "native segwit"
elif r == 2:
script_type = "nested segwit"
elif r == 3:
script_type = "custom"
# calculated derivation or get custom from keyboard entry
if script_type == "custom":
derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
self.settings.custom_derivation = derivation # save for next time
else:
derivation = Settings.calc_derivation(validate_network_text, "single sig", script_type)
if derivation == "" or derivation == None:
self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
return Path.SEED_TOOLS_SUB_MENU
self.menu_view.draw_modal(["Checking Address", ""], "", "")
version = bip32.detect_version(derivation, default="xpub", network=validate_network)
root = bip32.HDKey.from_seed(seed.seed, version=validate_network["xprv"])
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation)
xpub = xprv.to_public()
for i in range(500):
r_pubkey = xpub.derive([0,i]).key
c_pubkey = xpub.derive([1,i]).key
recieve_address = ""
change_address = ""
if "P2PKH" in address_type:
recieve_address = script.p2pkh(r_pubkey).address(network=validate_network)
change_address = script.p2pkh(c_pubkey).address(network=validate_network)
elif "Bech32" in address_type:
recieve_address = script.p2wpkh(r_pubkey).address(network=validate_network)
change_address = script.p2wpkh(c_pubkey).address(network=validate_network)
elif "P2SH" in address_type:
recieve_address = script.p2sh(script.p2wpkh(r_pubkey)).address(network=validate_network)
change_address = script.p2sh(script.p2wpkh(c_pubkey)).address(network=validate_network)
if address == recieve_address:
self.menu_view.draw_modal(["Receive Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if address == change_address:
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
return Path.MAIN_MENU
if r == 2:
# second QR scan need PSBT now
decoder2 = scan_qr("Scan Backup JSON QR")
if decoder2.isComplete() and decoder2.isWalletDescriptor():
desc_str = decoder2.getWalletDescriptor()
desc_str = desc_str.replace("\n","").replace(" ","")
try:
if len(re.findall (r'\[([0-9,a-f,A-F]+?)(\/[0-9,\/,h\']+?)\].*?(\/0\/\*)', desc_str)) > 0:
p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h\']+?\].*?)(\/0\/\*)')
desc_str = p.sub(r'\1/{0,1}/*', desc_str)
elif len(re.findall (r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])', desc_str)) > 0:
p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])')
desc_str = p.sub(r'\1/{0,1}/*\2', desc_str)
except:
desc_str = decoder2.getWalletDescriptor()
print(desc_str)
desc = Descriptor.from_string(desc_str)
else:
return Path.MAIN_MENU
#
# Validate if address from descriptor
#
for i in range(1000):
r_derived = desc.derive(i,branch_index=0)
c_derived = desc.derive(i,branch_index=1)
recieve_address = r_derived.address(validate_network)
change_address = c_derived.address(validate_network)
if address == recieve_address:
self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if address == change_address:
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
return Path.MAIN_MENU
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
self.renderer.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
@@ -701,11 +911,20 @@ class Controller(Singleton):
self.renderer.draw_modal(["Generating PSBT QR ..."])
e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist)
while True:
image = e.nextPartImage(240,240,1)
self.renderer.show_image(image)
time.sleep(0.05)
if self.buttons.check_for_low(B.KEY_RIGHT):
cur_time = int(time.time() * 1000)
if cur_time - self.buttons.last_input_time > self.screensaver_activation_ms and not self.screensaver.is_running:
self.start_screensaver()
self.buttons.update_last_input_time()
else:
image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color)
View.DispShowImage(image)
time.sleep(0.05)
if self.buttons.check_for_low(B.KEY_RIGHT):
break
elif self.buttons.check_for_low(B.KEY_UP):
self.prev_qr_background_color()
elif self.buttons.check_for_low(B.KEY_DOWN):
self.next_qr_background_color()
# Return to Main Menu
return Path.MAIN_MENU
@@ -779,6 +998,47 @@ class Controller(Singleton):
return Path.SETTINGS_SUB_MENU
### next_qr_background_colors()
def next_qr_background_color(self):
if self.current_bg_qr_color == "FFFFFF":
self.current_bg_qr_color = "DDDDDD"
elif self.current_bg_qr_color == "DDDDDD":
self.current_bg_qr_color = "BBBBBB"
elif self.current_bg_qr_color == "BBBBBB":
self.current_bg_qr_color = "999999"
elif self.current_bg_qr_color == "999999":
self.current_bg_qr_color = "777777"
elif self.current_bg_qr_color == "777777":
self.current_bg_qr_color = "555555"
elif self.current_bg_qr_color == "555555":
self.current_bg_qr_color = "333333"
elif self.current_bg_qr_color == "333333":
self.current_bg_qr_color = "FFFFFF"
self.settings.qr_background_color = self.current_bg_qr_color
### prev_qr_background_colors()
def prev_qr_background_color(self):
if self.current_bg_qr_color == "333333":
self.current_bg_qr_color = "555555"
elif self.current_bg_qr_color == "555555":
self.current_bg_qr_color = "777777"
elif self.current_bg_qr_color == "777777":
self.current_bg_qr_color = "999999"
elif self.current_bg_qr_color == "999999":
self.current_bg_qr_color = "BBBBBB"
elif self.current_bg_qr_color == "BBBBBB":
self.current_bg_qr_color = "DDDDDD"
elif self.current_bg_qr_color == "DDDDDD":
self.current_bg_qr_color = "FFFFFF"
elif self.current_bg_qr_color == "FFFFFF":
self.current_bg_qr_color = "333333"
self.settings.qr_background_color = self.current_bg_qr_color
### Show Donate Screen and QR
def show_camera_rotation_tool(self):
r = self.settings_tools_view.display_camera_rotation()
+5 -1
View File
@@ -146,7 +146,11 @@ class Buttons(Singleton):
return True
def check_for_low(self, key) -> bool:
return self.GPIO.input(key) == self.GPIO.LOW
if self.GPIO.input(key) == self.GPIO.LOW:
self.update_last_input_time()
return True
else:
return False
def has_any_input(self) -> bool:
for key in B.ALL_KEYS:
@@ -1,3 +1,4 @@
import hashlib
import unicodedata
from embit import bip39
@@ -5,7 +6,6 @@ from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
from typing import List
def calculate_checksum(partial_mnemonic: list, wordlist) -> List[str]:
""" Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """
if len(partial_mnemonic) not in [11, 23]:
@@ -39,8 +39,7 @@ def generate_mnemonic_from_bytes(entropy_bytes) -> List[str]:
def generate_mnemonic_from_dice(roll_data: str) -> List[str]:
entropyinteger = int(roll_data, 6)
entropy_bytes = entropyinteger.to_bytes(32, byteorder="little")
entropy_bytes = hashlib.sha256(roll_data.encode()).digest()
# Return as a list
return bip39.mnemonic_from_bytes(entropy_bytes).split()
+223 -32
View File
@@ -1,3 +1,8 @@
from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from enum import IntEnum
import re
import json
import base64
import json
import logging
@@ -20,6 +25,8 @@ logger = logging.getLogger(__name__)
from urtypes.crypto import PSBT as UR_PSBT
###
### DecodeQR Class
### Purpose: used to process images or string data from animated qr codes with psbt data to create binary/base64 psbt
@@ -29,11 +36,13 @@ class DecodeQR:
def __init__(self, **kwargs):
self.complete = False
self.qr_type = None
self.ur_decoder = URDecoder() # UR2 decoder
self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder
self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder
self.base64_qr = Base64DecodeQR() # Single Segments Base64
self.base43_qr = Base43DecodeQR() # Single Segment Base43
self.ur_decoder = URDecoder() # UR2 decoder
self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder
self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder
self.base64_qr = Base64DecodeQR() # Single Segments Base64
self.base43_qr = Base43DecodeQR() # Single Segment Base43
self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address
self.specter_wallet_qr = SpecterDecodeWalletQR() # Specter Desktop Wallet Export decoder
self.settings_qr = SettingsQR() # Settings config in .ini format
self.wordlist = None
@@ -125,6 +134,18 @@ class DecodeQR:
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
elif self.qr_type == QRType.BITCOINADDRESSQR:
rt = self.address_qr.add(qr_str, QRType.BITCOINADDRESSQR)
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
elif self.qr_type == QRType.SPECTERWALLETQR:
rt = self.specter_wallet_qr.add(qr_str)
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
else:
return DecodeQRStatus.INVALID
@@ -143,7 +164,8 @@ class DecodeQR:
if self.complete:
if self.qr_type == QRType.PSBTUR2:
cbor = self.ur_decoder.result_message().cbor
return cbor_decode(cbor)
return UR_PSBT.from_cbor(cbor).data
# return cbor_decode(cbor)
elif self.qr_type == QRType.PSBTSPECTER:
return self.specter_qr.getData()
elif self.qr_type == QRType.PSBTURLEGACY:
@@ -173,6 +195,15 @@ class DecodeQR:
def get_settings_config_name(self):
return self.settings_qr.config_name
def getAddress(self):
return self.address_qr.getAddress()
def getAddressType(self):
return self.address_qr.getAddressType()
def getWalletDescriptor(self):
return self.specter_wallet_qr.getWalletDescriptor()
def getPercentComplete(self) -> int:
if self.qr_type == QRType.PSBTUR2:
@@ -220,6 +251,16 @@ class DecodeQR:
@property
def is_json(self):
return self.qr_type in [QRType.SETTINGS, QRType.JSON]
def isAddress(self):
if self.qr_type == QRType.BITCOINADDRESSQR:
return True
return False
def isWalletDescriptor(self):
if self.qr_type in (QRType.SPECTERWALLETQR, QRType.URWALLETQR, QRType.BLUEWALLETQR):
return True
return False
def qrType(self):
return self.qr_type
@@ -241,18 +282,37 @@ class DecodeQR:
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBTUR2
elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
return QRType.PSBTSPECTER
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
return QRType.PSBTURLEGACY
elif DecodeQR.isBase64PSBT(s):
return QRType.PSBTBASE64
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
# Wallet Descriptor
desc_str = s.replace("\n","").replace(" ","")
if re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
# when not a SPECTER Base64 PSBT from above, assume it's json
return QRType.SPECTERWALLETQR
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
# if json starting with label and contains descriptor, assume specter wallet json
return QRType.SPECTERWALLETQR
# create 4 letter wordlist only if not PSBT (performance gain)
try:
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
except:
_4LETTER_WORDLIST = []
# Seed
if re.search(r'\d{48,96}', s):
return QRType.SEEDSSQR
# Bitcoin Address
elif DecodeQR.isBitcoinAddress(s):
return QRType.BITCOINADDRESSQR
# Seed
elif all(x in wordlist for x in s.strip().split(" ")):
# checks if all words in list are in bip39 word list
return QRType.SEEDMNEMONIC
@@ -310,7 +370,7 @@ class DecodeQR:
for c in v[::-1]:
digit = chars.find(bytes([c]))
if digit == -1:
raise BaseDecodeError('Forbidden character {} for base {}'.format(c, 43))
raise Exception('Forbidden character {} for base {}'.format(c, 43))
# naive but slow variant: long_value += digit * (base**i)
long_value += digit * power_of_base
power_of_base *= 43
@@ -329,6 +389,16 @@ class DecodeQR:
result.extend(b'\x00' * nPad)
result.reverse()
return bytes(result)
@staticmethod
def isBitcoinAddress(s):
if re.search(r'^bitcoin\:.*', s, re.IGNORECASE):
return True
elif re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s):
return True
else:
return False
###
### SpecterDecodePSBTQR Class
@@ -501,17 +571,6 @@ class Base64DecodeQR:
return None
@staticmethod
def currentSegmentNum(segment) -> int:
return self.collected_segments
@staticmethod
def totalSegmentNum(segment) -> int:
return self.total_segments
@staticmethod
def parseSegment(segment) -> str:
return segment
###
### Base43DecodeQR Class
@@ -539,17 +598,6 @@ class Base43DecodeQR:
def getData(self):
return self.data
@staticmethod
def currentSegmentNum(segment) -> int:
return self.collected_segments
@staticmethod
def totalSegmentNum(segment) -> int:
return self.total_segments
@staticmethod
def parseSegment(segment) -> str:
return segment
###
### SeedQR Class
@@ -750,6 +798,149 @@ class SettingsQR:
logger.exception(e)
return DecodeQRStatus.INVALID
###
### BitcoinAddressQR Class
### Purpose: used in DecodeQR to decode single frame representing a bitcoin address
###
class BitcoinAddressQR:
def __init__(self, wordlist=None):
self.total_segments = 1
self.collected_segments = 0
self.complete = False
self.address = None
self.address_type = None
def add(self, segment, qr_type=QRType.BITCOINADDRESSQR):
r = re.search(r'((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})', segment)
if r != None:
self.address = r.group(1)
if re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) != None:
self.complete = True
self.collected_segments = 1
# get address type
r = re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address)
if r != None:
r = r.group(2)
if r == "1":
self.address_type = "P2PKH-main" # Legacy
elif r == "m" or r == "n":
self.address_type = "P2PKH-test" # Legacy
elif r == "3":
self.address_type = "P2SH-main" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH)
elif r == "2":
self.address_type = "P2SH-test" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH)
elif r == "bc1":
self.address_type = "Bech32-main"
elif r == "tb1":
self.address_type = "Bech32-test"
return DecodeQRStatus.COMPLETE
return DecodeQRStatus.INVALID
def getAddress(self):
if self.address != None:
return self.address
return None
def getAddressType(self):
if self.address != None:
if self.address_type != None:
return self.address_type
else:
return "Unknown"
return None
###
### SpecterDecodeWalletQR Class
### Purpose: used in DecodeQR to decode animated frames to get a wallet descriptor from Specter Desktop
###
class SpecterDecodeWalletQR:
def __init__(self):
self.total_segments = None
self.collected_segments = 0
self.complete = False
self.segments = []
def add(self, segment):
if self.total_segments == None:
self.total_segments = SpecterDecodeWalletQR.totalSegmentNum(segment)
self.segments = [None] * self.total_segments
elif self.total_segments != SpecterDecodeWalletQR.totalSegmentNum(segment):
raise Exception('Specter Desktop segment total changed unexpectedly')
if self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] == None:
self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] = SpecterDecodeWalletQR.parseSegment(segment)
self.collected_segments += 1
if self.total_segments == self.collected_segments:
if self.validateWalletDescriptor():
self.complete = True
return DecodeQRStatus.COMPLETE
else:
return DecodeQRStatus.INVALID
return DecodeQRStatus.PART_COMPLETE # new segment added
return DecodeQRStatus.PART_EXISTING # segment not added because it's already been added
def validateJson(self) -> str:
try:
j = "".join(self.segments)
json.loads(j)
except json.decoder.JSONDecodeError:
return False
return True
def validateWalletDescriptor(self):
if self.validateJson():
j = "".join(self.segments)
data = json.loads(j)
if "descriptor" in data:
return True
return False
def getWalletDescriptor(self) -> str:
if self.validateWalletDescriptor():
j = "".join(self.segments)
data = json.loads(j)
return data['descriptor']
return None
def is_complete(self) -> bool:
if self.complete and self.validateWalletDescriptor():
return True
return False
@staticmethod
def currentSegmentNum(segment) -> int:
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(1))
else:
return 1
@staticmethod
def totalSegmentNum(segment) -> int:
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(2))
else:
return 1
@staticmethod
def parseSegment(segment) -> str:
try:
return re.search(r'^p(\d+)of(\d+) (.+$)', segment, re.IGNORECASE).group(3)
except:
return segment
###
### DecodeQRStatus Class IntEum
### Purpose: used in DecodeQR to communicate status of adding qr frame/segment
+88 -7
View File
@@ -9,6 +9,9 @@ from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode)
from seedsigner.helpers.qr import QR
from seedsigner.models import Seed, QRType, EncodeQRDensity
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
###
### EncodeQR Class
### Purpose: used to encode psbt for displaying as qr image
@@ -63,6 +66,8 @@ class EncodeQR:
self.encoder = UREncodePSBTQR(self.psbt, self.qr_density)
elif self.qr_type == QRType.SEEDSSQR:
self.encoder = SeedSSQR(self.seed_phrase, self.wordlist)
elif self.qr_type == QRType.URXPUBQR:
self.encoder = URXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist)
elif self.qr_type == QRType.XPUBQR:
self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist)
elif self.qr_type == QRType.SPECTERXPUBQR:
@@ -79,9 +84,12 @@ class EncodeQR:
def part2Image(self, part, width=240, height=240, border=3):
return self.qr.qrimage_io(part, width, height, border)
def nextPartImage(self, width=240, height=240, border=3):
def nextPartImage(self, width=240, height=240, border=3, background="FFFFFF"):
part = self.nextPart()
return self.qr.qrimage_io(part, width, height, border)
if self.qr_type == QRType.SEEDSSQR:
return self.qr.qrimage(part, width, height, border)
else:
return self.qr.qrimage_io(part, width, height, border, background=background)
def isComplete(self):
return self.encoder.isComplete()
@@ -97,10 +105,8 @@ class UREncodePSBTQR:
def __init__(self, p, qr_density):
self.psbt = p
self.qr_max_fragement_size = 20
cbor_encoder = CBOREncoder()
cbor_encoder.encodeBytes(self.psbt.serialize())
qr_ur_bytes = UR("crypto-psbt", cbor_encoder.get_bytes())
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 10
@@ -309,4 +315,79 @@ class SpecterXPubQR(XPubQR):
return len(self.parts)
def isComplete(self):
return self.sent_complete
return self.sent_complete
class URXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist):
XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist)
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 10
elif qr_density == EncodeQRDensity.MEDIUM:
self.qr_max_fragement_size = 30
elif qr_density == EncodeQRDensity.HIGH:
self.qr_max_fragement_size = 120
def derivation_to_keypath(path: str) -> list:
arr = path.split("/")
if arr[0] == "m":
arr = arr[1:]
if len(arr) == 0:
return Keypath([],self.root.my_fingerprint, None)
if arr[-1] == "":
# trailing slash
arr = arr[:-1]
for i, e in enumerate(arr):
if e[-1] == "h" or e[-1] == "'":
arr[i] = PathComponent(int(e[:-1]), True)
else:
arr[i] = PathComponent(int(e), False)
return Keypath(arr, self.root.my_fingerprint, len(arr))
origin = derivation_to_keypath(derivation)
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
'chain_code': self.xpub.chain_code,
'origin': origin,
'parent_fingerprint': self.xpub.fingerprint})
ur_outputs = []
if len(origin.components) > 0:
if origin.components[0].index == 84: # Native Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 49: # Nested Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 48: # Multisig
if len(origin.components) >= 4:
if origin.components[3].index == 2: # Native Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
elif origin.components[3].index == 1: # Nested Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
# If empty, add all script types
if len(ur_outputs) == 0:
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey))
ur_account = Account(self.root.my_fingerprint, ur_outputs)
qr_ur_bytes = UR("crypto-account", ur_account.to_cbor())
self.ur2_encode = UREncoder(qr_ur_bytes, self.qr_max_fragement_size, 0)
def seqLen(self):
return self.ur2_encode.fountain_encoder.seq_len()
def nextPart(self) -> str:
return self.ur2_encode.next_part().upper()
def isComplete(self):
return self.ur2_encode.is_complete()
+6 -1
View File
@@ -15,7 +15,12 @@ class QRType(IntEnum):
SEEDUR2 = 7
SEEDMNEMONIC = 8
SEED4LETTERMNEMONIC = 9
SETTINGS = 23
XPUBQR = 20
SPECTERXPUBQR = 21
SETTINGS = 22
URXPUBQR = 22
BITCOINADDRESSQR = 30
SPECTERWALLETQR = 40
URWALLETQR = 41
BLUEWALLETQR = 42
INVALID = 100
+12 -3
View File
@@ -212,8 +212,6 @@ class SettingsDefinition:
def to_html(cls):
return ""
class Settings(Singleton):
SETTINGS_FILENAME = "settings.json"
@@ -330,6 +328,15 @@ class Settings(Singleton):
def text_color(self):
from seedsigner.gui.components import GUIConstants
return GUIConstants.BODY_FONT_COLOR
@property
def qr_background_color(self):
return self._data["display"]["qr_background_color"]
@qr_background_color.setter
def qr_background_color(self, value):
self._data["display"]["qr_background_color"] = value
self.__writeConfig()
@property
def camera_rotation(self):
@@ -387,8 +394,10 @@ class Settings(Singleton):
def getXPubType(software):
if software == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
return QRType.SPECTERXPUBQR
else:
elif software == "Blue Wallet":
return QRType.XPUBQR
else:
return QRType.URXPUBQR
@property
def qr_density_name(self):
+6 -5
View File
@@ -502,13 +502,14 @@ class SeedToolsView(View):
def dice_arrow_press(self):
self.roll_number += 1
if self.dice_selected == 6:
self.roll_data += "0"
else:
self.roll_data += str(self.dice_selected)
self.roll_data += str(self.dice_selected)
# Reset for the next UI render
self.dice_selected = 5
if self.roll_number > 45:
self.dice_selected = 5
else:
self.dice_selected = 2
if self.roll_number < 100:
self.draw_dice(self.dice_selected)
+2 -2
View File
@@ -44,7 +44,7 @@ class SigningToolsView(View):
receiving_addr_str2 += "receiving addresses"
elif len(p.destination_addresses) == 1:
receiving_addr_str1 += "receiving address"
receiving_addr_str2 += "last 13: ..." + p.destination_addresses[0][-13:]
receiving_addr_str2 += "last 13: ... " + p.destination_addresses[0][-13:]
else:
receiving_addr_str1 += "Self-Transfer"
@@ -52,7 +52,7 @@ class SigningToolsView(View):
self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=Fonts.get_font("Assistant-Medium", 22))
if len(receiving_addr_str2) > 0:
tw, th = self.draw.textsize(receiving_addr_str2, font=Fonts.get_font("Assistant-Medium", 22))
self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=Fonts.get_font("Assistant-Medium", 22))
self.draw.text(((240 - tw) / 2, 105), receiving_addr_str2, fill=View.color, font=Fonts.get_font("Assistant-Medium", 22))
if p.spend_amount > 0:
+15
View File
@@ -0,0 +1,15 @@
[system]
debug = False
default_language = en
persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = 555555
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/0/0
Vendored Submodule
+1
Submodule src/vendor/urtypes added at 8d643aa7f5
+106 -3
View File
@@ -333,8 +333,111 @@ def test_short_4_letter_mnemonic_qr():
assert d.isComplete() == True
assert d.getSeedPhrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"]
def test_bitcoin_address():
bad1 = "loremipsum"
bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae"
bad3 = "121802020768124106400009195602431595117715840445"
legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY"
legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe"
main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a"
main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX"
test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm"
main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000"
main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(bad1)
assert d.qrType() == QRType.INVALID
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(legacy_address1)
assert d.getAddress() == legacy_address1
assert d.getAddressType() == "P2PKH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(legacy_address2)
assert d.getAddress() == legacy_address2
assert d.getAddressType() == "P2PKH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(main_bech32_address)
assert d.getAddress() == main_bech32_address
assert d.getAddressType() == "Bech32-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(test_bech32_address)
assert d.getAddress() == test_bech32_address
assert d.getAddressType() == "Bech32-test"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(main_nested_segwit_address)
assert d.getAddress() == main_nested_segwit_address
assert d.getAddressType() == "P2SH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(test_nested_segwit_address)
assert d.getAddress() == test_nested_segwit_address
assert d.getAddressType() == "P2SH-test"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(main_bech32_address2)
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.getAddressType() == "Bech32-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(main_bech32_address3)
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.getAddressType() == "Bech32-main"
def test_seed_qr():
seed = "121802020768124106400009195602431595117715840445"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(seed)
assert d.qrType() == QRType.SEEDSSQR
assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()
def test_specter_wallet_json():
parts = [
'p1of3 {"label": "SeedSigner Dev Funds", "blockheight": 692143, "descriptor": "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7Wpofr'
, 'p2of3 bYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3mi'
, 'p3of3 nevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e", "devices": [{"type": "other", "label": "Keith"}, {"type": "other", "label": "Nick"}, {"type": "other", "label": "Richard"}, {"type": "other", "label": "Stephan"}, {"type": "other", "label": "SeedSigner 1"}, {"type": "other", "label": "SeedSigner 2"}]}'
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(parts[1])
d.addString(parts[2])
d.addString(parts[0])
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True
assert d.getWalletDescriptor() == "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e"
def test_specter_wallet_json2():
1_part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}'
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(1_part)
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True
+10
View File
@@ -85,4 +85,14 @@ def test_specter_xpub_qr():
assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J"
assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5"
def test_ur_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.MEDIUM, wordlist=bip39.WORDLIST)
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE"
+40
View File
@@ -30,3 +30,43 @@ def test_calculate_checksum():
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist=bip39.WORDLIST)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
def test_verify_against_coldcard_sample():
""" https://coldcard.com/docs/verifying-dice-roll-math """
dice_rolls = "123456"
expected = "mirror reject rookie talk pudding throw happy era myth already payment own sentence push head sting video explain letter bomb casual hotel rather garment"
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected
def test_known_dice_rolls():
""" Given 99 known dice rolls, the resulting mnemonic should be valid and match the expected. """
dice_rolls = "522222222222222222222222222222222222222222222555555555555555555555555555555555555555555555555555555"
expected = "resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious"
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected
dice_rolls = "222222222222222222222222222222222222222222222555555555555555555555555555555555555555555555555555555"
expected = "garden uphold level clog sword globe armor issue two cute scorpion improve verb artwork blind tail raw butter combine move produce foil feature wave"
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected
dice_rolls = "222222222222222222222222222222222222222222222555555555555555555555555555555555555555555555555555556"
expected = "lizard broken love tired depend eyebrow excess lonely advance father various cram ignore panic feed plunge miss regret boring unique galaxy fan detail fly"
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected