Skip to content

Repository files navigation

Bowser Wallet banner

Bowser Wallet can be used over Web Serial with the LNbits WatchOnly extension or its standalone web client. It can also operate air-gapped using a microSD card and the optional keypad.

Wallet entropy can be generated by the ESP32 hardware random-number generator or supplied by entering 100 rolls of a six-sided die.

All of the software and hardware designs are free, libre and open source (FLOSS).

Using generic, widely available hardware is a huge gain against the supply chain attack from purpose built devices, but it does not eliminate supply-chain risk entirely. Because the device has no secure element, it should be stored securely when not in use and treated as physically sensitive.

WebSerial mode is intended for smaller day-to-day amounts. For more sensitive use, the air-gapped microSD and keypad mode with dice-generated entropy is recommended. For significant funds, consider a mature, independently audited hardware wallet such as Trezor.

Maintainers are not responsible for any loss of funds.

What you need

  • A LILYGO TTGO T-Display ESP32
  • A USB data cable for flashing and connected operation
  • A computer with one of the following:
    • Chrome, Chromium, or Brave for the web installer and Web Serial
    • Arduino CLI or Arduino IDE for compiling and flashing manually
  • Optional: the LILYGO T-Display Keyboard Module and a microSD card for air-gapped commands and dice-roll wallet creation
  • Optional: the LNbits OnchainWallet extension for connected wallet operation
  • Optional: a case, or a complete kit from the LNbits shop

For use with

The LNbits WatchOnly Extension

The standalone Bowser Wallet webapp

Electrum

Installation

Web installer (easy)

Use the LNbits Hardware Wallet web installer.

After installation, the web installer and the device both show the SHA-256 of the installed application image. Compare all 64 characters with the firmware SHA-256 published in the corresponding GitHub release, then press # on the keypad to continue. A side button can be used on devices without a keypad.

Verify the firmware release hash

Every tagged release compiles the firmware from that tag in GitHub Actions, then publishes that exact source-built image and its ESP application image hash in the GitHub release notes and as an ESP_IMAGE_SHA256.txt release asset. The same CI-built files are deployed to the web installer. The hash is also calculated from the running application partition by the device when it boots. Compare all three values; they should be identical.

You can independently verify the firmware file included in this repository:

python3 tools/esp_image_hash.py \
  installer/firmware/esp32/current/wallet.ino.bin

The command validates the SHA-256 embedded at the end of the ESP application image and prints its 64-character hexadecimal value. It exits with an error if the file is not an ESP application image or its embedded digest is invalid.

This is deliberately not the same operation as running sha256sum over the whole .bin file: the ESP image digest covers the image content preceding the final 32-byte embedded digest. As a result, a whole-file SHA-256 is expected to be different.

Matching hashes confirm that the application image on the device is byte-for- byte the image compiled from the tagged source and published for the release. Perform the comparison using a trusted copy of the GitHub release page; a hash displayed by the same potentially compromised source as the firmware does not independently prove authenticity. For the strongest verification, inspect the tagged source and the pinned release workflow, or build the tagged source yourself. Exact byte-for-byte reproducibility can also depend on matching the toolchain and build environment because ESP application metadata includes a hash of the build output.

Build from source with Arduino CLI (tinfoil)

Install Arduino CLI using the normal method for your operating system.

Install pyserial

# Install pyserial
sudo pip3 install pyserial

Then clone and build the firmware:

git clone https://github.com/lnbits/hardware-wallet
cd hardware-wallet

# Install the exact ESP32 core version used by this project.
arduino-cli config add board_manager.additional_urls \
  https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32@2.0.17
arduino-cli core list

# Connect the device and identify its serial port.
arduino-cli board list

# Set this to the port shown above. Common Linux ports are ttyACM0 and ttyUSB0.
HWW_PORT=/dev/ttyACM0

# Compile and upload for the LILYGO T-Display.
arduino-cli compile --verbose --clean --upload \
  --fqbn esp32:esp32:ttgo-lora32 \
  --port "$HWW_PORT" \
  --libraries "$PWD/libraries" \
  "$PWD/wallet"

Device commands

The LNbits OnchainWallet extension or another client communicates with the device using commands in this form:

/command-name {param1} {param2} ... {paramN}

  • Parameter order is significant.
  • Use a minus (-) when an optional positional value is omitted.
  • Example: /password my-password-1

The device can respond with a string in the same form:

/command-name {response1} {response2} ... {responseN}

Each command is documented in its implementation file:

Run from a microSD card (air-gapped)

  1. Mount the microSD card on a computer.
  2. Create commands.in.txt in the card's root directory.
  3. Add commands to the file. Start with the commented complete command template, consult the output reference, or see the worked examples.
  4. Safely eject the card and insert it into the hardware wallet.
  5. Reboot the device. It will detect and execute commands.in.txt.
  6. Wait for command execution to finish, then return the card to the computer.
  7. Read the generated files:
    • commands.out.txt contains command results such as signed PSBTs.
    • commands.log.txt contains diagnostic logs.

When signing an SD-card PSBT, the device displays every destination and amount, then the fee, and requires a physical approval for each item. It asks for one final physical confirmation before producing a signature. # or button 1 accepts; * or button 2 rejects.

Wallets created by current firmware are stored as an authenticated encrypted record. Separate encryption and authentication keys are derived from the password with a random salt and PBKDF2-HMAC-SHA256; the stored verifier is not the mnemonic-encryption key. A wallet created by older firmware is migrated to this format only after its correct password is entered successfully. Keep a tested seed backup before upgrading firmware.

Create a wallet from dice rolls

The air-gapped T-Display Keyboard Module can create a 24-word BIP39 wallet using a physical six-sided die:

  1. Put /create your-password in commands.in.txt. The password must contain at least 8 characters and cannot contain spaces.
  2. Insert the microSD card and reboot the device.
  3. Roll a fair six-sided die 100 times, entering each result with keypad keys 1 through 6. Press * to remove the most recent entry.
  4. At 100/100, press # to create the wallet.
  5. Write down each seed word shown on the device. Press # for the next word and * for the previous word. Press # on word 24 to finish.

The keypad matrix uses columns GPIO 33, 32, and 25 and rows GPIO 21, 27, 26, and 22. The firmware hashes the exact 100 ASCII dice digits with SHA-256 and uses the resulting 256 bits directly as BIP39 entropy. This makes the process reproducible for recovery and provides more than 256 bits of input entropy when the die is fair.

Neither the dice sequence nor the seed words are written to the microSD card by /create; commands.out.txt only receives /create 1 on success. Protect or remove commands.in.txt, because it contains the wallet password. The wallet is persisted in the device's existing password-encrypted storage even if a previous /pair command disabled persistence for SD restores. The existing /seed command also keeps the mnemonic off the microSD card: it displays one word at a time on the hardware screen. Press # or button 1 to advance, and * or button 2 to go back. Advancing from word 24 finishes the review.

Entropy lines (the important bit)

  • uBitcoin obtains random words from the ESP32 hardware RNG through esp_random().
  • Before using that RNG, the wallet checks its conditioned output for stuck values, stuck bit positions, repeated words, and gross bit imbalance in hardwareRngPassesHealthCheck().
  • The wallet adds 32 bytes directly from the ESP32 hardware RNG through esp_fill_random().
  • Dice wallet creation hashes the exact 100-character dice-roll buffer with SHA-256, then passes the resulting 32 bytes directly as BIP39 entropy to mnemonicFromEntropy().

Troubleshooting

  • If the device does not appear as a serial port, install the CP210x USB-to-UART driver.
  • If uploading stalls at Connecting..., hold the device's BOOT button while the connection starts, then release it.
  • On Linux, ensure your user has permission to access the serial device.

Questions? Join the LNbits Telegram group or the MakerBits Telegram group.

This project uses the uBitcoin library.

About

Use a world of microcontrollers to contruct your own bitcoin hardware wallet

Resources

Stars

92 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages