8.9 KiB
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.txtREADME.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, andtestblock 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
--fileis used, output is written as an AES-encrypted ZIP viapyzipper. - TTY safety guard + --force: If stdout is piped/redirected (non-TTY), the tool refuses to print sensitive data unless
--forceis 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-screensuppresses printing sensitive data to stdout.
Installation
Prerequisites
- Python 3.11+
- Recommended: a virtual environment
Setup
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
Dependencies (top-level intent)
bip-utils— BIP39 + BIP derivation logicPGPy— encryption to OpenPGP public keyspynacl+base58— Solana seed/key handlingpyzipper— AES-encrypted ZIP writing (only needed when using--file)
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
3) Generate with PGP encryption and ZIP (recommended for “at-rest” storage)
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file
4) Recover (addresses) from mnemonic
python ./src/pyhdwallet.py recover --mnemonic "abandon abandon ... about"
5) Recover with interactive input + ZIP artifact
python ./src/pyhdwallet.py recover --interactive --file
6) Run tests
python ./src/pyhdwallet.py test
Outputs and file structure
Stdout behavior
genprints the mnemonic + derived addresses by default (intended for debug/test comparisons).--off-screensuppresses 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. 20260106_161830Z):
- No PGP: zip contains
test_wallet_<UTC>.json, zip nametest_wallet_<UTC>.zip - With PGP: zip contains
encrypted_wallet_<UTC>.asc, zip nameencrypted_wallet_<UTC>.zip
Password handling for ZIP
- Default:
--zip-password-mode promptprompts 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 autogenerates a Base58 password (length controlled by--zip-password-len). - If auto mode is used, password is shown only if
--show-generated-passwordis 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
fetchkey (online)
Download and verify a PGP public key from a URL.
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.
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;.asccontent)--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.
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-privaterequires--pgp-pubkey-file(private keys only travel inside encrypted payload).--include-sourcecontrols whether mnemonic/seed is included inside the encrypted payload (only meaningful when PGP is used).
test (offline)
Run minimal self-tests.
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:
# 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)
genprinting the mnemonic is intentionally “debug/test” behavior. Assume stdout can be recorded (scrollback, logging, screen recording, CI logs).- Prefer
--off-screenfor reduced exposure. - Prefer
--fileso artifacts go into.wallet/and are AES-encrypted viapyzipper. - For stronger at-rest security: combine
--pgp-pubkey-file+--fileso the ZIP contains only an encrypted.ascpayload. PGPy encryption style followsPGPMessage.new(...)thenpubkey.encrypt(...).
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
“Missing dependency: pyzipper” but pip says installed
You likely installed into a different venv. Use:
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:
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.
Changelog
-
v1.0.5
--unsafe-printremoved;genprints mnemonic by default (debug/test behavior).--outputremoved; file payload is always JSON (unencrypted) or.asc(PGP encrypted).--fileis 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:
--forceto 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.