925 lines
29 KiB
Markdown
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.
|