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

29 KiB

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)

# 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:

# 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:

# 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)

# 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)

python ./src/pyhdwallet.py gen

2) Generate and save AES ZIP artifact to .wallet/

python ./src/pyhdwallet.py gen --file
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file

4) Recover (addresses) from mnemonic

python ./src/pyhdwallet.py recover --mnemonic-stdin
# (then paste mnemonic and press Ctrl-D)

5) Recover with interactive input + ZIP artifact

python ./src/pyhdwallet.py recover --interactive --file

6) Run built-in smoke test

python ./src/pyhdwallet.py test

7) Run full regression test suite

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

# 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:

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:

# 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):

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):

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

# 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
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:

# 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/)
  • 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:

{
  "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.

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.

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.

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.

python ./src/pyhdwallet.py test

For comprehensive regression testing, use the pytest suite:

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:

# 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:

    # 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:

    # 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:

    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:

# 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):

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:

# 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:

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:

# 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:

# 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:

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:

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 section above.

Running Tests (Dev)

# 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

# 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.