307 lines
9.0 KiB
Markdown
307 lines
9.0 KiB
Markdown
# pyhdwallet v1.0.5 (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
|
||
- `.wallet/` — generated wallet artifacts (should be gitignored)
|
||
- `requirements.in`, `requirements.txt`
|
||
- `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`). [web:46]
|
||
- **AES-encrypted ZIP artifacts**: When `--file` is used, output is written as an AES-encrypted ZIP via `pyzipper`. [web:102]
|
||
- **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.
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
### Prerequisites
|
||
|
||
- Python 3.11+
|
||
- Recommended: a virtual environment
|
||
|
||
### Setup
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
source .venv/bin/activate
|
||
python -m pip install -r requirements.txt
|
||
```
|
||
|
||
### Dependencies (top-level intent)
|
||
|
||
- `bip-utils` — BIP39 + BIP derivation logic
|
||
- `PGPy` — encryption to OpenPGP public keys [web:46]
|
||
- `pynacl` + `base58` — Solana seed/key handling
|
||
- `pyzipper` — AES-encrypted ZIP writing (only needed when using `--file`) [web:102]
|
||
|
||
---
|
||
|
||
## 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 "abandon abandon ... about"
|
||
```
|
||
|
||
### 5) Recover with interactive input + ZIP artifact
|
||
|
||
```bash
|
||
python ./src/pyhdwallet.py recover --interactive --file
|
||
```
|
||
|
||
### 6) Run tests
|
||
|
||
```bash
|
||
python ./src/pyhdwallet.py test
|
||
```
|
||
|
||
---
|
||
|
||
## 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`. [web:102]
|
||
|
||
Default output folder:
|
||
|
||
- `./.wallet/`
|
||
|
||
Override folder:
|
||
|
||
- `--wallet-location /path/to/folder`
|
||
|
||
Naming uses UTC timestamps (e.g. `20260106_161830Z`):
|
||
|
||
- 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. [web:102]
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
### 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]
|
||
```
|
||
|
||
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
|
||
|
||
---
|
||
|
||
### 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) [web:46]
|
||
- `--pgp-ignore-usage-flags`
|
||
|
||
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 or seed and optionally write a ZIP artifact.
|
||
|
||
```bash
|
||
python ./src/pyhdwallet.py recover [options]
|
||
```
|
||
|
||
Input options (choose one):
|
||
|
||
- `--mnemonic "..."` (not recommended due to shell history; prefer `--interactive`)
|
||
- `--seed HEX` (128 hex chars / 64 bytes)
|
||
- `--interactive`
|
||
|
||
Additional options:
|
||
|
||
- same chain/PGP/ZIP/off-screen/force options as `gen`
|
||
|
||
Notes:
|
||
|
||
- `--export-private` requires `--pgp-pubkey-file` (private keys only travel inside encrypted payload).
|
||
- `--include-source` controls whether mnemonic/seed is included inside the encrypted payload (only meaningful when PGP is used).
|
||
|
||
---
|
||
|
||
### test (offline)
|
||
|
||
Run minimal self-tests.
|
||
|
||
```bash
|
||
python ./src/pyhdwallet.py test
|
||
```
|
||
|
||
---
|
||
|
||
## 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`. [web:102]
|
||
- 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(...)`. [web:46]
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
### “Missing dependency: pyzipper” but pip says installed
|
||
|
||
You likely installed into a different venv. Use:
|
||
|
||
```bash
|
||
python -m pip install pyzipper
|
||
python -c "import pyzipper; print(pyzipper.__version__)"
|
||
```
|
||
|
||
### Unzipping AES ZIP issues
|
||
|
||
Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract if needed:
|
||
|
||
```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
|
||
```
|
||
|
||
AES ZIP support is the reason `pyzipper` is used. [web:102]
|
||
|
||
---
|
||
|
||
## Changelog
|
||
|
||
- **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). [web:46]
|
||
- `--file` is now a boolean flag that writes **only** AES-encrypted ZIP artifacts (no raw output files). [web:102]
|
||
- 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).
|
||
|
||
- **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.
|