├── .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 | ![](./docs/pictures/kit.jpg) 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 | ![](./docs/pictures/wallet_screens.jpg) 67 | 68 | ### Key generation and recovery 69 | 70 | ![](./docs/pictures/init_screens.jpg) 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 | ![](./pictures/waveshare_wiring.jpg) 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 | ![](../pictures/gallery/snap-case-double-2.jpg) 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 | ![](../pictures/gallery/barebones_v2.png) 30 | 31 | ## Full metal DIY 32 | 33 | ![](../pictures/gallery/davewhiiite.jpg) 34 | 35 | https://github.com/davewhiiite/wraith 36 | 37 | ## Enclosure by Thomas 38 | 39 | ![](../pictures/gallery/thomas.jpg) 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 | ![](../../pictures/gallery/snap-case-bronze-black-1.jpg) 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 | ![](../../pictures/gallery/snap-case-printed-parts-1.jpg) 18 | 19 | - The SDL files are attached to this site. 20 | 21 | ### Remove all screws from the scanner 22 | 23 | ![](../../pictures/gallery/snap-case-remove-screws-from-scanner-1.jpg) 24 | 25 | ### Mount the scanner module 26 | 27 | ![](../../pictures/gallery/snap-case-mount-scanner-1.jpg) 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 | ![](../../pictures/gallery/snap-case-connect-scanner-1.jpg) 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 | ![](../../pictures/gallery/snap-case-plug-together-1.jpg) 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 | ![](./snap-case-double-1.jpg) 10 | 11 | Created with a Prusa MK3s using the template from [enclosures/snapcase](../../enclosures/snapcase). 12 | 13 | ### @davewhiiite 14 | 15 | ![davewhiiite](./davewhiiite.jpg) 16 | 17 | ### @seedsigner 18 | 19 | ![seedsigner](./barebones_v2.png) 20 | 21 | ### @lunaticoin 22 | 23 | ![lunaticoin](./lunaticoin.jpg) 24 | 25 | ### @Thomas1378 26 | 27 | ![thomas](./thomas.jpg) 28 | 29 | ### @bitcoinheiro 30 | 31 | ![bitcoinhero](./bitcoinhero.jpg) 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 | ![kkdao](./kkdao.jpg) 38 | 39 | ### @dimaatmelodromru 40 | 41 | ![dimaatmelodromru](./dimaatmelodromru.jpg) 42 | 43 | ### @k9ert 44 | 45 | ![k9ert](./k9ert.jpg) 46 | 47 | ### @gorazdko 48 | 49 | ![gorazdko](./gorazdko.jpg) 50 | 51 | ### @bavarianledger 52 | 53 | ![bavarianledger](./bavarianledger.jpg) 54 | 55 | ### @kdmukai 56 | 57 | ![kdmukai](./kdmukai.jpg) 58 | 59 | ### @stepansnigirev 60 | 61 | ![stepansnigirev](./stepansnigirev.jpg) 62 | 63 | ### @davewhiiite 64 | 65 | ![davewhiiite](./davewhiiite1.jpg) 66 | ![davewhiiite](./davewhiiite2.jpg) 67 | ![davewhiiite](./davewhiiite3.jpg) 68 | 69 | ### Re-use the original packaging of the STM32F469I-DISCO @henrik 70 | ![henrik](./org_packaging_signer_1.jpg) 71 | ![henrik](./org_packaging_signer_2.jpg) 72 | ![henrik](./org_packaging_signer_3.jpg) 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 | ![Print 1](./3d_case/Specter_Print_1.jpg) 14 | 15 | ![Print 2](./3d_case/Specter_Print_2.jpg) 16 | 17 | ![Print 3](./3d_case/Specter_Print_3.jpg) 18 | 19 | ![Print 4](./3d_case/Specter_Print_4.jpg) 20 | 21 | ![Print 5](./3d_case/Specter_Print_5.jpg) 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 | ![Post 1](./3d_case/Specter_Post_1.jpg) 29 | 30 | ![Post 2](./3d_case/Specter_Post_2.jpg) 31 | 32 | ![Post 3](./3d_case/Specter_Post_3.jpg) 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 | ![Post 4](./3d_case/Specter_Post_4.jpg) 37 | 38 | ![Post 5](./3d_case/Specter_Post_5.jpg) 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 | ![Post 6](./3d_case/Specter_Post_6.jpg) 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 | ![Assembly 1](./3d_case/Specter_Assembly_1.jpg) 60 | 61 | 1. Attach the female/female spacers to lower holes in the Specter Shield PCB with two M3 screws. 62 | 63 | ![Assembly 2](./3d_case/Specter_Assembly_2.jpg) 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 | ![Assembly 3](./3d_case/Specter_Assembly_3.jpg) 68 | 69 | 3. Attach STM discovery board and fix in place with 4 M3 screws. 70 | 71 | ![Assembly 4](./3d_case/Specter_Assembly_4.jpg) 72 | 73 | 4. Attach front case beginning on the left (side with the SD card slot). 74 | 75 | ![Assembly 5](./3d_case/Specter_Assembly_5.jpg) 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 | ![Assembly 6](./3d_case/Specter_Assembly_6.jpg) 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 | ![Open 1](./3d_case/Specter_Open_1.jpg) 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 | ![](./3dshield.jpg) 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 | ``` --------------------------------------------------------------------------------