Compare commits
14 Commits
84953dbe5a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b47935de86 | |||
|
|
46f748301e | ||
|
|
27ee16f21a | ||
|
|
0a748dd8b0 | ||
|
|
c41928688c | ||
| d4f6e3d207 | |||
|
|
a6c84f81ee | ||
| 109829f1f5 | |||
|
|
2f7433b704 | ||
|
|
6457ec2cee | ||
|
|
0949fe9792 | ||
| 21b9389591 | |||
|
|
369c8595a1 | ||
|
|
2807982209 |
84
.gitignore
vendored
84
.gitignore
vendored
@@ -1,29 +1,79 @@
|
||||
.venv/
|
||||
.venv312/
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyc
|
||||
.vscode/
|
||||
.env
|
||||
.DS_Store
|
||||
.idea/
|
||||
logs/
|
||||
*.log
|
||||
coverage/
|
||||
_toDelete/
|
||||
_toDelete/
|
||||
dist/
|
||||
build/
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
.ipynb_checkpoints/
|
||||
.envrc
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv312/
|
||||
.venv-*/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
wheelhouse/
|
||||
|
||||
# PyInstaller temporary files
|
||||
src/*_frozen.py
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.cache/
|
||||
coverage/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
.pyre/
|
||||
.pytype/
|
||||
.cache/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.envrc
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
*.sqlite3
|
||||
*.db
|
||||
*.asc
|
||||
.venv/
|
||||
|
||||
# Project specific
|
||||
.wallet/
|
||||
releases/*/
|
||||
_toDelete/
|
||||
.potentialfix.md
|
||||
|
||||
# PGP keys (except test data)
|
||||
*.asc
|
||||
!tests/data/recipient.asc
|
||||
|
||||
# Vendor cleanup (wrong architectures/test builds)
|
||||
vendor/linux-aarch64/
|
||||
vendor/macos-arm64-docker/
|
||||
vendor/*-docker/
|
||||
|
||||
# Keep official vendor directories
|
||||
# vendor/macos-arm64/
|
||||
# vendor/linux-x86_64/
|
||||
# vendor/PROVENANCE.md
|
||||
|
||||
174
BestPractices.md
174
BestPractices.md
@@ -1,174 +0,0 @@
|
||||
# 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 [web:14]
|
||||
- Physically disable network interfaces if possible (remove WiFi card, tape over Ethernet port)
|
||||
|
||||
2. **Transfer dependencies offline**:
|
||||
|
||||
```bash
|
||||
# On an online machine, download dependencies
|
||||
pip download -r requirements.txt -d ./offline-deps
|
||||
|
||||
# Transfer ./offline-deps to air-gapped machine via USB
|
||||
# On air-gapped machine:
|
||||
pip install --no-index --find-links=./offline-deps -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Verify code integrity**:
|
||||
- Check the git commit hash matches the version you reviewed
|
||||
- Optionally run `pytest -v tests/test_vectors.py` on the air-gapped machine to verify derivation logic
|
||||
|
||||
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 [web:19]
|
||||
|
||||
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) [web:18]. However:
|
||||
|
||||
- These attacks require physical proximity and pre-installed malware [web:18]
|
||||
- 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 [web:18]
|
||||
|
||||
### 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) [file:2]
|
||||
- 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 [file:2]
|
||||
- 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 [file:2]:
|
||||
|
||||
```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 (git commit hash + 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
|
||||
|
||||
---
|
||||
**Key additions:**
|
||||
|
||||
1. **Air-gapped setup procedure** with offline dependency installation
|
||||
2. **Threat model** (what air-gapping protects against and doesn't)
|
||||
3. **Physical security** best practices for mnemonic backup
|
||||
4. **Test suite as verification tool** before generating production keys
|
||||
5. **Entropy sources** explanation (dice rolls for paranoid users)
|
||||
6. **PGP fingerprint pinning** rationale
|
||||
7. **Operational security checklist** for production use
|
||||
8. **Risk-based guidance** (when air-gapping is overkill vs. essential)
|
||||
9. **"Trust but verify"** philosophy for crypto software
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,7 +1,7 @@
|
||||
|
||||
# Dockerfile
|
||||
# Base build environment for hdwalletpy: Python 3.11 + build tools + venv support
|
||||
FROM python:3.11-slim
|
||||
# Build environment for hdwalletpy: Python 3.12 + build tools + venv support
|
||||
# Used for: (1) Building Linux x86_64 wheels, (2) Development container
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install build tools, headers, and venv module
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -9,8 +9,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory for bind mounts
|
||||
WORKDIR /app
|
||||
|
||||
# Default command (can be overridden)
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
358
Makefile
358
Makefile
@@ -1,90 +1,334 @@
|
||||
|
||||
# Makefile for hdwalletpy workflow
|
||||
# - Build a reusable Docker image
|
||||
# - Build reusable Docker image (Python 3.12)
|
||||
# - Vendor multi-platform wheels for offline air-gapped use
|
||||
# - Compile wheels in container (wheelhouse)
|
||||
# - Create host venv and install from wheelhouse
|
||||
# - Create host venv and install from wheelhouse or vendor/
|
||||
# - Optionally run inside a warm container
|
||||
|
||||
# ---------- Platform Detection ----------
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
PLATFORM := macos
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
ARCH := arm64
|
||||
else
|
||||
$(error Unsupported macOS architecture: $(UNAME_M))
|
||||
endif
|
||||
else ifeq ($(UNAME_S),Linux)
|
||||
PLATFORM := linux
|
||||
ARCH := x86_64
|
||||
else
|
||||
$(error Unsupported OS: $(UNAME_S))
|
||||
endif
|
||||
|
||||
VENDOR_DIR := vendor/$(PLATFORM)-$(ARCH)
|
||||
|
||||
# ---------- Config ----------
|
||||
IMAGE := python-build-env:3.11
|
||||
CONTAINER := hdwallet
|
||||
IMAGE := hdwallet-build:3.12
|
||||
CONTAINER := hdwallet-dev
|
||||
WORKDIR := /app
|
||||
VENV_HOST := .venv_host
|
||||
VENV_HOST := .venv
|
||||
VENV_CONTAINER := /opt/venv
|
||||
WHEELHOUSE := wheelhouse
|
||||
|
||||
# Vendor directories for air-gapped deployment
|
||||
VENDOR_MACOS := vendor/macos-arm64
|
||||
VENDOR_LINUX := vendor/linux-x86_64
|
||||
|
||||
# ---------- Help ----------
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " make build-image - Build Docker image with build tools and python3-venv"
|
||||
@echo " make wheels - Build dependency wheels into ./$(WHEELHOUSE)"
|
||||
@echo " make venv-host - Create host venv $(VENV_HOST) and install from wheelhouse"
|
||||
@echo " make up - Start a warm container named $(CONTAINER)"
|
||||
@echo " make shell - Open a shell in the warm container"
|
||||
@echo " make venv-container - Create venv inside container at $(VENV_CONTAINER) and install requirements"
|
||||
@echo " make down - Stop and remove the warm container"
|
||||
@echo " make clean-venv - Remove host venv $(VENV_HOST)"
|
||||
@echo " make clean-wheelhouse - Remove local wheel cache $(WHEELHOUSE)"
|
||||
@echo " make clean - Clean venv and wheelhouse"
|
||||
@echo "pyhdwallet build system (macOS + Linux compatible)"
|
||||
@echo ""
|
||||
@echo "📦 Vendoring (for offline/air-gapped use):"
|
||||
@echo " make vendor-macos - Build macOS ARM64 wheels (requires macOS native)"
|
||||
@echo " make vendor-linux - Build Linux x86_64 wheels (Docker - any OS)"
|
||||
@echo " make vendor-all - Build wheels for both platforms (macOS native + Docker)"
|
||||
@echo " make verify-vendor - Test offline installation from vendor/"
|
||||
@echo ""
|
||||
@echo "🔨 Binary Distribution:"
|
||||
@echo " make binary - Build standalone binary for current platform"
|
||||
@echo " make binary-linux - Build Linux binary via Docker (any OS)"
|
||||
@echo " make binary-all - Build binaries for all platforms"
|
||||
@echo ""
|
||||
@echo "🚀 Release Management:"
|
||||
@echo " make release - Build complete release (binaries + vendors + checksums)"
|
||||
@echo " make release-binaries - Copy binaries to releases/vX.Y.Z/"
|
||||
@echo " make release-checksums - Generate SHA256SUMS for binaries"
|
||||
@echo " make release-test - Test release binaries"
|
||||
@echo " make clean-release - Remove all release artifacts"
|
||||
@echo ""
|
||||
@echo "🛠️ Development workflow (works on macOS + Linux):"
|
||||
@echo " make build-image - Build Docker image (Python 3.12)"
|
||||
@echo " make wheels - Build wheels into ./$(WHEELHOUSE)"
|
||||
@echo " make install - Create venv and install dependencies"
|
||||
@echo " make test - Run test suite"
|
||||
@echo " make up - Start warm dev container"
|
||||
@echo " make shell - Open shell in warm container"
|
||||
@echo " make down - Stop and remove dev container"
|
||||
@echo ""
|
||||
@echo "🧹 Cleanup:"
|
||||
@echo " make clean - Remove venv, wheelhouse, vendor/"
|
||||
@echo " make clean-vendor - Remove vendor/ only"
|
||||
@echo " make clean-release - Remove releases/ only"
|
||||
@echo ""
|
||||
@echo "ℹ️ Platform Info:"
|
||||
@echo " make info - Show platform detection info"
|
||||
@echo ""
|
||||
@echo "Platform notes:"
|
||||
@echo " • vendor-macos requires native macOS (uses .venv312 with ARM64 Python)"
|
||||
@echo " • vendor-linux works on any platform with Docker"
|
||||
@echo " • binary-linux works on any platform with Docker"
|
||||
@echo " • All other targets work on macOS and Linux"
|
||||
|
||||
|
||||
# ---------- Build reusable image ----------
|
||||
.PHONY: build-image
|
||||
build-image:
|
||||
docker build -t $(IMAGE) .
|
||||
docker build -t $(IMAGE) .
|
||||
|
||||
# ---------- Build wheels in container ----------
|
||||
# ---------- Vendoring for Air-Gapped Use ----------
|
||||
.PHONY: vendor-macos
|
||||
vendor-macos: requirements.txt
|
||||
@echo "Building macOS ARM64 wheels (native)..."
|
||||
@if [ ! -f ".venv312/bin/pip" ]; then \
|
||||
echo "ERROR: .venv312 not found. Create it first:"; \
|
||||
echo " python3.12 -m venv .venv312 && source .venv312/bin/activate"; \
|
||||
exit 1; \
|
||||
fi
|
||||
mkdir -p $(VENDOR_MACOS)
|
||||
.venv312/bin/pip download --dest $(VENDOR_MACOS) -r requirements.txt
|
||||
cd $(VENDOR_MACOS) && shasum -a 256 *.whl > SHA256SUMS
|
||||
@echo "✓ macOS ARM64 wheels: $(VENDOR_MACOS)/"
|
||||
|
||||
.PHONY: vendor-linux
|
||||
vendor-linux: requirements.txt build-image
|
||||
@echo "Building Linux x86_64 wheels (Docker)..."
|
||||
mkdir -p $(VENDOR_LINUX)
|
||||
docker run --rm \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
$(IMAGE) \
|
||||
bash -c " \
|
||||
pip install --upgrade pip && \
|
||||
pip download --dest $(VENDOR_LINUX) -r requirements.txt && \
|
||||
pip wheel --wheel-dir $(VENDOR_LINUX) --no-deps $(VENDOR_LINUX)/*.tar.gz 2>/dev/null || true && \
|
||||
rm -f $(VENDOR_LINUX)/*.tar.gz && \
|
||||
cd $(VENDOR_LINUX) && sha256sum *.whl > SHA256SUMS \
|
||||
"
|
||||
@echo "✓ Linux x86_64 wheels: $(VENDOR_LINUX)/"
|
||||
|
||||
.PHONY: vendor-all
|
||||
vendor-all: vendor-macos vendor-linux
|
||||
@echo ""
|
||||
@echo "✓ All platforms vendored:"
|
||||
@echo " macOS ARM64: $(VENDOR_MACOS)/ ($$(ls $(VENDOR_MACOS)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
@echo " Linux x86_64: $(VENDOR_LINUX)/ ($$(ls $(VENDOR_LINUX)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
@echo ""
|
||||
@echo "Commit with: git add vendor/ && git commit -m 'vendor: update wheels'"
|
||||
|
||||
.PHONY: verify-vendor
|
||||
verify-vendor:
|
||||
@echo "Testing offline installation from vendor/..."
|
||||
@bash -c ' \
|
||||
if [[ "$$OSTYPE" == "darwin"* ]]; then \
|
||||
PLATFORM="macos-arm64"; \
|
||||
else \
|
||||
PLATFORM="linux-x86_64"; \
|
||||
fi; \
|
||||
echo "Platform: $$PLATFORM"; \
|
||||
python3.12 -m venv .venv-verify && \
|
||||
source .venv-verify/bin/activate && \
|
||||
pip install --no-index --find-links=vendor/$$PLATFORM -r requirements.txt && \
|
||||
pytest -v tests/test_vectors.py && \
|
||||
python src/pyhdwallet.py test && \
|
||||
echo "" && \
|
||||
echo "✅ Vendor installation verified!" && \
|
||||
deactivate && \
|
||||
rm -rf .venv-verify \
|
||||
'
|
||||
|
||||
# ---------- Binary Building ----------
|
||||
.PHONY: binary
|
||||
binary:
|
||||
@echo "🔨 Building binary for current platform: $(PLATFORM)-$(ARCH)..."
|
||||
ifeq ($(PLATFORM),macos)
|
||||
@if [ ! -f "build_binary.sh" ]; then \
|
||||
echo "❌ build_binary.sh not found!"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@bash build_binary.sh
|
||||
else
|
||||
@echo "⚠️ For Linux native builds, use build_binary.sh directly"
|
||||
@echo " Or use: make binary-linux (builds via Docker)"
|
||||
endif
|
||||
|
||||
.PHONY: binary-linux
|
||||
binary-linux:
|
||||
@./build_binary_linux.sh
|
||||
|
||||
.PHONY: binary-all
|
||||
binary-all:
|
||||
@echo "🏗️ Building binaries for all platforms..."
|
||||
@echo ""
|
||||
@echo "1️⃣ Building native binary..."
|
||||
@$(MAKE) binary
|
||||
@echo ""
|
||||
@echo "2️⃣ Building Linux binary via Docker..."
|
||||
@$(MAKE) binary-linux
|
||||
@echo ""
|
||||
@echo "✅ All binaries built:"
|
||||
@ls -lh dist/pyhdwallet* 2>/dev/null | awk '{print " " $$9 " (" $$5 ")"}' || echo " No binaries found"
|
||||
|
||||
# ---------- Development Workflow ----------
|
||||
.PHONY: wheels
|
||||
wheels: requirements.txt
|
||||
docker run --rm -it -v "$$PWD":$(WORKDIR) -w $(WORKDIR) $(IMAGE) bash -lc '\
|
||||
python -m pip install --upgrade pip setuptools wheel && \
|
||||
mkdir -p $(WHEELHOUSE) && \
|
||||
python -m pip wheel -r requirements.txt -w $(WHEELHOUSE) \
|
||||
'
|
||||
wheels: requirements.txt build-image
|
||||
docker run --rm \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
$(IMAGE) \
|
||||
bash -c " \
|
||||
pip install --upgrade pip setuptools wheel && \
|
||||
mkdir -p $(WHEELHOUSE) && \
|
||||
pip wheel -r requirements.txt -w $(WHEELHOUSE) \
|
||||
"
|
||||
|
||||
# ---------- Host venv from wheelhouse ----------
|
||||
.PHONY: venv-host
|
||||
venv-host: requirements.txt
|
||||
python3 -m venv $(VENV_HOST)
|
||||
. $(VENV_HOST)/bin/activate && \
|
||||
python -m pip install --upgrade pip setuptools wheel && \
|
||||
python -m pip install --no-index --find-links=$(WHEELHOUSE) -r requirements.txt && \
|
||||
python --version
|
||||
.PHONY: install
|
||||
install: requirements.txt
|
||||
@if [ ! -d "$(VENV_HOST)" ]; then \
|
||||
echo "Creating venv: $(VENV_HOST)"; \
|
||||
python3.12 -m venv $(VENV_HOST); \
|
||||
fi
|
||||
. $(VENV_HOST)/bin/activate && \
|
||||
pip install --upgrade pip && \
|
||||
pip install -r requirements.txt && \
|
||||
echo "✓ Virtual environment ready: $(VENV_HOST)"
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@if [ ! -d "$(VENV_HOST)" ]; then \
|
||||
echo "ERROR: Run 'make install' first"; \
|
||||
exit 1; \
|
||||
fi
|
||||
. $(VENV_HOST)/bin/activate && \
|
||||
pytest -v tests/test_vectors.py && \
|
||||
python src/pyhdwallet.py test
|
||||
|
||||
# ---------- Warm container lifecycle ----------
|
||||
.PHONY: up
|
||||
up:
|
||||
docker run -dit -v "$$PWD":$(WORKDIR) -w $(WORKDIR) --name $(CONTAINER) $(IMAGE) bash
|
||||
up: build-image
|
||||
docker run -dit \
|
||||
-v "$$PWD":$(WORKDIR) \
|
||||
-w $(WORKDIR) \
|
||||
--name $(CONTAINER) \
|
||||
$(IMAGE) \
|
||||
bash
|
||||
|
||||
.PHONY: shell
|
||||
shell:
|
||||
docker exec -it $(CONTAINER) bash
|
||||
|
||||
.PHONY: venv-container
|
||||
venv-container: requirements.txt
|
||||
docker exec -it $(CONTAINER) bash -lc '\
|
||||
test -x $(VENV_CONTAINER)/bin/python || ( \
|
||||
python -m venv $(VENV_CONTAINER) && \
|
||||
. $(VENV_CONTAINER)/bin/activate && \
|
||||
python -m pip install --upgrade pip setuptools wheel && \
|
||||
python -m pip install -r requirements.txt \
|
||||
) && \
|
||||
. $(VENV_CONTAINER)/bin/activate && python --version \
|
||||
'
|
||||
docker exec -it $(CONTAINER) bash
|
||||
|
||||
.PHONY: down
|
||||
down:
|
||||
- docker rm -f $(CONTAINER)
|
||||
- docker rm -f $(CONTAINER)
|
||||
|
||||
# ---------- Cleanup ----------
|
||||
.PHONY: clean-venv
|
||||
clean-venv:
|
||||
rm -rf $(VENV_HOST)
|
||||
|
||||
.PHONY: clean-wheelhouse
|
||||
clean-wheelhouse:
|
||||
rm -rf $(WHEELHOUSE)
|
||||
.PHONY: clean-vendor
|
||||
clean-vendor:
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-venv clean-wheelhouse
|
||||
clean: down
|
||||
rm -rf $(VENV_HOST) $(WHEELHOUSE) .venv-verify .venv-offline-test
|
||||
rm -rf dist/ build/ *.spec src/pyhdwallet_frozen.py
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
||||
# NOTE: vendor/ is kept (use 'make clean-vendor' to remove)
|
||||
|
||||
|
||||
# ---------- Platform-Aware Operations ----------
|
||||
.PHONY: info
|
||||
info:
|
||||
@echo "Platform Information:"
|
||||
@echo " OS: $(PLATFORM)"
|
||||
@echo " Architecture: $(ARCH)"
|
||||
@echo " Vendor dir: $(VENDOR_DIR)"
|
||||
@echo " Python: $(shell python3 --version 2>/dev/null || echo 'not found')"
|
||||
@echo " Docker: $(shell docker --version 2>/dev/null || echo 'not found')"
|
||||
|
||||
# ---------- Release Management ----------
|
||||
VERSION := $(shell grep 'VERSION = ' src/pyhdwallet.py | head -1 | sed 's/.*"\(.*\)".*/\1/' || echo "v1.1.0")
|
||||
RELEASE_DIR := releases/$(VERSION)
|
||||
|
||||
.PHONY: release-prep
|
||||
release-prep:
|
||||
@echo "📦 Preparing release $(VERSION)..."
|
||||
@mkdir -p $(RELEASE_DIR)
|
||||
|
||||
.PHONY: release-binaries
|
||||
release-binaries: release-prep binary binary-linux
|
||||
@echo "📦 Copying binaries to $(RELEASE_DIR)..."
|
||||
@cp dist/pyhdwallet $(RELEASE_DIR)/pyhdwallet-macos-arm64
|
||||
@cp dist/pyhdwallet-linux $(RELEASE_DIR)/pyhdwallet-linux-x86_64
|
||||
@echo "✅ Binaries copied"
|
||||
|
||||
.PHONY: release-checksums
|
||||
release-checksums: release-binaries
|
||||
@echo "🔐 Generating checksums..."
|
||||
@cd $(RELEASE_DIR) && shasum -a 256 pyhdwallet-* > SHA256SUMS
|
||||
@echo "✅ Checksums generated:"
|
||||
@cat $(RELEASE_DIR)/SHA256SUMS
|
||||
|
||||
.PHONY: release-vendors
|
||||
release-vendors: release-prep vendor-all
|
||||
@echo "📦 Copying vendor wheels to $(RELEASE_DIR)..."
|
||||
@mkdir -p $(RELEASE_DIR)/vendor
|
||||
@cp -r vendor/macos-arm64 $(RELEASE_DIR)/vendor/
|
||||
@cp -r vendor/linux-x86_64 $(RELEASE_DIR)/vendor/
|
||||
@echo "✅ Vendor wheels copied"
|
||||
|
||||
.PHONY: release
|
||||
release: release-checksums
|
||||
@echo ""
|
||||
@echo "🎉 Release $(VERSION) binaries ready!"
|
||||
@echo ""
|
||||
@if [ -d ".venv312" ]; then \
|
||||
echo "📦 Building vendor wheels..."; \
|
||||
$(MAKE) release-vendors; \
|
||||
else \
|
||||
echo "⚠️ Skipping vendor wheels (.venv312 not found)"; \
|
||||
echo " To include vendors: python3.12 -m venv .venv312 && make release-vendors"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "Contents:"
|
||||
@ls -lh $(RELEASE_DIR)/ | tail -n +2
|
||||
@echo ""
|
||||
@cat $(RELEASE_DIR)/SHA256SUMS
|
||||
@echo ""
|
||||
@echo "Next steps:"
|
||||
@echo " 1. Test: make release-test"
|
||||
@echo " 2. Tag: git tag -a $(VERSION) -m 'Release $(VERSION)'"
|
||||
@echo " 3. Push: git push origin $(VERSION)"
|
||||
|
||||
|
||||
|
||||
.PHONY: release-test
|
||||
release-test:
|
||||
@echo "🧪 Testing release binaries..."
|
||||
@echo ""
|
||||
@echo "Testing macOS binary:"
|
||||
@$(RELEASE_DIR)/pyhdwallet-macos-arm64 test || echo "⚠️ macOS binary test skipped (wrong platform)"
|
||||
@echo ""
|
||||
@echo "Testing Linux binary (via Docker):"
|
||||
@docker run --rm --platform linux/amd64 \
|
||||
-v "$$PWD/$(RELEASE_DIR)":/binaries \
|
||||
ubuntu:22.04 \
|
||||
/binaries/pyhdwallet-linux-x86_64 test
|
||||
|
||||
.PHONY: clean-release
|
||||
clean-release:
|
||||
@echo "🧹 Cleaning release artifacts..."
|
||||
@rm -rf releases/
|
||||
@echo "✅ Release artifacts cleaned"
|
||||
|
||||
307
README.md
307
README.md
@@ -1,151 +1,274 @@
|
||||
# hdwalletpy – Setup & Usage Guide
|
||||
# pyhdwallet – Secure HD Wallet Tool
|
||||
|
||||
This project provides a Python-based HD Wallet tool with optional Docker-based build automation for fast, reproducible environments.
|
||||
A Python command-line tool for generating and recovering BIP39 HD wallets with support for Ethereum, Solana, and Bitcoin. Includes **BIP85 deterministic child mnemonic derivation** for creating multiple isolated wallets from a single master seed. Designed for offline operation with optional PGP encryption and AES-encrypted ZIP artifacts.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation Options
|
||||
## 📦 Installation
|
||||
|
||||
### **Option 1: Standard Python Setup (Host Machine)**
|
||||
### **Quick Start (macOS/Linux with Internet)**
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/<your-username>/hdwalletpy.git
|
||||
cd hdwalletpy
|
||||
|
||||
# Install using automated script
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
The script automatically:
|
||||
|
||||
- Creates Python 3.12 virtual environment
|
||||
- Installs from vendored wheels (offline-capable)
|
||||
- Verifies installation with test suite
|
||||
- Leaves you in activated venv
|
||||
|
||||
---
|
||||
|
||||
### **Air-Gapped Installation (No Internet)**
|
||||
|
||||
This repository includes pre-built Python wheels for offline use.
|
||||
|
||||
**Supported platforms:**
|
||||
|
||||
- macOS ARM64 (M1/M2/M3/M4) - Python 3.12
|
||||
- Linux x86_64 (Ubuntu/Tails) - Python 3.12
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. On an online machine, clone and verify:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/hdwalletpy.git
|
||||
cd hdwalletpy
|
||||
|
||||
# Verify checksums
|
||||
cd vendor/linux-x86_64 # or macos-arm64
|
||||
sha256sum -c SHA256SUMS # Linux
|
||||
shasum -a 256 -c SHA256SUMS # macOS
|
||||
```
|
||||
|
||||
2. Create a virtual environment:
|
||||
2. Transfer entire repo to USB drive
|
||||
|
||||
3. On air-gapped machine:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
cd hdwalletpy
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
4. Generate wallet:
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
python src/pyhdwallet.py gen --off-screen --file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Option 2: Fast Setup Using Docker + Makefile**
|
||||
### **Developer Installation (with Docker)**
|
||||
|
||||
This method avoids installing compilers on your host and speeds up dependency builds.
|
||||
|
||||
#### **Step 1: Build Docker Image**
|
||||
For development or building wheels for other platforms:
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
make build-image
|
||||
```
|
||||
|
||||
Creates a reusable image `python-build-env:3.11` with build tools and `python3-venv`.
|
||||
# Build wheels for all platforms
|
||||
make vendor-all
|
||||
|
||||
#### **Step 2: Compile Wheels in Container**
|
||||
# Install development environment
|
||||
make install
|
||||
|
||||
```bash
|
||||
make wheels
|
||||
```
|
||||
|
||||
Builds all dependency wheels into `./wheelhouse` for fast installs.
|
||||
|
||||
#### **Step 3: Create Host Virtual Environment**
|
||||
|
||||
```bash
|
||||
make venv-host
|
||||
source .venv_host/bin/activate
|
||||
```
|
||||
|
||||
Installs dependencies from `wheelhouse` (no compilation needed).
|
||||
|
||||
#### **Optional: Work Inside a Warm Container**
|
||||
|
||||
```bash
|
||||
make up # Start container
|
||||
make shell # Open shell inside container
|
||||
make venv-container # Create container-only venv at /opt/venv
|
||||
```
|
||||
|
||||
#### **Cleanup**
|
||||
|
||||
```bash
|
||||
make clean # Remove host venv and wheelhouse
|
||||
make down # Stop/remove container
|
||||
# Run tests
|
||||
make test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Basic Usage
|
||||
|
||||
Run the tool with subcommands. For help, use `-h` (e.g., `gen -h`, `recover -h`).
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Generate mnemonic (prints to stdout; for debug/test)
|
||||
python ./src/pyhdwallet.py gen
|
||||
# Generate wallet (prints mnemonic - debug mode)
|
||||
python src/pyhdwallet.py gen
|
||||
|
||||
# Generate and save AES-encrypted ZIP artifact into ./.wallet (password prompted)
|
||||
python ./src/pyhdwallet.py gen --file
|
||||
# Generate with off-screen mode + encrypted ZIP
|
||||
python src/pyhdwallet.py gen --off-screen --file
|
||||
|
||||
# Generate with PGP encryption + ZIP artifact (ZIP contains only encrypted .asc payload)
|
||||
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file
|
||||
# Generate with PGP encryption + ZIP
|
||||
python src/pyhdwallet.py gen \
|
||||
--pgp-pubkey-file pubkeys/mykey.asc \
|
||||
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
|
||||
--off-screen \
|
||||
--file
|
||||
|
||||
# Recover from mnemonic (prefer --interactive to avoid shell history)
|
||||
python ./src/pyhdwallet.py recover --interactive
|
||||
# Derive BIP85 child mnemonic (12/15/18/21/24 words)
|
||||
python src/pyhdwallet.py gen-child \
|
||||
--interactive \
|
||||
--words 12 \
|
||||
--index 0
|
||||
|
||||
# Recover and save AES ZIP artifact
|
||||
python ./src/pyhdwallet.py recover --interactive --file
|
||||
# Derive BIP85 child with passphrase + encrypted output
|
||||
python src/pyhdwallet.py gen-child \
|
||||
--mnemonic-stdin \
|
||||
--passphrase \
|
||||
--words 24 \
|
||||
--index 0 \
|
||||
--pgp-pubkey-file pubkeys/mykey.asc \
|
||||
--off-screen \
|
||||
--file
|
||||
|
||||
# Fetch PGP public key (online)
|
||||
python ./src/pyhdwallet.py fetchkey "https://example.com/key.asc" --out mykey.asc
|
||||
# Recover wallet from mnemonic
|
||||
python src/pyhdwallet.py recover --interactive --file
|
||||
|
||||
# Fetch PGP public key (requires internet)
|
||||
python src/pyhdwallet.py fetchkey "https://example.com/key.asc" --out mykey.asc
|
||||
|
||||
# Run tests
|
||||
python ./src/pyhdwallet.py test
|
||||
python src/pyhdwallet.py test
|
||||
pytest -v tests/test_vectors.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Notes on `--file`, AES ZIP, and PGP
|
||||
## 🔐 Security Features
|
||||
|
||||
- `--file` writes **only** an AES-encrypted ZIP (no raw `.json`/`.asc` left on disk), using `pyzipper`.
|
||||
- If `--pgp-pubkey-file` is set, the ZIP contains a single ASCII-armored PGP message (`.asc`) created with PGPy-style `PGPMessage.new(...)` then `pubkey.encrypt(...)`.
|
||||
- **Offline-first**: Network access blocked during key generation/recovery
|
||||
- **BIP85 deterministic entropy**: Derive unlimited child mnemonics from master seed
|
||||
- **Test suite**: Regression tests with frozen vectors ensure derivation logic integrity
|
||||
- **PGP fingerprint pinning**: Prevents key substitution attacks
|
||||
- **TTY safety guard**: Refuses to print secrets when stdout is piped/redirected
|
||||
- **AES-encrypted outputs**: Wallet artifacts encrypted with `pyzipper`
|
||||
- **No shell history leaks**: Use `--interactive` or `--mnemonic-stdin` for recovery
|
||||
- **Interoperable**: BIP85 children compatible with Coldcard, Ian Coleman tool, etc.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ About `--force`
|
||||
## 🛠️ Makefile Targets
|
||||
|
||||
The tool includes a safety guard: if stdout is piped/redirected (non-TTY), it refuses to print sensitive output unless `--force` is set. Checking whether stdout is a terminal is commonly done via `isatty()`.
|
||||
|
||||
Example:
|
||||
**Vendoring (for air-gapped deployment):**
|
||||
|
||||
```bash
|
||||
python ./src/pyhdwallet.py gen > out.txt # likely refused (non-TTY)
|
||||
python ./src/pyhdwallet.py gen --force > out.txt # allowed
|
||||
make vendor-macos # Build macOS ARM64 wheels
|
||||
make vendor-linux # Build Linux x86_64 wheels (Docker)
|
||||
make vendor-all # Build for both platforms
|
||||
make verify-vendor # Test offline installation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Why Use Makefile?
|
||||
|
||||
- **Speed:** Avoid repeated `apt-get` installs; wheels cached locally.
|
||||
- **Reproducibility:** Same Docker image for builds; no environment drift.
|
||||
- **Convenience:** One-liner tasks (`make wheels`, `make venv-host`, `make shell`).
|
||||
- **Separation:** Host venv vs container venv for clean workflows.
|
||||
|
||||
---
|
||||
|
||||
## Common Makefile Targets
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
make build-image # Build Docker image with build tools
|
||||
make wheels # Compile wheels into ./wheelhouse
|
||||
make venv-host # Host venv install from wheelhouse
|
||||
make up # Start warm container
|
||||
make shell # Shell into warm container
|
||||
make venv-container # Container venv at /opt/venv
|
||||
make down # Stop/remove container
|
||||
make clean # Remove .venv_host and wheelhouse
|
||||
make install # Create venv and install dependencies
|
||||
make test # Run test suite
|
||||
make build-image # Build Docker image
|
||||
make shell # Open shell in Docker container
|
||||
make clean # Remove venvs and build artifacts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Full Documentation
|
||||
|
||||
- **[playbook.md](playbook.md)** - Complete command reference and operational guide
|
||||
- **[tests/](tests/)** - Regression test suite documentation
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Recommended Air-Gapped Setup
|
||||
|
||||
For maximum security when generating production wallets:
|
||||
|
||||
1. Use fresh Ubuntu Live USB or Tails OS
|
||||
2. Never connect to network after booting
|
||||
3. Transfer this repository via separate USB
|
||||
4. Run `./install_offline.sh`
|
||||
5. Generate wallet: `python src/pyhdwallet.py gen --off-screen --file`
|
||||
6. Write mnemonic to paper/metal backup
|
||||
7. Transfer encrypted ZIP to secure storage
|
||||
8. Wipe USB drives securely
|
||||
|
||||
See [playbook.md](playbook.md) for detailed air-gapped procedures.
|
||||
|
||||
## 🖥️ Platform Compatibility
|
||||
|
||||
### Makefile Targets
|
||||
|
||||
| Target | macOS | Linux | Notes |
|
||||
| -------- | ------- | ------- | ------- |
|
||||
| `make install` | ✅ | ✅ | Create venv + install deps |
|
||||
| `make test` | ✅ | ✅ | Run test suite |
|
||||
| `make vendor-macos` | ✅ | ❌ | Requires native macOS ARM64 |
|
||||
| `make vendor-linux` | ✅ | ✅ | Uses Docker (any OS) |
|
||||
| `make vendor-all` | ✅ | ⚠️ | Needs macOS for both platforms |
|
||||
| `make binary` | ✅ | ❌ | Requires `build_binary.sh` |
|
||||
| `make binary-linux` | ✅ | ✅ | Uses Docker (any OS) |
|
||||
| `make build-image` | ✅ | ✅ | Docker required |
|
||||
|
||||
### Quick Commands by Platform
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
make info # Show platform info
|
||||
make vendor-current # Build for current platform only
|
||||
make binary-current # Build binary for current platform
|
||||
```
|
||||
|
||||
**Linux:***
|
||||
|
||||
```bash
|
||||
make info # Show platform info
|
||||
make vendor-linux # Build Linux wheels
|
||||
make binary-linux # Build Linux binary
|
||||
```
|
||||
|
||||
***Cross-platform (requires Docker):***
|
||||
|
||||
```bash
|
||||
make vendor-linux # Works on any OS with Docker
|
||||
make binary-linux # Works on any OS with Docker
|
||||
```
|
||||
|
||||
## Quick Test
|
||||
|
||||
```bash
|
||||
# Check platform detection
|
||||
make info
|
||||
|
||||
# Build for your current platform
|
||||
make vendor-current
|
||||
|
||||
# Or just the targets you need
|
||||
make install # Works everywhere
|
||||
make test # Works everywhere
|
||||
```
|
||||
|
||||
## 📦 Binary Distribution
|
||||
|
||||
Pre-built standalone binaries are available for convenience (no Python required):
|
||||
|
||||
### Download Binary
|
||||
|
||||
```bash
|
||||
# macOS ARM64
|
||||
curl -LO https://github.com/user/hdwalletpy/releases/download/v1.1.0/pyhdwallet-macos-arm64
|
||||
chmod +x pyhdwallet-macos-arm64
|
||||
./pyhdwallet-macos-arm64 test
|
||||
|
||||
# Linux x86_64
|
||||
curl -LO https://github.com/user/hdwalletpy/releases/download/v1.1.0/pyhdwallet-linux
|
||||
chmod +x pyhdwallet-linux
|
||||
./pyhdwallet-linux test
|
||||
```
|
||||
|
||||
---
|
||||
## 🆕 What's New in v1.1.0
|
||||
|
||||
- **BIP85 child mnemonic derivation** via new `gen-child` command
|
||||
- Derive 12/15/18/21/24-word child mnemonics from master seed
|
||||
- Full interoperability with BIP85-compatible wallets (Coldcard, etc.)
|
||||
- Optional master BIP39 passphrase support
|
||||
- Verified against official BIP85 test vectors
|
||||
- No new dependencies required (uses existing bip-utils + stdlib)
|
||||
|
||||
---
|
||||
|
||||
115
build-all-platforms.sh
Executable file
115
build-all-platforms.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Building wheels for all platforms using Python 3.12..."
|
||||
echo ""
|
||||
|
||||
# Check Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "ERROR: Docker is not running. Please start Docker Desktop."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. macOS ARM64 (via Docker with Python 3.12)
|
||||
echo "==> Building macOS ARM64 wheels (Docker + Python 3.12)..."
|
||||
docker run --rm \
|
||||
--platform linux/arm64 \
|
||||
-v $(pwd):/work \
|
||||
-w /work \
|
||||
python:3.12-slim \
|
||||
bash -c "
|
||||
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
|
||||
pip install --upgrade pip > /dev/null
|
||||
mkdir -p vendor/macos-arm64-docker
|
||||
pip download --dest vendor/macos-arm64-docker -r requirements.txt
|
||||
pip wheel --wheel-dir vendor/macos-arm64-docker --no-deps vendor/macos-arm64-docker/*.tar.gz 2>/dev/null || true
|
||||
rm -f vendor/macos-arm64-docker/*.tar.gz
|
||||
"
|
||||
echo "✓ macOS ARM64: $(ls vendor/macos-arm64-docker/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
|
||||
# Actually, let's use native Mac Python 3.12 if available, otherwise use what we have
|
||||
echo ""
|
||||
echo "==> Building macOS ARM64 wheels (native - using your .venv312)..."
|
||||
mkdir -p vendor/macos-arm64
|
||||
|
||||
# Use the Python from your working venv
|
||||
if [ -f ".venv312/bin/pip" ]; then
|
||||
echo "Using Python from .venv312..."
|
||||
.venv312/bin/pip download --dest vendor/macos-arm64 -r requirements.txt
|
||||
echo "✓ macOS ARM64: $(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
else
|
||||
echo "ERROR: .venv312 not found!"
|
||||
echo "Please activate your working venv first:"
|
||||
echo " source .venv312/bin/activate"
|
||||
echo " pip download --dest vendor/macos-arm64 -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Linux x86_64 (via Docker)
|
||||
echo ""
|
||||
echo "==> Building Linux x86_64 wheels (Docker + Python 3.12)..."
|
||||
docker run --rm \
|
||||
-v $(pwd):/work \
|
||||
-w /work \
|
||||
python:3.12-slim \
|
||||
bash -c "
|
||||
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
|
||||
pip install --upgrade pip > /dev/null
|
||||
mkdir -p vendor/linux-x86_64
|
||||
pip download --dest vendor/linux-x86_64 -r requirements.txt
|
||||
pip wheel --wheel-dir vendor/linux-x86_64 --no-deps vendor/linux-x86_64/*.tar.gz 2>/dev/null || true
|
||||
rm -f vendor/linux-x86_64/*.tar.gz
|
||||
"
|
||||
echo "✓ Linux x86_64: $(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels"
|
||||
|
||||
# 3. Generate SHA256 checksums
|
||||
echo ""
|
||||
echo "==> Generating checksums..."
|
||||
for platform in macos-arm64 linux-x86_64; do
|
||||
if [ -d "vendor/$platform" ] && [ "$(ls -A vendor/$platform/*.whl 2>/dev/null)" ]; then
|
||||
(cd vendor/$platform && shasum -a 256 *.whl > SHA256SUMS)
|
||||
echo "✓ Checksums for $platform"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. Create provenance file
|
||||
echo ""
|
||||
echo "==> Creating provenance documentation..."
|
||||
cat > vendor/PROVENANCE.md << EOF
|
||||
# Dependency Provenance
|
||||
|
||||
All wheels generated on: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
Python version: 3.12
|
||||
Build machine: $(uname -s) $(uname -m)
|
||||
Docker used: Yes (python:3.12-slim)
|
||||
|
||||
## Platforms
|
||||
- macOS ARM64: Built using .venv312 (Python 3.12)
|
||||
- Linux x86_64: Built using Docker python:3.12-slim
|
||||
|
||||
## Package Versions
|
||||
$(cat requirements.txt | head -20)
|
||||
|
||||
## Integrity Verification
|
||||
Each platform directory contains SHA256SUMS for verification:
|
||||
\`\`\`bash
|
||||
cd vendor/linux-x86_64
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
\`\`\`
|
||||
|
||||
Last updated: $(date -u +"%Y-%m-%d")
|
||||
Built by: $(git config user.name) <$(git config user.email)>
|
||||
Commit: $(git rev-parse --short HEAD 2>/dev/null || echo "not in git")
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "✓ All platforms built successfully!"
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " macOS ARM64: vendor/macos-arm64/ ($(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
echo " Linux x86_64: vendor/linux-x86_64/ ($(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Verify: ls vendor/*/SHA256SUMS"
|
||||
echo " 2. Test: ./install_offline.sh"
|
||||
echo " 3. Commit: git add vendor/ && git commit -m 'vendor: add multi-platform wheels'"
|
||||
76
build_binary.sh
Executable file
76
build_binary.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔨 Building pyhdwallet standalone binary..."
|
||||
|
||||
# Check venv more strictly
|
||||
if [ -z "$VIRTUAL_ENV" ] || [[ "$VIRTUAL_ENV" != *"hdwalletpy"* ]]; then
|
||||
echo "❌ Not in virtual environment!"
|
||||
echo ""
|
||||
echo "Activate it first:"
|
||||
echo " source .venv/bin/activate"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install PyInstaller if needed
|
||||
if ! pip show pyinstaller &>/dev/null; then
|
||||
echo "📦 Installing PyInstaller..."
|
||||
pip install pyinstaller
|
||||
fi
|
||||
|
||||
# Create patched version for frozen builds
|
||||
echo "📝 Creating frozen-compatible version..."
|
||||
python3 << 'PATCH'
|
||||
with open('src/pyhdwallet.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Patch _require() for PyInstaller
|
||||
content = content.replace(
|
||||
"""def _require(mod: str, pkg: str) -> None:
|
||||
try:
|
||||
__import__(mod)""",
|
||||
"""def _require(mod: str, pkg: str) -> None:
|
||||
# Skip check in PyInstaller frozen executable
|
||||
if getattr(sys, 'frozen', False):
|
||||
return
|
||||
try:
|
||||
__import__(mod)"""
|
||||
)
|
||||
|
||||
with open('src/pyhdwallet_frozen.py', 'w') as f:
|
||||
f.write(content)
|
||||
PATCH
|
||||
|
||||
# Clean previous builds
|
||||
rm -rf build/ dist/ *.spec
|
||||
|
||||
# Build
|
||||
echo "🔧 Building with PyInstaller..."
|
||||
pyinstaller --onefile \
|
||||
--name pyhdwallet \
|
||||
--clean \
|
||||
--collect-all bip_utils \
|
||||
--collect-all pgpy \
|
||||
--collect-all nacl \
|
||||
--collect-all pyzipper \
|
||||
--collect-all coincurve \
|
||||
--copy-metadata coincurve \
|
||||
src/pyhdwallet_frozen.py
|
||||
|
||||
# Clean up temp file
|
||||
rm src/pyhdwallet_frozen.py
|
||||
|
||||
# Test
|
||||
echo ""
|
||||
echo "✅ Testing binary..."
|
||||
./dist/pyhdwallet test
|
||||
|
||||
# Show results
|
||||
echo ""
|
||||
echo "🎉 Binary created successfully!"
|
||||
echo " Location: dist/pyhdwallet"
|
||||
echo " Size: $(du -h dist/pyhdwallet | cut -f1)"
|
||||
echo ""
|
||||
echo "Quick test:"
|
||||
echo " ./dist/pyhdwallet gen --help"
|
||||
echo " ./dist/pyhdwallet gen-child --help"
|
||||
70
build_binary_linux.sh
Executable file
70
build_binary_linux.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🐧 Building Linux x86_64 binary via Docker (Ubuntu 22.04 base)..."
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
-v "$PWD":/work \
|
||||
-w /work \
|
||||
ubuntu:22.04 \
|
||||
bash -c '
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install Python 3.12 and build tools
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq software-properties-common
|
||||
add-apt-repository -y ppa:deadsnakes/ppa
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
python3.12 \
|
||||
python3.12-venv \
|
||||
python3.12-dev \
|
||||
binutils \
|
||||
gcc \
|
||||
g++ \
|
||||
make \
|
||||
libffi-dev
|
||||
|
||||
# Install pip for Python 3.12
|
||||
python3.12 -m ensurepip
|
||||
python3.12 -m pip install -q --upgrade pip
|
||||
python3.12 -m pip install -q pyinstaller
|
||||
python3.12 -m pip install -q -r requirements.txt
|
||||
|
||||
echo "📝 Patching for frozen build..."
|
||||
python3.12 -c "
|
||||
import sys
|
||||
with open(\"src/pyhdwallet.py\", \"r\") as f:
|
||||
content = f.read()
|
||||
content = content.replace(
|
||||
\"def _require(mod: str, pkg: str) -> None:\\n try:\",
|
||||
\"def _require(mod: str, pkg: str) -> None:\\n import sys\\n if getattr(sys, \\\"frozen\\\", False): return\\n try:\"
|
||||
)
|
||||
with open(\"/tmp/pyhdwallet_frozen.py\", \"w\") as f:
|
||||
f.write(content)
|
||||
"
|
||||
|
||||
echo "🔧 Building with PyInstaller..."
|
||||
python3.12 -m PyInstaller --onefile --name pyhdwallet-linux --clean \
|
||||
--collect-all bip_utils \
|
||||
--collect-all pgpy \
|
||||
--collect-all nacl \
|
||||
--collect-all pyzipper \
|
||||
--collect-all coincurve \
|
||||
--copy-metadata coincurve \
|
||||
/tmp/pyhdwallet_frozen.py
|
||||
|
||||
echo "✅ Linux binary created (compatible with Ubuntu 22.04+)"
|
||||
'
|
||||
|
||||
echo ""
|
||||
echo "Binary location: dist/pyhdwallet-linux"
|
||||
echo "Size: $(du -h dist/pyhdwallet-linux 2>/dev/null | cut -f1 || echo 'N/A')"
|
||||
echo "GLIBC: $(docker run --rm --platform linux/amd64 -v "$PWD/dist":/b ubuntu:22.04 ldd /b/pyhdwallet-linux | grep libc.so || echo 'static')"
|
||||
echo ""
|
||||
echo "Test it:"
|
||||
echo " docker run --rm --platform linux/amd64 -v \"\$PWD/dist\":/bin ubuntu:22.04 /bin/pyhdwallet-linux test"
|
||||
117
install_offline.sh
Executable file
117
install_offline.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Detect platform
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
PLATFORM="linux-x86_64"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
ARCH=$(uname -m)
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
PLATFORM="macos-arm64"
|
||||
else
|
||||
echo "ERROR: Only macOS ARM64 is supported"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Unsupported platform: $OSTYPE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected platform: $PLATFORM"
|
||||
|
||||
# Check if vendor directory exists
|
||||
if [ ! -d "vendor/$PLATFORM" ]; then
|
||||
echo "ERROR: vendor/$PLATFORM not found!"
|
||||
echo "Run ./build-vendors-simple.sh to generate wheels"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for Python 3.12
|
||||
if ! command -v python3.12 &> /dev/null; then
|
||||
echo "ERROR: Python 3.12 not found!"
|
||||
echo "Please install Python 3.12:"
|
||||
echo " macOS: brew install python@3.12"
|
||||
echo " Linux: apt-get install python3.12 python3.12-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already in a venv
|
||||
if [ -n "$VIRTUAL_ENV" ]; then
|
||||
CURRENT_PYTHON=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
|
||||
if [[ "$CURRENT_PYTHON" != "3.12" ]]; then
|
||||
echo "WARNING: You are in a venv with Python $CURRENT_PYTHON"
|
||||
echo "Deactivating and creating new Python 3.12 venv..."
|
||||
deactivate || true
|
||||
else
|
||||
echo "Already in Python 3.12 venv, using it..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create or use existing venv
|
||||
VENV_DIR=".venv"
|
||||
if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/python3.12" ]; then
|
||||
echo "Creating Python 3.12 virtual environment..."
|
||||
python3.12 -m venv "$VENV_DIR"
|
||||
echo "✓ Virtual environment created: $VENV_DIR"
|
||||
else
|
||||
echo "Using existing venv: $VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate venv
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Verify Python version
|
||||
PYTHON_VERSION=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
|
||||
echo "Using Python $PYTHON_VERSION from: $(which python)"
|
||||
|
||||
if [[ "$PYTHON_VERSION" != "3.12" ]]; then
|
||||
echo "ERROR: Expected Python 3.12, got $PYTHON_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify checksums
|
||||
if [ -f "vendor/$PLATFORM/SHA256SUMS" ]; then
|
||||
echo ""
|
||||
echo "Verifying checksums..."
|
||||
(cd vendor/$PLATFORM && shasum -a 256 -c SHA256SUMS --quiet) || {
|
||||
echo "ERROR: Checksum verification failed!"
|
||||
exit 1
|
||||
}
|
||||
echo "✓ Checksums verified"
|
||||
fi
|
||||
|
||||
# Install
|
||||
echo ""
|
||||
echo "Installing from vendor/$PLATFORM..."
|
||||
pip install --upgrade pip --quiet
|
||||
pip install --no-index --find-links=vendor/$PLATFORM -r requirements.txt
|
||||
|
||||
# Test
|
||||
echo ""
|
||||
echo "Running tests..."
|
||||
python -m pytest -v tests/test_vectors.py || {
|
||||
echo "ERROR: Tests failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "Running built-in smoke test..."
|
||||
python src/pyhdwallet.py test || {
|
||||
echo "ERROR: Smoke test failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✓ Installation complete and verified!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Virtual environment: $VENV_DIR"
|
||||
echo "Python version: $(python --version)"
|
||||
echo ""
|
||||
echo "To activate this environment:"
|
||||
echo " source $VENV_DIR/bin/activate"
|
||||
echo ""
|
||||
echo "Generate a wallet with:"
|
||||
echo " python src/pyhdwallet.py gen --off-screen --file"
|
||||
echo ""
|
||||
615
playbook.md
615
playbook.md
@@ -1,4 +1,4 @@
|
||||
# pyhdwallet v1.0.5 (hdwalletpy)
|
||||
# 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.
|
||||
|
||||
@@ -10,13 +10,18 @@ Repository structure (current):
|
||||
- `bootstrap_vectors.py` — one-time script to generate test vectors
|
||||
- `vectors.json` — frozen expected values (committed)
|
||||
- `data/recipient.asc` — test PGP key (committed)
|
||||
- `.wallet/` — generated wallet artifacts (should be gitignored)
|
||||
- `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
|
||||
|
||||
@@ -27,33 +32,101 @@ Repository structure (current):
|
||||
- **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.11+
|
||||
- Recommended: a virtual environment
|
||||
- Python 3.12.x (required for vendored wheels)
|
||||
- Git (for cloning repository)
|
||||
|
||||
### Setup
|
||||
### Quick Installation (macOS/Linux with Internet)
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -r requirements.txt
|
||||
# Clone repository
|
||||
git clone https://github.com/<your-username>/hdwalletpy.git
|
||||
cd hdwalletpy
|
||||
|
||||
# Run automated installer
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
### Dependencies (top-level intent)
|
||||
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 (only needed when using `--file`)
|
||||
- `pyzipper` — AES-encrypted ZIP writing
|
||||
- `pytest` — test framework (development only)
|
||||
|
||||
---
|
||||
All dependencies are pre-downloaded in `vendor/` for offline use.
|
||||
|
||||
***
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -100,7 +173,7 @@ python ./src/pyhdwallet.py test
|
||||
pytest -v tests/test_vectors.py
|
||||
```
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -113,7 +186,8 @@ The project includes a comprehensive offline test suite that validates critical
|
||||
- **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
|
||||
- **CLI integration**: Smoke tests for `recover` command with correct and incorrect fingerprints
|
||||
- **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
|
||||
|
||||
@@ -154,16 +228,75 @@ 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.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
## Outputs and file structure
|
||||
## Vendored Dependencies
|
||||
|
||||
### Stdout behavior
|
||||
### 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)
|
||||
### `--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`.
|
||||
|
||||
@@ -175,12 +308,12 @@ Override folder:
|
||||
|
||||
- `--wallet-location /path/to/folder`
|
||||
|
||||
Naming uses UTC timestamps (e.g. `20260106_161830Z`):
|
||||
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
|
||||
### 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.
|
||||
@@ -189,10 +322,145 @@ Naming uses UTC timestamps (e.g. `20260106_161830Z`):
|
||||
|
||||
`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.
|
||||
@@ -209,7 +477,7 @@ Options:
|
||||
- `--off-screen`: reduced output / temp-file behavior
|
||||
- `--expected-fingerprint`: refuse if downloaded key fingerprint doesn't match (40 hex chars)
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
### gen (offline)
|
||||
|
||||
@@ -248,7 +516,7 @@ Safety options:
|
||||
- `--off-screen` (don't print sensitive output)
|
||||
- `--force` (only for non-TTY stdout; see below)
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
### recover (offline)
|
||||
|
||||
@@ -274,7 +542,7 @@ 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)
|
||||
|
||||
@@ -290,9 +558,9 @@ For comprehensive regression testing, use the pytest suite:
|
||||
pytest -v tests/test_vectors.py
|
||||
```
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
## When to use `--force`
|
||||
## When to Use `--force`
|
||||
|
||||
Use `--force` only if you **intentionally** want to print sensitive output while stdout is being piped/redirected.
|
||||
|
||||
@@ -314,17 +582,193 @@ 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)
|
||||
## 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
|
||||
|
||||
@@ -339,26 +783,51 @@ chmod +x ./src/pyhdwallet.py
|
||||
./src/pyhdwallet.py gen
|
||||
```
|
||||
|
||||
### "Missing dependency: pyzipper" but pip says installed
|
||||
### Python 3.12 not found
|
||||
|
||||
You likely installed into a different venv. Use:
|
||||
Vendored wheels require Python 3.12:
|
||||
|
||||
```bash
|
||||
python -m pip install pyzipper
|
||||
python -c "import pyzipper; print(pyzipper.__version__)"
|
||||
# macOS
|
||||
brew install python@3.12
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install python3.12 python3.12-venv
|
||||
|
||||
# Verify
|
||||
python3.12 --version
|
||||
```
|
||||
|
||||
### "Missing dependency: pytest" when running tests
|
||||
### "Missing dependency" errors
|
||||
|
||||
Install pytest in your virtualenv:
|
||||
Make sure you're installing from the correct vendor directory:
|
||||
|
||||
```bash
|
||||
python -m pip install pytest
|
||||
# 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 if needed:
|
||||
Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
@@ -373,8 +842,6 @@ print("Extracted to", out_dir)
|
||||
PY
|
||||
```
|
||||
|
||||
AES ZIP support is the reason `pyzipper` is used.
|
||||
|
||||
### Test failures after code changes
|
||||
|
||||
If `pytest -v tests/test_vectors.py` fails after you modified code:
|
||||
@@ -386,10 +853,58 @@ If `pytest -v tests/test_vectors.py` fails after you modified 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).
|
||||
@@ -398,22 +913,12 @@ If `pytest -v tests/test_vectors.py` fails after you modified code:
|
||||
- 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.
|
||||
|
||||
Key additions:
|
||||
|
||||
1. **Testing section** with full coverage explanation
|
||||
2. **Repository structure** updated to include `tests/` folder
|
||||
3. **Test artifacts** documentation
|
||||
4. **When to regenerate vectors** guidance
|
||||
5. **Troubleshooting** section for test failures
|
||||
6. **Changelog** updated with v1.0.5 test suite addition
|
||||
7. **Quick Start** updated with correct `recover` command syntax (`--mnemonic-stdin`)
|
||||
8. **Dependencies** updated to include pytest
|
||||
|
||||
[1](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/50321846/e5caab46-cb40-4f76-8ea9-60ed65bf4927/pyhdwallet.py)
|
||||
[2](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/images/50321846/085d5710-8637-45d5-8686-77c98df9dcdf/image.jpg)
|
||||
|
||||
7
pytest.ini
Normal file
7
pytest.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning:pgpy.constants
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
2
requirements-dev.in
Normal file
2
requirements-dev.in
Normal file
@@ -0,0 +1,2 @@
|
||||
-r requirements.txt # Include production deps
|
||||
pytest>=9.0.0
|
||||
81
requirements-dev.txt
Normal file
81
requirements-dev.txt
Normal file
@@ -0,0 +1,81 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile requirements-dev.in
|
||||
#
|
||||
base58==2.1.1
|
||||
# via -r requirements.txt
|
||||
bip-utils==2.10.0
|
||||
# via -r requirements.txt
|
||||
cbor2==5.8.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# cryptography
|
||||
# pynacl
|
||||
coincurve==21.0.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
crcmod==1.7
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
cryptography==46.0.3
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pgpy
|
||||
ecdsa==0.19.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
ed25519-blake2b==1.4.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
iniconfig==2.3.0
|
||||
# via pytest
|
||||
packaging==25.0
|
||||
# via pytest
|
||||
pgpy==0.6.0
|
||||
# via -r requirements.txt
|
||||
pluggy==1.6.0
|
||||
# via pytest
|
||||
py-sr25519-bindings==0.2.3
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pgpy
|
||||
pycparser==2.23
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# cffi
|
||||
pycryptodome==3.23.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pycryptodomex==3.23.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# pyzipper
|
||||
pygments==2.19.2
|
||||
# via pytest
|
||||
pynacl==1.6.2
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# bip-utils
|
||||
pytest==9.0.2
|
||||
# via -r requirements-dev.in
|
||||
pyzipper==0.3.6
|
||||
# via -r requirements.txt
|
||||
six==1.17.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# ecdsa
|
||||
@@ -1,6 +1,8 @@
|
||||
base58==2.1.1
|
||||
bip-utils==2.10.0
|
||||
pgpy==0.6.0
|
||||
pip-chill==1.0.3
|
||||
pip-tools==7.5.2
|
||||
pyzipper==0.3.6
|
||||
# Core crypto libraries
|
||||
bip-utils>=2.9.0
|
||||
PGPy>=0.6.0
|
||||
pynacl>=1.5.0
|
||||
base58>=2.1.1
|
||||
|
||||
# ZIP encryption
|
||||
pyzipper>=0.3.6
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
base58==2.1.1
|
||||
# via -r requirements.in
|
||||
bip-utils==2.10.0
|
||||
# via -r requirements.in
|
||||
build==1.3.0
|
||||
# via pip-tools
|
||||
cbor2==5.8.0
|
||||
# via bip-utils
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# cryptography
|
||||
# pynacl
|
||||
click==8.3.1
|
||||
# via pip-tools
|
||||
coincurve==21.0.0
|
||||
# via bip-utils
|
||||
crcmod==1.7
|
||||
@@ -28,14 +24,8 @@ ecdsa==0.19.1
|
||||
# via bip-utils
|
||||
ed25519-blake2b==1.4.1
|
||||
# via bip-utils
|
||||
packaging==25.0
|
||||
# via build
|
||||
pgpy==0.6.0
|
||||
# via -r requirements.in
|
||||
pip-chill==1.0.3
|
||||
# via -r requirements.in
|
||||
pip-tools==7.5.2
|
||||
# via -r requirements.in
|
||||
py-sr25519-bindings==0.2.3
|
||||
# via bip-utils
|
||||
pyasn1==0.6.1
|
||||
@@ -47,18 +37,10 @@ pycryptodome==3.23.0
|
||||
pycryptodomex==3.23.0
|
||||
# via pyzipper
|
||||
pynacl==1.6.2
|
||||
# via bip-utils
|
||||
pyproject-hooks==1.2.0
|
||||
# via
|
||||
# build
|
||||
# pip-tools
|
||||
# -r requirements.in
|
||||
# bip-utils
|
||||
pyzipper==0.3.6
|
||||
# via -r requirements.in
|
||||
six==1.17.0
|
||||
# via ecdsa
|
||||
wheel==0.45.1
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pyhdwallet v1.0.5 (Python 3.11+)
|
||||
pyhdwallet v1.1.0 (Python 3.11+)
|
||||
|
||||
Commands:
|
||||
- fetchkey (online): download ASCII-armored PGP public key and print SHA256 + fingerprint
|
||||
@@ -15,7 +15,7 @@ Behavior (current):
|
||||
- ZIP password is prompted via getpass; if echo-hiding fails, fallback to input() with loud warning.
|
||||
- When stdout is not a TTY, refuse to print sensitive data unless --force is provided.
|
||||
|
||||
Recover input policy (v1.0.5):
|
||||
Recover input policy (v1.1.0):
|
||||
- Seed recovery removed (mnemonic only).
|
||||
- No plaintext mnemonic via CLI args.
|
||||
- Use either:
|
||||
@@ -42,7 +42,24 @@ import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import sys
|
||||
|
||||
def _is_frozen():
|
||||
"""Check if running as PyInstaller bundle"""
|
||||
return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
def _require(mod: str, pkg: str) -> None:
|
||||
# In frozen executable, skip the check (assume deps are bundled)
|
||||
if _is_frozen():
|
||||
return
|
||||
|
||||
try:
|
||||
__import__(mod)
|
||||
except ImportError:
|
||||
print(f"❌ Missing dependency: {pkg}", file=sys.stderr)
|
||||
print(f"Install with: pip install {pkg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dependency checks
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -274,6 +291,135 @@ def get_master_fingerprint(seed_bytes: bytes) -> str:
|
||||
h160 = Hash160.QuickDigest(pubkey_bytes)
|
||||
return h160[:4].hex().upper()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# BIP85 helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def bip85_path(words: int, index: int, language: int = 0) -> str:
|
||||
"""
|
||||
Build BIP85 derivation path for BIP39 application.
|
||||
|
||||
BIP85 path format: m/83696968'/39'/language'/words'/index'
|
||||
For English (language=0): m/83696968'/39'/0'/words'/index'
|
||||
|
||||
Args:
|
||||
words: Child mnemonic word count (12/15/18/21/24)
|
||||
index: Derivation index
|
||||
language: BIP39 language (0=English, currently only 0 supported)
|
||||
|
||||
Returns:
|
||||
BIP85 path string
|
||||
"""
|
||||
if words not in {12, 15, 18, 21, 24}:
|
||||
raise ValueError(f"Invalid word count for BIP85: {words}")
|
||||
if index < 0:
|
||||
raise ValueError(f"BIP85 index must be >= 0, got {index}")
|
||||
return f"m/83696968'/39'/{language}'/{words}'/{index}'"
|
||||
|
||||
|
||||
def bip85_derive_entropy(seed_bytes: bytes, path: str) -> bytes:
|
||||
"""
|
||||
Derive 64-byte BIP85 entropy from master seed using specified path.
|
||||
|
||||
BIP85 spec:
|
||||
1. Derive BIP32 extended private key at path
|
||||
2. Extract 32-byte private key k
|
||||
3. HMAC-SHA512(key="bip-entropy-from-k", msg=k) -> 64 bytes
|
||||
|
||||
Args:
|
||||
seed_bytes: Master BIP39 seed (typically 64 bytes)
|
||||
path: BIP85 derivation path
|
||||
|
||||
Returns:
|
||||
64-byte entropy
|
||||
"""
|
||||
from bip_utils import Bip32Slip10Secp256k1
|
||||
import hmac
|
||||
|
||||
# Derive BIP32 node at path
|
||||
master = Bip32Slip10Secp256k1.FromSeed(seed_bytes)
|
||||
child_node = master.DerivePath(path)
|
||||
|
||||
# Extract 32-byte private key
|
||||
privkey_bytes = child_node.PrivateKey().Raw().ToBytes()
|
||||
if len(privkey_bytes) != 32:
|
||||
raise ValueError(f"BIP85: Expected 32-byte private key, got {len(privkey_bytes)}")
|
||||
|
||||
# HMAC-SHA512 with specific key
|
||||
hmac_key = b"bip-entropy-from-k"
|
||||
entropy64 = hmac.digest(hmac_key, privkey_bytes, hashlib.sha512)
|
||||
|
||||
if len(entropy64) != 64:
|
||||
raise ValueError(f"BIP85: HMAC output should be 64 bytes, got {len(entropy64)}")
|
||||
|
||||
return entropy64
|
||||
|
||||
|
||||
def bip85_entropy_to_mnemonic(entropy64: bytes, words: int) -> str:
|
||||
"""
|
||||
Convert BIP85 64-byte entropy to BIP39 mnemonic.
|
||||
|
||||
Truncates entropy64 to required length for specified word count:
|
||||
- 12 words: 16 bytes (128 bits)
|
||||
- 15 words: 20 bytes (160 bits)
|
||||
- 18 words: 24 bytes (192 bits)
|
||||
- 21 words: 28 bytes (224 bits)
|
||||
- 24 words: 32 bytes (256 bits)
|
||||
|
||||
Args:
|
||||
entropy64: 64-byte BIP85 entropy
|
||||
words: Target mnemonic word count
|
||||
|
||||
Returns:
|
||||
BIP39 English mnemonic string
|
||||
"""
|
||||
from bip_utils import Bip39MnemonicGenerator, Bip39Languages, Bip39EntropyGenerator
|
||||
|
||||
words_to_bytes = {12: 16, 15: 20, 18: 24, 21: 28, 24: 32}
|
||||
|
||||
if words not in words_to_bytes:
|
||||
raise ValueError(f"Invalid word count: {words}")
|
||||
|
||||
if len(entropy64) != 64:
|
||||
raise ValueError(f"BIP85: Expected 64-byte entropy, got {len(entropy64)}")
|
||||
|
||||
# Truncate to required length
|
||||
required_bytes = words_to_bytes[words]
|
||||
truncated_entropy = entropy64[:required_bytes]
|
||||
|
||||
# Generate mnemonic from truncated entropy
|
||||
mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(truncated_entropy)
|
||||
|
||||
return str(mnemonic)
|
||||
|
||||
|
||||
def bip85_derive_child_mnemonic(
|
||||
master_seed_bytes: bytes,
|
||||
child_words: int,
|
||||
index: int
|
||||
) -> Tuple[str, str, bytes, bytes]:
|
||||
"""
|
||||
Complete BIP85 derivation: master seed -> child mnemonic.
|
||||
|
||||
Args:
|
||||
master_seed_bytes: Master BIP39 seed
|
||||
child_words: Child mnemonic word count (12/15/18/21/24)
|
||||
index: BIP85 index
|
||||
|
||||
Returns:
|
||||
Tuple of (bip85_path, child_mnemonic, entropy64, truncated_entropy)
|
||||
"""
|
||||
path = bip85_path(child_words, index, language=0)
|
||||
entropy64 = bip85_derive_entropy(master_seed_bytes, path)
|
||||
|
||||
# Calculate truncated entropy for test vectors
|
||||
words_to_bytes = {12: 16, 15: 20, 18: 24, 21: 28, 24: 32}
|
||||
truncated_entropy = entropy64[:words_to_bytes[child_words]]
|
||||
|
||||
child_mnemonic = bip85_entropy_to_mnemonic(entropy64, child_words)
|
||||
|
||||
return path, child_mnemonic, entropy64, truncated_entropy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PGP helpers (PGPy)
|
||||
@@ -315,7 +461,7 @@ def pgp_fingerprint(armored: str) -> str:
|
||||
|
||||
|
||||
def fetch_ascii_armored_text(url: str, timeout: int = 15) -> Tuple[str, bytes]:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "pyhdwallet/1.0.5"})
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "pyhdwallet/1.0.6"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
data = resp.read()
|
||||
text = data.decode("utf-8", errors="strict")
|
||||
@@ -610,7 +756,7 @@ def build_payload_gen(
|
||||
addresses: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"version": "pyhdwallet v1.0.5",
|
||||
"version": "pyhdwallet v1.1.0",
|
||||
"purpose": "generated mnemonic backup",
|
||||
"mnemonic": mnemonic,
|
||||
"passphrase_used": passphrase_used,
|
||||
@@ -634,7 +780,7 @@ def build_payload_recover(
|
||||
addresses: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"version": "pyhdwallet v1.0.5",
|
||||
"version": "pyhdwallet v1.1.0",
|
||||
"purpose": "recovery payload",
|
||||
"master_fingerprint": fp,
|
||||
"solana_profile": sol_profile,
|
||||
@@ -658,10 +804,56 @@ def deterministic_inner_name(is_encrypted: bool, ts: str) -> str:
|
||||
return f"encrypted_wallet_{ts}.asc"
|
||||
return f"test_wallet_{ts}.json"
|
||||
|
||||
|
||||
def deterministic_zip_name(prefix: str, ts: str) -> str:
|
||||
return f"{prefix}_{ts}.zip"
|
||||
|
||||
def build_payload_gen_child(
|
||||
master_fingerprint: str,
|
||||
master_word_count: int,
|
||||
child_mnemonic: str,
|
||||
child_word_count: int,
|
||||
index: int,
|
||||
bip85_path: str,
|
||||
passphrase_used: bool,
|
||||
passphrase_hint: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build JSON payload for gen-child output.
|
||||
|
||||
Args:
|
||||
master_fingerprint: Master seed fingerprint (8 hex chars)
|
||||
master_word_count: Master mnemonic word count
|
||||
child_mnemonic: Derived child mnemonic
|
||||
child_word_count: Child mnemonic word count
|
||||
index: BIP85 index
|
||||
bip85_path: Full BIP85 derivation path
|
||||
passphrase_used: Whether master passphrase was used
|
||||
passphrase_hint: Optional passphrase hint
|
||||
|
||||
Returns:
|
||||
Payload dict ready for JSON serialization
|
||||
"""
|
||||
return {
|
||||
"version": "pyhdwallet v1.1.0",
|
||||
"purpose": "BIP85 derived child mnemonic",
|
||||
"master_fingerprint": master_fingerprint,
|
||||
"master_word_count": master_word_count,
|
||||
"passphrase_used": passphrase_used,
|
||||
"passphrase_hint": passphrase_hint,
|
||||
"bip85_metadata": {
|
||||
"path": bip85_path,
|
||||
"index": index,
|
||||
"language": 0, # English-only in v1
|
||||
"child_word_count": child_word_count,
|
||||
},
|
||||
"child_mnemonic": child_mnemonic,
|
||||
"interoperability_note": (
|
||||
"This child mnemonic is derived using BIP85 and is interoperable "
|
||||
"with any BIP85-compatible tool given the same master mnemonic, "
|
||||
"passphrase, and derivation parameters."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Commands
|
||||
@@ -881,6 +1073,160 @@ def cmd_recover(args) -> None:
|
||||
seed_bytes = None
|
||||
passphrase = None
|
||||
|
||||
def cmd_gen_child(args) -> None:
|
||||
"""
|
||||
Derive child BIP39 mnemonic from master using BIP85.
|
||||
|
||||
Offline command - runs under NetworkGuard.
|
||||
Input: master mnemonic via --interactive or --mnemonic-stdin
|
||||
Output: derived child mnemonic + metadata
|
||||
"""
|
||||
with NetworkGuard("gen-child"):
|
||||
require_for_offline([]) # Only need bip_utils, no chain-specific deps
|
||||
|
||||
# Validate passphrase-hint requires passphrase
|
||||
if args.passphrase_hint and not args.passphrase:
|
||||
raise ValueError("--passphrase-hint requires --passphrase")
|
||||
|
||||
# Validate index
|
||||
if args.index < 0:
|
||||
raise ValueError(f"--index must be >= 0, got {args.index}")
|
||||
|
||||
if args.off_screen:
|
||||
print("⚠️ Off-screen mode enabled: Child mnemonic will not be printed to stdout.")
|
||||
else:
|
||||
# Print warning banner (similar to gen)
|
||||
warn_gen_stdout_banner()
|
||||
require_tty_or_force(args.force, "derived child mnemonic to stdout")
|
||||
|
||||
from bip_utils import Bip39MnemonicValidator, Bip39SeedGenerator
|
||||
|
||||
# Read master mnemonic
|
||||
if args.interactive:
|
||||
print("📍 Enter MASTER mnemonic (word-by-word):")
|
||||
master_mnemonic = interactive_mnemonic_word_by_word()
|
||||
elif args.mnemonic_stdin:
|
||||
master_mnemonic = _read_stdin_all()
|
||||
if not master_mnemonic:
|
||||
raise ValueError("Empty stdin for --mnemonic-stdin")
|
||||
else:
|
||||
raise ValueError("Missing input mode (use --interactive / --mnemonic-stdin)")
|
||||
|
||||
# Validate master mnemonic
|
||||
if not Bip39MnemonicValidator().IsValid(master_mnemonic):
|
||||
raise ValueError("Invalid master BIP39 mnemonic")
|
||||
|
||||
# Count master words
|
||||
master_word_count = len(master_mnemonic.strip().split())
|
||||
|
||||
# Get master passphrase if requested
|
||||
passphrase = ""
|
||||
if args.passphrase:
|
||||
passphrase = getpass.getpass("Enter master BIP39 passphrase (hidden): ")
|
||||
|
||||
# Derive master seed
|
||||
master_seed_bytes = Bip39SeedGenerator(master_mnemonic).Generate(passphrase)
|
||||
master_fp = get_master_fingerprint(master_seed_bytes)
|
||||
|
||||
# BIP85 derivation
|
||||
print(f"📍 Deriving BIP85 child mnemonic ({args.words} words, index={args.index})...")
|
||||
bip85_path_str, child_mnemonic, entropy64, truncated_entropy = bip85_derive_child_mnemonic(
|
||||
master_seed_bytes,
|
||||
args.words,
|
||||
args.index
|
||||
)
|
||||
|
||||
# Print metadata and child mnemonic (unless off-screen)
|
||||
if not args.off_screen:
|
||||
print("\n" + "=" * 80)
|
||||
print("BIP85 DERIVED CHILD MNEMONIC")
|
||||
print("=" * 80)
|
||||
print(f"Master Fingerprint: {master_fp}")
|
||||
print(f"Master Word Count: {master_word_count}")
|
||||
print(f"Master Passphrase Used: {'Yes' if passphrase else 'No'}")
|
||||
if args.passphrase_hint:
|
||||
print(f"Passphrase Hint: {args.passphrase_hint}")
|
||||
print(f"\nBIP85 Derivation Path: {bip85_path_str}")
|
||||
print(f"Child Index: {args.index}")
|
||||
print(f"Child Word Count: {args.words}")
|
||||
print(f"\nDerived Child Mnemonic:\n{child_mnemonic}")
|
||||
print("\n" + "=" * 80)
|
||||
print("INTEROPERABILITY NOTE:")
|
||||
print("This child mnemonic is reproducible with any BIP85-compatible tool")
|
||||
print("given the same master mnemonic, passphrase, and derivation parameters.")
|
||||
print("=" * 80 + "\n")
|
||||
else:
|
||||
# In off-screen mode, print metadata only
|
||||
print(f"Master Fingerprint: {master_fp}")
|
||||
print(f"Master Word Count: {master_word_count}")
|
||||
print(f"Master Passphrase Used: {'Yes' if passphrase else 'No'}")
|
||||
print(f"BIP85 Path: {bip85_path_str}")
|
||||
print(f"Child Index: {args.index}")
|
||||
print(f"Child Word Count: {args.words}")
|
||||
print("✅ Derivation complete (child mnemonic suppressed in off-screen mode)")
|
||||
|
||||
# File output if requested
|
||||
if args.file:
|
||||
require_for_zip()
|
||||
ts = utc_timestamp_compact()
|
||||
is_pgp = bool(args.pgp_pubkey_file)
|
||||
|
||||
# Build payload
|
||||
payload = build_payload_gen_child(
|
||||
master_fingerprint=master_fp,
|
||||
master_word_count=master_word_count,
|
||||
child_mnemonic=child_mnemonic,
|
||||
child_word_count=args.words,
|
||||
index=args.index,
|
||||
bip85_path=bip85_path_str,
|
||||
passphrase_used=bool(passphrase),
|
||||
passphrase_hint=args.passphrase_hint or "",
|
||||
)
|
||||
|
||||
wallet_dir = Path(args.wallet_location) if args.wallet_location else default_wallet_dir()
|
||||
ensure_dir(wallet_dir)
|
||||
|
||||
if is_pgp:
|
||||
# PGP-encrypt inner content
|
||||
with open(args.pgp_pubkey_file, "r", encoding="utf-8") as f:
|
||||
pub = f.read()
|
||||
fpr = pgp_fingerprint(pub)
|
||||
require_fingerprint_match(fpr, args.expected_fingerprint, "encrypt(gen-child)")
|
||||
|
||||
inner_name = f"bip85_child_{ts}.asc"
|
||||
inner_text = pgp_encrypt_ascii_armored(pub, payload, ignore_usage_flags=args.pgp_ignore_usage_flags)
|
||||
inner_bytes = inner_text.encode("utf-8")
|
||||
zip_prefix = "bip85_child_encrypted"
|
||||
else:
|
||||
# Plain JSON inner content
|
||||
inner_name = f"bip85_child_{ts}.json"
|
||||
inner_text = json.dumps(payload, indent=2, ensure_ascii=False)
|
||||
inner_bytes = inner_text.encode("utf-8")
|
||||
zip_prefix = "bip85_child"
|
||||
|
||||
# Get ZIP password
|
||||
if args.zip_password_mode == "prompt":
|
||||
zip_password = prompt_zip_password_hidden_or_warn()
|
||||
else:
|
||||
zip_password = gen_base58_password(args.zip_password_len)
|
||||
if args.show_generated_password:
|
||||
print(f"ZIP password (auto-generated, base58): {zip_password}", file=sys.stderr)
|
||||
|
||||
# Write AES-encrypted ZIP
|
||||
zip_name = deterministic_zip_name(zip_prefix, ts)
|
||||
zip_path = wallet_dir / zip_name
|
||||
write_aes_zip(zip_path, inner_name, inner_bytes, zip_password)
|
||||
|
||||
print(f"✅ Wrote AES-encrypted ZIP: {zip_path}")
|
||||
print(f" Contains: {inner_name}")
|
||||
|
||||
# Clear sensitive data
|
||||
if args.off_screen:
|
||||
master_mnemonic = None
|
||||
master_seed_bytes = None
|
||||
passphrase = None
|
||||
child_mnemonic = None
|
||||
|
||||
|
||||
def cmd_test(args) -> None:
|
||||
with NetworkGuard("test"):
|
||||
@@ -915,8 +1261,9 @@ def cmd_test(args) -> None:
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="pyhdwallet v1.0.5 - Secure HD Wallet Tool")
|
||||
parser.add_argument("--version", action="version", version="pyhdwallet v1.0.5")
|
||||
parser = argparse.ArgumentParser(description="pyhdwallet v1.1.0 - Secure HD Wallet Tool")
|
||||
parser.add_argument("--version", action="version", version="pyhdwallet v1.1.0")
|
||||
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
p_fetch = sub.add_parser("fetchkey", help="Download ASCII-armored PGP pubkey from URL (online)")
|
||||
@@ -983,6 +1330,54 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
p_rec = sub.add_parser("recover", help="Recover addresses from mnemonic (offline)")
|
||||
add_common(p_rec)
|
||||
|
||||
p_child = sub.add_parser("gen-child", help="Derive child BIP39 mnemonic from master using BIP85 (offline)")
|
||||
|
||||
# Input modes (mutually exclusive, required)
|
||||
g_child = p_child.add_mutually_exclusive_group(required=True)
|
||||
g_child.add_argument("--interactive", action="store_true",
|
||||
help="Guided master mnemonic entry (English-only, per-word validation)")
|
||||
g_child.add_argument("--mnemonic-stdin", dest="mnemonic_stdin", action="store_true",
|
||||
help="Read master BIP39 mnemonic from stdin (non-interactive)")
|
||||
|
||||
# Child parameters
|
||||
p_child.add_argument("--words", type=int, choices=[12, 15, 18, 21, 24], default=12,
|
||||
help="Word count for derived child mnemonic (default: 12)")
|
||||
p_child.add_argument("--index", type=int, default=0,
|
||||
help="BIP85 derivation index (default: 0)")
|
||||
|
||||
# Passphrase
|
||||
p_child.add_argument("--passphrase", action="store_true",
|
||||
help="Prompt for master BIP39 passphrase interactively")
|
||||
p_child.add_argument("--passphrase-hint", default="",
|
||||
help="Hint only; never store the passphrase itself")
|
||||
|
||||
# Output modes
|
||||
p_child.add_argument("--force", action="store_true",
|
||||
help="Allow printing sensitive output even when stdout is not a TTY (dangerous).")
|
||||
p_child.add_argument("--off-screen", action="store_true",
|
||||
help="Suppress printing derived child mnemonic to stdout.")
|
||||
|
||||
# File output
|
||||
p_child.add_argument("--file", action="store_true",
|
||||
help="Write output to AES-encrypted ZIP in ./.wallet (deterministic name).")
|
||||
p_child.add_argument("--wallet-location", default="",
|
||||
help="Override default ./.wallet folder for --file output.")
|
||||
p_child.add_argument("--zip-password-mode", choices=["prompt", "auto"], default="prompt",
|
||||
help="ZIP password mode: prompt or auto-generate base58.")
|
||||
p_child.add_argument("--zip-password-len", type=int, default=12,
|
||||
help="Password length when --zip-password-mode auto is used.")
|
||||
p_child.add_argument("--show-generated-password", action="store_true",
|
||||
help="When using --zip-password-mode auto, print generated password to stderr.")
|
||||
|
||||
# PGP encryption (optional)
|
||||
p_child.add_argument("--pgp-pubkey-file", default=None,
|
||||
help="Path to ASCII-armored PGP public key for inner payload encryption")
|
||||
p_child.add_argument("--pgp-ignore-usage-flags", action="store_true",
|
||||
help="Ignore PGP key usage flags when encrypting")
|
||||
p_child.add_argument("--expected-fingerprint", default="",
|
||||
help="Refuse if PGP recipient key fingerprint does not match.")
|
||||
|
||||
g = p_rec.add_mutually_exclusive_group(required=True)
|
||||
g.add_argument("--interactive", action="store_true", help="Guided mnemonic entry (English-only, per-word validation)")
|
||||
g.add_argument("--mnemonic-stdin", dest="mnemonic_stdin", action="store_true", help="Read BIP39 mnemonic from stdin (non-interactive)")
|
||||
@@ -1007,6 +1402,9 @@ def main() -> None:
|
||||
if args.cmd == "recover":
|
||||
cmd_recover(args)
|
||||
return
|
||||
if args.cmd == "gen-child":
|
||||
cmd_gen_child(args)
|
||||
return
|
||||
if args.cmd == "test":
|
||||
cmd_test(args)
|
||||
return
|
||||
|
||||
52
tests/data/recipient.asc
Normal file
52
tests/data/recipient.asc
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGE3cD4BEADRf2MGzsF6lV/vtEZLSz+kMs7LWHfxycrwhK1rlqAEblMJdDrs
|
||||
odQ/Ep8y03TQ6iQfOaZHfthofvvEfYpNMEPRorvGDoW400haAooAxS4ieASy+YtB
|
||||
lUDQIF3HIk3cbTFDjsXsJhKDNhHxPmCnngwXz6l8xdz6JJr7QjUuprfoWztPwzQ2
|
||||
5M77jYi3M5K7e35b5dbvWq7hCsVoeYyj9BO+OVC48F/qfb91Ht9EgbBVpJK6Y9tN
|
||||
OIceRPoYw08WUCKM6hNRb8Nf0N+PY9edboAAEUhjkaCj64cGAzGhFBC4VCq3Dtc0
|
||||
GGQwlIGrooRhsghKlNKkCd2en3R+icMsbPdEdd3LpNF+iHAM/btZ30Wdw+woQQQR
|
||||
aQFf446Dshmu4d4pKTsHypw2aTmoqKzZ0ow/fKTq1MaASTezU3eGAQa7fr3n1xrY
|
||||
plWJISOcguJx/NO+XlW8B4AMW11xeeWoCvpxeGWuGk1SFa8RI7ExD4WKN+MCGkSE
|
||||
UZT0fQfSkQjr3fIw2q2WVjgkFN4Vu20e9ea4ZcSaeZvF6auj26Ypfa6JG+usOxDR
|
||||
fjNQ5II/FKrlZl/Ry9gDui6uvWbLQjEinA3hfDCzk33uIWjg3PU73bdh87vDhqFa
|
||||
NkF49Go6+J7AaeOAp6FgMwxLAh5HrvxNSsDhcApv82XhEo3EGX+M5vpnqQARAQAB
|
||||
tB1LY2MgTGVvIEMgPGtjY2xlb2NAZ21haWwuY29tPokCWAQTAQgAQhYhBKJ7lvKx
|
||||
abVJEBPS2okrgiwUqaoYBQJhN3A+AhsDBQkSzAMABQsJCAcCAyICAQYVCgkICwIE
|
||||
FgIDAQIeBwIXgAAKCRCJK4IsFKmqGK4ND/9ID2dnOBrobB8Y3hzwwVT1yvztODjE
|
||||
n3jA3F78cZhDaBJvcNFcmDHFyZSLsR4gtkTREuRtjF4bb/bN0lQsiwgTSSJ04bZm
|
||||
0Its04EQuZQw9hl/XxQEka+7GOi1jcUq/9n7A2DS1RkQ9kElJlR4MyvVUwohMmNA
|
||||
y1vkKMQrME2PGeqQ0mp3tlIpBBohEQzwB0d+dxjVyjpfiDVAMjD5OBf/tI1sND/R
|
||||
7fIDYTxOh8pDRWH4kJDr8LnDehGXPKab9IjK15DXzTS9FQ27QI654pZDlhZmj0GG
|
||||
fZvKb8EhjB0FeKiP+RnaTFI+fLd8iIi7aoTSiPqst1F7kDNi9xaqq08NA0tZwvRt
|
||||
GHKcdmZ+GF9Nui7j6X30oGPpUAccH033px4iVQB78qS5ZGAyIerW54gumGLxqQh7
|
||||
5ozIRxjjfMdD+R9ANa5hwQnv1nXlS0wOgzok0b86LV7Lid7NZ/1RO3lIt2CDFbZS
|
||||
OsmMy5XCeup4KT+lJyGqOCVznIzUDUA2ZQoX2cBxwPt60PFIxnCGnf8DAZAjY/WI
|
||||
6w+c4r+tnkWezDZ6T8P6+Vg+EgLg3ZJ1ZY4S7wFGCIQYxdZfBeIy1RmsHdA/jzOd
|
||||
zMsj0u+BJwbrvj8It+10n0mh/VaIhsK0/Mowf78/NvRziMZ3C9Y5g4YZxwK1f2x8
|
||||
B2tKo0QmtpTz9bkCDQRhN3A+ARAAtQRA2DXVIQBodQveLdng83EjsWEKBRZLeqAm
|
||||
KXjSe8LxsgjL36cttXvlA0UuD/dTa83KYrROhNzuSlGGQ1eDKl+VAgBI3Qbw3fTs
|
||||
ahK4vHX6cc3V4esPaUZU7P/xBzmhro/hHrF5vLMv7Sa4iNTJgdgez6uB9hccL0BD
|
||||
Ro8nO8qzZQJJnLFWy9Lk5jsiLxij7vvRFCTsUpHCeNIJ+/EHvn5oXXb775WYzTs0
|
||||
zRT2cZEJ1z8Ji+TGQUoukg2KBBwPMUI7ilscf5IbIJmXuWkyHBJa/PqrKuYuM8DC
|
||||
Iur+CdlInw4yet/i3y0t8wwrhaigz1kHURSExO1wasEmqlkCDcQsYfjF0Edyf22H
|
||||
CY/9vdX51Hf3p9KOi8lWclKYzWHMbX3PNEH5L3WRV6MxEFt8zCaue/3htAkn5Bts
|
||||
+AJ4a6bdBrO7zj8ZR9Xx/PsDRcH+vJL/mbiYsvyO2UJAoeS4Vf6/pG040sEfSNUB
|
||||
+FtbXgqqA9tjORKLekxKA4ZG1FIhLjL4JRJ8FrqMDmh2Cj7f0t4dI3Ubm8UymhHH
|
||||
t2771PE1vov0gKdtpyqWx/5MErdggiglI2lebBeefMB+NQSJJM7h3RrGpNTezmnx
|
||||
QBcGIgTrHVrXEaXUSV2/QdJwJicuTxROAf4wLhEf1OxPEzpgqvPjh7aAL+9+cxom
|
||||
scY+S4MAEQEAAYkCPAQYAQgAJhYhBKJ7lvKxabVJEBPS2okrgiwUqaoYBQJhN3A+
|
||||
AhsMBQkSzAMAAAoJEIkrgiwUqaoYaVcP/i0PF32Ri5WSy0CMN4nLgsEt1kOwpfQj
|
||||
8VaJl6YIxyHluCa2yXpc3GP4XuvP/GKO6ODRhlBWOQn532jDpFdkJnef/z0L9hct
|
||||
55/qxXc+7X65cw6T6ZfEJVCx4jt9mhm5g+1XOnVG+E/7DL97+AnKUYVepi2z6jfv
|
||||
Ql6HosEjXMRWwF8wF+5fwieqMA8yldkdzHcSb3fiWYycQUGPyXdJGht0mXrmfMh+
|
||||
4EUpHpu75ott+vj/yVFvDKWu1bhznA7Ma9HASV1QWmIvywNYUGcDcXEPVoYjTmBK
|
||||
Dz6S5sWWeTjJo+GPOfgF8BLK3xkZy3vULsAUq5xu9ryyvY2MdZeMN3RrJ7aRmFhh
|
||||
HZU+MK17LNWF3IqqJPJdJvV94mKouqHTyV7BGMs2y1eTC7mZBvmcZpm2ECqmg8RQ
|
||||
WXFospGZmcXYQVsgNIGy3nonzgF2EaUEqYKfl1P4zQvMOio79svp/0TuJVi9I/AW
|
||||
p+HbSJ6r75vxpBnk18N00wy5h3dhuc3MfiEnlXKbbr8Zstb9tJLy8Bm0KihDUyQX
|
||||
qFGnHPzRO36fh4eUkTlShzMrx9+bzkVTsjzK3dVviPnnNykfR63DJSuEOxT5kVPG
|
||||
geKpo+1BeSAXduK2SpEVvNBYlZFNki6sbuCvcGd49FTq0ZKFvUWtNTTvGSugeOSb
|
||||
gohx1u+yjx1L
|
||||
=vol8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
91
tests/generate_bip85_vectors.py
Executable file
91
tests/generate_bip85_vectors.py
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate BIP85 test vectors for vectors.json
|
||||
|
||||
Run this from the repo root:
|
||||
python3 tests/generate_bip85_vectors.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
import pyhdwallet
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
import json
|
||||
|
||||
# Test cases to generate
|
||||
test_cases = [
|
||||
{
|
||||
"description": "BIP85 test case 1: 12-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 2: 18-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 18,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 3: 24-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 24,
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 4: 12-word child, WITH passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "TREZOR",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
},
|
||||
]
|
||||
|
||||
print("Generating BIP85 test vectors...\n")
|
||||
print("=" * 80)
|
||||
|
||||
vectors = []
|
||||
|
||||
for case in test_cases:
|
||||
print(f"\n{case['description']}")
|
||||
print("-" * 80)
|
||||
|
||||
# Generate master seed
|
||||
master_seed = Bip39SeedGenerator(case["master_mnemonic"]).Generate(case["master_passphrase"])
|
||||
|
||||
# Derive child using BIP85
|
||||
path, child_mnemonic, entropy64, truncated_entropy = pyhdwallet.bip85_derive_child_mnemonic(
|
||||
master_seed,
|
||||
case["child_words"],
|
||||
case["index"]
|
||||
)
|
||||
|
||||
vector = {
|
||||
"description": case["description"],
|
||||
"master_mnemonic": case["master_mnemonic"],
|
||||
"master_passphrase": case["master_passphrase"],
|
||||
"child_words": case["child_words"],
|
||||
"index": case["index"],
|
||||
"bip85_path": path,
|
||||
"expected_entropy64_hex": entropy64.hex(),
|
||||
"expected_entropy_truncated_hex": truncated_entropy.hex(),
|
||||
"expected_child_mnemonic": child_mnemonic,
|
||||
}
|
||||
|
||||
vectors.append(vector)
|
||||
|
||||
print(f"Path: {path}")
|
||||
print(f"Entropy64: {entropy64.hex()}")
|
||||
print(f"Truncated: {truncated_entropy.hex()}")
|
||||
print(f"Child mnemonic: {child_mnemonic}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("\nJSON output for vectors.json:\n")
|
||||
print(json.dumps(vectors, indent=2))
|
||||
@@ -89,6 +89,91 @@ def test_address_derivation_integrity(vectors):
|
||||
)
|
||||
assert result["addresses"] == expected_addresses, f"Mismatch for profile {profile}"
|
||||
|
||||
def test_bip85_derivation(vectors):
|
||||
"""
|
||||
Verifies BIP85 child mnemonic derivation matches expected test vectors.
|
||||
|
||||
Tests:
|
||||
- Path construction
|
||||
- Entropy64 derivation (HMAC-SHA512)
|
||||
- Entropy truncation
|
||||
- Child mnemonic generation
|
||||
"""
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
for case in vectors["bip85"]:
|
||||
master_mnemonic = case["master_mnemonic"]
|
||||
master_passphrase = case["master_passphrase"]
|
||||
child_words = case["child_words"]
|
||||
index = case["index"]
|
||||
|
||||
# Generate master seed
|
||||
master_seed = Bip39SeedGenerator(master_mnemonic).Generate(master_passphrase)
|
||||
|
||||
# Derive child using BIP85
|
||||
path, child_mnemonic, entropy64, truncated_entropy = pyhdwallet.bip85_derive_child_mnemonic(
|
||||
master_seed,
|
||||
child_words,
|
||||
index
|
||||
)
|
||||
|
||||
# Assert path
|
||||
assert path == case["bip85_path"], f"Path mismatch: {path} != {case['bip85_path']}"
|
||||
|
||||
# Assert entropy64 (full 64-byte HMAC output)
|
||||
assert entropy64.hex() == case["expected_entropy64_hex"], \
|
||||
f"Entropy64 mismatch for {case['description']}"
|
||||
|
||||
# Assert truncated entropy
|
||||
assert truncated_entropy.hex() == case["expected_entropy_truncated_hex"], \
|
||||
f"Truncated entropy mismatch for {case['description']}"
|
||||
|
||||
# Assert child mnemonic
|
||||
assert child_mnemonic == case["expected_child_mnemonic"], \
|
||||
f"Child mnemonic mismatch for {case['description']}"
|
||||
|
||||
|
||||
def test_cli_gen_child_smoke(tmp_path, vectors):
|
||||
"""
|
||||
CLI smoke test for gen-child command.
|
||||
|
||||
Verifies:
|
||||
- Command runs without error
|
||||
- ZIP file is created
|
||||
- Off-screen mode works
|
||||
- File output works
|
||||
"""
|
||||
vector = vectors["bip85"][0] # Use first BIP85 test case
|
||||
master_mnemonic = vector["master_mnemonic"]
|
||||
|
||||
cmd = [
|
||||
sys.executable, "src/pyhdwallet.py", "gen-child",
|
||||
"--mnemonic-stdin",
|
||||
"--index", "0",
|
||||
"--words", "12",
|
||||
"--off-screen",
|
||||
"--file",
|
||||
"--zip-password-mode", "auto",
|
||||
"--wallet-location", str(tmp_path)
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=master_mnemonic.encode("utf-8"),
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"CLI gen-child failed: {result.stderr.decode()}"
|
||||
|
||||
# Check ZIP file created
|
||||
zips = list(tmp_path.glob("*.zip"))
|
||||
assert len(zips) == 1, f"Expected exactly one zip file, found {len(zips)}"
|
||||
|
||||
# Check filename pattern
|
||||
zip_name = zips[0].name
|
||||
assert zip_name.startswith("bip85_child_"), f"Unexpected zip name: {zip_name}"
|
||||
|
||||
|
||||
def test_cli_recover_smoke(tmp_path, vectors):
|
||||
"""
|
||||
Runs the CLI in a subprocess to verify end-to-end wiring
|
||||
|
||||
@@ -478,5 +478,51 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"bip85": [
|
||||
{
|
||||
"description": "BIP85 test case 1: 12-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/12'/0'",
|
||||
"expected_entropy64_hex": "ac98dac5d4f4ebad6056682ac95eb9ad9ba94fb68e96848264dad0b4357d002e41b3dd7a4c6f4ebc234be6938495840a73f59e9ba0e8e5c5208c94e6df2d7709",
|
||||
"expected_entropy_truncated_hex": "ac98dac5d4f4ebad6056682ac95eb9ad",
|
||||
"expected_child_mnemonic": "prosper short ramp prepare exchange stove life snack client enough purpose fold"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 2: 18-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 18,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/18'/0'",
|
||||
"expected_entropy64_hex": "fc039f51d67ed7dfd01552f27de28887cf3e58655153e44b023d37578321f7083241970730e522d3f20b38a5296c5e51e57e0429546629704a09c6d1e2d10829",
|
||||
"expected_entropy_truncated_hex": "fc039f51d67ed7dfd01552f27de28887cf3e58655153e44b",
|
||||
"expected_child_mnemonic": "winter brother stamp provide uniform useful doctor prevent venue upper peasant auto view club next clerk tone fox"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 3: 24-word child, no passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "",
|
||||
"child_words": 24,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/24'/0'",
|
||||
"expected_entropy64_hex": "d5a9cb46670566c4246b6e7af22e1dfc3668744ed831afea7ce2beea44e34e23e348e86091f24394f4be6253a7d5d24b91b1c4e0863b296e9e541e8018288897",
|
||||
"expected_entropy_truncated_hex": "d5a9cb46670566c4246b6e7af22e1dfc3668744ed831afea7ce2beea44e34e23",
|
||||
"expected_child_mnemonic": "stick exact spice sock filter ginger museum horse kit multiply manual wear grief demand derive alert quiz fault december lava picture immune decade jaguar"
|
||||
},
|
||||
{
|
||||
"description": "BIP85 test case 4: 12-word child, WITH passphrase",
|
||||
"master_mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"master_passphrase": "TREZOR",
|
||||
"child_words": 12,
|
||||
"index": 0,
|
||||
"bip85_path": "m/83696968'/39'/0'/12'/0'",
|
||||
"expected_entropy64_hex": "2b1d7c4f311137fa95f6302e64cdb88584d52b51b57d0430ee68e148b82baa3f8c40316397eb404f4573bc0c8e5c4bc14e4aa5f0f472a9d3587f494f1f7b3684",
|
||||
"expected_entropy_truncated_hex": "2b1d7c4f311137fa95f6302e64cdb885",
|
||||
"expected_child_mnemonic": "climb typical because giraffe beach wool fit ship common chapter hotel arm"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
vendor/linux-x86_64/SHA256SUMS
vendored
Normal file
18
vendor/linux-x86_64/SHA256SUMS
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
|
||||
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
|
||||
518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857 cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
|
||||
b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
|
||||
5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
1ef255bb24cb5116c7e6c83cf284db2146f0158b1fbe8c63ad7b1df214d141ba crcmod-1.7-cp312-cp312-linux_aarch64.whl
|
||||
549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
|
||||
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
|
||||
c2695b1eac2f6bda962450def9f92639532630e2a8ba694bc56db3f9aa9454da ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
|
||||
eb3aa8bcaf699be242e9c5e152cc5da37bc1dceb697454cd1b596cdb10f54f3c pgpy-0.6.0-py3-none-any.whl
|
||||
a3929c291408e67a1a11566f251b9f7d06c3fb3ae240caec44b9181de09e3fc9 py_sr25519_bindings-0.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
|
||||
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
|
||||
67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587 pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||
26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
|
||||
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
|
||||
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
|
||||
Binary file not shown.
BIN
vendor/linux-x86_64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-x86_64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-x86_64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-x86_64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-x86_64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/PGPy-0.6.0.tar.gz
vendored
Normal file
BIN
vendor/macos-arm64/PGPy-0.6.0.tar.gz
vendored
Normal file
Binary file not shown.
15
vendor/macos-arm64/SHA256SUMS
vendored
Normal file
15
vendor/macos-arm64/SHA256SUMS
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
|
||||
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
|
||||
4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9 cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl
|
||||
8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl
|
||||
1cb1cd19fb0be22e68ecb60ad950b41f18b9b02eebeffaac9391dc31f74f08f2 coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl
|
||||
109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl
|
||||
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
|
||||
4443adf871e224493c4ee4c06be205a10ea649a781132af883f6638fd7acc9d7 py_sr25519_bindings-0.2.3-cp312-cp312-macosx_11_0_arm64.whl
|
||||
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
|
||||
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
|
||||
187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
|
||||
06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6 pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
|
||||
c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl
|
||||
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
|
||||
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
|
||||
BIN
vendor/macos-arm64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
BIN
vendor/macos-arm64/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
BIN
vendor/macos-arm64/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
BIN
vendor/macos-arm64/coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/crcmod-1.7.tar.gz
vendored
Normal file
BIN
vendor/macos-arm64/crcmod-1.7.tar.gz
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl
vendored
Normal file
BIN
vendor/macos-arm64/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/ed25519-blake2b-1.4.1.tar.gz
vendored
Normal file
BIN
vendor/macos-arm64/ed25519-blake2b-1.4.1.tar.gz
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/py_sr25519_bindings-0.2.3-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
BIN
vendor/macos-arm64/py_sr25519_bindings-0.2.3-cp312-cp312-macosx_11_0_arm64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
vendored
Normal file
BIN
vendor/macos-arm64/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
vendored
Normal file
BIN
vendor/macos-arm64/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl
vendored
Normal file
BIN
vendor/macos-arm64/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/macos-arm64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/macos-arm64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user