Files
pyhdwallet/playbook.md
2026-01-09 19:30:10 +08:00

925 lines
29 KiB
Markdown

# pyhdwallet v1.1.0 (hdwalletpy)
A command-line tool for generating and recovering HD wallets (BIP39) with support for Ethereum, Solana, and Bitcoin. It is designed for offline operation, optional PGP encryption, and writing deterministic AES-encrypted ZIP "wallet artifacts" into a local `.wallet/` folder.
Repository structure (current):
- `src/pyhdwallet.py` — main CLI script
- `tests/` — offline regression test suite
- `test_vectors.py` — pytest suite for derivation integrity
- `bootstrap_vectors.py` — one-time script to generate test vectors
- `vectors.json` — frozen expected values (committed)
- `data/recipient.asc` — test PGP key (committed)
- `vendor/` — vendored Python wheels for offline installation
- `macos-arm64/` — macOS Apple Silicon wheels (Python 3.12)
- `linux-x86_64/` — Linux x86_64 wheels (Python 3.12)
- `.wallet/` — generated wallet artifacts (gitignored)
- `requirements.in`, `requirements.txt`
- `install_offline.sh` — automated offline installer
- `Dockerfile`, `Makefile` — Docker-based build tools
- `README.md`, `playbook.md`, `LICENSE`
**Note**: This tool requires a subcommand (e.g., `gen`, `recover`). Running without one displays help. Use `<subcommand> -h` for detailed options.
***
## Features
- **Offline-first**: `gen`, `recover`, and `test` block network access (best-effort in-process guard).
- **Multi-chain support**: Derives addresses for Ethereum, Solana, and Bitcoin.
- **PGP encryption**: Encrypts a JSON payload to a PGP public key and outputs an ASCII-armored PGP message (typically saved as `.asc`).
- **AES-encrypted ZIP artifacts**: When `--file` is used, output is written as an AES-encrypted ZIP via `pyzipper`.
- **TTY safety guard + --force**: If stdout is piped/redirected (non-TTY), the tool refuses to print sensitive data unless `--force` is explicitly set (to avoid accidental leaks into logs/files). `isatty()` is the classic way to detect whether stdout is connected to a terminal.
- **Off-screen mode**: `--off-screen` suppresses printing sensitive data to stdout.
- **Regression test suite**: Offline tests with frozen vectors ensure derivation logic, PGP fingerprinting, and seed generation remain stable across code changes.
- **Vendored dependencies**: Pre-built wheels for macOS ARM64 and Linux x86_64 enable true air-gapped installation.
***
## Installation
### Prerequisites
- Python 3.12.x (required for vendored wheels)
- Git (for cloning repository)
### Quick Installation (macOS/Linux with Internet)
```bash
# Clone repository
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Run automated installer
./install_offline.sh
```
The script automatically:
- Detects your platform (macOS ARM64 or Linux x86_64)
- Creates Python 3.12 virtual environment
- Installs from vendored wheels (no PyPI access needed)
- Verifies installation with test suite
- Leaves you in activated venv
### Air-Gapped Installation (No Internet)
For maximum security on an air-gapped machine (Tails OS, Ubuntu Live USB, etc.):
**On online machine:**
```bash
# Clone and verify
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Verify checksums
cd vendor/linux-x86_64 # or macos-arm64 for Mac
sha256sum -c SHA256SUMS # Linux
shasum -a 256 -c SHA256SUMS # macOS
# Transfer entire repo to USB
```
**On air-gapped machine:**
```bash
# Ensure Python 3.12 is installed
python3.12 --version
# Navigate to repository
cd hdwalletpy
# Run installer (creates venv, installs offline)
./install_offline.sh
# Generate wallet
python src/pyhdwallet.py gen --off-screen --file
```
### Manual Installation (Without Script)
```bash
# Create venv
python3.12 -m venv .venv
source .venv/bin/activate
# Install from vendored wheels
# For macOS ARM64:
pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt
# For Linux x86_64:
pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt
# Verify installation
pytest -v tests/test_vectors.py
python src/pyhdwallet.py test
```
### Dependencies (Top-Level Intent)
- `bip-utils` — BIP39 + BIP derivation logic
- `PGPy` — encryption to OpenPGP public keys
- `pynacl` + `base58` — Solana seed/key handling
- `pyzipper` — AES-encrypted ZIP writing
- `pytest` — test framework (development only)
All dependencies are pre-downloaded in `vendor/` for offline use.
***
## Quick Start
### 1) Generate (debug/test; prints mnemonic)
```bash
python ./src/pyhdwallet.py gen
```
### 2) Generate and save AES ZIP artifact to `.wallet/`
```bash
python ./src/pyhdwallet.py gen --file
```
### 3) Generate with PGP encryption and ZIP (recommended for "at-rest" storage)
```bash
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file
```
### 4) Recover (addresses) from mnemonic
```bash
python ./src/pyhdwallet.py recover --mnemonic-stdin
# (then paste mnemonic and press Ctrl-D)
```
### 5) Recover with interactive input + ZIP artifact
```bash
python ./src/pyhdwallet.py recover --interactive --file
```
### 6) Run built-in smoke test
```bash
python ./src/pyhdwallet.py test
```
### 7) Run full regression test suite
```bash
pytest -v tests/test_vectors.py
```
***
## Testing
### Regression Test Suite
The project includes a comprehensive offline test suite that validates critical functionality without requiring network access. All tests use committed test vectors to ensure deterministic, repeatable results.
#### Test Coverage
- **PGP fingerprint calculation**: Verifies that PGP key fingerprinting logic produces expected results and enforces fingerprint matching
- **BIP39 seed derivation**: Ensures mnemonics (with and without passphrases) always produce the same seed hex
- **Multi-chain address derivation**: Validates Ethereum, Bitcoin (3 address types), and Solana (3 profiles) derivation paths remain stable
- **BIP85 child mnemonic derivation**: Verifies BIP85 entropy generation, path construction, and child mnemonic output against official test vectors
- **CLI integration**: Smoke tests for `recover` and `gen-child` commands with correct and incorrect fingerprints
#### Running Tests
```bash
# Run all tests
pytest -v tests/test_vectors.py
# Run specific test
pytest -v tests/test_vectors.py::test_address_derivation_integrity
# Run with detailed output
pytest -vv tests/test_vectors.py
```
#### Test Artifacts (Committed)
- `tests/vectors.json` — Frozen expected values for all test cases
- `tests/data/recipient.asc` — Test PGP public key (safe to commit; public key only)
- `tests/bootstrap_vectors.py` — One-time script to regenerate vectors (run only when intentionally updating expected behavior)
#### When to Regenerate Test Vectors
Only regenerate `tests/vectors.json` when you **intentionally** change:
- Derivation paths (e.g., switching from `m/44'/60'/0'/0/i` to a different path)
- Solana profile logic
- BIP39 seed generation
To regenerate:
```bash
rm tests/vectors.json
python3 tests/bootstrap_vectors.py
pytest -v tests/test_vectors.py # Verify all pass
git add tests/vectors.json
git commit -m "test: update vectors after intentional derivation change"
```
**Warning**: If tests fail after a code change and you didn't intend to change behavior, **do not regenerate vectors**. Fix the code regression instead.
***
## Vendored Dependencies
### What is Vendoring?
This repository includes pre-built Python wheels in the `vendor/` directory, enabling installation without internet access or PyPI. This is critical for air-gapped security.
**Supported platforms:**
- macOS ARM64 (M1/M2/M3/M4) - Python 3.12
- Linux x86_64 (Ubuntu, Tails, Debian) - Python 3.12
### Building Vendor Wheels (Maintainers)
If you need to update dependencies or add new platforms:
```bash
# Using Makefile (recommended)
make vendor-all # Build for both macOS and Linux
make vendor-macos # macOS ARM64 only
make vendor-linux # Linux x86_64 only (requires Docker)
make verify-vendor # Test offline installation
# Commit updated wheels
git add vendor/
git commit -m "vendor: update dependencies to latest versions"
```
**Manual build (macOS):**
```bash
mkdir -p vendor/macos-arm64
source .venv312/bin/activate
pip download --dest vendor/macos-arm64 -r requirements.txt
cd vendor/macos-arm64 && shasum -a 256 *.whl > SHA256SUMS
```
**Manual build (Linux via Docker):**
```bash
docker run --rm -v $(pwd):/work -w /work python:3.12-slim bash -c "
apt-get update && apt-get install -y gcc g++ make libffi-dev
pip install --upgrade pip
mkdir -p vendor/linux-x86_64
pip download --dest vendor/linux-x86_64 -r requirements.txt
cd vendor/linux-x86_64 && sha256sum *.whl > SHA256SUMS
"
```
### Verifying Vendor Integrity
```bash
# Check checksums before using on air-gapped machine
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS # Linux
shasum -a 256 -c SHA256SUMS # macOS
```
***
## Outputs and File Structure
### Stdout Behavior
- `gen` prints the mnemonic + derived addresses by default (intended for debug/test comparisons).
- `--off-screen` suppresses printing sensitive data to stdout.
### `--file` Behavior (Deterministic, Secured Output)
If `--file` is present, the tool writes **only** an AES-encrypted ZIP file (no raw `.json`/`.asc` file is left on disk). AES ZIP is implemented using `pyzipper`.
Default output folder:
- `./.wallet/`
Override folder:
- `--wallet-location /path/to/folder`
Naming uses UTC timestamps (e.g. `20260108_011830Z`):
- No PGP: zip contains `test_wallet_<UTC>.json`, zip name `test_wallet_<UTC>.zip`
- With PGP: zip contains `encrypted_wallet_<UTC>.asc`, zip name `encrypted_wallet_<UTC>.zip`
### Password Handling for ZIP
- Default: `--zip-password-mode prompt` prompts for the ZIP password (attempts hidden entry).
- If hidden entry is not supported in the environment, it falls back to visible `input()` with loud warnings.
- Optional: `--zip-password-mode auto` generates a Base58 password (length controlled by `--zip-password-len`).
- If auto mode is used, password is shown **only if** `--show-generated-password` is set, and it prints to **stderr** (not stdout) to reduce accidental capture when stdout is redirected.
`pyzipper` supports AES encryption via `AESZipFile` and password-setting APIs.
***
## Commands
### gen-child (offline) - **NEW in v1.1.0**
Derive a child BIP39 mnemonic from a master mnemonic using BIP85 deterministic entropy.
**What is BIP85?**
BIP85 allows you to derive unlimited child mnemonics from a single master seed. Each child mnemonic is:
- **Deterministic**: Same master + index always produces the same child
- **Isolated**: Compromise of one child doesn't affect others or the master
- **Interoperable**: Works with Coldcard, Ian Coleman tool, and other BIP85-compatible wallets
- **Portable**: Transfer child mnemonics to hot wallets without exposing your master seed
**Use cases:**
- Generate separate wallets for different exchanges/services
- Create disposable wallets for testing
- Delegate wallets to family members or business partners
- Maintain one master backup while having multiple operational wallets
```bash
python ./src/pyhdwallet.py gen-child [options]
```
**Input options (choose one - required):**
- `--interactive` — Word-by-word guided master mnemonic entry (English-only, per-word validation)
- `--mnemonic-stdin` — Read master mnemonic from stdin (recommended; avoids shell history)
**Child parameters:**
- `--words {12,15,18,21,24}` — Child mnemonic word count (default: 12)
- `--index N` — BIP85 derivation index (default: 0; use different indexes for different children)
- `--passphrase` — Prompt for master BIP39 passphrase (optional but recommended if your master uses one)
- `--passphrase-hint HINT` — Store hint only (requires `--passphrase`)
**Output options:**
- `--off-screen` — Suppress printing child mnemonic to stdout (prints metadata only)
- `--force` — Allow printing to non-TTY stdout (dangerous; see security notes)
**File output options:**
- `--file` — Write AES-encrypted ZIP artifact to `.wallet/` (or `--wallet-location`)
- `--pgp-pubkey-file FILE` — PGP-encrypt the inner JSON payload
- `--expected-fingerprint FINGERPRINT` — Enforce PGP key pinning
- `--wallet-location PATH` — Override default `.wallet/` directory
- `--zip-password-mode {prompt,auto}` — ZIP password entry mode
- `--zip-password-len N` — Auto-generated password length (default: 12)
- `--show-generated-password` — Print auto-generated ZIP password to stderr
**BIP85 Derivation Path:**
The tool uses the standard BIP85 path for BIP39 application:
```
m/83696968'/39'/0'/{words}'/{index}'
```
- `83696968'` — BIP85 magic constant
- `39'` — BIP39 application
- `0'` — English language (only supported language in v1.1.0)
- `{words}'` — Child mnemonic word count (12/15/18/21/24)
- `{index}'` — Your chosen index
**Examples:**
```bash
# 1. Basic: Derive 12-word child at index 0 (interactive entry)
python src/pyhdwallet.py gen-child --interactive --words 12 --index 0
# 2. Derive 24-word child with passphrase (from stdin)
echo "your master mnemonic words here" | \
python src/pyhdwallet.py gen-child \
--mnemonic-stdin \
--passphrase \
--words 24 \
--index 0
# 3. Secure: Off-screen + PGP encryption + ZIP file
python src/pyhdwallet.py gen-child \
--interactive \
--words 12 \
--index 1 \
--pgp-pubkey-file pubkeys/recipient.asc \
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
--off-screen \
--file
# 4. Multiple children: Derive index 0, 1, 2 for different purposes
for i in 0 1 2; do
echo "master mnemonic" | python src/pyhdwallet.py gen-child \
--mnemonic-stdin --words 12 --index $i --off-screen --file
done
```
**Security Notes:**
- **Master seed safety**: Never expose your master mnemonic to online systems. Use `gen-child` only on air-gapped machines.
- **Index tracking**: Keep a record of which index corresponds to which purpose (e.g., index 0 = exchange A, index 1 = exchange B).
- **Passphrase consistency**: If your master seed uses a BIP39 passphrase, you MUST provide the same passphrase when deriving children.
- **Interoperability verification**: Always test child mnemonics in a separate wallet before sending funds to ensure compatibility.
**Interoperability:**
This implementation follows BIP85 specification and has been verified against official test vectors. Child mnemonics are compatible with:
- Coldcard hardware wallet (BIP85 menu)
- Ian Coleman BIP85 calculator ([https://iancoleman.io/bip39/](https://iancoleman.io/bip39/))
- Trezor (when BIP85 support is added)
- Any other BIP85-compliant tool
**Output Payload Structure:**
When using `--file`, the encrypted ZIP contains a JSON payload with:
```json
{
"version": "pyhdwallet v1.1.0",
"purpose": "BIP85 derived child mnemonic",
"master_fingerprint": "ABCD1234",
"master_word_count": 24,
"passphrase_used": true,
"passphrase_hint": "my hint",
"bip85_metadata": {
"path": "m/83696968'/39'/0'/12'/0'",
"index": 0,
"language": 0,
"child_word_count": 12
},
"child_mnemonic": "word1 word2 ...",
"interoperability_note": "..."
}
```
### fetchkey (online)
Download and verify a PGP public key from a URL.
```bash
python ./src/pyhdwallet.py fetchkey <url> [--out FILE] [--timeout SECONDS] [--off-screen] [--expected-fingerprint FINGERPRINT]
```
Options:
- `url`: URL to the ASCII-armored PGP public key
- `--out FILE`: save key to file
- `--timeout SECONDS`: request timeout (default 15)
- `--off-screen`: reduced output / temp-file behavior
- `--expected-fingerprint`: refuse if downloaded key fingerprint doesn't match (40 hex chars)
***
### gen (offline)
Generate a BIP39 mnemonic and derive addresses.
```bash
python ./src/pyhdwallet.py gen [options]
```
Core options:
- `--words {12,15,18,21,24}`
- `--dice-rolls "1 2 3 ..."` (space-separated; adds user entropy)
- `--passphrase` (prompts for BIP39 passphrase)
- `--passphrase-hint HINT`
- `--chains ethereum solana bitcoin`
- `--addresses N`
- `--sol-profile {phantom_bip44change,phantom_bip44,phantom_deprecated,solana_bip39_first32}`
PGP options:
- `--pgp-pubkey-file FILE` (encrypt payload to pubkey; `.asc` content)
- `--pgp-ignore-usage-flags`
- `--expected-fingerprint FINGERPRINT` (refuse if PGP key fingerprint doesn't match)
Artifact options:
- `--file` (write AES ZIP only)
- `--wallet-location PATH` (default: `./.wallet`)
- `--zip-password-mode {prompt,auto}`
- `--zip-password-len N` (default: 12)
- `--show-generated-password` (auto mode only; prints password to stderr)
Safety options:
- `--off-screen` (don't print sensitive output)
- `--force` (only for non-TTY stdout; see below)
***
### recover (offline)
Recover addresses from mnemonic and optionally write a ZIP artifact.
```bash
python ./src/pyhdwallet.py recover [options]
```
Input options (choose one):
- `--mnemonic-stdin` (recommended; read from stdin to avoid shell history)
- `--interactive` (word-by-word guided entry with validation)
Additional options:
- same chain/PGP/ZIP/off-screen/force options as `gen`
- `--export-private` (requires `--pgp-pubkey-file`; includes private keys in encrypted payload)
- `--include-source` (includes mnemonic in payload; requires `--pgp-pubkey-file`)
Notes:
- `--export-private` requires `--pgp-pubkey-file` (private keys only travel inside encrypted payload).
- `--include-source` controls whether mnemonic is included inside the encrypted payload (only meaningful when PGP is used).
***
### test (offline)
Run minimal self-tests.
```bash
python ./src/pyhdwallet.py test
```
For comprehensive regression testing, use the pytest suite:
```bash
pytest -v tests/test_vectors.py
```
***
## When to Use `--force`
Use `--force` only if you **intentionally** want to print sensitive output while stdout is being piped/redirected.
Why it exists:
- When you pipe/redirect output (e.g., `>` or `|`), stdout is typically **not a TTY**.
- The program checks `sys.stdout.isatty()` and refuses to print secrets by default to avoid accidental leakage into files/logs.
- `isatty()` is commonly used to detect terminal vs pipe/redirect.
Examples:
```bash
# This redirects stdout to a file (non-TTY). The tool will refuse unless forced.
python ./src/pyhdwallet.py gen > out.txt
# Explicitly override the safety guard (dangerous).
python ./src/pyhdwallet.py gen --force > out.txt
```
If running normally in your interactive terminal, stdout is a TTY and `--force` does nothing (output looks the same).
***
## Security Notes (Practical)
- `gen` printing the mnemonic is intentionally "debug/test" behavior. Assume stdout can be recorded (scrollback, logging, screen recording, CI logs).
- Prefer `--off-screen` for reduced exposure.
- Prefer `--file` so artifacts go into `.wallet/` and are AES-encrypted via `pyzipper`.
- For stronger at-rest security: combine `--pgp-pubkey-file` + `--file` so the ZIP contains only an encrypted `.asc` payload. PGPy encryption style follows `PGPMessage.new(...)` then `pubkey.encrypt(...)`.
- Use `--expected-fingerprint` to enforce PGP key pinning and prevent key substitution attacks.
- Install from vendored wheels on air-gapped machines to avoid supply chain attacks.
***
## Offline Security Best Practices
This tool is designed for offline use, but true security depends on the **environment** where you run it. Below are operational security recommendations for generating keys you can trust.
### Air-Gapped Setup (Highest Security)
For maximum security when generating production wallets, use an **air-gapped computer**—a device that has never been and will never be connected to any network.
**Recommended procedure:**
1. **Prepare a clean machine**:
- Use a dedicated laptop or bootable USB with a fresh Linux installation (e.g., Tails, Ubuntu Live USB)
- Never connect this device to WiFi, Ethernet, or Bluetooth
- Physically disable network interfaces if possible (remove WiFi card, tape over Ethernet port)
2. **Transfer repository offline**:
```bash
# On trusted online machine, clone and verify
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Verify checksums
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# Transfer entire repo to USB
```
3. **Verify code integrity on air-gapped machine**:
```bash
# Check checksums again
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# Run test suite to verify derivation logic
./install_offline.sh
pytest -v tests/test_vectors.py
```
4. **Generate wallet**:
```bash
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/recipient.asc --file --off-screen
```
5. **Transfer output safely**:
- Copy only the `.wallet/*.zip` file to USB (never copy `pyhdwallet.py` or Python environment back to online machine)
- The ZIP is AES-encrypted; the inner `.asc` is PGP-encrypted
6. **Destroy or securely wipe** the USB after transfer if it contained unencrypted secrets
### Threats Air-Gapping Mitigates
- **Remote attacks**: Malware cannot exfiltrate keys over the network
- **Clipboard hijacking**: No clipboard manager or remote access tool can intercept data
- **Browser/OS telemetry**: No accidental upload of terminal history or crash dumps
### Threats Air-Gapping Does NOT Fully Mitigate
Research shows that sophisticated attackers with physical access can potentially exfiltrate data from air-gapped systems via covert channels (acoustic, electromagnetic, optical). However:
- These attacks require physical proximity and pre-installed malware
- For individual users (not nation-state targets), air-gapping remains highly effective
- Countermeasures: Use the machine in a secure location, inspect for unfamiliar USB devices, verify software integrity before installation
### Physical Security
**Mnemonic handling:**
- Write the mnemonic on paper immediately; never store it digitally on the air-gapped machine
- Use a metal backup (e.g., Cryptosteel) for fire/water resistance
- Split storage across multiple secure locations if desired (Shamir's Secret Sharing for advanced users)
**Device handling:**
- After generating the wallet, optionally wipe the air-gapped device or destroy the bootable USB
- If reusing the device, use secure-erase tools (not just `rm`)
### Verification Through Testing
**Why the test suite matters for trust:**
The committed test suite (`tests/test_vectors.py`) allows you to verify that derivation logic hasn't been tampered with before generating production keys:
```bash
# On the air-gapped machine, before generating your wallet:
pytest -v tests/test_vectors.py
```
If all tests pass, you have cryptographic proof that:
- BIP39 seed derivation matches the well-known test vector ("abandon abandon..." → known seed)
- Derivation paths produce addresses matching public BIP39/BIP44 test vectors
- PGP fingerprinting logic works correctly
If tests fail, **do not generate keys**—the code may be compromised or buggy.
### Entropy Sources
**Built-in entropy (default):**
- Python's `secrets.token_bytes()` uses OS-provided CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows)
- This is cryptographically secure for typical use
**Additional user entropy (optional):**
```bash
python ./src/pyhdwallet.py gen --dice-rolls "4 2 6 1 3 5 ..." --file
```
- Roll a die 50+ times and input the results
- Your dice rolls are mixed with OS entropy via SHA256
- Protects against potential CSPRNG backdoors (theoretical concern)
### PGP Key Fingerprint Pinning
Always use `--expected-fingerprint` when encrypting to a PGP key to prevent key substitution attacks:
```bash
# First, get and verify the fingerprint of your recipient key
python ./src/pyhdwallet.py fetchkey https://example.com/mykey.asc --out mykey.asc
# Manually verify fingerprint matches what you expect (check via another channel)
# Then use it with pinning:
python ./src/pyhdwallet.py gen \
--pgp-pubkey-file mykey.asc \
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
--file
```
Without `--expected-fingerprint`, an attacker who controls the filesystem could swap `mykey.asc` with their own key.
### Operational Checklist
Before generating production wallets:
- [ ] Running on air-gapped machine or fresh Live USB
- [ ] Network physically disabled (no WiFi/Ethernet/Bluetooth)
- [ ] Code integrity verified (checksums + test suite passes)
- [ ] Using `--off-screen` to minimize terminal scrollback exposure
- [ ] Using `--file` to avoid leaving unencrypted files on disk
- [ ] Using `--pgp-pubkey-file` with `--expected-fingerprint` for key pinning
- [ ] Paper/metal backup prepared for mnemonic
- [ ] Output ZIP password stored separately from ZIP file
- [ ] Plan for USB secure wipe after transfer
### Lower-Risk Scenarios
Air-gapping is overkill for:
- **Learning/testing**: Use your regular laptop with `--off-screen` and `--file`
- **Small amounts**: Generate on a clean, updated machine with minimal software
- **Testnet wallets**: Standard laptop is fine
Air-gapping is recommended for:
- **Life savings or business funds**
- **Long-term cold storage** (multi-year hold)
- **Institutional custody scenarios**
### Trust But Verify
The only way to fully trust this tool (or any wallet software) is to:
1. **Read the source code** (`src/pyhdwallet.py` is ~1400 lines, single file)
2. **Verify test vectors** match published BIP39 test data
3. **Run the test suite** on your air-gapped machine
4. **Generate a test wallet** and verify addresses on a block explorer using a separate tool
Never trust wallet software blindly—especially for significant funds.
***
## Troubleshooting
### "Permission denied" running `./src/pyhdwallet.py`
Run via python, or set executable bit:
```bash
python ./src/pyhdwallet.py gen
# or
chmod +x ./src/pyhdwallet.py
./src/pyhdwallet.py gen
```
### Python 3.12 not found
Vendored wheels require Python 3.12:
```bash
# macOS
brew install python@3.12
# Ubuntu/Debian
sudo apt-get install python3.12 python3.12-venv
# Verify
python3.12 --version
```
### "Missing dependency" errors
Make sure you're installing from the correct vendor directory:
```bash
# Check your platform
uname -sm
# Install for macOS ARM64
pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt
# Install for Linux x86_64
pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt
```
### Checksum verification failures
If `SHA256SUMS` verification fails, the wheels may be corrupted or tampered with:
```bash
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# If failures occur, re-download from trusted source
# DO NOT use corrupted wheels for production wallets
```
### Unzipping AES ZIP issues
Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract:
```bash
python - <<'PY'
import pyzipper
zip_path = ".wallet/your_wallet.zip"
out_dir = "unzip_out"
pw = input("ZIP password: ").encode()
with pyzipper.AESZipFile(zip_path) as zf:
zf.pwd = pw
zf.extractall(out_dir)
print("Extracted to", out_dir)
PY
```
### Test failures after code changes
If `pytest -v tests/test_vectors.py` fails after you modified code:
1. **Did you intentionally change derivation logic?**
- Yes → Regenerate vectors: `rm tests/vectors.json && python3 tests/bootstrap_vectors.py`
- No → You have a regression bug; revert your changes and fix the code
2. **Is only one test failing?**
- Run with `-vv` for detailed diff: `pytest -vv tests/test_vectors.py::test_name`
***
## Development
### Building Vendor Wheels
See [Vendored Dependencies](#vendored-dependencies) section above.
### Running Tests (Dev)
```bash
# Quick test
python src/pyhdwallet.py test
# Full regression suite
pytest -v tests/test_vectors.py
# Specific test
pytest -v tests/test_vectors.py::test_address_derivation_integrity
# With coverage
pytest --cov=src tests/
```
### Docker Development Environment
```bash
# Build image
make build-image
# Start warm container
make up
make shell
# Inside container
python src/pyhdwallet.py test
```
***
## Changelog
- **v1.1.0** (2026-01-09)
- **Added**: BIP85 child mnemonic derivation via new `gen-child` command
- Supports deriving 12/15/18/21/24-word children from master seed
- Optional master BIP39 passphrase support
- Full interoperability with BIP85-compatible wallets (Coldcard, Ian Coleman tool)
- Verified against official BIP85 test vectors
- Added BIP85 regression tests to test suite
- No new dependencies required (uses existing `bip-utils` + stdlib)
- BIP85 path: `m/83696968'/39'/0'/{words}'/{index}'`
- **v1.0.5**
- `--unsafe-print` removed; `gen` prints mnemonic by default (debug/test behavior).
- `--output` removed; file payload is always JSON (unencrypted) or `.asc` (PGP encrypted).
- `--file` is now a boolean flag that writes **only** AES-encrypted ZIP artifacts (no raw output files).
- Added: `--wallet-location`, `--zip-password-mode`, `--zip-password-len`, `--show-generated-password`.
- Added: `--force` to override the non-TTY printing safety guard (use only when redirecting/piping).
- Added: `--expected-fingerprint` for PGP key pinning.
- Added: Offline regression test suite with frozen vectors (`tests/test_vectors.py`).
- Added: Vendored dependencies for macOS ARM64 and Linux x86_64 (Python 3.12).
- Added: `install_offline.sh` automated installer for air-gapped deployment.
- Changed: `recover` now uses `--mnemonic-stdin` and `--interactive` instead of `--mnemonic` CLI arg.
- Updated: Dockerfile and Makefile now support vendoring workflow.
- **v1.0.3** Documentation and CLI behavior updates.
- **v1.0.2** Added off-screen behaviors and security improvements.
- **v1.0.1** Renamed to pyhdwallet and added version flag.
- **v1.0.0** Initial release.