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