├── .gitignore
├── .gitmodules
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── boot
├── debug
│ ├── boot.py
│ ├── debug.py
│ └── hardwaretest.py
└── main
│ └── boot.py
├── build_firmware.sh
├── demo_apps
├── .gitignore
├── __init__.py
└── helloworld.py
├── docs
├── README.md
├── assembly.md
├── build.md
├── communication.md
├── datasheets
│ ├── Mikroe-Rakinda-LV3296.pdf
│ └── Waveshare-GROW-GM65-S.pdf
├── descriptors.md
├── development.md
├── enclosures
│ ├── README.md
│ ├── seedsigner
│ │ ├── BBv1_Lower.stl
│ │ ├── BBv1_Upper.stl
│ │ ├── BBv2_Lower.stl
│ │ └── BBv2_Upper.stl
│ ├── snapcase
│ │ ├── README.md
│ │ ├── Specter-DIY Snap Case - Backside.stl
│ │ ├── Specter-DIY Snap Case - Frontside.stl
│ │ └── Specter-DIY Snap Case - Scanner.stl
│ └── thomas
│ │ ├── DIY-Front-Display-v0.12.stl
│ │ ├── DIY-Platte-v0.21.stl
│ │ └── Scan_case-v0.2.stl
├── faq.md
├── pictures
│ ├── gallery
│ │ ├── README.md
│ │ ├── barebones_v2.png
│ │ ├── bavarianledger.jpg
│ │ ├── bitcoinhero.jpg
│ │ ├── davewhiiite.jpg
│ │ ├── davewhiiite1.jpg
│ │ ├── davewhiiite2.jpg
│ │ ├── davewhiiite3.jpg
│ │ ├── dimaatmelodromru.jpg
│ │ ├── gorazdko.jpg
│ │ ├── k9ert.jpg
│ │ ├── kdmukai.jpg
│ │ ├── kkdao.jpg
│ │ ├── lunaticoin.jpg
│ │ ├── org_packaging_signer_1.jpg
│ │ ├── org_packaging_signer_2.jpg
│ │ ├── org_packaging_signer_3.jpg
│ │ ├── snap-case-bronze-black-1.jpg
│ │ ├── snap-case-connect-scanner-1.jpg
│ │ ├── snap-case-double-1.jpg
│ │ ├── snap-case-double-2.jpg
│ │ ├── snap-case-mount-scanner-1.jpg
│ │ ├── snap-case-plug-together-1.jpg
│ │ ├── snap-case-printed-parts-1.jpg
│ │ ├── snap-case-remove-screws-from-scanner-1.jpg
│ │ ├── stepansnigirev.jpg
│ │ └── thomas.jpg
│ ├── init_screens.jpg
│ ├── kit.jpg
│ ├── kit_with_case.jpg
│ ├── wallet_screens.jpg
│ └── waveshare_wiring.jpg
├── quickstart.md
├── reproducible-build.md
├── roadmap.md
├── security.md
├── shopping.md
└── simulator.md
├── hwidevice.py
├── manifests
├── debug.py
├── disco.py
└── unix.py
├── mkdocs.yml
├── requirements.txt
├── shell.nix
├── shield
├── 01_Structure_Diagram.pdf
├── 02_Power.pdf
├── 03_Peripherals.pdf
├── 3d_case
│ ├── Specter_Assembly_1.jpg
│ ├── Specter_Assembly_2.jpg
│ ├── Specter_Assembly_3.jpg
│ ├── Specter_Assembly_4.jpg
│ ├── Specter_Assembly_5.jpg
│ ├── Specter_Assembly_6.jpg
│ ├── Specter_Open_1.jpg
│ ├── Specter_Post_1.jpg
│ ├── Specter_Post_2.jpg
│ ├── Specter_Post_3.jpg
│ ├── Specter_Post_4.jpg
│ ├── Specter_Post_5.jpg
│ ├── Specter_Post_6.jpg
│ ├── Specter_Print_1.jpg
│ ├── Specter_Print_2.jpg
│ ├── Specter_Print_3.jpg
│ ├── Specter_Print_4.jpg
│ └── Specter_Print_5.jpg
├── 3dprinting.md
├── 3dshield.jpg
├── Alternative_3D_Printed_Case
│ ├── 01_Main_Body.stl
│ ├── 02_Top_Panel.stl
│ ├── 03_Bottom_Panel.stl
│ ├── 04_Camera_Holder.stl
│ ├── 05_Button.stl
│ ├── 06_Spacer.stl
│ └── Assembly_Instructions.pdf
├── README.md
└── specter-shield
│ ├── Gerber
│ ├── Gerber.OutputStatus
│ ├── specter-shield_v1-macro.APR_LIB
│ ├── specter-shield_v1.EXTREP
│ ├── specter-shield_v1.GBL
│ ├── specter-shield_v1.GBO
│ ├── specter-shield_v1.GBP
│ ├── specter-shield_v1.GBS
│ ├── specter-shield_v1.GP1
│ ├── specter-shield_v1.GP2
│ ├── specter-shield_v1.GTL
│ ├── specter-shield_v1.GTO
│ ├── specter-shield_v1.GTP
│ ├── specter-shield_v1.GTS
│ ├── specter-shield_v1.Outline
│ ├── specter-shield_v1.REP
│ ├── specter-shield_v1.RUL
│ └── specter-shield_v1.apr
│ ├── NC Drill
│ ├── NC Drill.OutputStatus
│ ├── specter-shield_v1-RoundHoles.TXT
│ ├── specter-shield_v1-SlotHoles.TXT
│ ├── specter-shield_v1.DRR
│ └── specter-shield_v1.LDP
│ ├── Pick Place
│ ├── Pick Place for specter-shield_v1.txt
│ └── Pick Place.OutputStatus
│ ├── Specter-shield_v1 - Layers Description.xls
│ ├── specter-shield_v1 ArtWork BackSide3D.PDF
│ ├── specter-shield_v1 ArtWork.PDF
│ ├── specter-shield_v1 BOM.xlsx
│ └── specter-shield_v1 Part Layout.PDF
├── simulate.py
├── src
├── app.py
├── apps
│ ├── __init__.py
│ ├── backup.py
│ ├── bip85.py
│ ├── blindingkeys
│ │ ├── __init__.py
│ │ └── app.py
│ ├── compatibility.py
│ ├── getrandom.py
│ ├── label.py
│ ├── signmessage
│ │ ├── __init__.py
│ │ └── signmessage.py
│ ├── wallets
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── commands.py
│ │ ├── liquid
│ │ │ ├── __init__.py
│ │ │ ├── manager.py
│ │ │ └── wallet.py
│ │ ├── manager.py
│ │ ├── screens.py
│ │ └── wallet.py
│ └── xpubs
│ │ ├── __init__.py
│ │ ├── screens.py
│ │ └── xpubs.py
├── config_default.py
├── errors.py
├── gui
│ ├── __init__.py
│ ├── async_gui.py
│ ├── common.py
│ ├── components
│ │ ├── __init__.py
│ │ ├── battery.py
│ │ ├── keyboard.py
│ │ ├── mnemonic.py
│ │ ├── modal.py
│ │ ├── qrcode.py
│ │ └── theme.py
│ ├── core.py
│ ├── decorators.py
│ ├── screens
│ │ ├── __init__.py
│ │ ├── alert.py
│ │ ├── input.py
│ │ ├── menu.py
│ │ ├── mnemonic.py
│ │ ├── progress.py
│ │ ├── prompt.py
│ │ ├── qralert.py
│ │ ├── screen.py
│ │ ├── settings.py
│ │ └── transaction.py
│ ├── specter.py
│ └── tcp_gui.py
├── helpers.py
├── hosts
│ ├── __init__.py
│ ├── core.py
│ ├── qr.py
│ ├── sd.py
│ └── usb.py
├── keystore
│ ├── __init__.py
│ ├── core.py
│ ├── flash.py
│ ├── javacard
│ │ ├── __init__.py
│ │ ├── applets
│ │ │ ├── __init__.py
│ │ │ ├── applet.py
│ │ │ ├── blindoracle.py
│ │ │ ├── memorycard.py
│ │ │ ├── secureapplet.py
│ │ │ └── securechannel.py
│ │ └── util.py
│ ├── memorycard.py
│ ├── ram.py
│ └── sdcard.py
├── main.py
├── platform.py
├── qrencoder.py
├── rng.py
└── specter.py
├── test
├── integration
│ ├── README.md
│ ├── requirements.txt
│ ├── run_tests.py
│ ├── simulator.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_basic.py
│ │ └── test_with_rpc.py
│ └── util
│ │ ├── __init__.py
│ │ ├── controller.py
│ │ ├── misc.py
│ │ └── rpc.py
├── run_tests.py
└── tests
│ ├── __init__.py
│ ├── test_compatibility.py
│ ├── test_keystore.py
│ ├── test_revault.py
│ ├── test_sign.py
│ ├── test_wallets.py
│ └── util.py
└── udev
├── 49-micropython.rules
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | debug.h
2 | bin
3 | .venv
4 | fs
5 | testdir
6 | __pycache__
7 | release
8 | .idea
9 | .vscode
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "f469-disco"]
2 | path = f469-disco
3 | url = https://github.com/diybitcoinhardware/f469-disco
4 | [submodule "bootloader"]
5 | path = bootloader
6 | url = https://github.com/cryptoadvance/specter-bootloader
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9.15@sha256:b5f024fa682187ef9305a2b5d2c4bb583bef83356259669fc80273bb2222f5ed
2 | ENV LANG C.UTF-8
3 |
4 | ARG DEBIAN_FRONTEND=noninteractive
5 |
6 | # ARM Embedded Toolchain
7 | # Integrity is checked using the MD5 checksum provided by ARM at https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads
8 | RUN curl -sSfL -o arm-toolchain.tar.bz2 "https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2?revision=05382cca-1721-44e1-ae19-1e7c3dc96118&rev=05382cca172144e1ae191e7c3dc96118&hash=3ACFE672E449EBA7A21773EE284A88BC7DFA5044" && \
9 | echo 2b9eeccc33470f9d3cda26983b9d2dc6 arm-toolchain.tar.bz2 > /tmp/arm-toolchain.md5 && \
10 | md5sum --check /tmp/arm-toolchain.md5 && rm /tmp/arm-toolchain.md5 && \
11 | tar xf arm-toolchain.tar.bz2 -C /opt && \
12 | rm arm-toolchain.tar.bz2
13 |
14 | # Adding GCC to PATH and defining rustup/cargo home directories
15 | ENV PATH=/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:$PATH
16 |
17 | # Installing python requirements
18 | COPY bootloader/tools/requirements.txt .
19 | RUN pip3 install -r requirements.txt
20 |
21 | WORKDIR /app
22 |
23 | CMD ["/usr/bin/env", "bash", "./build_firmware.sh"]
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 cryptoadvance
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TARGET_DIR = bin
2 | BOARD ?= STM32F469DISC
3 | FLAVOR ?= SPECTER
4 | USER_C_MODULES ?= ../../../usermods
5 | MPY_DIR ?= f469-disco/micropython
6 | FROZEN_MANIFEST_DISCO ?= ../../../../manifests/disco.py
7 | FROZEN_MANIFEST_DEBUG ?= ../../../../manifests/debug.py
8 | FROZEN_MANIFEST_UNIX ?= ../../../../manifests/unix.py
9 | DEBUG ?= 0
10 | USE_DBOOT ?= 0
11 |
12 | $(TARGET_DIR):
13 | mkdir -p $(TARGET_DIR)
14 |
15 | # check submodules
16 | $(MPY_DIR)/mpy-cross/Makefile:
17 | git submodule update --init --recursive
18 |
19 | # cross-compiler
20 | mpy-cross: $(TARGET_DIR) $(MPY_DIR)/mpy-cross/Makefile
21 | @echo Building cross-compiler
22 | make -C $(MPY_DIR)/mpy-cross \
23 | DEBUG=$(DEBUG) && \
24 | cp $(MPY_DIR)/mpy-cross/mpy-cross $(TARGET_DIR)
25 |
26 | # disco board with bitcoin library
27 | disco: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/stm32
28 | @echo Building firmware
29 | make -C $(MPY_DIR)/ports/stm32 \
30 | BOARD=$(BOARD) \
31 | FLAVOR=$(FLAVOR) \
32 | USE_DBOOT=$(USE_DBOOT) \
33 | USER_C_MODULES=$(USER_C_MODULES) \
34 | FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) \
35 | DEBUG=$(DEBUG) && \
36 | arm-none-eabi-objcopy -O binary \
37 | $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
38 | $(TARGET_DIR)/specter-diy.bin && \
39 | cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \
40 | $(TARGET_DIR)/specter-diy.hex
41 |
42 | # disco board with bitcoin library
43 | debug: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/stm32
44 | @echo Building firmware
45 | make -C $(MPY_DIR)/ports/stm32 \
46 | BOARD=$(BOARD) \
47 | FLAVOR=$(FLAVOR) \
48 | USE_DBOOT=$(USE_DBOOT) \
49 | USER_C_MODULES=$(USER_C_MODULES) \
50 | FROZEN_MANIFEST=$(FROZEN_MANIFEST_DEBUG) \
51 | DEBUG=$(DEBUG) && \
52 | arm-none-eabi-objcopy -O binary \
53 | $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \
54 | $(TARGET_DIR)/debug.bin && \
55 | cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \
56 | $(TARGET_DIR)/debug.hex
57 |
58 |
59 | # unixport (simulator)
60 | unix: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/unix
61 | @echo Building binary with frozen files
62 | make -C $(MPY_DIR)/ports/unix \
63 | USER_C_MODULES=$(USER_C_MODULES) \
64 | FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) && \
65 | cp $(MPY_DIR)/ports/unix/micropython $(TARGET_DIR)/micropython_unix
66 |
67 | simulate: unix
68 | $(TARGET_DIR)/micropython_unix simulate.py
69 |
70 | test: unix
71 | $(TARGET_DIR)/micropython_unix tests/run_tests.py
72 |
73 | all: mpy-cross disco unix
74 |
75 | clean:
76 | rm -rf $(TARGET_DIR)
77 | make -C $(MPY_DIR)/mpy-cross clean
78 | make -C $(MPY_DIR)/ports/unix \
79 | USER_C_MODULES=$(USER_C_MODULES) \
80 | FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) clean
81 | make -C $(MPY_DIR)/ports/stm32 \
82 | BOARD=$(BOARD) \
83 | USER_C_MODULES=$(USER_C_MODULES) \
84 | FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) clean
85 |
86 | .PHONY: all clean
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Specter-DIY
2 |
3 | "Cypherpunks write code. We know that someone has to write software to defend privacy,
4 | and since we can't get privacy unless we all do, we're going to write it."
5 | A Cypherpunk's Manifesto - Eric Hughes - 9 March 1993
6 |
7 | ...and Cypherpunks do build their own Bitcoin Hardware Wallets.
8 |
9 | 
10 |
11 | The idea of the project is to build a hardware wallet from off-the-shelf components.
12 | Even though we have [an extension board](./shield) that puts everything in a nice form-factor and helps you to avoid any soldering, we will continue supporting and maintaining compatibility with standard components.
13 |
14 | We also want to keep the project flexible such that it can work on any other set of components with minimal changes. Maybe you want to make a hardware wallet on a different architecture (RISC-V?), with an audio modem as a communication channel - you should be able to do it. It should be easy to add or change functionality of Specter and we try to abstract logical modules as much as we can.
15 |
16 | QR codes are a default way for Specter to communicate with the host. QR codes are pretty convenient and allow the user to be in control of the data transmission - every QR code has a very limited capacity and communication happens unidirectionally. And it's airgapped - you don't need to connect the wallet to the computer at any time.
17 |
18 | For secret storage we support agnostic mode (wallet forgets all secrets when turned off), reckless mode (stores secrets in flash of the application microcontroller) and secure element integration is coming soon.
19 |
20 | Our main focus is multisignature setup with other hardware wallets, but wallet can also work as a single signer. We try to make it compatible with Bitcoin Core where we can - PSBT for unsigned transactions, wallet descriptors for importing/exporting multisig wallets. To communicate with Bitcoin Core easier we are also working on [Specter Desktop app](https://github.com/cryptoadvance/specter-desktop) - a small python flask server talking to your Bitcoin Core node.
21 |
22 | Most of the firmware is written in MicroPython which makes the code easy to audit and change. We use [secp256k1](https://github.com/bitcoin-core/secp256k1) library from Bitcoin Core for elliptic curve calculations and [LittlevGL](https://lvgl.io/) library for GUI.
23 |
24 | ## DISCLAIMER
25 |
26 | The project has significantly matured, to the extent that the security level of Specter-DIY is now comparable to commercial hardware wallets on the market. We implemented a secure bootloader that verifies firmware upgrades, so you can be sure that only signed firmware can be uploaded to the device after initial setup. However, unlike with commercial signing devices the bootloader has to be installed manually by the user and is not set in the factory of the device vendor. Thus, pay extra attention during the initial firmware installation and make sure you verified PGP signatures and flash the firmware from a secure computer.
27 |
28 | If something doesn't work open an issue here or ask a question in our [Telegram group](https://t.me/+VEinVSYkW5TUl5Ai).
29 |
30 | ## Documentation
31 |
32 | All the docs are stored in the [`docs/`](./docs) folder:
33 |
34 | - [`shopping.md`](./docs/shopping.md) explains what to buy
35 | - [`assembly.md`](./docs/assembly.md) shows how to put everything together.
36 | - [`quickstart.md`](./docs/quickstart.md) guides you through the initial steps how to get firmware on the board
37 | - [`reproducible-build.md`](./docs/reproducible-build.md) describes how to build the initial firmware and upgrade files with the same hash as in the release using Docker
38 | - [`build.md`](./docs/build.md) describes how to build the firmware and the simulator yourself
39 | - [`security.md`](./docs/security.md) explains possible attack vectors and security model of the project
40 | - [`development.md`](./docs/development.md) explains how to start developing on Specter
41 | - [`simulator.md`](./docs/simulator.md) shows how to run a simulator on unix/macOS
42 | - [`communication.md`](./docs/communication.md) defines communication protocol with the host over QR codes and USB
43 | - [`roadmap.md`](./docs/roadmap.md) explains what we need to implement before we can consider the wallet be ready to use with real funds.
44 |
45 | Specter-Shield documentation and all the files are available in the [`shield/`](./shield) folder:
46 |
47 | - [What it looks like](./shield/README.md)
48 | - [How to print a 3d case](./shield/3dprinting.md)
49 |
50 | Supported networks: Mainnet, Testnet, Regtest, Signet.
51 |
52 | ## USB communication on Linux
53 |
54 | You may need to set up udev rules and add yourself to `dialout` group. Read more in [`udev`](./udev/README.md) folder.
55 |
56 | ## Video and screenshots
57 |
58 | Check out [this video](https://www.youtube.com/watch?v=1H7FqG_FmCw) to get an idea how to assemble it and how it works.
59 |
60 | Here is a [Gallery](./docs/pictures/gallery/README.md) with devices assembled by the community.
61 |
62 | A few pictures of the UI:
63 |
64 | ### Wallet screens
65 |
66 | 
67 |
68 | ### Key generation and recovery
69 |
70 | 
71 |
--------------------------------------------------------------------------------
/boot/debug/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | # can run arbitrary Python, but best to keep it minimal
3 | import pyb, os, micropython, time
4 |
5 | # power hold
6 | pwr = pyb.Pin("B15", pyb.Pin.OUT)
7 | pwr.on()
8 |
9 | version = "0100900001"
10 |
11 | leds = [pyb.LED(i) for i in range(1,5)]
12 | # poweroff on button press
13 | def pwrcb(e):
14 | micropython.schedule(poweroff, 0)
15 |
16 | # callback scheduled from the interrupt
17 | def poweroff(_):
18 | for led in leds:
19 | led.toggle()
20 | os.sync()
21 | time.sleep_ms(300)
22 | pwr.off()
23 | time.sleep_ms(300)
24 | # will never reach here
25 | for led in leds:
26 | led.toggle()
27 |
28 | pyb.ExtInt(pyb.Pin('B1'), pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_NONE, pwrcb)
29 |
30 | # configure usb from start if you want,
31 | # otherwise will be configured after PIN
32 | # pyb.usb_mode("VCP+MSC") # debug mode with USB and mounted storages from start
33 | # pyb.usb_mode("VCP") # debug mode with USB from start
34 | # disable at start
35 | # pyb.usb_mode(None)
36 | # os.dupterm(None,0)
37 | # os.dupterm(None,1)
38 |
39 | # inject version to platform module
40 | import platform
41 | platform.version = version
42 |
43 | # uncomment to run some custom main:
44 | pyb.main("hardwaretest.py")
45 |
--------------------------------------------------------------------------------
/boot/debug/debug.py:
--------------------------------------------------------------------------------
1 | # write here some bootstrap code for your debugging
2 |
3 | from keystore.javacard.util import get_connection
4 | from keystore.javacard.applets.memorycard import MemoryCardApplet
5 |
6 | conn = get_connection()
7 | app = MemoryCardApplet(conn)
--------------------------------------------------------------------------------
/boot/debug/hardwaretest.py:
--------------------------------------------------------------------------------
1 | from gui.specter import SpecterGUI
2 | from gui.screens.screen import Screen
3 | import lvgl as lv
4 | import asyncio
5 | from platform import delete_recursively, fpath, mount_sdram, get_version
6 | from hosts import QRHost
7 | from keystore.javacard.applets.memorycard import MemoryCardApplet
8 | from keystore.javacard.util import get_connection
9 |
10 | class HardwareTest:
11 | def __init__(self):
12 | self.rampath = mount_sdram()
13 | self.gui = SpecterGUI()
14 | Screen.COLORS["none"] = lv.color_hex(0xeeeeee)
15 | Screen.network = "none"
16 | self.qr = None
17 |
18 | def start(self):
19 | self.gui.start(dark=False)
20 | asyncio.run(self.main())
21 |
22 | async def main(self):
23 | buttons = [
24 | (1, "Wipe the device storage"),
25 | (2, "Configure QR code scanner"),
26 | (3, "Scan something"),
27 | (4, "Test smartcard"),
28 | ]
29 | while True:
30 | res = await self.gui.menu(buttons,
31 | title="Factory test, version %s" % get_version(),
32 | note="This firmware is used to test electrical connections between the discovery board and other components.\nIt can also erase the content of the internal storage\n(factory reset).")
33 | if res == 1:
34 | conf = await self.gui.prompt("Wipe the device?",
35 | "This will delete everything from internal storage.")
36 | if conf:
37 | await self.wipe()
38 | elif res == 2:
39 | if self.qr is None:
40 | self.qr = QRHost(self.rampath+"/qr")
41 | self.qr.init()
42 | self.qr.start(self)
43 | if self.qr.is_configured:
44 | await self.gui.alert("Success!", "QR code scanner is configured")
45 | else:
46 | await self.gui.alert("Fail...", "Something went wrong. Maybe reboot and try again...")
47 | elif res == 3:
48 | if self.qr is None:
49 | self.qr = QRHost(self.rampath+"/qr")
50 | self.qr.init()
51 | self.qr.start(self)
52 | await self.qr.enable()
53 | s = await self.qr.get_data()
54 | if s:
55 | data = s.read().decode()
56 | await self.gui.alert("Here's what we scanned:", data)
57 | else:
58 | conn = get_connection()
59 | if not conn.isCardInserted():
60 | await self.gui.alert("Card is not present!",
61 | "Smartcard is not inserted")
62 | else:
63 | try:
64 | conn.connect(conn.T1_protocol)
65 | except:
66 | pass
67 | try:
68 | app = MemoryCardApplet(conn)
69 | app.open_secure_channel()
70 | print(app.get_pin_status())
71 | if app.is_pin_set:
72 | await self.gui.alert("Smartcard works!", "Pin is set")
73 | else:
74 | await self.gui.alert("Smartcard works!", "Pin is not set")
75 | except Exception as e:
76 | await self.gui.alert("Something went wrong...",
77 | "We got an exception: %r" % e)
78 | await asyncio.sleep_ms(30)
79 |
80 | async def host_exception_handler(self, e):
81 | pass
82 |
83 | async def wipe(self):
84 | try:
85 | delete_recursively(fpath("/flash"))
86 | delete_recursively(fpath("/qspi"))
87 | await self.gui.alert("Success!", "All the content is deleted.")
88 | except Exception as e:
89 | await self.gui.alert("Fail!", "Something bad happened:\n"+str(e))
90 |
91 |
92 | if __name__ == '__main__':
93 | HardwareTest().start()
--------------------------------------------------------------------------------
/boot/main/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | # can run arbitrary Python, but best to keep it minimal
3 | import pyb, os, micropython, time
4 | import sys
5 |
6 | # Clean sys.path from qspi
7 | # Shouldn't happen in production, but just in case.
8 | for p in sys.path:
9 | if "qspi" in sys.path:
10 | sys.path.remove(p)
11 |
12 | # power hold
13 | pwr = pyb.Pin("B15", pyb.Pin.OUT)
14 | pwr.on()
15 |
16 | version = "0100900099"
17 |
18 | # get i2c
19 | i2c = pyb.I2C(1)
20 | i2c.init()
21 | # start measurements
22 | if 112 in i2c.scan():
23 | i2c.mem_write(0b00010000, 112, 0)
24 |
25 | leds = [pyb.LED(i) for i in range(1,5)]
26 | # poweroff on button press
27 | def pwrcb(e):
28 | micropython.schedule(poweroff, 0)
29 |
30 | # callback scheduled from the interrupt
31 | def poweroff(_):
32 | # make sure it disables power no matter what
33 | try:
34 | for led in leds:
35 | led.toggle()
36 | # stop battery manangement
37 | if 112 in i2c.scan():
38 | i2c.mem_write(0, 112, 0)
39 | # sync filesystem
40 | os.sync()
41 | time.sleep_ms(300)
42 | finally:
43 | # disable power
44 | pwr.off()
45 | time.sleep_ms(300)
46 | # will never reach here
47 | for led in leds:
48 | led.toggle()
49 |
50 | pyb.ExtInt(pyb.Pin('B1'), pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_NONE, pwrcb)
51 |
52 | # configure usb from start if you want,
53 | # otherwise will be configured after PIN
54 | # pyb.usb_mode("VCP+MSC") # debug mode with USB and mounted storages from start
55 | # pyb.usb_mode("VCP") # debug mode with USB from start
56 | # disable at start
57 | pyb.usb_mode(None)
58 | os.dupterm(None,0)
59 | os.dupterm(None,1)
60 |
61 | # inject version and i2c to platform module
62 | import platform
63 | platform.version = version
64 | platform.i2c = i2c
65 |
--------------------------------------------------------------------------------
/build_firmware.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | INFO="\e[1;36m"
3 | ENDCOLOR="\e[0m"
4 |
5 | echo -e "${INFO}
6 | ══════════════════════ Building main firmware ═════════════════════════════
7 | ${ENDCOLOR}"
8 | make clean
9 | make disco USE_DBOOT=1
10 |
11 | echo -e "${INFO}
12 | ═════════════════════ Building secure bootloader ══════════════════════════
13 | ${ENDCOLOR}"
14 | cd bootloader
15 | make clean
16 | make stm32f469disco READ_PROTECTION=1 WRITE_PROTECTION=1
17 | cd -
18 |
19 | echo -e "${INFO}
20 | ══════════════════════ Assembling final binaries ══════════════════════════
21 | ${ENDCOLOR}"
22 | mkdir -p release
23 |
24 | python3 ./bootloader/tools/make-initial-firmware.py -s ./bootloader/build/stm32f469disco/startup/release/startup.hex -b ./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex -f ./bin/specter-diy.hex -bin ./release/initial_firmware.bin
25 | echo -e "Initial firmware saved to release/initial_firmware.bin"
26 |
27 | python3 ./bootloader/tools/upgrade-generator.py gen -f ./bin/specter-diy.hex -p stm32f469disco ./release/specter_upgrade.bin
28 | cp ./release/specter_upgrade.bin ./release/specter_upgrade_unsigned.bin
29 | echo "Unsigned upgrate file saved to release/specter_upgrade_unsigned.bin"
30 |
31 | HASH=$(python3 ./bootloader/tools/upgrade-generator.py message ./release/specter_upgrade.bin)
32 |
33 | echo "
34 | ╔═════════════════════════════════════════════════════════════════════════╗
35 | ║ Message to sign with vendor keys: ║
36 | ║ ║
37 | ║ ${HASH} ║
38 | ║ ║
39 | ╚═════════════════════════════════════════════════════════════════════════╝
40 | "
41 |
42 |
43 | echo -e "${INFO}
44 | ═════════════════════ Adding signature to the binary ══════════════════════
45 | ${ENDCOLOR}"
46 |
47 | while true; do
48 | echo "Provide a signature to add to the upgrade file, or just hit enter to stop."
49 | read SIGNATURE
50 | if [ -z $SIGNATURE ]; then
51 | break
52 | fi
53 | python3 ./bootloader/tools/upgrade-generator.py import-sig -s $SIGNATURE ./release/specter_upgrade.bin
54 | echo "Signature is added: ${SIGNATURE}"
55 | done
56 |
57 | echo -e "${INFO}
58 | ═════════════════════════ Hashes of the binaries: ═════════════════════════
59 | ${ENDCOLOR}"
60 |
61 | cd release
62 | sha256sum *.bin > sha256.txt
63 | cat sha256.txt
64 |
65 | echo "
66 | Hashes saved to release/sha256.txt file.
67 | "
--------------------------------------------------------------------------------
/demo_apps/.gitignore:
--------------------------------------------------------------------------------
1 | *.mpy
--------------------------------------------------------------------------------
/demo_apps/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | 'helloworld',
3 | ]
4 |
--------------------------------------------------------------------------------
/demo_apps/helloworld.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo of a single-file app extending Specter functionality.
3 | This app returns hello to the host
4 | """
5 | from app import BaseApp, AppError
6 | from io import BytesIO
7 | from binascii import hexlify
8 | from gui.screens import Prompt
9 | # Should be called App if you use a single file
10 |
11 |
12 | class App(BaseApp):
13 | """Allows to query random bytes from on-board TRNG."""
14 | prefixes = [b"hello"]
15 |
16 | async def process_host_command(self, stream, show_screen):
17 | """
18 | If command with one of the prefixes is received
19 | it will be passed to this method.
20 | Should return a tuple:
21 | - stream (file, BytesIO etc)
22 | - meta object with title and note
23 | """
24 | # reads prefix from the stream (until first space)
25 | prefix = self.get_prefix(stream)
26 | if prefix != b"hello":
27 | # WTF? It's not our data...
28 | raise AppError("Prefix is not valid: %s" % prefix.decode())
29 | name = stream.read().decode()
30 | # ask the user if he really wants it
31 | # build a screen
32 | scr = Prompt("Say hello?",
33 | "Are you sure you want to say hello to\n\n%s?\n\n"
34 | "Saying hello can compromise your security!"
35 | % name)
36 | # show screen and wait for result
37 | res = await show_screen(scr)
38 | # check if he confirmed
39 | if not res:
40 | return
41 | obj = {
42 | "title": "Hello!",
43 | }
44 | d = b"Hello " + name.encode()
45 | return BytesIO(d), obj
46 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | ## Table of contents
4 |
5 | - [`shopping.md`](./shopping.md) explains what to buy
6 | - [`assembly.md`](./assembly.md) shows how to put everything together.
7 | - [`quickstart.md`](./quickstart.md) guides you through the initial steps how to get firmware on the board
8 | - [`security.md`](./security.md) explains possible attack vectors and security model of the project
9 | - [`development.md`](./development.md) explains how to start developing on Specter
10 | - [`simulator.md`](./simulator.md) shows how to run a simulator on unix/macOS
11 | - [`communication.md`](./communication.md) defines communication protocol with the host over QR codes and USB
12 | - [`roadmap.md`](./roadmap.md) explains what we need to implement before we can consider the wallet be ready to use with real funds.
--------------------------------------------------------------------------------
/docs/assembly.md:
--------------------------------------------------------------------------------
1 | # Assembly of Specter-DIY
2 |
3 | ## Connecting Waveshare Barcode scanner
4 |
5 | The wallet firmware will configure the scanner for you on the first run, so no manual preconfiguration is required.
6 |
7 | Here is how you connect the scanner to the board:
8 |
9 | 
10 |
11 | For convenience you can buy an Arduino Protype shield and solder & mount everything on it (i.e. [this one](https://www.digikey.com/catalog/en/partgroup/proto-shield-rev3-uno-size/79347))
12 |
13 | ## Power management
14 |
15 | On the top side of the board there is a jumper that defines where it will take power. You can change position of the jumper and select power source to be one of the USB ports or VIN pin and connect external battery there (should be 5V).
16 |
17 | ## Enclosure for DIY
18 |
19 | Check out the [`enclosures`](./enclosures) folder.
20 |
21 | ## [Be creative!](./pictures/gallery/README.md)
22 |
23 | Assemble your own Specter-DIY and send us the pictures (make a pull request or reach out to us).
24 |
25 | Check out the [Gallery](./pictures/gallery/README.md) with Specters assembled by the community!
--------------------------------------------------------------------------------
/docs/build.md:
--------------------------------------------------------------------------------
1 | # Build
2 |
3 | Clone the repository recursively `git clone https://github.com/cryptoadvance/specter-diy.git --recursive`
4 |
5 | `bootloader` folder contains a [secure bootloader](https://github.com/cryptoadvance/specter-bootloader) that you can customize with your own firmware signing keys.
6 |
7 | ## Prerequisities
8 |
9 | ### Nix shell
10 |
11 | The easiest way to get all necessary tools is to run `nix-shell` from the root of the repository. You need to have [Nix](https://nixos.org/) installed.
12 |
13 | ### Prerequisities: Board
14 |
15 | To compile the firmware for the board you will need `arm-none-eabi-gcc` compiler.
16 |
17 | **Debian/Ubuntu**:
18 | ```sh
19 | sudo apt-get install build-essential gcc-arm-none-eabi binutils-arm-none-eabi gdb-multiarch openocd
20 | ```
21 |
22 | **Archlinux**:
23 | ```sh
24 | sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils openocd base-devel python-case
25 | ```
26 | You might need change default gcc flag settings with `CFLAGS_EXTRA="-w"`. Export it or set the variable before of `make`
27 | to avoid warnings being raised as errors.
28 |
29 | **MacOS**:
30 | ```sh
31 | brew tap ArmMbed/homebrew-formulae
32 | brew install arm-none-eabi-gcc
33 | ```
34 |
35 | On **Windows**: Install linux subsystem and follow Linux instructions.
36 |
37 | ### Prerequisities: Simulator
38 |
39 | You may need to install SDL2 library to simulate the screen of the device.
40 |
41 | **Linux**:
42 | ```sh
43 | sudo apt install libsdl2-dev
44 | ```
45 |
46 | **MacOS**:
47 | ```sh
48 | brew install sdl2
49 | ```
50 |
51 | **Windows**:
52 | - `sudo apt install libsdl2-dev` on Linux side.
53 | - install and launch [Xming](https://sourceforge.net/projects/xming/) on Windows side
54 | - set `export DISPLAY=:0` on linux part
55 |
56 | ## Build
57 |
58 | To build custom bootloader and firmware that you will be able to sign check out the bootloader doc on [self-signed firmware](https://github.com/cryptoadvance/specter-bootloader/blob/master/doc/selfsigned.md). To wipe flash and remove protections on the device with the secure bootloader check out [this doc](https://github.com/cryptoadvance/specter-bootloader/blob/master/doc/remove_protection.md).
59 |
60 | To build an open firmware (no bootloader and signature verifications) run `make disco`. The result is the `bin/specter-diy.bin` file that you can copy-paste to the board over miniUSB.
61 |
62 | To build a simulator run `make unix` - it will compile a micropython simulator for mac/unix and store it under `bin/micropython_unix`.
63 |
64 | To launch a simulator either run `bin/micropython_unix simulate.py` or simly run `make simulate`.
65 |
66 | If something is not working you can clean up with `make clean`
67 |
68 | ## Run Unittests
69 |
70 | Currently unittests work only on linuxport, and there are... not many... Contributions are very welcome!
71 |
72 | ```
73 | make test
74 | ```
75 |
--------------------------------------------------------------------------------
/docs/communication.md:
--------------------------------------------------------------------------------
1 | # Communcation with the host
2 |
3 | ## QR codes
4 |
5 | ### Address verification
6 |
7 | To verify receiving address hardware wallet expects the following string:
8 |
9 | ```
10 | bitcoin:
?index=
11 | ```
12 |
13 | Prefix `bitcoin:` is optional. Index should be the last index of the derivation path for one of existing wallet descriptors.
14 |
15 | Specter tries to derive an address for descriptors of all existing wallets and displays the address on the screen if one of derived addresses matches.
16 |
17 | For example, if Specter has two wallets:
18 |
19 | - Simple with descriptor `wpkh([b317ec86/84h/1h/0h]vpub5YHLPnkkpPW1ecL7Di7Gv2wDHDtBNqRdt17gMULpxJ27ZA1MmW7xbZjdg1S7d5JKaJ8CiZEmRUHrEB6CGuLomA6ioVa1Pcke6fEb5CzDBU1)`
20 | - Multisig with descriptor `wsh(sortedmulti(2,[b317ec86/48h/1h/0h/2h]tpubDEToKMGFhyuP6kfwvjtYaf56khzS1cUcwc47C6aMH6bQ8sNVLMcCK6jr21YDCkU2QhTK5CAnddhfgZ8dD4EL1wGCaAKZaGFeVVdXHaJMTMn,[f04828fe/48h/1h/0h/2h]tpubDFekS5zvPSdW6WWjH2p7vPRkxmeeNGnirmj36AUyoAYbJvfKBj6UARWR5gQ6FRrr98dzT1XFTi6rfGo9AAAeutY1S6SoWijQ8BKxDhYQzDR,[d3c05b2e/48h/1h/0h/2h]tpubDFnAczXQTHxuBh7FxrpLDHBidkC1Di54pTPSPMu4AQjKziFQQTTEFXEVugqm8ucKQhJfLGesBjRZWtLpqAkAmecoXtvaPwCzf4teqrY7Uu5))`
21 |
22 | and it scanned QR code with data `bitcoin:bcrt1qd3mtrhysk3k4w6fmu7ayjvwk6q98c2dpf0p4x87zauu8rcgq5dzq73tyrx?index=2`
23 |
24 | it will try to derive receiving addresses for both wallets appending `/0/2` to every descriptor key. In this case for Multisig wallet the address will match, therefore it will display to the user this address with a title `Address #2 from wallet "Multisig"`.
25 |
26 | *Note that wallets are defined for particular network, so if you have a multisig wallet on regtest doesn't mean that it exists on testnet as well, and Specter only checks wallets in currently selected network.*
27 |
28 | ### Adding wallet to Specter
29 |
30 | In order to sign transaction or verify an address Specter needs to know about corresponding wallet. By default only `wpkh` wallet is created for each network, so all multisig wallets need to be imported.
31 |
32 | To import the wallet using QR codes user needs to get to the **Wallets** menu and click on **Add wallet**. Scanned QR code should be of the following form:
33 |
34 | ```
35 | addwallet &
36 | ```
37 |
38 | Descriptors used in Specter are almost the same as in [Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) with a few differences - they support miniscript, multiple branches definitions and use default derivations `/{0,1}/*`. Check out [descriptors.md](./descriptors.md) for details.
39 |
40 | Example of the multisig wallet import code:
41 |
42 | ```
43 | addwallet My multisig&wsh(sortedmulti(2,[b317ec86/48h/1h/0h/2h]tpubDEToKMGFhyuP6kfwvjtYaf56khzS1cUcwc47C6aMH6bQ8sNVLMcCK6jr21YDCkU2QhTK5CAnddhfgZ8dD4EL1wGCaAKZaGFeVVdXHaJMTMn,[f04828fe/48h/1h/0h/2h]tpubDFekS5zvPSdW6WWjH2p7vPRkxmeeNGnirmj36AUyoAYbJvfKBj6UARWR5gQ6FRrr98dzT1XFTi6rfGo9AAAeutY1S6SoWijQ8BKxDhYQzDR,[d3c05b2e/48h/1h/0h/2h]tpubDFnAczXQTHxuBh7FxrpLDHBidkC1Di54pTPSPMu4AQjKziFQQTTEFXEVugqm8ucKQhJfLGesBjRZWtLpqAkAmecoXtvaPwCzf4teqrY7Uu5))
44 | ```
45 |
46 | It will promt the user and then create a wallet called "My multisig" with 2 of 3 multisig policy with sorted public keys.
47 |
48 | ### Signing transaction
49 |
50 | Just display a base64-encoded PSBT transaction as a QR code.
51 |
52 | We also added one special case for bip32 derivations - if fingerprint in derivation is set to `00000000` it is replaced by the fingerprint of the device. We treat this fingerprint as a mark from software wallet that it doesn't know the fingerprint of the device.
53 |
54 | In this case PSBT transaction can be constructed with a correct derivation path even if fingerprint is not known to the software wallet, but the derivation path is known - for example when `zpub` or `ypub` is imported software wallet knows the depth of the derivation (normally `3`), purpose (`84` for `zpub` and `49` for `ypub`), coin type (`0` - Mainnet for `zpub` and `ypub`, `1` - Testnet for `upub` or `vpub`) and master key child number. So `zpub` and `ypub` normally contain full derivation path of the key without master fingerprint.
55 |
56 | Signed transaction is also displayed as a base64-encoded PSBT transaction with all unnecessary fields removed - only global transaction and partial signatures for all inputs remain there. All other fields are removed to save space in the QR code. This means that software wallet needs to keep original PSBT and combine them when signed PSBT is scanned.
57 |
58 | ## USB communication
59 |
60 | We use human-readable plain text messages, because we can and they are way easier to debug even though they are not optimal in sense of space. Each command should end with `\r` or `\r\n`.
61 |
62 | The following commands are supported:
63 |
64 | - `fingerprint` - returns hex fingerprint of the root key.
65 | - `xpub ` - returns xpub with derivation. For hardened derivation both `h` and `'` can be used. For example `xpub m/84h/1h/0h`.
66 | - `sign ` - asks user to confirm transaction signing.
67 | - `showaddr [witness_script_hex]` - show address of `type` with `derivation`. `type` can be `wpkh`, `sh-wpkh`, `pkh`, `sh`, `sh-wsh` or `wsh`. Witness script is required for non-pkh wallets.
68 | - `importwallet &` - asks user to confirm adding new `wallet` with `descriptor`.
69 |
70 | ## SD card
71 |
72 | `.psbt` and `.txt` files are supported. The content of the file is processed like a USB or QR code, so it can be a transaction, wallet import command or address verification command.
73 |
--------------------------------------------------------------------------------
/docs/datasheets/Mikroe-Rakinda-LV3296.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/datasheets/Mikroe-Rakinda-LV3296.pdf
--------------------------------------------------------------------------------
/docs/datasheets/Waveshare-GROW-GM65-S.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/datasheets/Waveshare-GROW-GM65-S.pdf
--------------------------------------------------------------------------------
/docs/descriptors.md:
--------------------------------------------------------------------------------
1 | # Descriptors support
2 |
3 | All normal Bitcoin descriptors work. Aside from that we have a few extensions:
4 |
5 |
6 | ## Multiple branches in descriptors
7 |
8 | To save some space in the QR codes we allow adding descriptors with multiple branches in one go. If you want to use `wpkh(xpub/0/*)` for receiving addresses and `wpkh(xpub/1/*)` for change addresses you can combine them in a single descriptor: `wpkh(xpub/{0,1}/*)` - the wallet will treat first index of the `{}` set part as the branch for receiving addresses and the second one as change addresses.
9 |
10 | You can also specify more than two branches, and branch indexes can be different for different cosigners, so this descriptor is very weird but totally valid:
11 |
12 | ```
13 | wsh(sortedmulti(2,xpubA/{22,33,44}/*,xpubB/34/*/{1,8,6},pubkey3))
14 | ```
15 |
16 | Here for receiving address number 17 the wallet will use the script from `wsh(sortedmulti(2,xpubA/22/17,xpubB/34/17/1,pubkey3))`.
17 |
18 | The only requirement is that the number of indexes in all sets is the same (3 in the case above).
19 |
20 | ## Default derivations
21 |
22 | If the descriptor contains master public keys but doesn't contain wildcard derivations, the default derivation `/{0,1}/*` will be added to all extended keys in the descriptor. If at least one of the xpubs has a wildcard derivation the descriptor will not be changed.
23 |
24 | The descriptor `wpkh(xpub)` will be converted into `wpkh(xpub/{0,1}/*)`.
25 |
26 | ## Miniscript
27 |
28 | Specter supports miniscript, but doesn't support policy-to-miniscript compilation (because it's way too expensive). We perform some checks on the miniscipt, so only `B` scripts are allowed on the top level and all arguments in sub-miniscripts have to have properties according to the [spec](http://bitcoin.sipa.be/miniscript/).
29 |
30 | You can use http://bitcoin.sipa.be/miniscript/ to generate a descriptor from a policy and then import it to the wallet.
31 |
32 | For example, a policy "I can spend now, or in 100 days my wife can spend" can be converted into the wallet like so:
33 |
34 | Policy: `or(9@pk(xpubA),and(older(14400),pk(B)))` (my key is 9-times more likely)
35 |
36 | Miniscript: `or_d(pk(xpubA),and_v(v:pkh(xpubB),older(14400)))`
37 |
38 | Descriptor: `wsh(or_d(pk(xpubA),and_v(v:pkh(xpubB),older(14400))))`
39 |
40 | As here we don't have any wildcard derivations the default derivations of `/{0,1}/*` will be appended to the xpubs.
41 |
--------------------------------------------------------------------------------
/docs/development.md:
--------------------------------------------------------------------------------
1 | # Developer notes for Specter-DIY
2 |
3 | ## Compiling the code yourself
4 |
5 | We use this build as a platform for Specter: https://github.com/diybitcoinhardware/f469-disco
6 |
7 | To compile the firmware you will need `arm-none-eabi-gcc` compiler.
8 |
9 | On MacOS install it using brew: `brew install arm-none-eabi-gcc`
10 |
11 | On Debian: `sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi gdb-arm-none-eabi openocd`
12 |
13 | On Arch Linux: `sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-gdb arm-none-eabi-newlib openocd`
14 |
15 | Run `make disco` to get the binary or `make unix` to compile the simulator. They will be in the `bin` folder.
16 |
17 | `specter-diy.bin` file is the firmware that you need to copy to the device.
18 |
19 | The easiest way to start developing is to use a [simulator](./simulator.md), and when you are done - try it on a real hardware.
20 |
21 | ## Enabling developer mode
22 |
23 | By default developer mode and USB communication are turned off. This means that when you connect the board to the computer it will NOT mount the `PYBFLASH` anymore and there will be no way to connect to debug shell.
24 |
25 | To turn on the developer mode get to the main screen (enter PIN code, generate recovery phrase, enter password), and then go to **Settings - Security - turn on Developer mode - Save**.
26 |
27 | Now the board will restart and get mounted to the computer as before. You can also connect to the board over miniUSB and get to interactive console (baudrate 115200). You can use `screen` or `putty` or `minicom` for that, i.e. `screen /dev/tty.usbmodem14403 115200`.
28 |
29 | ## Writing a simple app
30 |
31 | Specter can be extended with custom apps. Most of the functionality is already splitted into apps, like `WalletManager` to manage your wallets, `MessageApp` to sign bitcoin messages, `XpubApp` to show master public keys etc.
32 |
33 | Check out the [apps](../src/apps) folder to understand how they work.
34 |
35 | TODO: More detailed description
36 |
--------------------------------------------------------------------------------
/docs/enclosures/README.md:
--------------------------------------------------------------------------------
1 | # Enclosures for Specter-DIY
2 |
3 | There are a few case designs made by the community:
4 |
5 | ## Specter-DIY Snap Case
6 |
7 | 
8 |
9 | Print it yourself enclosure with the waveshare scanner on the backside.
10 |
11 | The printable STL files are available for download in the [snapcase](./snapcase/) folder.
12 |
13 | Please note that the prints are quite tight around the hardware components. They are well tested with a Prusa MK3s and Specter hardware components from late 2022. Other printers or newer or older hardware may produce results that do not fit. Please check carefully that the prints do not exert pressure on any hardware components and don't exert any pressure yourself.
14 |
15 | Contact me if you have any questions or suggestions. Success stories are also very welcome. :) You can reach me on [X (@kayth21)](https://x.com/kayth21) or on [Telegram (@kayth_21)](https://t.me/kayth_21).
16 |
17 | You can also buy a Snap Case, or a fully assembled Specter-DIY, from [bitcoin-store.org](https://bitcoin-store.org).
18 |
19 | ## SeedSigner's "Barebones" enclosure, v2
20 |
21 | Updated design from @SeedSigner (Twitter/Telegram) with a smaller form factor & MicroSD access.
22 |
23 | The printable .STL files for v1 and v2 are available for download from the [docs/enclosures/seedsigner](./seedsigner/) folder.
24 |
25 | You can also buy the v2, or a fully assembled Specter-DIY, from [his shop](https://btc-hardware-solutions.square.site/).
26 |
27 | (E.U. residents can contact @surfacePlasmon (Twitter) or @daz21 (Telegram) to purchase an enclosure via mail or visit diynodes.com)
28 |
29 | 
30 |
31 | ## Full metal DIY
32 |
33 | 
34 |
35 | https://github.com/davewhiiite/wraith
36 |
37 | ## Enclosure by Thomas
38 |
39 | 
40 |
41 | [3d files](./thomas)
42 |
--------------------------------------------------------------------------------
/docs/enclosures/seedsigner/BBv1_Lower.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/seedsigner/BBv1_Lower.stl
--------------------------------------------------------------------------------
/docs/enclosures/seedsigner/BBv1_Upper.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/seedsigner/BBv1_Upper.stl
--------------------------------------------------------------------------------
/docs/enclosures/seedsigner/BBv2_Lower.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/seedsigner/BBv2_Lower.stl
--------------------------------------------------------------------------------
/docs/enclosures/seedsigner/BBv2_Upper.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/seedsigner/BBv2_Upper.stl
--------------------------------------------------------------------------------
/docs/enclosures/snapcase/README.md:
--------------------------------------------------------------------------------
1 | # Specter-DIY Snap Case
2 |
3 | 
4 |
5 | Specter-DIY Snap Case is a print it yourself enclosure with the waveshare scanner on the backside. It consists of a scanner part, in which the scanner is held and the cables are fixed, and a frontside and a backside part, which can be plugged together.
6 |
7 | ## Please note
8 |
9 | The prints are quite tight around the hardware components. They are well tested with a Prusa MK3s and Specter hardware components from late 2022. Other printers or newer or older hardware may produce results that do not fit. Please check carefully that the prints do not exert pressure on any hardware components and don't exert any pressure yourself.
10 |
11 | ## Assembly
12 |
13 | Please note that this manual assumes that you are familiar with the basic assembly of a Specter-DIY and therefore only addresses the specialties associated with this case. If not, please head over to the [docs](../../).
14 |
15 | ### Print all the parts
16 |
17 | 
18 |
19 | - The SDL files are attached to this site.
20 |
21 | ### Remove all screws from the scanner
22 |
23 | 
24 |
25 | ### Mount the scanner module
26 |
27 | 
28 |
29 | - Use the two small screws to lock the scanner in the designated position.
30 |
31 | ### Connect the scanner to the board
32 |
33 | 
34 |
35 | - Place the middle part on the board.
36 | - Slide the cables through the channels.
37 | - Connect the cables through the pins to the corresponding slots.
38 |
39 | ### Plug together the case
40 |
41 | 
42 |
43 | - Please double check that the parts are right side up.
44 | - Because of the snaps, you need a little pressure to put them together. Before you put them together, please make sure that the top part fits well over the display.
45 |
46 | ## Contact
47 |
48 | Contact me if you have any questions or suggestions. Success stories are also very welcome. :) You can reach me on [X (@kayth21)](https://x.com/kayth21) or on [Telegram (@kayth_21)](https://t.me/kayth_21).
49 |
50 | ## Shop
51 |
52 | You can also buy a Snap Case, or a fully assembled Specter-DIY, from [bitcoin-store.org](https://bitcoin-store.org).
--------------------------------------------------------------------------------
/docs/enclosures/snapcase/Specter-DIY Snap Case - Backside.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/snapcase/Specter-DIY Snap Case - Backside.stl
--------------------------------------------------------------------------------
/docs/enclosures/snapcase/Specter-DIY Snap Case - Frontside.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/snapcase/Specter-DIY Snap Case - Frontside.stl
--------------------------------------------------------------------------------
/docs/enclosures/snapcase/Specter-DIY Snap Case - Scanner.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/snapcase/Specter-DIY Snap Case - Scanner.stl
--------------------------------------------------------------------------------
/docs/enclosures/thomas/DIY-Front-Display-v0.12.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/thomas/DIY-Front-Display-v0.12.stl
--------------------------------------------------------------------------------
/docs/enclosures/thomas/DIY-Platte-v0.21.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/thomas/DIY-Platte-v0.21.stl
--------------------------------------------------------------------------------
/docs/enclosures/thomas/Scan_case-v0.2.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/enclosures/thomas/Scan_case-v0.2.stl
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # General Questions
2 |
3 | ## *What do I need to build Specter-DIY?*
4 |
5 | To build Specter yourself you need:
6 |
7 | - STM32F469 discovery board
8 | - QR Code scanner from Waveshare
9 | - Power Bank
10 | - miniUSB cable
11 | - A few pin connectors
12 |
13 | [Shopping list link](https://github.com/cryptoadvance/specter-diy/blob/master/docs/shopping.md) + [assembly link](https://github.com/cryptoadvance/specter-diy/blob/master/docs/assembly.md)
14 | Waveshare QR scanner is recommended as it has a good quality/price ratio.
15 |
16 | ## *Is specter-DIY safe to use?*
17 |
18 | Yes, kinda. The security level of Specter-DIY is comparable to commercial hardware wallets present on the market. We implemented a secure bootloader that verifies firmware upgrades, so you can be sure that only signed firmware can be uploaded to the device after initial setup.
19 |
20 | Note that *initial* firmware installation is *not verified* and the security of this process is limited to the security of your computer. During initial installation make sure you verified PGP signatures and flash the firmware from a secure computer.
21 |
22 | ## *I'm wondering what if someone takes the device? How does Specter-DIY approach this scenario?*
23 |
24 | There are no known attacks that would allow extraction of the keys from the device - our secure bootloader sets a security flag of the microcontroller and the only thing the attacker can do is erase the device completely and install malicious firmware.
25 |
26 | To mitigate this attack Specter-DIY has "anti-phishing words" on the PIN screen that are displayed when you start entering the PIN code. These words are unique for every device and will change if the device is erased. So make sure these words remain the same when you are entering the PIN code.
27 |
28 | By default the device operates in amnesic mode - it forgets your recovery phrase when you power it off. This means you need to enter your recovery phrase every time when you use the device. Optionally you can save the recovery phrase on the device itself, or on the SD card. In the latter case, the SD card works as a second factor - you can access your keys only if you have both the device and the SD card.
29 |
30 | ## *Currently there is a `specter_hwi.py` file, which implements the HWIClient for Specter-DIY. Is there any reason you didn't add that directly to HWI?*
31 |
32 | We do not plan to add Specter-DIY to HWI - we discourage USB as a communication channel for Specter-DIY. Use QR codes or SD card instead.
33 |
34 | ## *Do you have a physical security design?*
35 |
36 | Physical security of Specter-DIY is limited to the security features available on the microcontroller. It theoretically can be hacked using glitching attacks, but they were not demonstrated on this MCU series.
37 |
38 | We also have support for secure smartcards as key storage, but it requires a special adaptor board (Specter Shield). We are planning to add support for off-the-shelf smartcard readers, but no time estimates here.
39 |
40 | ## *Is there a simulator I can try the Specter-DIY with?*
41 |
42 | Yes. Specter-DIY has a [simulator](https://github.com/cryptoadvance/specter-diy/blob/master/docs/simulator.md).
43 |
44 | ## *How do I load firmware updates to Specter-DIY?*
45 |
46 | Initial setup on an empty board happens over miniUSB, but this is the only time you need to connect your device to the computer. After that you need to use SD card for upgrades - insert an SD card with upgrade file to the device and turn it on. Integrity and signatures of the upgrade file are checked by the secure bootloader.
47 |
48 | ## *Can Specter-DIY register cosigner xpubs like ColdCard? I know you wipe private keys on shutdown, but do you save stuff like that?*
49 |
50 | Yes, we keep wallet descriptors and other public info.
51 |
52 | ## *Once you add the javacard (secure element) you'll save the private keys, too?*
53 |
54 | With the secure element you will have three options:
55 |
56 | - agnostic mode, forgets key after shutdown
57 | - store key on the smartcard but do all crypto on application MCU
58 | - store key and do crypto on the secure element
59 |
60 | At the moment, we have implementation for the first two options. Last seems to be the most secure, but then you need to trust proprietary crypto implementation. The second option saves the private key on the secure element under pin protection, and it can be encrypted, so the secure element never knows the private keys.
61 |
62 | # Troubleshooting questions
63 |
64 | # I can't flash my device via Mini-USB ?
65 |
66 | There is a file called `FAIL.txt` saying `The interface firmware FAILED to reset/halt the target MCU`
67 |
68 | The Device is probably protected. You can't upgrade via MiniUSB anymore but just via SD-card. Please carefully read the Release-notes.
69 |
70 | # I want to do a factory-reset. How can i remove the protection?
71 |
72 | https://github.com/cryptoadvance/specter-bootloader/blob/master/doc/remove_protection.md
73 |
74 | ## *Does anyone have any tips on mounting the power bank and QR code scanner to the STM32 board in a somewhat ergonomic manner?*
75 |
76 | Use the smallest powerbank possible. Check out the [gallery](https://github.com/cryptoadvance/specter-diy/blob/master/docs/pictures/gallery/README.md) to see how people do it.
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/docs/pictures/gallery/README.md:
--------------------------------------------------------------------------------
1 | # Gallery
2 |
3 | Here are a few pictures from people who assembled their own Specter.
4 |
5 | If you did it as well we will be happy to put it here.
6 |
7 | ### @kayth21
8 |
9 | 
10 |
11 | Created with a Prusa MK3s using the template from [enclosures/snapcase](../../enclosures/snapcase).
12 |
13 | ### @davewhiiite
14 |
15 | 
16 |
17 | ### @seedsigner
18 |
19 | 
20 |
21 | ### @lunaticoin
22 |
23 | 
24 |
25 | ### @Thomas1378
26 |
27 | 
28 |
29 | ### @bitcoinheiro
30 |
31 | 
32 |
33 | Created 3d printed storage box for this setup with small battery attached and waveshare scanner on the side. Available at https://www.tinkercad.com/things/46xyXAhv0Fy
34 |
35 | ### @kkdao
36 |
37 | 
38 |
39 | ### @dimaatmelodromru
40 |
41 | 
42 |
43 | ### @k9ert
44 |
45 | 
46 |
47 | ### @gorazdko
48 |
49 | 
50 |
51 | ### @bavarianledger
52 |
53 | 
54 |
55 | ### @kdmukai
56 |
57 | 
58 |
59 | ### @stepansnigirev
60 |
61 | 
62 |
63 | ### @davewhiiite
64 |
65 | 
66 | 
67 | 
68 |
69 | ### Re-use the original packaging of the STM32F469I-DISCO @henrik
70 | 
71 | 
72 | 
73 |
74 | ### @you?
75 |
--------------------------------------------------------------------------------
/docs/pictures/gallery/barebones_v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/barebones_v2.png
--------------------------------------------------------------------------------
/docs/pictures/gallery/bavarianledger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/bavarianledger.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/bitcoinhero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/bitcoinhero.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/davewhiiite.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/davewhiiite.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/davewhiiite1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/davewhiiite1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/davewhiiite2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/davewhiiite2.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/davewhiiite3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/davewhiiite3.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/dimaatmelodromru.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/dimaatmelodromru.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/gorazdko.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/gorazdko.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/k9ert.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/k9ert.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/kdmukai.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/kdmukai.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/kkdao.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/kkdao.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/lunaticoin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/lunaticoin.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/org_packaging_signer_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/org_packaging_signer_1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/org_packaging_signer_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/org_packaging_signer_2.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/org_packaging_signer_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/org_packaging_signer_3.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-bronze-black-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-bronze-black-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-connect-scanner-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-connect-scanner-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-double-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-double-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-double-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-double-2.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-mount-scanner-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-mount-scanner-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-plug-together-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-plug-together-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-printed-parts-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-printed-parts-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/snap-case-remove-screws-from-scanner-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/snap-case-remove-screws-from-scanner-1.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/stepansnigirev.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/stepansnigirev.jpg
--------------------------------------------------------------------------------
/docs/pictures/gallery/thomas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/gallery/thomas.jpg
--------------------------------------------------------------------------------
/docs/pictures/init_screens.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/init_screens.jpg
--------------------------------------------------------------------------------
/docs/pictures/kit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/kit.jpg
--------------------------------------------------------------------------------
/docs/pictures/kit_with_case.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/kit_with_case.jpg
--------------------------------------------------------------------------------
/docs/pictures/wallet_screens.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/wallet_screens.jpg
--------------------------------------------------------------------------------
/docs/pictures/waveshare_wiring.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/docs/pictures/waveshare_wiring.jpg
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | # Installing the compiled code
2 |
3 | With the secure bootloader initial installation of the firmware is slightly different. Upgrades are easier and don't require to connect hardware wallet to the computer.
4 |
5 | ## Flashing initial firmware
6 |
7 | **Note** If you don't want to use binaries from the releases check out the [bootloader documentation](https://github.com/cryptoadvance/specter-bootloader/blob/master/doc/selfsigned.md) that explains how to compile and configure it to use your public keys instead of ours.
8 |
9 | - If you are upgrading from versions below `1.4.0` or uploading the firmware for the first time, use the `initial_firmware_.bin` from the [releases](https://github.com/cryptoadvance/specter-diy/releases) page.
10 | - Verify the signature of the `sha256.signed.txt` file against [Stepan's PGP key](https://stepansnigirev.com/ss-specter-release.asc)
11 | - Verify the hash of the `initial_firmware_.bin` against the hash stored in the `sha256.signed.txt`
12 | - If you are upgrading from an empty bootloader or you see the bootloader error message that firmware is not valid, check out the next section - [Flashing signed Specter firmware](#flashing-signed-specter-firmware).
13 | - Make sure the [power jumper](./assembly.md) of your discovery board is at STLK position
14 | - Connect the discovery board to your computer via the **miniUSB** cable on the top of the board.
15 | - The board should appear as a removable disk named `DIS_F469NI`.
16 | - Copy the `initial_firmware_.bin` file into the root of the `DIS_F469NI` filesystem.
17 | - When the board is done flashing the firmware the board will reset itself and reboot to the bootloader. Bootloader will check the firmware and boot into the main firmware. If see an error message that no firmware is found - follow the update instructions and upload firmware via SD card.
18 | - Now you can switch the power jumper where you like it and power the board from the powerbank or battery.
19 |
20 | > Flashing initial firmware via copy-paste of the `.bin` file sometimes fails - often because of the cable, or if you connect the device through a USB hub. In this case you can try a few more times (normally works in 2-3 attempts).
21 | >
22 | > If it fails all the time you can use [stlink](https://github.com/stlink-org/stlink/releases/latest) open-source tool. Install it and type in your terminal: `st-flash write 0x8000000`. It usually works much more reliable.
23 |
24 | ## Upgrading firmware
25 |
26 | - Download the `specter_upgrade_.bin` from the [releases](https://github.com/cryptoadvance/specter-diy/releases).
27 | - Copy this binary to the root of the SD card (FAT-formatted, 32 GB max)
28 | - Make sure only one `specter_upgrade***.bin` file is in the root directory
29 | - Insert SD card to the SD slot of the discovery board and power on the board
30 | - Bootloader will flash the firmware and will notify you when it's done.
31 | - Reboot the board - you will see Specter-DIY interface now, it will suggest you to select your PIN code
32 |
33 | Whenever a new release is out just download the `specter_upgrade_.bin` from the releases, drop it to the SD card and upgrade the device just like in the previous step. Bootloader will make sure only signed firmware can be loaded to the board.
34 |
35 | ## How to find out firmware version
36 |
37 | Go to the `Device settings` menu - it will show the version number under the title of the screen.
38 |
--------------------------------------------------------------------------------
/docs/reproducible-build.md:
--------------------------------------------------------------------------------
1 | # Reproducible build
2 |
3 | With [docker](https://docs.docker.com/get-docker/) you can build the firmware yourself in the same environment as we do, and verify that binaries in github releases have the same hash. This way you can be sure that firmware upgrades signed by our public keys are actually built from the code in this repository, no backdoors included.
4 |
5 | From the root of the repository:
6 |
7 | 1. Set up bootloader to use production keys:
8 |
9 | ```sh
10 | cp bootloader/keys/production/pubkeys.c bootloader/keys/selfsigned/
11 | ```
12 |
13 | 2. Build a docker container:
14 |
15 | ```sh
16 | docker build -t diy .
17 | ```
18 |
19 | 3. Run the container in interactive mode:
20 |
21 | ```sh
22 | docker run -ti -v `pwd`:/app diy
23 | ```
24 |
25 | At the end of the build you will be presented with a base32 encoded hash of the firmware upgrade file that should be signed and asked to provide signatures.
26 |
27 | Get signatures from the description of the github release and enter one by one in the same order as provided in the release.
28 |
29 | After adding signatures binaries in the `release` folder should be exactly the same as in github release. Hashes of the binaries will be saved to `release/sha256.txt`.
30 |
31 | # Apple M1 users
32 |
33 | For Apple M1 add a plafrom flag to the docker commands:
34 |
35 | ```sh
36 | docker build -t diy . --platform linux/x86_64
37 | docker run --platform linux/amd64 -ti -v `pwd`:/app diy
38 | ```
39 |
--------------------------------------------------------------------------------
/docs/roadmap.md:
--------------------------------------------------------------------------------
1 | # Roadmap for Specter-DIY development
2 |
3 | Security critical features:
4 |
5 | - optional secure bootloader checking signatures on the firmware
6 | - secure poweroff that erases all the secrets from RAM
7 | - moving device secret to the very beginning of the flash or even to the bootloader
8 | - add tests, fuzzing etc
9 | - optimize the code
10 |
11 | Documentation:
12 |
13 | - firmware protection instructions with tools from STM
14 |
15 | Would be nice to add:
16 |
17 | - Devkit (WIP)
18 | - SD card support
19 |
--------------------------------------------------------------------------------
/docs/security.md:
--------------------------------------------------------------------------------
1 | # Security of Specter-DIY
2 |
3 | ## Hardware
4 |
5 | Display is contolled by application MCU.
6 |
7 | Secure element integration is not there yet - at the moment secrets are also stored on the main MCU. But you can use the wallet without storing the secret - you need to enter your recovery phrase every time. Why to remember long passphrase if you can remember the whole mnemonic?
8 |
9 | Device uses external flash to store some files (QSPI), but all user files are signed by the wallet and checked when loaded.
10 |
11 | QR scanning functionality is on a separate microcontroller so all image processing happens outside of security-critical MCU. At the moment USB and SD card are still managed by the main MCU, so don't use SD card and USB if you want to reduce attack surface.
12 |
13 | ## PIN protection (user authentication)
14 |
15 | During the first boot a unique secret is generated on the main MCU. This secret allows you to verify that the device was not replaced by a malicious one - when you enter the PIN code you see a list of words that will remain the same while the secret is there.
16 |
17 | Your PIN code together with this unique secret are used to generate a decryption key for your Bitcoin keys (if you store them). So if the attacker would be able to bypass PIN screen, decryption will still fail.
18 |
19 | If you have locked the firmware (TODO: add instructions link here) it will effectively lock the secret as well, so if the attacker flashes different firmware to the board the secret gets erased and you will be able to recognize it when you start entering PIN code - words sequence will be different.
20 |
21 | ## Generation of the recovery phrase
22 |
23 | This is one of the most important parts of the wallet - to generate the key securely. To do this well we use multiple sources of entropy:
24 |
25 | - TRNG of the microcontroller. Proprietary, certified and probably good but we don't trust it
26 | - Touchscreen. Every time you touch the screen we measure the position and the moment when this touch happened (in microcontroller ticks at 180MHz).
27 | - Built-in microphones - not yet. The board has two microphones, so hardware wallet can listen to you and mix in this data to the entropy pool.
28 |
29 | All this entropy is hashed together and converted to your recovery phrase. The resulting entropy is always better than any of the individual sources.
30 |
31 | ## High level logic - wallets
32 |
33 | Specter operates as a key storage. It holds HD private keys that can be involved in wallets. Wallets are defined by their [descriptors](./descriptors.md). We support miniscript as well.
34 |
35 | Every wallet belongs to a particular network. This means that if you imported a wallet on `testnet` it will not be available on `mainnet` or `regtest` - you need to switch to this network and import the wallet separately.
36 |
37 | ## Transactions verification
38 |
39 | The following rules apply to transactions that wallet will sign:
40 |
41 | - if mixed inputs from different wallets are found the user is warned ([attack](https://blog.trezor.io/details-of-the-multisig-change-address-issue-and-its-mitigation-6370ad73ed2a))
42 | - change outputs show the name of the wallet they are sent to
43 | - to use a multisig or miniscript you first need to import the wallet by adding wallet descriptor (over QR, USB or SD card)
44 |
45 |
--------------------------------------------------------------------------------
/docs/shopping.md:
--------------------------------------------------------------------------------
1 | # Shopping list for Specter-DIY
2 |
3 | Here we describe what to buy, and in [assembly.md](./assembly.md) we explain how to put it together and a few notes about the board - power jumpers, USB ports etc.
4 |
5 | ## Discovery board
6 |
7 | Main part of the device is the developer board:
8 |
9 | - STM32F469I-DISCO developer board (i.e. from [Mouser](https://eu.mouser.com/ProductDetail/STMicroelectronics/STM32F469I-DISCO?qs=kWQV1gtkNndotCjy2DKZ4w==) or [Digikey](https://www.digikey.com/product-detail/en/stmicroelectronics/STM32F469I-DISCO/497-15990-ND/5428811))
10 | - **Mini**USB cable
11 | - Standard MicroUSB cable to communicate over USB
12 |
13 | Optional but recommended:
14 | - [Waveshare QR code scanner](https://www.waveshare.com/barcode-scanner-module.htm) with long pin headers like [these](https://eu.mouser.com/ProductDetail/Samtec/DW-02-10-T-S-571?qs=sGAEpiMZZMvlX3nhDDO4AE5PKXAQeC6NPk%2FcLBS9yKI%3D) or [these](https://www.amazon.com/gp/product/B015KA0RRU/) to connect the scanner to the board (4 pin headers needed).
15 |
16 | Check out the assembly video [on youtube](https://youtu.be/1H7FqG_FmCw)
17 |
18 | We are currently working on [an extension board](../shield) that includes a smartcard slot, QR code scanner, battery and a 3d printed case, but it doesn't include the main part — discovery board that you need to order separately. This way supply chain attack is still not an issue as the security-critical components are bought from random electronic store.
19 |
20 | You can start using Specter even without any extra components, but you will be able to communicate with it over USB or the built-in SD card slot. Using Specter over USB means it is not airgapped so you lose an important security property.
21 |
22 | ## QR scanner
23 |
24 | For QR code scanner you have several options.
25 |
26 | **Option 1. Recommended.** Resonably good scanner from Waveshare (40$)
27 |
28 | [Waveshare scanner](https://www.waveshare.com/barcode-scanner-module.htm) - you will need to find a way how to mount it nicely, maybe use some kind of Arduino Prototype shield and some ducktape. For wiring see [assembly.md](./assembly.md).
29 |
30 | No soldering required, but if you have soldering skills you can make the wallet way nicer ;)
31 |
32 | **Option 2.** Very nice scanner from Mikroe but pretty expensive (150$):
33 |
34 | [Barcode Click](https://www.mikroe.com/barcode-click) + [Adapter](https://www.mikroe.com/arduino-uno-click-shield)
35 |
36 | **Option 3.** Any other QR scanner
37 |
38 | You can find some cheap scanners in China. Their quality is often not that great, but you can take a chance. Maybe even ESPcamera would do the job. You only need to connect power, UART (pins D0 and D1), and trigger to D5.
39 |
40 | **Option 4.** No scanner.
41 |
42 | Then you can only use Specter with USB / SD Card.
43 |
44 | Unless you build your own communication module that uses something else instead of QR codes - audiomodem, bluetooth or whatever else. As soon as it can be triggered and send the data over serial you can do whatever you want.
45 |
46 | ## Optional components
47 |
48 | If you add a tiny powerbank or a battery & power charger/booster, your wallet becomes completely self-contained ;)
49 |
--------------------------------------------------------------------------------
/docs/simulator.md:
--------------------------------------------------------------------------------
1 | # Simulator
2 |
3 | Simulator requires `SDL` library to be installed (`sudo apt install libsdl2-dev` on Linux and `brew install sdl2` on Mac).
4 |
5 | Run `make unix` to get it. If everything goes well a `micropython_unix` binary will appear in the `bin` folder.
6 |
7 | Start simulator using `make simulate`.
8 |
9 | You should see the screen with the wallet interface. As in unixport we don't have QR code scanner or USB connector, so instead it simulates serial communication and USB on TCP ports: `5941` for QR scanner and `8789` for USB connection.
10 |
11 | You can connect to these ports using `telnet` and type whatever you expect to be scanned / sent from the host.
12 |
13 | The simulator is also printing content of the QR codes displayed on the screen to the console.
14 |
15 | The simulator create folders in `./fs`:
16 |
17 | - `fs/flash` - files that would be stored in the internal flash of the MCU
18 | - `fs/qspi` - files in external QSPI chip (untrusted, everything is stored encrypted and authenticated)
19 | - `fs/ramdisk` - files in external SPIRAM memory (work as temporary storage for host communication, untrusted)
20 | - `fs/sd` - SD card
21 |
22 |
23 |
--------------------------------------------------------------------------------
/manifests/debug.py:
--------------------------------------------------------------------------------
1 | include('../f469-disco/manifests/disco.py')
2 | freeze('../src')
3 | freeze('../boot/debug')
--------------------------------------------------------------------------------
/manifests/disco.py:
--------------------------------------------------------------------------------
1 | include('../f469-disco/manifests/disco.py')
2 | freeze('../src')
3 | freeze('../boot/main')
--------------------------------------------------------------------------------
/manifests/unix.py:
--------------------------------------------------------------------------------
1 | include('../f469-disco/manifests/unix.py')
2 | freeze('../src')
3 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Specter DIY Documentation
2 | site_url: https://docs.specter.solutions/diy
3 | repo_url: https://github.com/cryptoadvance/specter-diy/
4 | nav:
5 | - Home:
6 | - 'Introduction': README.md
7 | - 'FAQ': faq.md
8 | - roadmap.md
9 | - Hardware:
10 | - shopping.md
11 | - assembly.md
12 | - Software:
13 | - quickstart.md
14 | - multisig-security-tradeoffs.md
15 | - Development:
16 | - 'Developing': development.md
17 | - communication.md
18 | - simulator.md
19 | theme:
20 | name: readthedocs
21 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs==1.3.0
2 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {} }:
2 | pkgs.mkShell {
3 | nativeBuildInputs = [
4 | pkgs.buildPackages.gcc-arm-embedded-9
5 | pkgs.buildPackages.python39
6 | pkgs.openocd
7 | pkgs.stlink
8 | pkgs.SDL2
9 | ];
10 | hardeningDisable = ["all"];
11 | }
12 |
--------------------------------------------------------------------------------
/shield/01_Structure_Diagram.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/01_Structure_Diagram.pdf
--------------------------------------------------------------------------------
/shield/02_Power.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/02_Power.pdf
--------------------------------------------------------------------------------
/shield/03_Peripherals.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/03_Peripherals.pdf
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_1.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_2.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_3.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_4.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_5.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Assembly_6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Assembly_6.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Open_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Open_1.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_1.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_2.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_3.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_4.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_5.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Post_6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Post_6.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Print_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Print_1.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Print_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Print_2.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Print_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Print_3.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Print_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Print_4.jpg
--------------------------------------------------------------------------------
/shield/3d_case/Specter_Print_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3d_case/Specter_Print_5.jpg
--------------------------------------------------------------------------------
/shield/3dprinting.md:
--------------------------------------------------------------------------------
1 | # Specter DIY Case 3D Printing
2 |
3 |
4 | ## 3D Printing
5 |
6 | STL and Prusa Gcode files can be downloaded at the following links:
7 |
8 | - Grabcad: https://grabcad.com/library/specter-diy-hardware-wallet-case-1
9 | - Thingiverse: https://www.thingiverse.com/thing:4671552
10 |
11 | The Prusa Gcode files include a high temperature and low temperature version, we have had good results in PLA with the low temperature setting.
12 |
13 | 
14 |
15 | 
16 |
17 | 
18 |
19 | 
20 |
21 | 
22 |
23 | ## Post 3D Printing
24 |
25 | 1. Remove all support material including the ribs on the back case.
26 | Careful on the inside where the QR code scanner mount is, this is relatively fragile.
27 |
28 | 
29 |
30 | 
31 |
32 | 
33 |
34 | 2. For smooth button operation remove the thin layer of support material underneath the button. This can be tested without components inside to ensure there is no friction when pressing.
35 |
36 | 
37 |
38 | 
39 |
40 | 3. M3 nuts of 2mm thickness are used to fix components in the back case, these snap into the case but are best glued in.
41 |
42 | 
43 |
44 | ## Specter DIY Assembly
45 |
46 | Assembly of Specter DIY components and Specter DIY 3D printed case
47 |
48 | Required:
49 |
50 | - Specter DIY Case
51 | - Specter Shield
52 | - STM Discovery Board STM32F469I-DISCO
53 | - 6x M3x5mm screws low profile head
54 | - 2x M3 nuts 3mm thick
55 | - 2x 9mm male/female Spacers (6mm thread)
56 | - 2x 12mm female/female Spacers
57 | - 2x M1.4x3mm screws for QR Scanner
58 |
59 | 
60 |
61 | 1. Attach the female/female spacers to lower holes in the Specter Shield PCB with two M3 screws.
62 |
63 | 
64 |
65 | 2. Place Specter Shield in the back case. Fix in position with the two male/female spacers using the two M3 nuts to increase offset.
66 |
67 | 
68 |
69 | 3. Attach STM discovery board and fix in place with 4 M3 screws.
70 |
71 | 
72 |
73 | 4. Attach front case beginning on the left (side with the SD card slot).
74 |
75 | 
76 |
77 | 5. Then push right side down, carefully moving the button past the display edge. To snap housing closed takes a bit of pressure.
78 |
79 | 6. When fully closed the display should be flush with the front of the case. Check that all USB ports are correctly aligned and the Smartcard fits.
80 |
81 | 
82 |
83 |
84 | ## Opening the case
85 |
86 | To open the case it is best to start above the button on the right side. With finger nail lever front case up. Then repeat on the lower right side.
87 |
88 | 
89 |
90 | Lift the front case away from the display, carefully moving the button past the display edge.
91 |
92 |
--------------------------------------------------------------------------------
/shield/3dshield.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/3dshield.jpg
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/01_Main_Body.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/01_Main_Body.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/02_Top_Panel.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/02_Top_Panel.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/03_Bottom_Panel.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/03_Bottom_Panel.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/04_Camera_Holder.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/04_Camera_Holder.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/05_Button.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/05_Button.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/06_Spacer.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/06_Spacer.stl
--------------------------------------------------------------------------------
/shield/Alternative_3D_Printed_Case/Assembly_Instructions.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/Alternative_3D_Printed_Case/Assembly_Instructions.pdf
--------------------------------------------------------------------------------
/shield/README.md:
--------------------------------------------------------------------------------
1 | # Specter Shield
2 |
3 | Specter shield is an extension board for F469-Discovery board by STMicroelectronics. It uses a standard Arduino headers so it might work with other boards with Arduino headers as well.
4 |
5 | It includes a QR scanner, smartcard slot and a battery. All elements are not security-critical - QR scanner only captures images and sends scanned data to the main MCU over dead-simple serial interface, smartcard controller learns nothing about the data transmitted to the secure element as communication with it is encrypted.
6 |
7 | Structure diagram, pinout and schematics are available in this folder and on [circuitmaker](https://circuitmaker.com/Projects/Details/MikhailTolkachev/specter-shield). To manufacture the kit yourself just send the content of the [`specter-shield`](./specter-shield/) folder to the PCB manufacturer.
8 |
9 | For the QR scanner we use GROW GM65-S scanner.
10 |
11 | Available in [our shop](https://specter.solutions/shop/specter-shield/). Assembled kit look like this:
12 |
13 | 
14 |
15 | ## Print the case for Specter-DIY + Shield:
16 |
17 | - Design by @geometrick-design: https://www.thingiverse.com/thing:4671552, [instructions](3dprinting.md)
18 | - Design by @SeedSigner: https://www.thingiverse.com/thing:4733846, [files and instructions](./Alternative_3D_Printed_Case)
19 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/Gerber.OutputStatus:
--------------------------------------------------------------------------------
1 | [OutputStatus]
2 | OutputFileNames0=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GTL
3 | OutputFileNames1=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GP1
4 | OutputFileNames2=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GP2
5 | OutputFileNames3=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GBL
6 | OutputFileNames4=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GTO
7 | OutputFileNames5=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GTP
8 | OutputFileNames6=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GTS
9 | OutputFileNames7=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GBS
10 | OutputFileNames8=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GBP
11 | OutputFileNames9=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.GBO
12 | OutputFileNames10=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.Outline
13 | OutputFileNames11=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.RUL
14 | OutputFileNames12=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.EXTREP
15 | OutputFileNames13=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Gerber\specter-shield_v1.REP
16 | Success=1
17 | HasRun=1
18 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/specter-shield_v1-macro.APR_LIB:
--------------------------------------------------------------------------------
1 | G04:AMPARAMS|DCode=23|XSize=0.3mm|YSize=0.52mm|CornerRadius=0.03mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=180.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
2 | %AMROUNDEDRECTD23*
3 | 21,1,0.3000,0.4600,0,0,180.0*
4 | 21,1,0.2400,0.5200,0,0,180.0*
5 | 1,1,0.0600,-0.1200,0.2300*
6 | 1,1,0.0600,0.1200,0.2300*
7 | 1,1,0.0600,0.1200,-0.2300*
8 | 1,1,0.0600,-0.1200,-0.2300*
9 | %
10 | G04:AMPARAMS|DCode=27|XSize=2.8mm|YSize=1.8mm|CornerRadius=0.27mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
11 | %AMROUNDEDRECTD27*
12 | 21,1,2.8000,1.2600,0,0,270.0*
13 | 21,1,2.2600,1.8000,0,0,270.0*
14 | 1,1,0.5400,-0.6300,-1.1300*
15 | 1,1,0.5400,-0.6300,1.1300*
16 | 1,1,0.5400,0.6300,1.1300*
17 | 1,1,0.5400,0.6300,-1.1300*
18 | %
19 | G04:AMPARAMS|DCode=47|XSize=3.1mm|YSize=1.3mm|CornerRadius=0.325mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
20 | %AMROUNDEDRECTD47*
21 | 21,1,3.1000,0.6500,0,0,270.0*
22 | 21,1,2.4500,1.3000,0,0,270.0*
23 | 1,1,0.6500,-0.3250,-1.2250*
24 | 1,1,0.6500,-0.3250,1.2250*
25 | 1,1,0.6500,0.3250,1.2250*
26 | 1,1,0.6500,0.3250,-1.2250*
27 | %
28 | G04:AMPARAMS|DCode=71|XSize=3mm|YSize=2.3mm|CornerRadius=0mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=180.000|XOffset=0mm|YOffset=0mm|HoleType=Slot|Shape=OvalRelief|Width=0.25mm|Gap=0.25mm|Entries=4|*
29 | %AMTHOVALD71*
30 | 21,1,0.7000,2.3000,0,0,180.0*
31 | 1,1,2.3000,0.3500,0.0000*
32 | 1,1,2.3000,-0.3500,0.0000*
33 | 21,0,0.7000,1.8000,0,0,180.0*
34 | 1,0,1.8000,0.3500,0.0000*
35 | 1,0,1.8000,-0.3500,0.0000*
36 | 4,0,4,0.2616,0.0884,1.0748,0.9016,1.2516,0.7248,0.4384,-0.0884,0.2616,0.0884,0.0*
37 | 4,0,4,-0.4384,0.0884,-1.2516,-0.7248,-1.0748,-0.9016,-0.2616,-0.0884,-0.4384,0.0884,0.0*
38 | 4,0,4,0.4384,0.0884,1.2516,-0.7248,1.0748,-0.9016,0.2616,-0.0884,0.4384,0.0884,0.0*
39 | 4,0,4,-0.2616,0.0884,-1.0748,0.9016,-1.2516,0.7248,-0.4384,-0.0884,-0.2616,0.0884,0.0*
40 | %
41 | G04:AMPARAMS|DCode=79|XSize=2.65mm|YSize=1.1mm|CornerRadius=0.275mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
42 | %AMROUNDEDRECTD79*
43 | 21,1,2.6500,0.5500,0,0,0.0*
44 | 21,1,2.1000,1.1000,0,0,0.0*
45 | 1,1,0.5500,1.0500,-0.2750*
46 | 1,1,0.5500,-1.0500,-0.2750*
47 | 1,1,0.5500,-1.0500,0.2750*
48 | 1,1,0.5500,1.0500,0.2750*
49 | %
50 | G04:AMPARAMS|DCode=103|XSize=0.2mm|YSize=0.42mm|CornerRadius=0mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=180.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
51 | %AMROUNDEDRECTD103*
52 | 21,1,0.2000,0.4200,0,0,180.0*
53 | 21,1,0.2000,0.4200,0,0,180.0*
54 | 1,1,0.0000,-0.1000,0.2100*
55 | 1,1,0.0000,0.1000,0.2100*
56 | 1,1,0.0000,0.1000,-0.2100*
57 | 1,1,0.0000,-0.1000,-0.2100*
58 | %
59 | G04:AMPARAMS|DCode=107|XSize=2.9mm|YSize=1.9mm|CornerRadius=0.32mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
60 | %AMROUNDEDRECTD107*
61 | 21,1,2.9000,1.2600,0,0,270.0*
62 | 21,1,2.2600,1.9000,0,0,270.0*
63 | 1,1,0.6400,-0.6300,-1.1300*
64 | 1,1,0.6400,-0.6300,1.1300*
65 | 1,1,0.6400,0.6300,1.1300*
66 | 1,1,0.6400,0.6300,-1.1300*
67 | %
68 | G04:AMPARAMS|DCode=127|XSize=2.9mm|YSize=1.1mm|CornerRadius=0.225mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
69 | %AMROUNDEDRECTD127*
70 | 21,1,2.9000,0.6500,0,0,270.0*
71 | 21,1,2.4500,1.1000,0,0,270.0*
72 | 1,1,0.4500,-0.3250,-1.2250*
73 | 1,1,0.4500,-0.3250,1.2250*
74 | 1,1,0.4500,0.3250,1.2250*
75 | 1,1,0.4500,0.3250,-1.2250*
76 | %
77 | G04:AMPARAMS|DCode=139|XSize=2.7mm|YSize=1.15mm|CornerRadius=0.3mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
78 | %AMROUNDEDRECTD139*
79 | 21,1,2.7000,0.5500,0,0,0.0*
80 | 21,1,2.1000,1.1500,0,0,0.0*
81 | 1,1,0.6000,1.0500,-0.2750*
82 | 1,1,0.6000,-1.0500,-0.2750*
83 | 1,1,0.6000,-1.0500,0.2750*
84 | 1,1,0.6000,1.0500,0.2750*
85 | %
86 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/specter-shield_v1.EXTREP:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------------------
2 | Gerber File Extension Report For: specter-shield_v1.GBR 12.12.2019 23:16:17
3 | ------------------------------------------------------------------------------------------
4 |
5 |
6 | ------------------------------------------------------------------------------------------
7 | Layer Extension Layer Description
8 | ------------------------------------------------------------------------------------------
9 | .GTL Top Layer
10 | .GP1 Power
11 | .GP2 Ground
12 | .GBL Bottom Layer
13 | .GTO Top Overlay
14 | .GTP Top Paste
15 | .GTS Top Solder
16 | .GBS Bottom Solder
17 | .GBP Bottom Paste
18 | .GBO Bottom Overlay
19 | .Outline Outline
20 | ------------------------------------------------------------------------------------------
21 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/specter-shield_v1.GBP:
--------------------------------------------------------------------------------
1 | G04 Layer_Color=16770453*
2 | %FSLAX44Y44*%
3 | %MOMM*%
4 | G71*
5 | G01*
6 | G75*
7 | %ADD10C,2.0000*%
8 | %ADD18R,0.5500X0.6000*%
9 | %ADD19R,0.6200X0.6600*%
10 | %ADD20R,0.6600X0.6200*%
11 | %ADD24R,0.6000X0.5500*%
12 | %ADD26R,0.8500X0.9500*%
13 | %ADD30R,1.4500X1.2500*%
14 | %ADD74O,1.2000X2.2000*%
15 | %ADD75R,2.4000X2.4000*%
16 | %ADD76O,0.7000X0.2800*%
17 | %ADD77O,0.2800X0.7000*%
18 | %ADD78O,0.3000X1.4000*%
19 | G04:AMPARAMS|DCode=79|XSize=2.65mm|YSize=1.1mm|CornerRadius=0.275mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
20 | %AMROUNDEDRECTD79*
21 | 21,1,2.6500,0.5500,0,0,0.0*
22 | 21,1,2.1000,1.1000,0,0,0.0*
23 | 1,1,0.5500,1.0500,-0.2750*
24 | 1,1,0.5500,-1.0500,-0.2750*
25 | 1,1,0.5500,-1.0500,0.2750*
26 | 1,1,0.5500,1.0500,0.2750*
27 | %
28 | %ADD79ROUNDEDRECTD79*%
29 | %ADD80R,0.8000X0.3500*%
30 | D10*
31 | X4221000Y2218000D02*
32 | D03*
33 | X3739000Y2206000D02*
34 | D03*
35 | X3778000Y3185000D02*
36 | D03*
37 | D18*
38 | X3794370Y3001000D02*
39 | D03*
40 | Y3009500D02*
41 | D03*
42 | X4055657Y2967300D02*
43 | D03*
44 | Y2958800D02*
45 | D03*
46 | X4180000Y2992000D02*
47 | D03*
48 | Y3000500D02*
49 | D03*
50 | X4176000Y3176750D02*
51 | D03*
52 | Y3185250D02*
53 | D03*
54 | D19*
55 | X3814935Y3081650D02*
56 | D03*
57 | Y3090450D02*
58 | D03*
59 | X3784285Y3009500D02*
60 | D03*
61 | Y3000700D02*
62 | D03*
63 | X3826750Y3006535D02*
64 | D03*
65 | Y2997735D02*
66 | D03*
67 | X3815750Y3006450D02*
68 | D03*
69 | Y2997650D02*
70 | D03*
71 | X3804370Y3009500D02*
72 | D03*
73 | Y3000700D02*
74 | D03*
75 | X4067000Y2967300D02*
76 | D03*
77 | Y2958500D02*
78 | D03*
79 | X4110000Y3046000D02*
80 | D03*
81 | Y3037200D02*
82 | D03*
83 | X4140000Y2987000D02*
84 | D03*
85 | Y2995800D02*
86 | D03*
87 | X4191000Y2992000D02*
88 | D03*
89 | Y2983200D02*
90 | D03*
91 | X4202000Y2992000D02*
92 | D03*
93 | Y2983200D02*
94 | D03*
95 | D20*
96 | X3836950Y3006535D02*
97 | D03*
98 | X3845750D02*
99 | D03*
100 | X3836950Y2997050D02*
101 | D03*
102 | X3845750D02*
103 | D03*
104 | X3836950Y2969300D02*
105 | D03*
106 | X3828150D02*
107 | D03*
108 | X3836950Y2959300D02*
109 | D03*
110 | X3828150D02*
111 | D03*
112 | X4004000Y2946000D02*
113 | D03*
114 | X3995200D02*
115 | D03*
116 | X4036400D02*
117 | D03*
118 | X4027600D02*
119 | D03*
120 | X3781650Y3067343D02*
121 | D03*
122 | X3790450D02*
123 | D03*
124 | D24*
125 | X3781950Y3057720D02*
126 | D03*
127 | X3790450D02*
128 | D03*
129 | X3781950Y3048770D02*
130 | D03*
131 | X3790450D02*
132 | D03*
133 | X3781950Y3039770D02*
134 | D03*
135 | X3790450D02*
136 | D03*
137 | X3781950Y3030770D02*
138 | D03*
139 | X3790450D02*
140 | D03*
141 | Y3021220D02*
142 | D03*
143 | X3781950D02*
144 | D03*
145 | X3984850Y2946000D02*
146 | D03*
147 | X3976350D02*
148 | D03*
149 | X4055726Y2978000D02*
150 | D03*
151 | X4047226D02*
152 | D03*
153 | X4153500Y2971000D02*
154 | D03*
155 | X4162000D02*
156 | D03*
157 | D26*
158 | X3944995Y2944705D02*
159 | D03*
160 | X3958495D02*
161 | D03*
162 | D30*
163 | X3901469Y2961300D02*
164 | D03*
165 | Y2977800D02*
166 | D03*
167 | D74*
168 | X3995400Y2629400D02*
169 | D03*
170 | X4020800D02*
171 | D03*
172 | X3970000D02*
173 | D03*
174 | X3868400Y2806400D02*
175 | D03*
176 | X3944600Y2873400D02*
177 | D03*
178 | X3970000D02*
179 | D03*
180 | X4020800D02*
181 | D03*
182 | X3995400D02*
183 | D03*
184 | X3944600Y2629400D02*
185 | D03*
186 | X3893800Y2806400D02*
187 | D03*
188 | D75*
189 | X3829450Y3048220D02*
190 | D03*
191 | D76*
192 | X3809450Y3060720D02*
193 | D03*
194 | Y3055720D02*
195 | D03*
196 | Y3050720D02*
197 | D03*
198 | Y3045720D02*
199 | D03*
200 | Y3040720D02*
201 | D03*
202 | Y3035720D02*
203 | D03*
204 | X3849450D02*
205 | D03*
206 | Y3040720D02*
207 | D03*
208 | Y3045720D02*
209 | D03*
210 | Y3050720D02*
211 | D03*
212 | Y3055720D02*
213 | D03*
214 | Y3060720D02*
215 | D03*
216 | D77*
217 | X3816950Y3028220D02*
218 | D03*
219 | X3821950D02*
220 | D03*
221 | X3826950D02*
222 | D03*
223 | X3831950D02*
224 | D03*
225 | X3836950D02*
226 | D03*
227 | X3841950D02*
228 | D03*
229 | Y3068220D02*
230 | D03*
231 | X3836950D02*
232 | D03*
233 | X3831950D02*
234 | D03*
235 | X3826950D02*
236 | D03*
237 | X3821950D02*
238 | D03*
239 | X3816950D02*
240 | D03*
241 | D78*
242 | X3951500Y2965050D02*
243 | D03*
244 | X3946500D02*
245 | D03*
246 | X3956500D02*
247 | D03*
248 | X3961500D02*
249 | D03*
250 | X3966500D02*
251 | D03*
252 | X3971500D02*
253 | D03*
254 | X3976500D02*
255 | D03*
256 | X4001500D02*
257 | D03*
258 | X3996500D02*
259 | D03*
260 | X3991500D02*
261 | D03*
262 | X3986500D02*
263 | D03*
264 | X3981500D02*
265 | D03*
266 | D79*
267 | X3928750Y2976550D02*
268 | D03*
269 | X4019250D02*
270 | D03*
271 | D80*
272 | X4153500Y2987000D02*
273 | D03*
274 | X4167000Y2982000D02*
275 | D03*
276 | Y2992000D02*
277 | D03*
278 | M02*
279 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/specter-shield_v1.Outline:
--------------------------------------------------------------------------------
1 | G04 Layer_Color=16740166*
2 | %FSLAX44Y44*%
3 | %MOMM*%
4 | G71*
5 | G01*
6 | G75*
7 | %ADD48C,0.2000*%
8 | D48*
9 | X4248000Y2030000D02*
10 | Y2042700D01*
11 | X4235300Y2030000D02*
12 | X4248000D01*
13 | X3648000D02*
14 | Y3300000D01*
15 | X4248000D01*
16 | Y2042700D02*
17 | Y3300000D01*
18 | X3648000Y2030000D02*
19 | X4235300D01*
20 | M02*
21 |
--------------------------------------------------------------------------------
/shield/specter-shield/Gerber/specter-shield_v1.RUL:
--------------------------------------------------------------------------------
1 | DRC Rules Export File for PCB: C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\specter-shield_v1.CMPcbDoc
2 | RuleKind=ShortCircuit|RuleName=ShortCircuit|Scope=Board|Allowed=0
3 | RuleKind=Clearance|RuleName=Clearance|Scope=Board|Minimum=7.87
4 | RuleKind=Width|RuleName=Width|Scope=Board|Minimum=7.87
5 | RuleKind=SolderMaskExpansion|RuleName=SolderMaskExpansion|Scope=Board|Minimum=1.97
6 |
--------------------------------------------------------------------------------
/shield/specter-shield/NC Drill/NC Drill.OutputStatus:
--------------------------------------------------------------------------------
1 | [OutputStatus]
2 | OutputFileNames0=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\NC Drill\specter-shield_v1-RoundHoles.TXT
3 | OutputFileNames1=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\NC Drill\specter-shield_v1-SlotHoles.TXT
4 | OutputFileNames2=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\NC Drill\specter-shield_v1.LDP
5 | OutputFileNames3=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\NC Drill\specter-shield_v1.DRR
6 | Success=1
7 | HasRun=1
8 |
--------------------------------------------------------------------------------
/shield/specter-shield/NC Drill/specter-shield_v1-SlotHoles.TXT:
--------------------------------------------------------------------------------
1 | M48
2 | ;Layer_Color=6321
3 | ;FILE_FORMAT=4:4
4 | METRIC,LZ
5 | ;TYPE=PLATED
6 | T3F00S00C0.5000
7 | T5F00S00C0.8000
8 | ;TYPE=NON_PLATED
9 | %
10 | G90
11 | G05
12 | T03
13 | G00X03898Y02041
14 | M15
15 | G01Y02048
16 | M16
17 | G00X03832Y02041
18 | M15
19 | G01Y02048
20 | M16
21 | T05
22 | G00X042245Y03123852
23 | M15
24 | G01X042175
25 | M16
26 | G00X042245Y03206148
27 | M15
28 | G01X042175
29 | M16
30 | M17
31 | M30
32 |
--------------------------------------------------------------------------------
/shield/specter-shield/NC Drill/specter-shield_v1.DRR:
--------------------------------------------------------------------------------
1 | ---------------------------------------------------------------------------
2 | NCDrill File Report For: specter-shield_v1.CMPcbDoc 12.12.2019 23:16:20
3 | ---------------------------------------------------------------------------
4 |
5 | Layer Pair : Top Layer to Bottom Layer
6 | ASCII RoundHoles File : specter-shield_v1-RoundHoles.TXT
7 | ASCII SlotHoles File : specter-shield_v1-SlotHoles.TXT
8 |
9 | Tool Hole Size Hole Type Hole Count Plated Tool Travel
10 | ---------------------------------------------------------------------------
11 | T1 0.2mm (7.874mil) Round 6 98.49 mm (3.88 Inch)
12 | T2 0.3mm (11.811mil) Round 277 844.57 mm (33.25 Inch)
13 | T3 0.5mm (19.685mil) Slot 2 7612.06 mm (299.69 Inch)
14 | T4 0.762mm (30mil) Round 2 4.50 mm (0.18 Inch)
15 | T5 0.8mm (31.496mil) Slot 2 7712.11 mm (303.63 Inch)
16 | T6 0.8128mm (32mil) Round 3 4.06 mm (0.16 Inch)
17 | T7 1.29mm (50.787mil) Round 2 7.00 mm (0.28 Inch)
18 | T8 2.1mm (82.677mil) Round 2 48.87 mm (1.92 Inch)
19 | T9 3.5mm (137.795mil) Round 4 NPTH 221.85 mm (8.73 Inch)
20 | ---------------------------------------------------------------------------
21 | Totals 300 16553.54 mm (651.71 Inch)
22 |
23 | Total Processing Time (hh:mm:ss) : 00:00:00
24 |
--------------------------------------------------------------------------------
/shield/specter-shield/NC Drill/specter-shield_v1.LDP:
--------------------------------------------------------------------------------
1 | Layer Pairs Export File for PCB: C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\specter-shield_v1.CMPcbDoc
2 | LayersSetName=Top_Bot_Thru_Holes|DrillFile=specter-shield_v1-roundholes.txt|LayerPairs=gtl,gbl
3 | LayersSetName=Top_Bot_Slot_Holes|DrillFile=specter-shield_v1-slotholes.txt|LayerPairs=gtl,gbl
4 |
--------------------------------------------------------------------------------
/shield/specter-shield/Pick Place/Pick Place.OutputStatus:
--------------------------------------------------------------------------------
1 | [OutputStatus]
2 | OutputFileNames0=C:\ProgramData\Altium\CircuitMaker {8B7EC335-09F7-4C06-BE02-0DA28D6608ED}\Projects\D0458375-9538-47B1-9145-FF061B1E4159\df23c73e-d452-4377-8dea-c4358ea655c1\Outputs\Pick Place\Pick Place for specter-shield_v1.txt
3 | Success=1
4 | HasRun=1
5 |
--------------------------------------------------------------------------------
/shield/specter-shield/Specter-shield_v1 - Layers Description.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/specter-shield/Specter-shield_v1 - Layers Description.xls
--------------------------------------------------------------------------------
/shield/specter-shield/specter-shield_v1 ArtWork BackSide3D.PDF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/specter-shield/specter-shield_v1 ArtWork BackSide3D.PDF
--------------------------------------------------------------------------------
/shield/specter-shield/specter-shield_v1 ArtWork.PDF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/specter-shield/specter-shield_v1 ArtWork.PDF
--------------------------------------------------------------------------------
/shield/specter-shield/specter-shield_v1 BOM.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/specter-shield/specter-shield_v1 BOM.xlsx
--------------------------------------------------------------------------------
/shield/specter-shield/specter-shield_v1 Part Layout.PDF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/shield/specter-shield/specter-shield_v1 Part Layout.PDF
--------------------------------------------------------------------------------
/simulate.py:
--------------------------------------------------------------------------------
1 | import sys
2 | sys.path.append('./src')
3 | sys.path.append('./f469-disco/libs/common')
4 | sys.path.append('./f469-disco/libs/unix')
5 | sys.path.append('./f469-disco/usermods/udisplay_f469/display_unixport')
6 |
7 | import main
8 |
9 | main.main()
--------------------------------------------------------------------------------
/src/app.py:
--------------------------------------------------------------------------------
1 | """Base app that Specter can run"""
2 | from errors import BaseError
3 | from platform import maybe_mkdir, delete_recursively
4 |
5 |
6 | class BaseApp:
7 | # set text of a button here and a callback if you want to have
8 | # a GUI-triggered events - it will create a menu button
9 | # on the main screen
10 | button = None
11 | # prefixes for commands that this app recognizes
12 | # should be byte sequences, no spaces i.e. b"appcommand"
13 | prefixes = []
14 | # button = ("My App menu item", callback name)
15 |
16 | # temp storage for command processing:
17 | TEMPDIR = None
18 | # global settings injected by the Specter class
19 | GLOBAL = {}
20 |
21 | def __init__(self, path: str):
22 | """path is the folder where this app should store data"""
23 | maybe_mkdir(path)
24 | self.path = path
25 | self.communicate = None
26 |
27 | def can_process(self, stream):
28 | """Checks if the app can process this stream"""
29 | prefix = self.get_prefix(stream)
30 | return prefix in self.prefixes
31 |
32 | def get_prefix(self, stream):
33 | """Gets prefix from the stream (first "word")"""
34 | prefix = stream.read(20)
35 | if b" " not in prefix:
36 | if len(prefix) < 20:
37 | return prefix
38 | stream.seek(0)
39 | return None
40 | prefix = prefix.split(b" ")[0]
41 | # point to the beginning of the data
42 | stream.seek(len(prefix) + 1)
43 | return prefix
44 |
45 | def init(self, keystore, network, show_loader, communicate):
46 | """
47 | This method is called when new key is loaded
48 | or a different network is selected.
49 | `show_loader` is a function that is used to display a loader while processing data
50 | `communicate` is an async function that allows cross-app communication with another apps.
51 | Pass a readable stream to `communicate` function to simulate host command processing.
52 | Optionally you can pass a name of the app to communicate with
53 | by calling i.e. `await self.communicate(stream, app="wallets")`
54 | """
55 | self.keystore = keystore
56 | self.network = network
57 | self.show_loader = show_loader
58 | self.communicate = communicate
59 |
60 | def wipe(self):
61 | """
62 | Delete all the contents of the app
63 | including the app folder itself.
64 | """
65 | delete_recursively(self.path, include_self=True)
66 |
67 | @property
68 | def tempdir(self):
69 | if self.TEMPDIR is None:
70 | return None
71 | maybe_mkdir(self.TEMPDIR)
72 | path = self.TEMPDIR+"/"+type(self).__name__
73 | maybe_mkdir(path)
74 | return path
75 |
76 |
77 | class AppError(BaseError):
78 | NAME = "Application error"
79 |
--------------------------------------------------------------------------------
/src/apps/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "wallets", # wallet manager, main app for bitcoin tx stuff
3 | "xpubs", # master public keys and device fingerprint
4 | "signmessage", # adds bitcoin message signing functionality
5 | "getrandom", # allows to query random bytes from on-board TRNG
6 | "label", # allows settings and getting a label for device
7 | "backup", # creates and loads backups (only loads for now)
8 | "blindingkeys", # blinding keys for liquid wallets
9 | "compatibility", # compatibility layer that converts json/files to Specter format
10 | "bip85", # bip85 derivation of new mnemonics, xprvs etc
11 | ]
12 |
--------------------------------------------------------------------------------
/src/apps/backup.py:
--------------------------------------------------------------------------------
1 | """
2 | Backup app - can load secrets from a text file or qr code that starts with
3 | bip39:
4 | """
5 | from app import BaseApp, AppError
6 | from embit import bip39
7 | from gui.screens import Prompt
8 | from gui.components.mnemonic import MnemonicTable
9 | import lvgl as lv
10 |
11 | # Should be called App if you use a single file
12 | class App(BaseApp):
13 | """Allows to load mnemonic from text file / QR code"""
14 | name = "backup"
15 | prefixes = [b"bip39:"]
16 |
17 | async def process_host_command(self, stream, show_fn):
18 | # reads prefix from the stream (until first space)
19 | prefix = self.get_prefix(stream)
20 | if prefix not in self.prefixes:
21 | # WTF? It's not our data...
22 | raise AppError("Prefix is not valid: %s" % prefix.decode())
23 | mnemonic = stream.read().strip().decode()
24 | if not bip39.mnemonic_is_valid(mnemonic):
25 | raise AppError("Invalid mnemonic!")
26 | scr = Prompt("Load this mnemonic to memory?", "Mnemonic:")
27 | table = MnemonicTable(scr)
28 | table.align(scr.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30)
29 | table.set_mnemonic(mnemonic)
30 | confirm = await show_fn(scr)
31 | if confirm:
32 | self.keystore.set_mnemonic(mnemonic)
33 | return confirm
34 |
--------------------------------------------------------------------------------
/src/apps/blindingkeys/__init__.py:
--------------------------------------------------------------------------------
1 | from .app import BlindingKeysApp as App
2 |
--------------------------------------------------------------------------------
/src/apps/blindingkeys/app.py:
--------------------------------------------------------------------------------
1 | from app import BaseApp, AppError
2 | from gui.screens import QRAlert, Prompt
3 |
4 | from embit.liquid.networks import NETWORKS
5 | from helpers import is_liquid
6 | from io import BytesIO
7 |
8 |
9 | class BlindingKeysApp(BaseApp):
10 | """
11 | WalletManager class manages your wallets.
12 | It stores public information about the wallets
13 | in the folder and signs it with keystore's id key
14 | """
15 |
16 | button = None
17 | BTNTEXT = "Blinding key"
18 | prefixes = [b"slip77"] # [b"bprv", b"bpub", b"slip77"]
19 | name = "blindingkeys"
20 |
21 | def __init__(self, path):
22 | pass
23 |
24 | def init(self, *args, **kwargs):
25 | super().init(*args, **kwargs)
26 | if is_liquid(self.network):
27 | self.button = self.BTNTEXT
28 | else:
29 | self.button = None
30 |
31 | async def menu(self, show_screen, show_all=False):
32 | await show_screen(QRAlert("Standard SLIP-77 blinding key",
33 | self.keystore.slip77_key.wif(NETWORKS[self.network]),
34 | note="Blinding private key allows your software wallet\nto track your balance."
35 | ))
36 | return False
37 |
38 | async def process_host_command(self, stream, show_screen):
39 | if self.keystore.is_locked:
40 | raise AppError("Device is locked")
41 | # reads prefix from the stream (until first space)
42 | prefix = self.get_prefix(stream)
43 | if prefix == b"slip77":
44 | if not await show_screen(Prompt("Confirm the action",
45 | "Send master blinding private key\nto the host?\n\n"
46 | "Host is requesting your\nSLIP-77 blinding key.\n\n"
47 | "It will be able to watch your funds and unblind transactions.")):
48 | return False
49 | return BytesIO(self.keystore.slip77_key.wif(NETWORKS[self.network])), {}
50 | raise AppError("Unknown command")
51 |
52 | def wipe(self):
53 | # nothing to delete
54 | pass
55 |
--------------------------------------------------------------------------------
/src/apps/compatibility.py:
--------------------------------------------------------------------------------
1 | """
2 | This app parses data in various format that are not native to Specter
3 | and converts to commands that Specter will understand.
4 | After processing it sends converted command to a corresponding app.
5 | """
6 | from app import BaseApp, AppError
7 | from io import BytesIO
8 | import json
9 | from helpers import read_until
10 | from embit import bip32
11 | from binascii import unhexlify
12 |
13 | CC_TYPES = {"BIP45": "sh", "P2WSH-P2SH": "sh-wsh", "P2WSH": "wsh"}
14 |
15 | # functions that are app-agnostic, helps to test parsing
16 | def parse_software_wallet_json(obj):
17 | """Parse software export json"""
18 | if "descriptor" not in obj:
19 | raise AppError("Invalid wallet json")
20 | # get descriptor without checksum
21 | desc = obj["descriptor"].split("#")[0]
22 | # replace /0/* to /{0,1}/* to add change descriptor
23 | desc = desc.replace("/0/*", "/{0,1}/*")
24 | label = obj.get("label", "Imported wallet")
25 | return label, desc
26 |
27 |
28 | def parse_cc_wallet_txt(stream):
29 | """Parse coldcard wallet format"""
30 | name = "Imported wallet"
31 | script_type = None
32 | sigs_required = None
33 | global_derivation = None
34 | sigs_total = None
35 | cosigners = []
36 | current_derivation = None
37 | # cycle until we read everything
38 | char = b"\n"
39 | while char is not None:
40 | line, char = read_until(stream, b"\r\n", max_len=300)
41 | # skip comments
42 | while char is not None and (line.startswith(b"#") or len(line.strip()) == 0):
43 | # BW comment on derivation
44 | if line.startswith(b"# derivation:"):
45 | current_derivation = bip32.parse_path(line.split(b":")[1].decode().strip())
46 | line, char = read_until(stream, b"\r\n", max_len=300)
47 | if b":" not in line:
48 | continue
49 | arr = line.split(b":")
50 | if len(arr) > 2:
51 | raise AppError("Invalid file format")
52 | k, v = [a.strip().decode() for a in arr]
53 | if k == "Name":
54 | name = v
55 | elif k == "Policy":
56 | nums = [int(num) for num in v.split(" of ")]
57 | assert len(nums) == 2
58 | m, n = nums
59 | assert m > 0 and n >= m
60 | sigs_required = m
61 | sigs_total = n
62 | elif k == "Format":
63 | assert v in CC_TYPES
64 | script_type = CC_TYPES[v]
65 | elif k == "Derivation":
66 | der = bip32.parse_path(v)
67 | if len(cosigners) == 0:
68 | global_derivation = der
69 | else:
70 | current_derivation = der
71 | # fingerprint
72 | elif len(k) == 8:
73 | cosigners.append((unhexlify(k), current_derivation or global_derivation, bip32.HDKey.from_string(v)))
74 | current_derivation = None
75 | assert None not in [global_derivation, sigs_total, sigs_required, script_type, name]
76 | assert len(cosigners) == sigs_total
77 | xpubs = ["[%s]%s/{0,1}/*" % (bip32.path_to_str(der, fingerprint=fgp), xpub) for fgp, der, xpub in cosigners]
78 | desc = "sortedmulti(%d,%s)" % (sigs_required, ",".join(xpubs))
79 | for sc in reversed(script_type.split("-")):
80 | desc = "%s(%s)" % (sc, desc)
81 | return name, desc
82 |
83 |
84 | class App(BaseApp):
85 | name = "compatibility"
86 | prefixes = []
87 |
88 | def can_process(self, stream):
89 | """Detects if it can process the stream"""
90 | c = stream.read(16)
91 | # rewind
92 | stream.seek(-len(c), 1)
93 | # check if it's a json
94 | if c.startswith(b"{"):
95 | return True
96 | # looks like coldcard wallet format
97 | if c.startswith(b"#") or c.startswith(b"Name:"):
98 | return True
99 | return False
100 |
101 | async def process_host_command(self, stream, show_fn):
102 | # check if we've got filename, not a stream:
103 | if isinstance(stream, str):
104 | with open(stream, "rb") as f:
105 | return await self.process_host_command(f, show_fn)
106 | # processing stream now
107 | c = stream.read(16)
108 | # rewind
109 | stream.seek(-len(c), 1)
110 | if c.startswith(b"{"):
111 | obj = json.load(stream)
112 | if "descriptor" in obj:
113 | # this is wallet export json (Specter Desktop, FullyNoded and others)
114 | return await self.parse_software_wallet_json(obj, show_fn)
115 | elif c.startswith(b"#") or c.startswith(b"Name:"):
116 | return await self.parse_cc_wallet_txt(stream, show_fn)
117 | raise AppError("Failed parsing data")
118 |
119 | async def get_wallet_name_suggestion(self, label):
120 | s, _ = await self.communicate(BytesIO(b"listwallets"), app="wallets")
121 | names = json.load(s)
122 | suggestion = label
123 | i = 0
124 | while suggestion in names:
125 | suggestion = "%s (%d)" % (label, i)
126 | i += 1
127 | return suggestion
128 |
129 | async def parse_software_wallet_json(self, obj, show_fn):
130 | label, desc = parse_software_wallet_json(obj)
131 | label = await self.get_wallet_name_suggestion(label)
132 | data = "addwallet %s&%s" % (label, desc)
133 | stream = BytesIO(data.encode())
134 | return await self.communicate(stream, app="wallets", show_fn=show_fn)
135 |
136 | async def parse_cc_wallet_txt(self, stream, show_fn):
137 | label, desc = parse_cc_wallet_txt(stream)
138 | label = await self.get_wallet_name_suggestion(label)
139 | data = "addwallet %s&%s" % (label, desc)
140 | stream = BytesIO(data.encode())
141 | return await self.communicate(stream, app="wallets", show_fn=show_fn)
142 |
--------------------------------------------------------------------------------
/src/apps/getrandom.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo of a single-file app extending Specter functionality.
3 | This app allows to query random bytes from on-board TRNG.
4 | """
5 | from app import BaseApp, AppError
6 | from io import BytesIO
7 | from binascii import hexlify
8 | from rng import get_random_bytes
9 |
10 | # Should be called App if you use a single file
11 |
12 |
13 | class App(BaseApp):
14 | """Allows to query random bytes from on-board TRNG."""
15 | name = "random"
16 | prefixes = [b"getrandom"]
17 |
18 | async def process_host_command(self, stream, show_fn):
19 | """
20 | If command with one of the prefixes is received
21 | it will be passed to this method.
22 | Should return a tuple:
23 | - stream (file, BytesIO etc)
24 | - meta object with title and note
25 | """
26 | # reads prefix from the stream (until first space)
27 | prefix = self.get_prefix(stream)
28 | if prefix != b"getrandom":
29 | # WTF? It's not our data...
30 | raise AppError("Prefix is not valid: %s" % prefix.decode())
31 | # by default we return 32 bytes
32 | num_bytes = 32
33 | try:
34 | num_bytes = int(stream.read().decode().strip())
35 | except:
36 | pass
37 | if num_bytes < 0:
38 | raise AppError("Seriously? %d bytes? No..." % num_bytes)
39 | if num_bytes > 1000:
40 | raise AppError("Sorry, 1k bytes max.")
41 | obj = {"title": "Here is your entropy", "note": "%d bytes" % num_bytes}
42 | return BytesIO(hexlify(get_random_bytes(num_bytes))), obj
43 |
--------------------------------------------------------------------------------
/src/apps/label.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo of a single-file app extending Specter functionality.
3 | This app allows to set a label for the device.
4 | """
5 | from app import BaseApp, AppError
6 | from gui.screens import Prompt
7 | from io import BytesIO
8 |
9 | # Should be called App if you use a single file
10 |
11 |
12 | class App(BaseApp):
13 | """Allows to set a label for the device."""
14 | name = "label"
15 | prefixes = [b"getlabel", b"setlabel"]
16 |
17 | async def process_host_command(self, stream, show_screen):
18 | """
19 | If command with one of the prefixes is received
20 | it will be passed to this method.
21 | Should return a tuple:
22 | - stream (file, BytesIO etc)
23 | - meta object with title and note
24 | """
25 | # reads prefix from the stream (until first space)
26 | prefix = self.get_prefix(stream)
27 | if prefix == b"getlabel":
28 | label = self.get_label()
29 | obj = {"title": "Device's label is: %s" % label}
30 | stream = BytesIO(label.encode())
31 | return stream, obj
32 | elif prefix == b"setlabel":
33 | label = stream.read().strip().decode("ascii")
34 | if not label:
35 | raise AppError("Device label cannot be empty")
36 | scr = Prompt(
37 | "\n\nSet device label to: %s\n" % label,
38 | "Current device label: %s" % self.get_label(),
39 | )
40 | res = await show_screen(scr)
41 | if res is False:
42 | return None
43 | self.set_label(label)
44 | obj = {"title": "New device label: %s" % label}
45 | return BytesIO(label.encode()), obj
46 | else:
47 | raise AppError("Invalid command")
48 |
49 | def get_label(self):
50 | try:
51 | with open(self.path + "/label") as f:
52 | label = f.read()
53 | return label
54 | except Exception:
55 | return "Specter-DIY"
56 |
57 | def set_label(self, label):
58 | try:
59 | with open(self.path + "/label", "w") as f:
60 | f.write(label)
61 | except Exception:
62 | return AppError("Failed to save new label")
63 |
--------------------------------------------------------------------------------
/src/apps/signmessage/__init__.py:
--------------------------------------------------------------------------------
1 | # main app should be loaded as App here
2 | # so Specter can use this class to load the app
3 | from .signmessage import MessageApp as App
4 |
--------------------------------------------------------------------------------
/src/apps/signmessage/signmessage.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo of a simple app that extends Specter with custom functionality.
3 | """
4 | from app import BaseApp, AppError
5 | from gui.screens import Prompt
6 |
7 | from embit import ec, bip32, script, compact
8 | from embit.liquid.networks import NETWORKS
9 | from hashlib import sha256
10 | from binascii import b2a_base64, unhexlify, a2b_base64, hexlify
11 | import secp256k1
12 | from io import BytesIO
13 |
14 |
15 | class MessageApp(BaseApp):
16 | """
17 | This app can sign a text message with a private key.
18 | """
19 |
20 | prefixes = [b"signmessage"]
21 | name = "message"
22 |
23 | async def process_host_command(self, stream, show_screen):
24 | """
25 | If command with one of the prefixes is received
26 | it will be passed to this method.
27 | Should return a tuple:
28 | - stream (file, BytesIO etc)
29 | - meta object with title and note
30 | """
31 | # reads prefix from the stream (until first space)
32 | prefix = self.get_prefix(stream)
33 | if prefix != b"signmessage":
34 | # WTF? It's not our data...
35 | raise AppError("Prefix is not valid: %s" % prefix.decode())
36 | # data format: message to signderivation_path
37 | # read all and delete all crap at the end (if any)
38 | # also message should be utf-8 decodable
39 | data = stream.read().strip()
40 | if b" " not in data:
41 | raise AppError("Invalid data encoding")
42 | arr = data.split(b" ")
43 | derivation_path = arr[0].decode()
44 | message = b" ".join(arr[1:])
45 | # if we have fingerprint
46 | if not derivation_path.startswith("m"):
47 | fingerprint = unhexlify(derivation_path[:8])
48 | if fingerprint != self.keystore.fingerprint:
49 | raise AppError("Not my fingerprint")
50 | derivation_path = "m" + derivation_path[8:]
51 | # Returns a list of indexes
52 | derivation_path = bip32.parse_path(derivation_path)
53 |
54 | if message.startswith(b"ascii:"):
55 | message = message[len(b"ascii:") :]
56 | elif message.startswith(b"base64:"):
57 | message = a2b_base64(message[len(b"base64:") :])
58 | else:
59 | raise AppError("Invalid message encoding!")
60 | # try to decode with ascii characters
61 | try:
62 | msg = "Message:\n\n"
63 | msg += "__________________________________\n"
64 | msg += message.decode("ascii")
65 | msg += "\n__________________________________"
66 | # ask the user if he really wants to sign this message
67 | except:
68 | msg = "Hex message:\n\n%s" % hexlify(message).decode()
69 | scr = Prompt(
70 | "Sign message with private key at %s?" % bip32.path_to_str(derivation_path),
71 | msg,
72 | )
73 | res = await show_screen(scr)
74 | if res is False:
75 | return False
76 | sig = self.sign_message(derivation_path, message)
77 | # for GUI we can also return an object with helpful data
78 | pub = self.keystore.get_xpub(derivation_path).get_public_key()
79 | # default - legacy
80 | addr = script.p2pkh(pub).address(NETWORKS[self.network])
81 | if len(derivation_path) > 0:
82 | if derivation_path[0] == (0x80000000 + 84):
83 | addr = script.p2wpkh(pub).address(NETWORKS[self.network])
84 | if derivation_path[0] == (0x80000000 + 49):
85 | addr = script.p2sh(script.p2wpkh(pub)).address(NETWORKS[self.network])
86 | note = "Address: %s" % addr
87 | note += "\nDerivation path: %s" % bip32.path_to_str(derivation_path)
88 | obj = {
89 | "title": "Message signature:",
90 | "note": note,
91 | }
92 | return BytesIO(sig), obj
93 |
94 | def sign_message(self, derivation, msg: bytes, compressed: bool = True) -> bytes:
95 | """Sign message with private key"""
96 | msghash = sha256(
97 | sha256(
98 | b"\x18Bitcoin Signed Message:\n" + compact.to_bytes(len(msg)) + msg
99 | ).digest()
100 | ).digest()
101 | sig, flag = self.keystore.sign_recoverable(derivation, msghash)
102 | c = 4 if compressed else 0
103 | flag = bytes([27 + flag + c])
104 | ser = flag + secp256k1.ecdsa_signature_serialize_compact(sig._sig)
105 | return b2a_base64(ser).strip().decode()
106 |
--------------------------------------------------------------------------------
/src/apps/wallets/__init__.py:
--------------------------------------------------------------------------------
1 | from .app import WalletsApp as App
2 |
--------------------------------------------------------------------------------
/src/apps/wallets/app.py:
--------------------------------------------------------------------------------
1 | from app import BaseApp
2 | import platform
3 | from .manager import WalletManager
4 | from .liquid.manager import LWalletManager
5 | from helpers import is_liquid
6 |
7 | class WalletsApp(BaseApp):
8 | # This is a dummy app that can switch between Bitcoin and Liquid wallet managers
9 |
10 | def __init__(self, path):
11 | self.root_path = path
12 | platform.maybe_mkdir(path)
13 | self.path = None
14 | self.manager = None
15 |
16 | @property
17 | def button(self):
18 | return self.manager.button if self.manager else None
19 |
20 | @property
21 | def prefixes(self):
22 | return self.manager.prefixes if self.manager else None
23 |
24 | @property
25 | def name(self):
26 | return self.manager.name if self.manager else None
27 |
28 | def init(self, keystore, network, *args, **kwargs):
29 | """Loads or creates default wallets for new keystore or network"""
30 | old_network = self.network if hasattr(self, "network") else None
31 | super().init(keystore, network, *args, **kwargs)
32 | # switching the network - use different wallet managers for liquid or btc
33 | if old_network is None or self.manager is None or is_liquid(old_network) != is_liquid(network):
34 | if is_liquid(network):
35 | self.manager = LWalletManager(self.root_path)
36 | else:
37 | self.manager = WalletManager(self.root_path)
38 | return self.manager.init(keystore, network, *args, **kwargs)
39 |
40 | async def menu(self, *args, **kwargs):
41 | return await self.manager.menu(*args, **kwargs)
42 |
43 | def can_process(self, *args, **kwargs):
44 | return self.manager.can_process(*args, **kwargs)
45 |
46 | async def process_host_command(self, *args, **kwargs):
47 | return await self.manager.process_host_command(*args, **kwargs)
48 |
49 | def wipe(self):
50 | return self.manager.wipe()
51 |
--------------------------------------------------------------------------------
/src/apps/wallets/commands.py:
--------------------------------------------------------------------------------
1 | """Commands coming from the GUI"""
2 |
3 | DELETE = 187
4 | EDIT = 1
5 | MENU = 2
6 | INFO = 3
7 | EXPORT = 4
--------------------------------------------------------------------------------
/src/apps/wallets/liquid/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/src/apps/wallets/liquid/__init__.py
--------------------------------------------------------------------------------
/src/apps/wallets/liquid/wallet.py:
--------------------------------------------------------------------------------
1 | from ..wallet import *
2 | from platform import maybe_mkdir, delete_recursively, get_preallocated_ram
3 | from embit import ec, hashes, script, compact
4 | from embit.liquid.networks import NETWORKS
5 | from embit.liquid.descriptor import LDescriptor
6 | from embit.descriptor.arguments import AllowedDerivation
7 | from embit.liquid.pset import LInputScope, LOutputScope
8 | import hashlib
9 | import secp256k1
10 |
11 | # error that happened during rangeproof_rewind
12 | class RewindError(Exception):
13 | pass
14 |
15 | class LWallet(Wallet):
16 | DescriptorClass = LDescriptor
17 | Networks = NETWORKS
18 |
19 | def fill_scope(self, scope, fingerprint, stream=None, rangeproof_offset=None, surj_proof_offset=None):
20 | """
21 | Fills derivation paths in inputs.
22 | Returns:
23 | - True if all went well
24 | - False if wallet doesn't own input
25 | """
26 | if not self.owns(scope):
27 | return False
28 | der = self.get_derivation(scope.bip32_derivations)
29 | if der is None:
30 | return False
31 | idx, branch_idx = der
32 | desc = self.descriptor.derive(idx, branch_index=branch_idx)
33 | # find keys with our fingerprint
34 | for key in desc.keys:
35 | if key.fingerprint == fingerprint:
36 | pub = key.get_public_key()
37 | # fill our derivations
38 | scope.bip32_derivations[pub] = DerivationPath(
39 | fingerprint, key.derivation
40 | )
41 | # if liquid - unblind / blind etc
42 | if desc.is_blinded:
43 | try:
44 | if not self.fill_pset_scope(scope, desc, stream, rangeproof_offset, surj_proof_offset):
45 | return False
46 | except RewindError as e:
47 | print(e)
48 | return False
49 | # fill script
50 | scope.witness_script = desc.witness_script()
51 | scope.redeem_script = desc.redeem_script()
52 | return True
53 |
54 | def fill_pset_scope(self, scope, desc, stream=None, rangeproof_offset=None, surj_proof_offset=None):
55 | # if we don't have a rangeproof offset - nothing we can really do
56 | if rangeproof_offset is None:
57 | return True
58 | # pointer and length of preallocated memory for rangeproof rewind
59 | memptr, memlen = get_preallocated_ram()
60 | # for inputs we check if rangeproof is there
61 | # check if we actually need to rewind
62 | if None not in [scope.asset, scope.value, scope.asset_blinding_factor, scope.value_blinding_factor]:
63 | # verify that asset and value blinding factors lead to value and asset commitments
64 | return True
65 | stream.seek(rangeproof_offset)
66 | l = compact.read_from(stream)
67 | vout = scope.utxo if isinstance(scope, LInputScope) else scope.blinded_vout
68 | blinding_key = desc.blinding_key.get_blinding_key(vout.script_pubkey).secret
69 | # get the nonce for unblinding
70 | pub = secp256k1.ec_pubkey_parse(vout.ecdh_pubkey)
71 | secp256k1.ec_pubkey_tweak_mul(pub, blinding_key)
72 | sec = secp256k1.ec_pubkey_serialize(pub)
73 | nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest()
74 | commit = secp256k1.pedersen_commitment_parse(vout.value)
75 | gen = secp256k1.generator_parse(vout.asset)
76 | try:
77 | value, vbf, msg, _, _ = secp256k1.rangeproof_rewind_from(
78 | stream, l, memptr, memlen,
79 | nonce, commit, vout.script_pubkey.data, gen
80 | )
81 | except ValueError as e:
82 | raise RewindError(str(e))
83 | asset = msg[:32]
84 | abf = msg[32:64]
85 | scope.value = value
86 | scope.value_blinding_factor = vbf
87 | scope.asset = asset
88 | scope.asset_blinding_factor = abf
89 | return True
90 |
--------------------------------------------------------------------------------
/src/apps/xpubs/__init__.py:
--------------------------------------------------------------------------------
1 | from .xpubs import XpubApp as App
2 |
--------------------------------------------------------------------------------
/src/apps/xpubs/screens.py:
--------------------------------------------------------------------------------
1 | """Bitcoin-related screens"""
2 | import lvgl as lv
3 | from gui.common import PADDING, styles, add_button_pair
4 | from gui.decorators import on_release
5 | from gui.screens.qralert import QRAlert
6 |
7 |
8 | class XPubScreen(QRAlert):
9 | CREATE_WALLET = 0x01 # command to create a wallet
10 | def __init__(
11 | self,
12 | xpub,
13 | slip132=None,
14 | prefix=None,
15 | title="Your master public key",
16 | qr_width=None,
17 | button_text="Close"
18 | ):
19 | message = xpub
20 | if slip132 is not None:
21 | message = slip132
22 | if prefix is not None:
23 | message = prefix + message
24 | super().__init__(title, message, message, qr_width=320)
25 | self.message.set_style(0, styles["small"])
26 | self.xpub = xpub
27 | self.prefix = prefix
28 | self.slip132 = slip132
29 |
30 | if prefix is not None:
31 | lbl = lv.label(self)
32 | lbl.set_text("Show derivation path")
33 | lbl.set_pos(2 * PADDING, 500)
34 | self.prefix_switch = lv.sw(self)
35 | self.prefix_switch.on(lv.ANIM.OFF)
36 | self.prefix_switch.align(lbl, lv.ALIGN.OUT_LEFT_MID, 350, 0)
37 |
38 | if slip132 is not None:
39 | lbl = lv.label(self)
40 | lbl.set_text("Use SLIP-132")
41 | lbl.set_pos(2 * PADDING, 560)
42 | self.slip_switch = lv.sw(self)
43 | self.slip_switch.on(lv.ANIM.OFF)
44 | self.slip_switch.align(lbl, lv.ALIGN.OUT_LEFT_MID, 350, 0)
45 |
46 | if prefix is not None:
47 | self.prefix_switch.set_event_cb(on_release(self.toggle_event))
48 | if slip132 is not None:
49 | self.slip_switch.set_event_cb(on_release(self.toggle_event))
50 | add_button_pair(
51 | lv.SYMBOL.SAVE + " Save to SD", on_release(self.save_to_sd),
52 | lv.SYMBOL.PLUS + " Create wallet", on_release(self.create_wallet),
53 | y=610, scr=self,
54 | )
55 |
56 | def save_to_sd(self):
57 | """
58 | Returns the xpub in the form we want to save
59 | (canonical / slip39, with or without derivation)
60 | """
61 | self.set_value(self.message.get_text())
62 |
63 | def create_wallet(self):
64 | self.set_value(self.CREATE_WALLET)
65 |
66 | def toggle_event(self):
67 | msg = self.xpub
68 | if self.slip132 is not None and self.slip_switch.get_state():
69 | msg = self.slip132
70 | if self.prefix is not None and self.prefix_switch.get_state():
71 | msg = self.prefix + msg
72 | self.message.set_text(msg)
73 | self.qr.set_text(msg)
74 |
--------------------------------------------------------------------------------
/src/config_default.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | simulator = sys.platform != "pyboard"
5 |
6 | # to overwrite these settings create a config.py file
7 |
8 | if simulator:
9 | if len(sys.argv) > 1:
10 | storage_root = sys.argv[1]
11 | else:
12 | storage_root = "./fs"
13 | try:
14 | os.mkdir(storage_root)
15 | except:
16 | pass
17 |
18 | else:
19 | storage_root = ""
20 |
21 | # pin that triggers QR code
22 | # if command mode failed
23 | QRSCANNER_TRIGGER = "D2"
24 |
--------------------------------------------------------------------------------
/src/errors.py:
--------------------------------------------------------------------------------
1 | class BaseError(Exception):
2 | """
3 | All generic custom errors inherit from this one
4 | """
5 |
6 | NAME = "Error"
7 |
--------------------------------------------------------------------------------
/src/gui/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import init, update, ioloop
2 | from . import screens
3 | from . import async_gui
4 |
--------------------------------------------------------------------------------
/src/gui/components/__init__.py:
--------------------------------------------------------------------------------
1 | from .qrcode import QRCode
2 | from .mnemonic import MnemonicTable
3 | from .keyboard import HintKeyboard
4 | from .theme import styles
5 |
--------------------------------------------------------------------------------
/src/gui/components/battery.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .theme import styles
3 |
4 | class Battery(lv.obj):
5 | VALUE = None
6 | CHARGING = None
7 | LEVELS = [
8 | (95, lv.SYMBOL.BATTERY_FULL, "00D100"),
9 | (75, lv.SYMBOL.BATTERY_3, "00D100"),
10 | (50, lv.SYMBOL.BATTERY_2, "FF9A00"),
11 | (25, lv.SYMBOL.BATTERY_1, "F10000"),
12 | (0, lv.SYMBOL.BATTERY_EMPTY, "F10000"),
13 | ]
14 |
15 | def __init__(self, *args, **kwargs):
16 | super().__init__(*args, **kwargs)
17 | self.set_style(lv.style_transp_tight)
18 | self.level = lv.label(self)
19 | self.level.set_recolor(True)
20 | self.icon = lv.label(self)
21 | self.charge = lv.label(self)
22 | self.set_size(30,20)
23 | # self.bar = lv.bar(self)
24 | self.update()
25 |
26 | def update(self):
27 | if self.VALUE is None:
28 | self.icon.set_text("")
29 | self.level.set_text("")
30 | self.charge.set_text("")
31 | return
32 | for v, icon, color in self.LEVELS:
33 | if self.VALUE >= v:
34 | if self.CHARGING:
35 | self.level.set_text("#00D100 "+icon+" #")
36 | else:
37 | self.level.set_text("#"+color+" "+icon+" #")
38 | break
39 | self.icon.set_text(lv.SYMBOL.BATTERY_EMPTY)
40 | if self.CHARGING:
41 | self.charge.set_text(lv.SYMBOL.CHARGE)
42 | self.charge.align(self.icon, lv.ALIGN.CENTER, 0, 0)
43 | else:
44 | self.charge.set_text("")
45 |
--------------------------------------------------------------------------------
/src/gui/components/keyboard.py:
--------------------------------------------------------------------------------
1 | """A keyboard with a hint when you press it"""
2 | import lvgl as lv
3 | from ..decorators import feed_touch
4 | from .theme import styles
5 |
6 |
7 | class HintKeyboard(lv.btnm):
8 | def __init__(self, scr, *args, **kwargs):
9 | super().__init__(scr, *args, **kwargs)
10 | self.hint = lv.btn(scr)
11 | self.hint.set_size(50, 60)
12 | self.hint_lbl = lv.label(self.hint)
13 | self.hint_lbl.set_text(" ")
14 | self.hint_lbl.set_style(0, styles["title"])
15 | self.hint_lbl.set_size(50, 60)
16 | self.hint.set_hidden(True)
17 | self.callback = None
18 | super().set_event_cb(self.cb)
19 |
20 | def set_event_cb(self, callback):
21 | self.callback = callback
22 |
23 | def get_event_cb(self):
24 | return self.callback
25 |
26 | def cb(self, obj, event):
27 | if event == lv.EVENT.PRESSING:
28 | feed_touch()
29 | c = obj.get_active_btn_text()
30 | if c is not None and len(c) <= 2:
31 | self.hint.set_hidden(False)
32 | self.hint_lbl.set_text(c)
33 | point = lv.point_t()
34 | indev = lv.indev_get_act()
35 | lv.indev_get_point(indev, point)
36 | self.hint.set_pos(point.x - 25, point.y - 130)
37 |
38 | elif event == lv.EVENT.RELEASED:
39 | self.hint.set_hidden(True)
40 |
41 | if self.callback is not None:
42 | self.callback(obj, event)
43 |
--------------------------------------------------------------------------------
/src/gui/components/mnemonic.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .theme import styles
3 |
4 |
5 | class MnemonicTable(lv.table):
6 | def __init__(self, *args, **kwargs):
7 | super().__init__(*args, **kwargs)
8 | self.words = [""]
9 | # styles
10 | cell_style = lv.style_t()
11 | lv.style_copy(cell_style, styles["theme"].style.label.prim)
12 | cell_style.body.opa = 0
13 | cell_style.text.font = lv.font_roboto_22
14 |
15 | num_style = lv.style_t()
16 | lv.style_copy(num_style, cell_style)
17 | num_style.text.opa = lv.OPA._40
18 |
19 | self.set_col_cnt(4)
20 | self.set_row_cnt(12)
21 | self.set_col_width(0, 40)
22 | self.set_col_width(2, 40)
23 | self.set_col_width(1, 180)
24 | self.set_col_width(3, 180)
25 |
26 | self.set_style(lv.page.STYLE.BG, cell_style)
27 | self.set_style(lv.table.STYLE.CELL1, cell_style)
28 | self.set_style(lv.table.STYLE.CELL2, num_style)
29 |
30 | for i in range(12):
31 | self.set_cell_value(i, 0, "%d" % (i + 1))
32 | self.set_cell_value(i, 2, "%d" % (i + 13))
33 | self.set_cell_type(i, 0, lv.table.STYLE.CELL2)
34 | self.set_cell_type(i, 2, lv.table.STYLE.CELL2)
35 |
36 | def set_mnemonic(self, mnemonic: str):
37 | self.words = mnemonic.split()
38 | self.update()
39 |
40 | def update(self):
41 | for i in range(24):
42 | row = i % 12
43 | col = 1 + 2 * (i // 12)
44 | if i < len(self.words):
45 | self.set_cell_value(row, col, self.words[i])
46 | else:
47 | self.set_cell_value(row, col, "")
48 |
49 | def get_mnemonic(self) -> str:
50 | return " ".join(self.words)
51 |
52 | def get_last_word(self) -> str:
53 | if len(self.words) == 0:
54 | return ""
55 | else:
56 | return self.words[-1]
57 |
58 | def del_char(self):
59 | if len(self.words) == 0:
60 | return
61 | if len(self.words[-1]) == 0:
62 | self.words = self.words[:-1]
63 | else:
64 | self.words[-1] = self.words[-1][:-1]
65 | self.update()
66 |
67 | def autocomplete_word(self, word):
68 | if len(self.words) > 24:
69 | return
70 | if len(self.words) == 0:
71 | self.words.append(word)
72 | else:
73 | self.words[-1] = word
74 | if len(self.words) < 24:
75 | self.words.append("")
76 | self.update()
77 |
78 | def add_char(self, c):
79 | if len(self.words) > 24:
80 | return
81 | if len(self.words) == 0:
82 | self.words.append(c)
83 | else:
84 | self.words[-1] += c
85 | self.update()
86 |
--------------------------------------------------------------------------------
/src/gui/components/modal.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 |
3 | class Modal(lv.obj):
4 | """mbox with semi-transparent background"""
5 | def __init__(self, parent, *args, **kwargs):
6 | # Create a base object for the modal background
7 | super().__init__(parent, *args, **kwargs)
8 |
9 | # Create a full-screen background
10 | modal_style = lv.style_t()
11 | lv.style_copy(modal_style, lv.style_plain_color)
12 | # Set the background's style
13 | modal_style.body.main_color = modal_style.body.grad_color = lv.color_make(0,0,0)
14 | modal_style.body.opa = lv.OPA._50
15 |
16 | self.set_style(modal_style)
17 | self.set_pos(0, 0)
18 | self.set_size(parent.get_width(), parent.get_height())
19 |
20 | self.mbox = lv.mbox(self)
21 | self.mbox.set_width(400)
22 | self.mbox.align(None, lv.ALIGN.IN_TOP_MID, 0, 200)
23 |
24 | def set_text(self, text):
25 | self.mbox.set_text(text)
26 |
--------------------------------------------------------------------------------
/src/gui/components/theme.py:
--------------------------------------------------------------------------------
1 | styles = {}
2 |
--------------------------------------------------------------------------------
/src/gui/core.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | import time
3 |
4 | import display
5 |
6 | from .common import init_styles
7 |
8 |
9 | def init(blocking=True, dark=True):
10 | # display.init(not blocking)
11 |
12 | # Initialize the styles
13 | init_styles(dark=dark)
14 |
15 | scr = lv.obj()
16 | lv.scr_load(scr)
17 | update()
18 |
19 |
20 | def update(dt: int = 30):
21 | display.update(dt)
22 |
23 |
24 | def ioloop(dt: int = 30):
25 | while True:
26 | time.sleep_ms(dt)
27 | update(dt)
28 |
--------------------------------------------------------------------------------
/src/gui/decorators.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | import time
3 | import rng
4 |
5 |
6 | def feed_touch():
7 | """
8 | Gets a point from the touchscreen
9 | and feeds it to random number pool
10 | """
11 | point = lv.point_t()
12 | indev = lv.indev_get_act()
13 | lv.indev_get_point(indev, point)
14 | # now we can take bytes([point.x % 256, point.y % 256])
15 | # and feed it into hash digest
16 | t = time.ticks_cpu()
17 | random_data = t.to_bytes(4, "big") + bytes([point.x % 256, point.y % 256])
18 | rng.feed(random_data)
19 |
20 |
21 | def feed_rng(func):
22 | """Any callback will contribute to random number pool"""
23 |
24 | def wrapper(o, e):
25 | if e == lv.EVENT.PRESSING:
26 | feed_touch()
27 | func(o, e)
28 |
29 | return wrapper
30 |
31 |
32 | def on_release(func):
33 | """Handy decorator if you only care about click event"""
34 |
35 | def wrapper(o, e):
36 | if e == lv.EVENT.PRESSING:
37 | feed_touch()
38 | elif e == lv.EVENT.RELEASED and func is not None:
39 | func()
40 |
41 | return wrapper
42 |
43 |
44 | def cb_with_args(callback, *args, **kwargs):
45 | """Pass arguments to the lv callback"""
46 |
47 | def cb():
48 | if callback is not None:
49 | callback(*args, **kwargs)
50 |
51 | return cb
52 |
--------------------------------------------------------------------------------
/src/gui/screens/__init__.py:
--------------------------------------------------------------------------------
1 | from .screen import Screen
2 | from .menu import Menu
3 | from .alert import Alert
4 | from .prompt import Prompt
5 | from .qralert import QRAlert
6 | from .progress import Progress
7 | from .input import PinScreen, InputScreen, DerivationScreen, NumericScreen
8 | from .mnemonic import MnemonicScreen, NewMnemonicScreen, RecoverMnemonicScreen
9 | from .transaction import TransactionScreen
10 | from .settings import DevSettings
11 |
--------------------------------------------------------------------------------
/src/gui/screens/alert.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .screen import Screen
3 | from ..common import add_label, add_button
4 | from ..decorators import on_release
5 |
6 |
7 | class Alert(Screen):
8 | def __init__(
9 | self, title, message, button_text=(lv.SYMBOL.LEFT + " Back"), note=None
10 | ):
11 | super().__init__()
12 | self.title = add_label(title, scr=self, style="title")
13 | obj = self.title
14 | if note is not None:
15 | self.note = add_label(note, scr=self, style="hint")
16 | self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
17 | obj = self.note
18 | self.page = lv.page(self)
19 | self.page.set_size(480, 600)
20 | self.message = add_label(message, scr=self.page)
21 | self.page.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 0)
22 |
23 | if button_text is not None:
24 | self.close_button = add_button(scr=self, callback=on_release(self.release))
25 |
26 | self.close_label = lv.label(self.close_button)
27 | self.close_label.set_text(button_text)
28 |
--------------------------------------------------------------------------------
/src/gui/screens/menu.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .screen import Screen
3 | from ..common import add_label, add_button
4 | from ..decorators import on_release, cb_with_args
5 |
6 |
7 | class Menu(Screen):
8 | def __init__(
9 | self, buttons=[], title="What do you want to do?", note=None, y0=60, last=None
10 | ):
11 | super().__init__()
12 | y = y0
13 | self.title = add_label(title, style="title", scr=self)
14 | if note is not None:
15 | self.note = add_label(note, style="hint", scr=self)
16 | self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
17 | y += self.note.get_height()
18 | self.page = lv.page(self)
19 | h = 800 - y - 20
20 | self.page.set_size(480, h)
21 | self.page.set_y(y)
22 | y = 0
23 | self.buttons = []
24 | # value, text, enable, color
25 | for value, text, *args in buttons:
26 | if text is not None:
27 | if value is not None:
28 | enable = len(args) == 0 or args[0]
29 | if enable:
30 | cb = on_release(cb_with_args(self.set_value, value))
31 | else:
32 | cb = None
33 | btn = add_button(text, cb, y=y, scr=self.page)
34 | if not enable:
35 | btn.set_state(lv.btn.STATE.INA)
36 | # color
37 | if len(args) > 1:
38 | color = args[1]
39 | style = lv.style_t()
40 | lv.style_copy(style, btn.get_style(lv.btn.STYLE.REL))
41 | style.body.main_color = lv.color_hex(color)
42 | style.body.grad_color = lv.color_hex(color)
43 | btn.set_style(lv.btn.STYLE.REL, style)
44 |
45 | self.buttons.append(btn)
46 | y += 85
47 | else:
48 | add_label(text.upper(), y=y + 10, style="hint", scr=self.page)
49 | y += 45
50 | else:
51 | y += 40
52 | if last is not None:
53 | self.add_back_button(*last)
54 | self.page.set_height(h - 100)
55 |
56 | def add_back_button(self, value, text=None):
57 | if text is None:
58 | text = lv.SYMBOL.LEFT + " Back"
59 | add_button(text, on_release(cb_with_args(self.set_value, value)), scr=self)
60 |
--------------------------------------------------------------------------------
/src/gui/screens/progress.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .alert import Alert
3 | from ..common import add_label
4 |
5 |
6 | class Progress(Alert):
7 | """
8 | Shows progress (rotating thingy), also can show
9 | percentage of the progress or checkboxes for parts of QR code
10 | Use tick() to rotate, set_progress(float or list) to set progress
11 | """
12 |
13 | def __init__(self, title, message, button_text="Cancel"):
14 | super().__init__(title, message, button_text=button_text)
15 | self.arc = lv.arc(self)
16 | self.start = 0
17 | self.end = 30
18 | self.arc.set_angles(self.start, self.end)
19 | self.arc.align(self, lv.ALIGN.CENTER, 0, -150)
20 | self.message.align(self.arc, lv.ALIGN.OUT_BOTTOM_MID, 0, 120)
21 | self.progress = add_label("", scr=self, style="title")
22 | self.progress.align(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30)
23 | self.progress.set_recolor(True)
24 |
25 | def tick(self, d: int = 10):
26 | self.start = (self.start - 2 * d) % 360
27 | self.end = (self.end - d) % 360
28 | self.arc.set_angles(self.start, self.end)
29 |
30 | def set_progress(self, val):
31 | txt = ""
32 | if isinstance(val, list):
33 | ok = "#00F100 " + lv.SYMBOL.OK + " # "
34 | no = "#FF9A00 " + lv.SYMBOL.CLOSE + " # "
35 | txt = " ".join([ok if e else no for e in val])
36 | elif val > 0:
37 | txt = "%d%%" % int(val * 100)
38 | self.progress.set_text(txt)
39 |
--------------------------------------------------------------------------------
/src/gui/screens/prompt.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .screen import Screen
3 | from ..common import add_label, add_button_pair
4 | from ..decorators import on_release, cb_with_args
5 |
6 |
7 | class Prompt(Screen):
8 | def __init__(self, title="Are you sure?", message="Make a choice",
9 | confirm_text="Confirm", cancel_text="Cancel", note=None, warning=None):
10 | super().__init__()
11 | self.title = add_label(title, scr=self, style="title")
12 | if note is not None:
13 | self.note = add_label(note, scr=self, style="hint")
14 | self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
15 | obj = self.note
16 | self.page = lv.page(self)
17 | self.page.set_size(480, 600)
18 | self.message = add_label(message, scr=self.page)
19 | self.page.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 0)
20 | # Initialize an empty icon label. It will display nothing until a symbol is set.
21 | self.icon = lv.label(self)
22 | self.icon.set_text("")
23 |
24 | (self.cancel_button, self.confirm_button) = add_button_pair(
25 | cancel_text,
26 | on_release(cb_with_args(self.set_value, False)),
27 | confirm_text,
28 | on_release(cb_with_args(self.set_value, True)),
29 | scr=self,
30 | )
31 |
32 | if warning:
33 | self.warning = add_label(warning, scr=self, style="warning")
34 | # Display warning symbol in the icon label
35 | self.icon.set_text(lv.SYMBOL.WARNING)
36 |
37 | # Align warning text
38 | y_pos = self.cancel_button.get_y() - 60 # above the buttons
39 | x_pos = self.get_width() // 2 - self.warning.get_width() // 2 # in the center of the prompt
40 | self.warning.set_pos(x_pos, y_pos)
41 |
42 | # Align warning icon to the left of the title
43 | self.icon.align(self.title, lv.ALIGN.IN_LEFT_MID, 90, 0)
44 |
45 |
--------------------------------------------------------------------------------
/src/gui/screens/qralert.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .alert import Alert
3 | from ..common import add_qrcode, add_button
4 | from ..decorators import on_release
5 |
6 |
7 | class QRAlert(Alert):
8 | def __init__(
9 | self,
10 | title="QR Alert!",
11 | message="Something happened",
12 | qr_message=None,
13 | qr_width=None,
14 | button_text="Close",
15 | note=None,
16 | transcribe=False,
17 | ):
18 | if qr_message is None:
19 | qr_message = message
20 | super().__init__(title, message, button_text, note=note)
21 | self.qr = add_qrcode(qr_message, scr=self, width=qr_width)
22 | self.qr.align(self.page, lv.ALIGN.IN_TOP_MID, 0, 20)
23 | self.message.align(self.qr, lv.ALIGN.OUT_BOTTOM_MID, 0, 20)
24 | if transcribe:
25 | btn = add_button("Toggle transcribe", on_release(self.toggle_transcribe), scr=self)
26 | btn.align(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 20)
27 |
28 | def toggle_transcribe(self):
29 | self.qr.spacing = 0 if self.qr.spacing else 3
30 |
--------------------------------------------------------------------------------
/src/gui/screens/screen.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | import asyncio
3 | from ..common import styles, HOR_RES
4 | from ..core import update
5 | from ..components.modal import Modal
6 | from ..components.battery import Battery
7 |
8 | class Screen(lv.obj):
9 | network = "test"
10 | COLORS = {
11 | "main": lv.color_hex(0xFF9A00),
12 | "test": lv.color_hex(0x00F100),
13 | "regtest": lv.color_hex(0x00CAF1),
14 | "signet": lv.color_hex(0xBD10E0),
15 | "liquidv1": lv.color_hex(0x46B4A5),
16 | "elementsregtest": lv.color_hex(0x00CAF1),
17 | "liquidtestnet": lv.color_hex(0xC0F100),
18 | }
19 | mbox = None
20 | def __init__(self):
21 | super().__init__()
22 | self.waiting = True
23 | self._value = None
24 | self.battery = Battery(self)
25 | self.battery.align(self, lv.ALIGN.IN_TOP_RIGHT, -20, 10)
26 |
27 | if type(self).network in type(self).COLORS:
28 | self.topbar = lv.obj(self)
29 | s = lv.style_t()
30 | lv.style_copy(s, styles["theme"].style.btn.rel)
31 | s.body.main_color = type(self).COLORS[type(self).network]
32 | s.body.grad_color = type(self).COLORS[type(self).network]
33 | s.body.opa = 200
34 | s.body.radius = 0
35 | s.body.border.width = 0
36 | self.topbar.set_style(s)
37 | self.topbar.set_size(HOR_RES, 5)
38 | self.topbar.set_pos(0, 0)
39 |
40 | def release(self):
41 | self.waiting = False
42 |
43 | def get_value(self):
44 | """
45 | Redefine this function to get value entered by the user
46 | """
47 | return self._value
48 |
49 | def set_value(self, value):
50 | self._value = value
51 | self.release()
52 |
53 | async def result(self):
54 | self.waiting = True
55 | while self.waiting:
56 | await asyncio.sleep_ms(10)
57 | return self.get_value()
58 |
59 | def show_loader(self,
60 | text="Please wait until the process is complete.",
61 | title="Processing..."):
62 | if self.mbox is None:
63 | self.mbox = Modal(self)
64 | self.mbox.set_text("\n\n"+title+"\n\n"+text+"\n\n")
65 | # trigger update of the screen
66 | update()
67 | update()
68 |
69 | def hide_loader(self):
70 | if self.mbox is None:
71 | return
72 | self.mbox.del_async()
73 | self.mbox = None
74 | update()
75 |
--------------------------------------------------------------------------------
/src/gui/screens/settings.py:
--------------------------------------------------------------------------------
1 | import lvgl as lv
2 | from .prompt import Prompt
3 | from ..common import add_label, add_button
4 | from ..decorators import on_release
5 |
6 | class HostSettings(Prompt):
7 | def __init__(self, controls, title="Host setttings", note=None, controls_empty_text="No settings available"):
8 | super().__init__(title, "")
9 | y = 40
10 | if note is not None:
11 | self.note = add_label(note, style="hint", scr=self)
12 | self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
13 | y += self.note.get_height()
14 | self.controls = controls
15 | self.switches = []
16 | for control in controls:
17 | label = add_label(control["label"], y, scr=self.page)
18 | hint = add_label(
19 | control.get("hint", ""),
20 | y + 30,
21 | scr=self.page,
22 | style="hint",
23 | )
24 | switch = lv.sw(self.page)
25 | switch.align(hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
26 | lbl = add_label(" OFF ON ", scr=self.page)
27 | lbl.align(switch, lv.ALIGN.CENTER, 0, 0)
28 | if control.get("value", False):
29 | switch.on(lv.ANIM.OFF)
30 | self.switches.append(switch)
31 | y = lbl.get_y() + 80
32 | else:
33 | label = add_label(controls_empty_text, y, scr=self.page)
34 | self.confirm_button.set_event_cb(on_release(self.update))
35 | self.cancel_button.set_event_cb(on_release(lambda: self.set_value(None)))
36 |
37 | def update(self):
38 | self.set_value([switch.get_state() for switch in self.switches])
39 |
40 | class DevSettings(Prompt):
41 | def __init__(self, dev=False, usb=False, note=None):
42 | super().__init__("Device settings", "")
43 | if note is not None:
44 | self.note = add_label(note, style="hint", scr=self)
45 | self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5)
46 | y = 70
47 | usb_label = add_label("USB communication", y, scr=self.page)
48 | usb_hint = add_label(
49 | "If USB is enabled the device will be able "
50 | "to talk to your computer. This increases "
51 | "attack surface but sometimes makes it "
52 | "more convenient to use.",
53 | y + 40,
54 | scr=self.page,
55 | style="hint",
56 | )
57 | self.usb_switch = lv.sw(self.page)
58 | self.usb_switch.align(usb_hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 20)
59 | lbl = add_label(" OFF ON ", scr=self.page)
60 | lbl.align(self.usb_switch, lv.ALIGN.CENTER, 0, 0)
61 | if usb:
62 | self.usb_switch.on(lv.ANIM.OFF)
63 |
64 | # y += 200
65 | # dev_label = add_label("Developer mode", y, scr=self.page)
66 | # dev_hint = add_label(
67 | # "In developer mode internal flash will "
68 | # "be mounted to your computer so you could "
69 | # "edit files, but your secrets will be visible as well. "
70 | # "Also enables interactive shell through miniUSB port.",
71 | # y + 40,
72 | # scr=self.page,
73 | # style="hint",
74 | # )
75 | # self.dev_switch = lv.sw(self.page)
76 | # self.dev_switch.align(dev_hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 20)
77 | # lbl = add_label(" OFF ON ", scr=self.page)
78 | # lbl.align(self.dev_switch, lv.ALIGN.CENTER, 0, 0)
79 | # if dev:
80 | # self.dev_switch.on(lv.ANIM.OFF)
81 | self.confirm_button.set_event_cb(on_release(self.update))
82 | self.cancel_button.set_event_cb(on_release(lambda: self.set_value(None)))
83 |
84 | self.wipebtn = add_button(
85 | lv.SYMBOL.TRASH + " Wipe device", on_release(self.wipe), scr=self
86 | )
87 | self.wipebtn.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, -140)
88 | style = lv.style_t()
89 | lv.style_copy(style, self.wipebtn.get_style(lv.btn.STYLE.REL))
90 | style.body.main_color = lv.color_hex(0x951E2D)
91 | style.body.grad_color = lv.color_hex(0x951E2D)
92 | self.wipebtn.set_style(lv.btn.STYLE.REL, style)
93 |
94 | def wipe(self):
95 | self.set_value(
96 | {
97 | "dev": False, # self.dev_switch.get_state(),
98 | "usb": self.usb_switch.get_state(),
99 | "wipe": True,
100 | }
101 | )
102 |
103 | def update(self):
104 | self.set_value(
105 | {
106 | "dev": False, # self.dev_switch.get_state(),
107 | "usb": self.usb_switch.get_state(),
108 | "wipe": False,
109 | }
110 | )
111 |
--------------------------------------------------------------------------------
/src/gui/specter.py:
--------------------------------------------------------------------------------
1 | from .async_gui import AsyncGUI
2 | from .screens import (
3 | Screen,
4 | Progress,
5 | MnemonicScreen,
6 | NewMnemonicScreen,
7 | RecoverMnemonicScreen,
8 | DevSettings,
9 | )
10 | import asyncio
11 |
12 |
13 | class SpecterGUI(AsyncGUI):
14 | """Specter-related GUI"""
15 |
16 | async def show_mnemonic(self, mnemonic: str):
17 | """
18 | Shows mnemonic on the screen
19 | """
20 | scr = MnemonicScreen(mnemonic)
21 | await self.load_screen(scr)
22 | return await scr.result()
23 |
24 | async def new_mnemonic(self, generator, wordlist, fix):
25 | """
26 | Generates a new mnemonic and shows it on the screen
27 | """
28 | scr = NewMnemonicScreen(generator, wordlist, fix)
29 | await self.load_screen(scr)
30 | return await scr.result()
31 |
32 | async def recover(self, checker=None, lookup=None, fix=None):
33 | """
34 | Asks the user for his recovery phrase.
35 | checker(mnemonic) - a function that validates recovery phrase
36 | lookup(word, num_candidates) - a function that
37 | returns num_candidates words starting with word
38 | """
39 | scr = RecoverMnemonicScreen(checker, lookup, fix)
40 | await self.load_screen(scr)
41 | return await scr.result()
42 |
43 | def set_network(self, net):
44 | """Changes color of the top line on all screens to network color"""
45 | Screen.network = net
46 |
47 | async def show_progress(self, host, title, message):
48 | """
49 | Shows progress screen and cancel button
50 | to cancel communication with the host
51 | """
52 | scr = Progress(title, message, button_text="Cancel")
53 | await self.open_popup(scr)
54 | asyncio.create_task(self.coro(host, scr))
55 |
56 | async def coro(self, host, scr):
57 | """
58 | Waits for one of two events:
59 | - either user presses something on the screen
60 | - or host finishes processing
61 | Also updates progress screen
62 | """
63 | while host.in_progress and scr.waiting:
64 | await asyncio.sleep_ms(30)
65 | scr.tick(5)
66 | scr.set_progress(host.progress)
67 | if host.in_progress:
68 | host.abort()
69 | if scr.waiting:
70 | scr.waiting = False
71 | await self.close_popup()
72 |
73 | async def devscreen(self, dev=False, usb=False, note=None):
74 | scr = DevSettings(dev=dev, usb=usb, note=note)
75 | await self.load_screen(scr)
76 | return await scr.result()
77 |
--------------------------------------------------------------------------------
/src/gui/tcp_gui.py:
--------------------------------------------------------------------------------
1 | from .specter import SpecterGUI
2 | import pyb
3 | import json
4 | import asyncio
5 | import sys
6 |
7 | class TCPGUI(SpecterGUI):
8 | """
9 | Simulated GUI for testing.
10 | User interaction can be provided over telnet on port 8787
11 | """
12 | def __init__(self, *args, **kwargs):
13 | self.tcp = pyb.UART('"S') # will be on port 8787
14 | super().__init__(*args, **kwargs)
15 |
16 | def start(self, *args, **kwargs):
17 | super().start(*args, **kwargs)
18 | asyncio.create_task(self.tcp_loop())
19 |
20 | async def tcp_loop(self):
21 | res = b""
22 | while True:
23 | await asyncio.sleep_ms(30)
24 | try:
25 | # trying to read something
26 | chunk = self.tcp.read(100)
27 | # if we didn't get anything - return
28 | if chunk is None:
29 | continue
30 | res += chunk
31 | if b"\r" in res or b"\n" in res:
32 | arr = res.replace(b"\r",b"\n").split(b"\n")
33 | cmd = arr[0].decode()
34 | if cmd == "quit":
35 | print("QUIT!")
36 | sys.exit(1)
37 | res = b""
38 | val = json.loads("[%s]" % cmd)[0]
39 | if self.scr is not None:
40 | self.scr.set_value(val)
41 | except Exception as e:
42 | print(e)
43 | res = b""
44 |
45 | async def open_popup(self, scr):
46 | try:
47 | self.tcp.write(b"%s\r\n" % type(scr).__name__)
48 | except:
49 | pass
50 | return await super().open_popup(scr)
51 |
52 |
53 | async def load_screen(self, scr):
54 | try:
55 | self.tcp.write(b"%s\r\n" % type(scr).__name__)
56 | except:
57 | pass
58 | return await super().load_screen(scr)
59 |
--------------------------------------------------------------------------------
/src/hosts/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import HostError, Host
2 | from .qr import QRHost
3 | from .sd import SDHost
4 | from .usb import USBHost
5 |
--------------------------------------------------------------------------------
/src/hosts/core.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from platform import maybe_mkdir
3 | from errors import BaseError
4 | import json
5 | from gui.screens.settings import HostSettings
6 | from gui.screens import Alert
7 |
8 | class HostError(BaseError):
9 | NAME = "Host error"
10 |
11 |
12 | class Host:
13 | """
14 | Abstract Host class
15 | Manages communication with the host
16 | Can be unidirectional like QRHost
17 | or bidirectional like USBHost or SDHost
18 | """
19 |
20 | # time to wait after init
21 | RECOVERY_TIME = 1
22 | # store device settings here with unique filename
23 | # common for all hosts
24 | SETTINGS_DIR = None
25 | # set the button on the main screen
26 | # should be a tuple (text, callback)
27 | # keep None if you don't need a button
28 | button = None
29 | # button text for settings menu, None if nothing to configure
30 | settings_button = None
31 | # link to specter instance
32 | parent = None
33 |
34 | def __init__(self, path):
35 | # storage for data
36 | self.path = path
37 | maybe_mkdir(path)
38 | if self.SETTINGS_DIR:
39 | maybe_mkdir(self.SETTINGS_DIR)
40 | # set manager
41 | self.manager = None
42 | # check this flag in update function
43 | # if disabled - throw all incoming data
44 | self.enabled = False
45 | self.initialized = False
46 | # default settings, extend it with more settings if applicable
47 | self.settings = { "enabled": True }
48 | # if host can be triggered by the user
49 | # this is monitored by the manager
50 | # self.in_progress = False
51 | # this is the current state of the host
52 | # can be a float between 0 and 1 or
53 | # a list of [True, False, ...] (for QR code)
54 | # self.progress = 0
55 |
56 | def init(self):
57 | """
58 | Define here what should happen when host is initialized
59 | Configure hardware, do selfchecks etc.
60 | """
61 | pass
62 |
63 | @property
64 | def is_enabled(self):
65 | return self.settings.get("enabled", True)
66 |
67 | @property
68 | def settings_fname(self):
69 | return self.SETTINGS_DIR + "/" + type(self).__name__ + ".settings"
70 |
71 | def load_settings(self, keystore):
72 | try:
73 | adata, _ = keystore.load_aead(self.settings_fname, key=keystore.settings_key)
74 | settings = json.loads(adata.decode())
75 | self.settings = settings
76 | except Exception as e:
77 | print(e)
78 | return self.settings
79 |
80 | def save_settings(self, keystore):
81 | keystore.save_aead(self.settings_fname,
82 | adata=json.dumps(self.settings).encode(),
83 | key=keystore.settings_key
84 | )
85 |
86 | async def settings_menu(self, show_screen, keystore):
87 | title = self.settings_button or "Settings"
88 | controls = [{
89 | "label": "Enable " + title,
90 | "hint": "This setting will completely enable or disable this communication channel and remove corresponding button from the main menu",
91 | "value": self.settings["enabled"]
92 | }]
93 | scr = HostSettings(controls, title=title)
94 | res = await show_screen(scr)
95 | if res:
96 | self.settings["enabled"] = res[0]
97 | self.save_settings(keystore)
98 | await show_screen(Alert("Success!", "\n\nSettings updated!", button_text="Close"))
99 |
100 | def start(self, manager, rate: int = 10):
101 | self.manager = manager
102 | asyncio.create_task(self.update_loop(rate))
103 |
104 | async def update(self):
105 | """
106 | Define here what should happen in a loop
107 | Like fetch data from uart or usb.
108 | """
109 | pass
110 |
111 | async def update_loop(self, dt: int):
112 | while not self.enabled:
113 | await asyncio.sleep_ms(100)
114 | while True:
115 | if self.enabled:
116 | try:
117 | await self.update()
118 | except Exception as e:
119 | self.abort()
120 | if self.manager is not None:
121 | await self.manager.host_exception_handler(e)
122 | # Keep await sleep here
123 | # It allows other functions to run
124 | await asyncio.sleep_ms(dt)
125 |
126 | def abort(self):
127 | """What should happen if exception?"""
128 | pass
129 |
130 | async def enable(self):
131 | """
132 | What should happen when host enables?
133 | Maybe you want to remove all pending data first?
134 | """
135 | if not self.initialized:
136 | self.init()
137 | await asyncio.sleep_ms(self.RECOVERY_TIME)
138 | self.initialized = True
139 | self.enabled = True
140 |
141 | async def disable(self):
142 | """
143 | What should happen when host disables?
144 | """
145 | self.enabled = False
146 |
147 | async def get_data(self, raw=False, chunk_timeout=0.1):
148 | """Implement how to get transaction from unidirectional host"""
149 | raise HostError("Data loading is not implemented for this class")
150 |
151 | async def send_psbt(self, psbt):
152 | """Implement how to send the signed transaction to the host"""
153 | raise HostError("Sending data is not implemented for this class")
154 |
155 | def user_canceled(self):
156 | """
157 | Define what should happen if user pressed cancel.
158 | Maybe you want to tell the host that user cancelled?
159 | """
160 | pass
161 |
--------------------------------------------------------------------------------
/src/keystore/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import KeyStoreError, PinError
2 | from .flash import FlashKeyStore
3 |
--------------------------------------------------------------------------------
/src/keystore/core.py:
--------------------------------------------------------------------------------
1 | """Base classes for inheritance"""
2 | from errors import BaseError
3 |
4 |
5 | class KeyStoreError(BaseError):
6 | NAME = "Keystore error"
7 |
8 |
9 | class PinError(KeyStoreError):
10 | NAME = "PIN error"
11 |
12 |
13 | class KeyStore:
14 | NAME = "Generic Keystore"
15 | NOTE = "Base class"
16 | COLOR = "FF9A00"
17 | path = None
18 | load_button = None
19 |
20 |
21 | @classmethod
22 | def is_available(cls):
23 | return True
24 |
--------------------------------------------------------------------------------
/src/keystore/javacard/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/src/keystore/javacard/__init__.py
--------------------------------------------------------------------------------
/src/keystore/javacard/applets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/src/keystore/javacard/applets/__init__.py
--------------------------------------------------------------------------------
/src/keystore/javacard/applets/applet.py:
--------------------------------------------------------------------------------
1 | from ..util import encode
2 | from binascii import hexlify
3 | from uscard import SmartcardException
4 |
5 |
6 | class ISOException(Exception):
7 | pass
8 |
9 |
10 | class AppletException(Exception):
11 | pass
12 |
13 |
14 | class Applet:
15 | SELECT = b"\x00\xA4\x04\x00" # select command
16 | NAME = "GenericApplet"
17 | version = "0.1.0" # default
18 | platform = "JavaCard OS"
19 |
20 | def __init__(self, connection, aid):
21 | self.conn = connection
22 | self.aid = aid
23 |
24 | def select(self):
25 | self.request(self.SELECT + encode(self.aid))
26 |
27 | def request(self, apdu, retry=True):
28 | if not self.conn.isCardInserted():
29 | raise AppletException("Card is not present")
30 | data = self.conn.transmit(apdu)
31 | sw = bytes(data[-2:])
32 | if sw != b"\x90\x00":
33 | raise ISOException(hexlify(sw).decode())
34 | if isinstance(data[0], bytes):
35 | return data[0]
36 | else:
37 | return data[:-2]
38 |
--------------------------------------------------------------------------------
/src/keystore/javacard/applets/blindoracle.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/src/keystore/javacard/applets/blindoracle.py
--------------------------------------------------------------------------------
/src/keystore/javacard/applets/memorycard.py:
--------------------------------------------------------------------------------
1 | from .secureapplet import SecureApplet, SecureError
2 | from .applet import ISOException
3 |
4 |
5 | class MemoryCardApplet(SecureApplet):
6 | GET_SECRET = b"\x05\x00"
7 | SET_SECRET = b"\x05\x01"
8 | NAME = "MemoryCard"
9 |
10 | def __init__(self, connection):
11 | aid = b"\xB0\x0B\x51\x11\xCB\x01"
12 | super().__init__(connection, aid)
13 |
14 | def save_secret(self, secret: bytes):
15 | return self.sc.request(self.SET_SECRET + secret)
16 |
17 | def get_secret(self):
18 | return self.sc.request(self.GET_SECRET)
19 |
20 | @property
21 | def is_empty(self):
22 | return True
23 |
--------------------------------------------------------------------------------
/src/keystore/javacard/applets/secureapplet.py:
--------------------------------------------------------------------------------
1 | from .applet import Applet, ISOException, AppletException
2 | from .securechannel import SecureChannel, SecureError
3 | import hashlib
4 |
5 |
6 | def encode(data):
7 | return bytes([len(data)]) + data
8 |
9 |
10 | class SecureApplet(Applet):
11 | SECURE_RANDOM = b"\x01\x00"
12 | PIN_STATUS = b"\x03\x00"
13 | UNLOCK = b"\x03\x01"
14 | LOCK = b"\x03\x02"
15 | CHANGE_PIN = b"\x03\x03"
16 | SET_PIN = b"\x03\x04"
17 | ECHO = b"\x00\x00"
18 | # PIN status codes
19 | PIN_UNSET = 0
20 | PIN_LOCKED = 1
21 | PIN_UNLOCKED = 2
22 | PIN_BRICKED = 3
23 |
24 | def __init__(self, connection, aid):
25 | super().__init__(connection, aid)
26 | # secure channel
27 | self.sc = SecureChannel(self)
28 | self._pin_attempts_left = None
29 | self._pin_attempts_max = None
30 | self._pin_status = None
31 |
32 | @property
33 | def card_pubkey(self):
34 | """Public key of the card,
35 | in secp256k1 representation"""
36 | return self.sc.card_pubkey
37 |
38 | def open_secure_channel(self):
39 | self.sc.open()
40 |
41 | def close_secure_channel(self):
42 | self.sc.close()
43 |
44 | @property
45 | def is_secure_channel_open(self):
46 | return self.sc.is_open
47 |
48 | def get_pin_status(self):
49 | status = self.sc.request(self.PIN_STATUS)
50 | (self._pin_attempts_left, self._pin_attempts_max, self._pin_status) = list(
51 | status
52 | )
53 | return tuple(status)
54 |
55 | def get_random(self):
56 | return self.sc.request(self.SECURE_RANDOM)
57 |
58 | @property
59 | def is_pin_set(self):
60 | if self._pin_status is None:
61 | self.get_pin_status()
62 | return self._pin_status > 0
63 |
64 | @property
65 | def pin_attempts_left(self):
66 | if self._pin_status is None:
67 | self.get_pin_status()
68 | return self._pin_attempts_left
69 |
70 | @property
71 | def pin_attempts_max(self):
72 | if self._pin_status is None:
73 | self.get_pin_status()
74 | return self._pin_attempts_max
75 |
76 | @property
77 | def is_locked(self):
78 | if self._pin_status is None:
79 | self.get_pin_status()
80 | return self._pin_status in [self.PIN_LOCKED, self.PIN_BRICKED]
81 |
82 | def set_pin(self, pin):
83 | if self.is_pin_set:
84 | raise AppletException("PIN is already set")
85 | # we always set sha256(pin) so it's constant length
86 | h = hashlib.sha256(pin.encode()).digest()
87 | self.sc.request(self.SET_PIN + h)
88 | # update status
89 | self.get_pin_status()
90 |
91 | def change_pin(self, old_pin, new_pin):
92 | if not self.is_pin_set:
93 | raise AppletException("PIN is not set")
94 | if self.is_locked:
95 | raise AppletException("Unlock the card first")
96 | h1 = hashlib.sha256(old_pin.encode()).digest()
97 | h2 = hashlib.sha256(new_pin.encode()).digest()
98 | self.sc.request(self.CHANGE_PIN + encode(h1) + encode(h2))
99 | # update status
100 | self.get_pin_status()
101 |
102 | def ping(self):
103 | assert self.sc.request(self.ECHO + b"ping") == b"ping"
104 |
105 | def unlock(self, pin):
106 | if not self.is_locked:
107 | return
108 | try:
109 | # we always set sha256(pin) so it's constant length
110 | h = hashlib.sha256(pin.encode()).digest()
111 | self.sc.request(self.UNLOCK + h)
112 | finally:
113 | # update status
114 | self.get_pin_status()
115 |
116 | def lock(self):
117 | self.sc.request(self.LOCK)
118 | # update status
119 | self.get_pin_status()
120 |
--------------------------------------------------------------------------------
/src/keystore/javacard/util.py:
--------------------------------------------------------------------------------
1 | """
2 | Simple helper functions to get reader and connection
3 | """
4 | import uscard as sc
5 | from pyb import Pin
6 |
7 | reader = None
8 | conn = None
9 |
10 |
11 | def get_reader():
12 | global reader
13 | if reader is not None:
14 | return reader
15 | reader = sc.Reader(
16 | name="Specter card reader",
17 | ifaceId=2,
18 | ioPin=Pin.cpu.A2,
19 | clkPin=Pin.cpu.A4,
20 | rstPin=Pin.cpu.G10,
21 | presPin=Pin.cpu.C2,
22 | pwrPin=Pin.cpu.C5,
23 | )
24 | return reader
25 |
26 |
27 | def get_connection():
28 | global conn
29 | if conn is not None:
30 | return conn
31 | reader = get_reader()
32 | conn = reader.createConnection()
33 | return conn
34 |
35 |
36 | def encode(data):
37 | return bytes([len(data)]) + data
38 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from specter import Specter
3 | from gui.specter import SpecterGUI
4 |
5 | from keystore.core import KeyStore
6 | from keystore.sdcard import SDKeyStore
7 | from keystore.memorycard import MemoryCard
8 |
9 | from hosts import SDHost, QRHost, USBHost, Host
10 | import platform
11 | from helpers import load_apps
12 | from app import BaseApp
13 | import display
14 |
15 | def main(apps=None, network="main", keystore_cls=None):
16 | """
17 | apps: list of apps to load
18 | network: default network to operate
19 | keystores: list of KeyStore classes that can be used
20 | """
21 | # Init display first as it also inits the SDRAM
22 | display.init(False)
23 | # create virtual file system /sdram
24 | # for temp untrusted data storage
25 | rampath = platform.mount_sdram()
26 |
27 | # set working path to empty folder in sdram
28 | if not platform.simulator:
29 | cwd = rampath+"/cwd"
30 | platform.maybe_mkdir(cwd)
31 | os.chdir(cwd)
32 |
33 | # define hosts - USB, QR, SDCard
34 | # each hosts gets it's own RAM folder for data
35 | Host.SETTINGS_DIR = platform.fpath("/qspi/hosts")
36 | Specter.SETTINGS_DIR = platform.fpath("/qspi/global")
37 | hosts = [
38 | USBHost(rampath + "/usb"),
39 | QRHost(rampath + "/qr"),
40 | SDHost(rampath+"/sd"),
41 | ]
42 | # temp storage in RAM for host commands processing
43 | BaseApp.TEMPDIR = rampath+"/tmp"
44 |
45 | # define GUI
46 | if not platform.simulator:
47 | gui = SpecterGUI()
48 | else:
49 | # this GUI can simulate user actions for automated testing
50 | from gui.tcp_gui import TCPGUI
51 | gui = TCPGUI()
52 |
53 | # inject the folder where keystore stores it's data
54 | KeyStore.path = platform.fpath("/flash/keystore")
55 | # detect keystore to use
56 | if keystore_cls is not None:
57 | keystores = [keystore_cls]
58 | else:
59 | keystores = [
60 | MemoryCard,
61 | SDKeyStore,
62 | ]
63 |
64 | # loading apps
65 | if apps is None:
66 | apps = load_apps()
67 |
68 | # make Specter instance
69 | settings_path = platform.fpath("/flash")
70 | specter = Specter(
71 | gui=gui,
72 | keystores=keystores,
73 | hosts=hosts,
74 | apps=apps,
75 | settings_path=settings_path,
76 | network=network,
77 | )
78 | specter.start()
79 |
80 |
81 | if __name__ == "__main__":
82 | main()
83 |
--------------------------------------------------------------------------------
/src/qrencoder.py:
--------------------------------------------------------------------------------
1 | import math
2 | from microur.util.bytewords import stream_pos
3 | from microur.encoder import UREncoder
4 | from bcur import bcur_encode_stream
5 | from helpers import b2a_base64_stream, read_write
6 |
7 | class QREncoder:
8 | """A simple encoder that just splits the data into chunks"""
9 | is_infinite = False
10 | MAX_PREFIX_LEN = 0
11 |
12 | def __init__(self, stream, part_len=300, tempfile=None):
13 | if tempfile is None:
14 | raise ValueError("Temp file is required for this encoder")
15 | with open(tempfile, "wb") as fout:
16 | self._start, self._len = self.convert(stream, fout)
17 | self.tempfile = tempfile
18 | self.f = None
19 | self._start = 0
20 | self._num = 0
21 | self.part_len = part_len
22 |
23 | def convert(self, fin, fout):
24 | # dummy convertion, just copy to the tempfile
25 | return 0, read_write(fin, fout)
26 |
27 | @property
28 | def part_len(self):
29 | return self._part_len
30 |
31 | @part_len.setter
32 | def part_len(self, part_len):
33 | if part_len > 2 * self.MAX_PREFIX_LEN:
34 | part_len -= self.MAX_PREFIX_LEN
35 | self._part_len = math.ceil(self._len / math.ceil(self._len / part_len))
36 |
37 | def get_full(self, maxlen=None):
38 | if maxlen is not None and maxlen < self._len:
39 | return ""
40 | self.f.seek(0, 0)
41 | return self.f.read()
42 |
43 | def __len__(self):
44 | return math.ceil(self._len / self.part_len)
45 |
46 | def __getitem__(self, idx):
47 | idx = idx % len(self)
48 | self.f.seek(self._start + idx*self.part_len, 0)
49 | return self.f.read(self.part_len)
50 |
51 | def __iter__(self):
52 | self._num = 0
53 | return self
54 |
55 | def __next__(self):
56 | if self._num >= len(self):
57 | raise StopIteration
58 | self._num += 1
59 | return self.__getitem__(self._num-1)
60 |
61 | def __enter__(self):
62 | if self.f is None:
63 | self.f = open(self.tempfile, "r")
64 | return self
65 |
66 | def __exit__(self, exc_type, exc_value, exc_tb):
67 | if self.f is not None:
68 | self.f.close()
69 |
70 | def __str__(self):
71 | return self.get_full()
72 |
73 | class Base64QREncoder(QREncoder):
74 | MAX_PREFIX_LEN = 8
75 |
76 | def convert(self, fin ,fout):
77 | return 0, b2a_base64_stream(fin, fout)
78 |
79 | def __getitem__(self, idx):
80 | idx = idx % len(self)
81 | self.f.seek(self._start + idx*self.part_len, 0)
82 | return "p%dof%d %s" % (idx+1, len(self), self.f.read(self.part_len))
83 |
84 | class LegacyBCUREncoder(QREncoder):
85 | MAX_PREFIX_LEN = 73 # uh... large one, and pretty useless
86 |
87 | def convert(self, fin ,fout):
88 | cur, sz = stream_pos(fin)
89 | sz, enc_hash = bcur_encode_stream(fin, fout, size=sz)
90 | self.enc_hash = enc_hash.decode()
91 | return 0, sz
92 |
93 | def get_full(self, maxlen=None):
94 | if maxlen is not None and maxlen < self._len+9:
95 | return ""
96 | self.f.seek(0, 0)
97 | return "UR:BYTES/" + self.f.read()
98 |
99 | def __getitem__(self, idx):
100 | if len(self) == 1:
101 | return "UR:BYTES/" + self.f.read()
102 | idx = idx % len(self)
103 | self.f.seek(self._start + idx*self.part_len, 0)
104 | return "UR:BYTES/%dOF%d/%s/%s" % (idx+1, len(self), self.enc_hash, self.f.read(self.part_len))
105 |
106 | class CryptoPSBTEncoder(QREncoder):
107 | is_infinite = True
108 | MAX_PREFIX_LEN = 22
109 |
110 | def __init__(self, *args, **kwargs):
111 | self.encoder = None
112 | super().__init__(*args, **kwargs)
113 |
114 | def __len__(self):
115 | return self.encoder.seq_len
116 |
117 | def get_full(self, maxlen=None):
118 | return "" # we don't support full here yet
119 |
120 | def __enter__(self):
121 | super().__enter__()
122 | self.encoder = UREncoder(UREncoder.CRYPTO_PSBT, self.f, self._part_len)
123 | return self
124 |
125 | def __exit__(self, *args, **kwargs):
126 | del self.encoder
127 | super().__exit__(*args, **kwargs)
128 |
129 | def __getitem__(self, idx):
130 | return self.encoder.get_part(idx)
131 |
132 | @property
133 | def part_len(self):
134 | if self.encoder:
135 | return self.encoder.part_len
136 | else:
137 | return self._part_len
138 |
139 | @part_len.setter
140 | def part_len(self, part_len):
141 | if part_len > 2 * self.MAX_PREFIX_LEN:
142 | part_len -= self.MAX_PREFIX_LEN
143 | if self.encoder:
144 | self.encoder.part_len = part_len
145 | self._part_len = self.encoder.part_len
146 | else:
147 | self._part_len = math.ceil(self._len / math.ceil(self._len / part_len))
148 |
--------------------------------------------------------------------------------
/src/rng.py:
--------------------------------------------------------------------------------
1 | # random number generator
2 | # if os.urandom is available - entropy goes from hardware TRNG
3 | # in simulator just use /dev/urandom
4 | import hashlib
5 |
6 | entropy_pool = b"7" * 64
7 |
8 | try:
9 | from os import urandom as get_trng_bytes
10 | except:
11 |
12 | def get_trng_bytes(nbytes):
13 | with open("/dev/urandom", "rb") as f:
14 | return f.read(nbytes)
15 |
16 |
17 | # assuming that entropy_pool has some real entropy
18 | # we can generate bytes using it as well
19 | # probably not the best way at the moment,
20 | # but anything is better than nothing
21 |
22 |
23 | def get_random_bytes(nbytes):
24 | global entropy_pool
25 | d = get_trng_bytes(nbytes)
26 | feed(d) # why not?
27 | # if more than 64 - just do trng
28 | if nbytes > 64:
29 | return d
30 | else:
31 | h = hashlib.sha512(entropy_pool)
32 | h.update(d)
33 | return h.digest()[:nbytes]
34 |
35 |
36 | # we hash together entropy pool and data we got
37 |
38 |
39 | def feed(data):
40 | global entropy_pool
41 | h = hashlib.sha512(entropy_pool)
42 | h.update(data)
43 | entropy_pool = h.digest()
44 |
--------------------------------------------------------------------------------
/test/integration/README.md:
--------------------------------------------------------------------------------
1 | # Integration tests
2 |
3 | These tests should run against Bitcoin Core (regtest mode).
4 |
5 | To run launch with python3 from this folder (you should have `micropython_unix` built in the `../../bin/` folder):
6 |
7 | ```
8 | python3 run_tests.py
9 | ```
10 |
--------------------------------------------------------------------------------
/test/integration/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | embit
3 |
--------------------------------------------------------------------------------
/test/integration/run_tests.py:
--------------------------------------------------------------------------------
1 | # this should run with python3
2 | import sys
3 | if sys.implementation.name == 'micropython':
4 | print("This file should run with python3, not micropython!")
5 | sys.exit(1)
6 | from util.controller import sim, core
7 | import unittest
8 |
9 | def main():
10 | # core.start() # start Bitcoin Core on regtest
11 | sim.start() # start simulator
12 | try:
13 | sim.load() # unlock, load mnemonic etc
14 | unittest.main('tests')
15 | finally:
16 | # core.shutdown()
17 | sim.shutdown()
18 |
19 | if __name__ == '__main__':
20 | main()
21 |
--------------------------------------------------------------------------------
/test/integration/simulator.py:
--------------------------------------------------------------------------------
1 | # This is a micropython file to start Specter Simulator
2 | import sys
3 | if sys.implementation.name != "micropython":
4 | print("This file should run from micropython!")
5 | sys.exit(1)
6 | sys.path.append('../../src')
7 | sys.path.append('../../f469-disco/libs/common')
8 | sys.path.append('../../f469-disco/libs/unix')
9 | sys.path.append('../../f469-disco/usermods/udisplay_f469/display_unixport')
10 |
11 | # make sure USB is enabled
12 | from specter import Specter
13 | Specter.usb = True
14 |
15 | import main
16 | # run on the regtest
17 | main.main(network="regtest")
--------------------------------------------------------------------------------
/test/integration/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .test_basic import *
2 | from .test_with_rpc import *
--------------------------------------------------------------------------------
/test/integration/tests/test_basic.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from util.controller import sim
3 | from embit.psbt import PSBT
4 |
5 | class BasicTest(TestCase):
6 |
7 | def test_sign_psbt(self):
8 | unsigned = b"cHNidP8BAHECAAAAAQWaIPxj7qSA0cbaKKz5Lk43V/8/FZeQw+IQ2tV6eJnbAAAAAAD9////AgfVTQUAAAAAFgAUUzfXvW1SC/+493dPMkR+9Ua1+7mAlpgAAAAAABYAFCwSoUTerJLG437IpfbWF8DgWx6kAAAAAAABAR8Kl+YFAAAAABYAFC80qhzwClOwVaKRoDp9RfCmmItSIgYDXUnszVTQCZ5DZ2J3x6bUYl1hHaiKXfSb+VF6d5Gnd6UYc8XaClQAAIABAACAAAAAgAEAAAAAAAAAACICAzra7/AYOHv1KHXP0Kgv8paA8ELhUBDLW3FrKXZzZpg2GHPF2gpUAACAAQAAgAAAAIABAAAAAgAAAAAA"
9 | signed = b"cHNidP8BAHECAAAAAQWaIPxj7qSA0cbaKKz5Lk43V/8/FZeQw+IQ2tV6eJnbAAAAAAD9////AgfVTQUAAAAAFgAUUzfXvW1SC/+493dPMkR+9Ua1+7mAlpgAAAAAABYAFCwSoUTerJLG437IpfbWF8DgWx6kAAAAAAAiAgNdSezNVNAJnkNnYnfHptRiXWEdqIpd9Jv5UXp3kad3pUcwRAIgDf80duROzcio5iPQ/RbThlXHzr2tmqFaIHR1SOMHHT8CIDGUwTINLkmIk6onOGtlFSQYibQfjhIkRxmx1LJa0NNGAQAAAA=="
10 | # confirm signing
11 | res = sim.query(b"sign "+unsigned, [True])
12 | # signed tx
13 | self.assertEqual(
14 | [inp.partial_sigs for inp in PSBT.from_string(res.decode()).inputs],
15 | [inp.partial_sigs for inp in PSBT.from_string(signed.decode()).inputs]
16 | )
17 |
18 | # cancel signing
19 | res = sim.query(b"sign "+unsigned, [False])
20 | self.assertEqual(res, b"error: User cancelled")
21 |
22 | def test_get_xpub(self):
23 | res = sim.query(b"fingerprint")
24 | self.assertEqual(res, b"73c5da0a")
25 | res = sim.query(b"xpub m/44h/1h/0h")
26 | self.assertEqual(res, b"tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba")
27 |
28 | def test_add_wallet(self):
29 | # and(pk(A),after(100)) -> and_v(v:pk(A),after(100))
30 | desc = "wsh(and_v(v:pk([73c5da0a/44h/1h/0h]tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba),after(100)))"
31 | inv_desc = "wsh(and_v(v:pk([73c5da0a/44h/1h/0h]xpub6BhcvYN2qwQKRviMKKBTfRcK1RmCTmM7JHsg67r3rwvymhUEt8gPHhnkugQaQ7UN8M5FfhEUfyVuSaK5fQzfUpvAcCxN4bAT9jyySbPGsTs),after(100)))"
32 | addresses = [
33 | b"bitcoin:bcrt1qd7mtkvjmm7rlpgjjfv3902h6c749d7xuss0pr6garuq8q2qu9xas5qs39e?index=0",
34 | b"bitcoin:bcrt1qyrmdmuy0ml7m7fw3834lek6anrx20nrlmh7yvhsulu0dhxvgvkds4n9dq5?index=2"
35 | ]
36 |
37 | # shouldn't find addresses before adding a wallet
38 | for addr in addresses:
39 | res = sim.query(addr)
40 | self.assertTrue(b"error: Can't find wallet owning address" in res)
41 |
42 | res = sim.query(f"addwallet timelocked&{inv_desc})".encode())
43 | self.assertTrue(b"error" in res)
44 |
45 | res = sim.query(f"addwallet timelocked&{desc}".encode(), [True])
46 |
47 | # should find addresses after adding a wallet
48 | for addr in addresses:
49 | res = sim.query(addr,[None])
50 | self.assertFalse(b"error: Can't find wallet owning address" in res)
51 |
52 |
--------------------------------------------------------------------------------
/test/integration/util/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cryptoadvance/specter-diy/24d1137ab9c1a080116bec702e0af4e5b618533e/test/integration/util/__init__.py
--------------------------------------------------------------------------------
/test/integration/util/controller.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import json
3 | import time
4 | import socket
5 | import subprocess
6 | import os
7 | import signal
8 |
9 | class TCPSocket:
10 | def __init__(self, port=8787):
11 | self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
12 | self.s.connect(("127.0.0.1", port))
13 |
14 | def readline(self, eol=b"\r\n", timeout=3):
15 | res = b""
16 | t0 = time.time()
17 | while not (eol in res):
18 | # timeout
19 | if time.time()-t0 > timeout:
20 | break
21 | try:
22 | raw = self.s.recv(1000)
23 | res += raw
24 | except Exception as e:
25 | print(e)
26 | time.sleep(0.01)
27 | return res
28 |
29 | def send(self, cmd):
30 | cmd = json.dumps([cmd])[1:-1]
31 | self.s.send(str(cmd).encode()+b"\r\n")
32 | res = self.readline()
33 |
34 | def query(self, data):
35 | self.s.send(data)
36 | return self.readline(eol=b"ACK\r\n")
37 |
38 | def receive(self):
39 | return self.readline()
40 |
41 | class SimController:
42 | def __init__(self):
43 | self.started = False
44 | self.gui = None
45 | self.usb = None
46 | try:
47 | shutil.rmtree("./fs/")
48 | except:
49 | pass
50 |
51 | def start(self):
52 | print("Starting up...")
53 | self.proc = subprocess.Popen("../../bin/micropython_unix simulator.py",
54 | stdout=subprocess.PIPE,
55 | shell=True, preexec_fn=os.setsid)
56 | time.sleep(1)
57 |
58 | def load(self):
59 | # command socket
60 | self.gui = TCPSocket(8787)
61 | # select PIN
62 | self.gui.send("")
63 | # confirm PIN
64 | self.gui.send("")
65 | # enter recovery phrase
66 | self.gui.send(1)
67 | self.gui.send("abandon "*11+"about")
68 | # now we can open usb communication
69 | time.sleep(1)
70 | self.usb = TCPSocket(8789)
71 |
72 | def shutdown(self):
73 | print("Shutting down...")
74 | if self.gui is not None:
75 | try:
76 | self.gui.s.send(b"quit\r\n")
77 | time.sleep(0.3)
78 | except:
79 | pass
80 | os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM) # Send the signal to all the process groups
81 | time.sleep(1)
82 |
83 | def query(self, data, commands=[]):
84 | if isinstance(data, str):
85 | data = data.encode()
86 | if data[-1:] not in b"\r\n":
87 | data = data + b"\r\n"
88 | res = self.usb.query(data)
89 | assert res == b"ACK\r\n"
90 | # if we need to confirm anything
91 | for command in commands:
92 | sim.gui.send(command)
93 | time.sleep(0.3)
94 | res = sim.usb.receive()
95 | return res.strip()
96 |
97 | class BitcoinCore:
98 | datadir = "./testdir/"
99 | def __init__(self):
100 | pass
101 |
102 | def start(self):
103 | print("starting Bitcon Core in regtest mode with datadir %s" % self.datadir)
104 | try:
105 | shutil.rmtree(self.datadir)
106 | except:
107 | pass
108 | try:
109 | os.mkdir(self.datadir)
110 | except:
111 | pass
112 | self.proc = subprocess.Popen("bitcoind -datadir=%s -regtest -fallbackfee=0.0002 -rpcuser=bitcoin -rpcpassword=secret -rpcport=18778 -port=18779" % self.datadir,
113 | stdout=subprocess.PIPE,
114 | shell=True, preexec_fn=os.setsid)
115 | time.sleep(1)
116 |
117 | def shutdown(self):
118 | print("shutting down Bitcon Core")
119 | os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM) # Send the signal to all the process groups
120 | time.sleep(3)
121 | try:
122 | shutil.rmtree(self.datadir)
123 | except:
124 | pass
125 |
126 | sim = SimController()
127 | core = BitcoinCore()
--------------------------------------------------------------------------------
/test/integration/util/misc.py:
--------------------------------------------------------------------------------
1 | from embit.descriptor import Descriptor
2 | from embit.networks import NETWORKS
3 | from .rpc import prepare_rpc
4 |
5 | def create_wallet(wname, d1: str, d2: str, rpc=None):
6 | if rpc is None:
7 | rpc = prepare_rpc()
8 | wdefault = rpc.wallet("")
9 | # to derive addresses
10 | desc1 = Descriptor.from_string(d1)
11 |
12 | # recv addr
13 | addr = desc1.derive(0).address(NETWORKS['regtest'])
14 |
15 | # to add checksums
16 | d1 = rpc.getdescriptorinfo(d1)["descriptor"]
17 | d2 = rpc.getdescriptorinfo(d2)["descriptor"]
18 | rpc.createwallet(wname, True, True)
19 | w = rpc.wallet(wname)
20 | info = w.getwalletinfo()
21 | # bitcoin core uses descriptor wallets by default so importmulti may fail
22 | use_descriptors = info.get("descriptors", False)
23 | if not use_descriptors:
24 | res = w.importmulti([{
25 | "desc": d1,
26 | "internal": False,
27 | "timestamp": "now",
28 | "watchonly": True,
29 | "range": 10,
30 | },{
31 | "desc": d2,
32 | "internal": True,
33 | "timestamp": "now",
34 | "watchonly": True,
35 | "range": 10,
36 | }],{"rescan": False})
37 | else:
38 | res = w.importdescriptors([{
39 | "desc": d1,
40 | "internal": False,
41 | "timestamp": "now",
42 | "watchonly": True,
43 | "active": True,
44 | },{
45 | "desc": d2,
46 | "internal": True,
47 | "timestamp": "now",
48 | "watchonly": True,
49 | "active": True,
50 | }])
51 | assert all([k["success"] for k in res])
52 | wdefault.sendtoaddress(addr, 0.1)
53 | rpc.mine()
54 | return w
55 |
--------------------------------------------------------------------------------
/test/run_tests.py:
--------------------------------------------------------------------------------
1 | import sys
2 | sys.path.append('../src')
3 | sys.path.append('../f469-disco/libs/common')
4 | sys.path.append('../f469-disco/libs/unix')
5 | sys.path.append('../f469-disco/usermods/udisplay_f469/display_unixport')
6 | sys.path.append('../f469-disco/tests')
7 |
8 | import unittest
9 | from tests import util
10 |
11 | util.clear_testdir()
12 | unittest.main('tests')
13 |
--------------------------------------------------------------------------------
/test/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .test_keystore import *
2 | from .test_wallets import *
3 | from .test_sign import *
4 | from .test_revault import *
5 | from .test_compatibility import *
--------------------------------------------------------------------------------
/test/tests/test_compatibility.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from apps.compatibility import parse_software_wallet_json, parse_cc_wallet_txt
3 | import json
4 | from io import BytesIO
5 |
6 | WALLET_SOFTWARE = b'{"label": "blah", "blockheight": 0, "descriptor": "wsh(sortedmulti(1,[fb7c1f11/48h/1h/0h/2h]tpubDExnGppazLhZPNadP8Q5Vgee2QcvbyAf9GvGaEY7ALVJREaG2vdTqv1MHRoDtPaYP3y1DGVx7wrKKhsLhs26GY263uE6Wi3qNbi71AHZ6p7/0/*,[33a2bf0c/48h/1h/0h/2h]tpubDF4cAhFDn6XSPhQtFECSkQm35oEzVyHHAiPa4Qy83fBtPw9nFJAodN6xF6nY7y2xKMGc5nbDFZfAac88oaurVzrCUxyhmc9J8W5tg3N5NkS/0/*))#vk844svv", "devices": [{"type": "specter", "label": "ability"}, {"type": "coldcard", "label": "hox"}]}'
7 |
8 | COLDCARD_FILE = """
9 | # Coldcard Multisig setup file (created on Specter Desktop)
10 | #
11 | Name: blah
12 | Policy: 1 of 2
13 | Derivation: m/48'/1'/0'/2'
14 | Format: P2WSH
15 | FB7C1F11: tpubDExnGppazLhZPNadP8Q5Vgee2QcvbyAf9GvGaEY7ALVJREaG2vdTqv1MHRoDtPaYP3y1DGVx7wrKKhsLhs26GY263uE6Wi3qNbi71AHZ6p7
16 | 33A2BF0C: tpubDF4cAhFDn6XSPhQtFECSkQm35oEzVyHHAiPa4Qy83fBtPw9nFJAodN6xF6nY7y2xKMGc5nbDFZfAac88oaurVzrCUxyhmc9J8W5tg3N5NkS
17 | """
18 |
19 | EXPECTED = ('blah', 'wsh(sortedmulti(1,[fb7c1f11/48h/1h/0h/2h]tpubDExnGppazLhZPNadP8Q5Vgee2QcvbyAf9GvGaEY7ALVJREaG2vdTqv1MHRoDtPaYP3y1DGVx7wrKKhsLhs26GY263uE6Wi3qNbi71AHZ6p7/{0,1}/*,[33a2bf0c/48h/1h/0h/2h]tpubDF4cAhFDn6XSPhQtFECSkQm35oEzVyHHAiPa4Qy83fBtPw9nFJAodN6xF6nY7y2xKMGc5nbDFZfAac88oaurVzrCUxyhmc9J8W5tg3N5NkS/{0,1}/*))')
20 |
21 | class CompatibilityTest(TestCase):
22 |
23 | def test_import(self):
24 | self.assertEqual(EXPECTED, parse_software_wallet_json(json.load(BytesIO(WALLET_SOFTWARE))))
25 | self.assertEqual(EXPECTED, parse_cc_wallet_txt(BytesIO(COLDCARD_FILE.encode())))
26 |
--------------------------------------------------------------------------------
/test/tests/test_keystore.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from keystore import FlashKeyStore
3 | import os
4 | import platform
5 |
6 | TEST_DIR = "testdir"
7 |
8 | def init_keystore(ks):
9 | platform.maybe_mkdir(ks.path)
10 | ks.load_secret(ks.path)
11 | ks.load_state()
12 | ks.initialized = True
13 |
14 | class FlashKeyStoreTest(TestCase):
15 |
16 | def get_keystore(self):
17 | """Clean up the test folder and create fresh keystore"""
18 | try:
19 | platform.delete_recursively(TEST_DIR)
20 | os.rmdir(TEST_DIR)
21 | except:
22 | pass
23 | FlashKeyStore.path = TEST_DIR
24 | return FlashKeyStore()
25 |
26 | def test_create_config(self):
27 | """Test initial config creation"""
28 | ks = self.get_keystore()
29 | init_keystore(ks)
30 | files = [f[0] for f in os.ilistdir(TEST_DIR)]
31 | self.assertTrue("secret" in files)
32 | self.assertTrue("pin" in files)
33 | self.assertEqual(ks.is_pin_set, False)
34 | self.assertEqual(ks.pin_attempts_left, ks.pin_attempts_max)
35 | self.assertTrue(ks.pin_attempts_left is not None)
36 |
37 | def test_change_secret(self):
38 | """Test wipe exception if secret is changed"""
39 | # create keystore
40 | ks = self.get_keystore()
41 | init_keystore(ks)
42 | files = [f[0] for f in os.ilistdir(TEST_DIR)]
43 | self.assertTrue("secret" in files)
44 | self.assertTrue("pin" in files)
45 | # now change secret value
46 | with open(TEST_DIR+"/secret", "wb") as f:
47 | # a different value
48 | f.write(b"5"*32)
49 | ks = FlashKeyStore()
50 | # check it raises
51 | with self.assertRaises(platform.CriticalErrorWipeImmediately):
52 | init_keystore(ks)
53 | # files are deleted
54 | files = [f[0] for f in os.ilistdir(TEST_DIR)]
55 | self.assertFalse("secret" in files)
56 | self.assertFalse("pin" in files)
57 |
58 | def test_change_pin_file(self):
59 | """Test wipe exception if pin state changed"""
60 | # create keystore
61 | ks = self.get_keystore()
62 | init_keystore(ks)
63 | # load signed pin state
64 | with open(TEST_DIR+"/pin", "rb") as f:
65 | # a different value
66 | content = f.read()
67 | # set invalid value
68 | content = content[1:] + b"1"
69 | # write new state
70 | with open(TEST_DIR+"/pin", "wb") as f:
71 | # a different value
72 | f.write(content)
73 | ks = FlashKeyStore()
74 | # check it raises
75 | with self.assertRaises(platform.CriticalErrorWipeImmediately):
76 | init_keystore(ks)
77 | # files are deleted
78 | files = [f[0] for f in os.ilistdir(TEST_DIR)]
79 | self.assertFalse("secret" in files)
80 | self.assertFalse("pin" in files)
81 |
--------------------------------------------------------------------------------
/test/tests/test_wallets.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from apps.wallets.wallet import Wallet
3 | from embit.descriptor import Key
4 |
5 | TEST_DIR = "testdir"
6 |
7 | class WalletsTest(TestCase):
8 |
9 | def test_descriptors(self):
10 | """Test initial config creation"""
11 | k = "[8cce63f8/84h/1h/0h]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2/<0;1>/*"
12 | descriptors = [
13 | "wpkh(%s)" % k,
14 | "sh(wpkh(%s))" % k,
15 | "wsh(sortedmulti(2,%s,%s,%s))" % (k,k,k),
16 | "sh(wsh(sortedmulti(2,%s,%s,%s)))" % (k,k,k),
17 | "wsh(multi(3,%s,%s,%s))" % (k,k,k),
18 | "sh(wsh(multi(2,%s,%s,%s)))" % (k,k,k),
19 | ]
20 | for desc in descriptors:
21 | w = Wallet.parse(desc)
22 | self.assertEqual(str(w.descriptor), desc)
23 |
24 | def test_invalid_desc(self):
25 | """Test initial config creation"""
26 | k = "[8cce63f8/84h/1h/0h]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2"
27 | descriptors = [
28 | "wqkh(%s)" % k,
29 | "(wpkh(%s))" % k,
30 | "wsh(sortedmulti(2,%s,%s,%s)" % (k,k,k),
31 | "wsh(sortedmulti(2,%s,%s,%s)))" % (k,k,k),
32 | "wsh(multi(4,%s,%s,%s))" % (k,k,k),
33 | "sh(wsh(multi(0,%s,%s,%s)))" % (k,k,k),
34 | ]
35 | for desc in descriptors:
36 | with self.assertRaises(Exception):
37 | w = Wallet.parse(desc)
38 | print(w, desc)
39 |
40 | def test_key(self):
41 | keys = [
42 | "[8cce63f8/84h/1h/0h]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
43 | "[8cce63f8]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
44 | "tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
45 | ]
46 | for k in keys:
47 | Key.parse(k)
48 |
49 | def test_invalid_keys(self):
50 | keys = [
51 | "[8c!e63f8/84h/1h/0h]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
52 | "[84h/1h/0h]tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
53 | "tpubDCZWXJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMXni2",
54 | "tpubDCZWxJ6kKqRHep5a2XycxrXRaTES1vs3ysfV7sdv5uhkaEgxBEdVbyQT46m3NcaLJqVNd41TYqDyQfvweLLXGmkxdHRnhxuJPf7BAWMX",
55 | ]
56 | for k in keys:
57 | with self.assertRaises(Exception):
58 | Key.parse(k)
59 | print(k)
60 |
61 | def test_taptree(self):
62 | d = "tr([73c5da0a/2/2/2]tpubDCPwGho2toLmdSELZ3o8v1D6RUUK7Y5keCjMyrSfE75aX2Mcx4MNEM6MnXDZR87GQ1ot4YNn2GGtiN5SvM12c6cvYMrt6avwtYNcRab2HFv/<0;1>/*,or_b(pk([73c5da0a/1/2/3]tpubDCpEkdSHkygNaquCRtW8Fuo3TchAXFSWUuYB9aryim58T4CWM9vLgt26uUV5wdtuvbSk7rWmQQCpcYhGjbHiBzWCYXeyRMJ98zSBWekaJJm/<0;1>/*),s:pk([73c5da0a/3/2/1]tpubDDrLDbxjL1d5FK8djVqUjD3xL1gkhaTXTL1rHzEavwA2ss4YpF8Qm82cKN89PEBRYk6JVTZULA872LuFGENTGdNYASDCrXKKZkU86A8HLqA/<0;1>/*)))"
63 | w = Wallet.parse(d)
64 | print(w)
65 |
--------------------------------------------------------------------------------
/test/tests/util.py:
--------------------------------------------------------------------------------
1 | from keystore.ram import RAMKeyStore
2 | from app import BaseApp
3 | from apps.wallets import App as WalletsApp
4 | import platform
5 |
6 | TEST_DIR = "testdir"
7 |
8 | def check_sigs(psbt1, psbt2):
9 | return [inp.partial_sigs for inp in psbt1.inputs] == [inp.partial_sigs for inp in psbt2.inputs]
10 |
11 | def clear_testdir():
12 | try:
13 | platform.delete_recursively(TEST_DIR, include_self=True)
14 | except:
15 | pass
16 |
17 | def show_loader(*args, **kwargs):
18 | """Dummy show_loader function that does nothing"""
19 | pass
20 |
21 | async def show(*args, **kwargs):
22 | """Dummy show function that always cancels (returns None)"""
23 | return None
24 |
25 | async def communicate(*args, **kwargs):
26 | """Dummy cross-app comunicate function that always cancels"""
27 | return None
28 |
29 | def get_keystore(mnemonic="ability "*11+"acid", password=""):
30 | """Returns a dummy keystore"""
31 | platform.maybe_mkdir(TEST_DIR)
32 | platform.maybe_mkdir(TEST_DIR+"/keystore")
33 | ks = RAMKeyStore()
34 | ks.path = TEST_DIR+"/keystore"
35 | ks.show_loader = show_loader
36 | ks.show = show
37 | ks.load_secret(ks.path)
38 | ks.initialized = True
39 | ks._unlock("1234")
40 | ks.set_mnemonic(mnemonic, password)
41 | return ks
42 |
43 | def get_wallets_app(keystore, network):
44 | platform.maybe_mkdir(TEST_DIR)
45 | platform.maybe_mkdir(TEST_DIR+"/wallets")
46 | platform.maybe_mkdir(TEST_DIR+"/tmp")
47 | BaseApp.tempdir = TEST_DIR+"/tmp"
48 | wapp = WalletsApp(TEST_DIR+"/wallets")
49 | wapp.init(keystore, network, show_loader, communicate)
50 | return wapp
51 |
--------------------------------------------------------------------------------
/udev/49-micropython.rules:
--------------------------------------------------------------------------------
1 | # f055:9800 - MicroPython board
2 | ATTRS{idVendor}=="f055", ATTRS{idProduct}=="9800|9801|9802", ENV{ID_MM_DEVICE_IGNORE}="1"
3 | ATTRS{idVendor}=="f055", ATTRS{idProduct}=="9800|9801|9802", ENV{MTP_NO_PROBE}="1"
4 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="f055", ATTRS{idProduct}=="9800|9801|9802", MODE:="0666"
5 | KERNEL=="ttyACM*", ATTRS{idVendor}=="f055", ATTRS{idProduct}=="9800|9801|9802", MODE:="0666"
6 |
--------------------------------------------------------------------------------
/udev/README.md:
--------------------------------------------------------------------------------
1 | # udev rules
2 |
3 | This directory contains udev rules for micropython & Specter DIY.
4 | These are necessary for the devices to be reachable on linux environments.
5 |
6 | - `49-micropython.rules` (Specter-DIY): http://wiki.micropython.org/Installation#USB-Permissioning-on-Linux
7 |
8 | Specter is connected as a virtual serial port, so you need to add yourself to `dialout` group.
9 |
10 | # Usage
11 |
12 | Apply these rules by copying them to `/etc/udev/rules.d/` and notifying `udevadm`.
13 | Your user will need to be added to the `plugdev` group, which needs to be created if it does not already exist.
14 |
15 | ```
16 | $ sudo cp 49-micropython.rules /etc/udev/rules.d/
17 | $ sudo udevadm trigger
18 | $ sudo udevadm control --reload-rules
19 | $ sudo groupadd plugdev
20 | $ sudo usermod -aG plugdev `whoami`
21 | $ sudo usermod -aG dialout `whoami`
22 | ```
--------------------------------------------------------------------------------