Compare commits
6 Commits
369c8595a1
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6c84f81ee | ||
| 109829f1f5 | |||
|
|
2f7433b704 | ||
|
|
6457ec2cee | ||
|
|
0949fe9792 | ||
| 21b9389591 |
35
README.md
35
README.md
@@ -1,6 +1,6 @@
|
|||||||
# pyhdwallet – Secure HD Wallet Tool
|
# pyhdwallet – Secure HD Wallet Tool
|
||||||
|
|
||||||
A Python command-line tool for generating and recovering BIP39 HD wallets with support for Ethereum, Solana, and Bitcoin. Designed for offline operation with optional PGP encryption and AES-encrypted ZIP artifacts.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ This repository includes pre-built Python wheels for offline use.
|
|||||||
|
|
||||||
**Supported platforms:**
|
**Supported platforms:**
|
||||||
|
|
||||||
- macOS ARM64 (M1/M2/M3) - Python 3.12
|
- macOS ARM64 (M1/M2/M3/M4) - Python 3.12
|
||||||
- Linux x86_64 (Ubuntu/Tails) - Python 3.12
|
- Linux x86_64 (Ubuntu/Tails) - Python 3.12
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
@@ -102,6 +102,22 @@ python src/pyhdwallet.py gen \
|
|||||||
--off-screen \
|
--off-screen \
|
||||||
--file
|
--file
|
||||||
|
|
||||||
|
# Derive BIP85 child mnemonic (12/15/18/21/24 words)
|
||||||
|
python src/pyhdwallet.py gen-child \
|
||||||
|
--interactive \
|
||||||
|
--words 12 \
|
||||||
|
--index 0
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# Recover wallet from mnemonic
|
# Recover wallet from mnemonic
|
||||||
python src/pyhdwallet.py recover --interactive --file
|
python src/pyhdwallet.py recover --interactive --file
|
||||||
|
|
||||||
@@ -118,11 +134,13 @@ pytest -v tests/test_vectors.py
|
|||||||
## 🔐 Security Features
|
## 🔐 Security Features
|
||||||
|
|
||||||
- **Offline-first**: Network access blocked during key generation/recovery
|
- **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
|
- **Test suite**: Regression tests with frozen vectors ensure derivation logic integrity
|
||||||
- **PGP fingerprint pinning**: Prevents key substitution attacks
|
- **PGP fingerprint pinning**: Prevents key substitution attacks
|
||||||
- **TTY safety guard**: Refuses to print secrets when stdout is piped/redirected
|
- **TTY safety guard**: Refuses to print secrets when stdout is piped/redirected
|
||||||
- **AES-encrypted outputs**: Wallet artifacts encrypted with `pyzipper`
|
- **AES-encrypted outputs**: Wallet artifacts encrypted with `pyzipper`
|
||||||
- **No shell history leaks**: Use `--interactive` or `--mnemonic-stdin` for recovery
|
- **No shell history leaks**: Use `--interactive` or `--mnemonic-stdin` for recovery
|
||||||
|
- **Interoperable**: BIP85 children compatible with Coldcard, Ian Coleman tool, etc.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -170,3 +188,16 @@ For maximum security when generating production wallets:
|
|||||||
8. Wipe USB drives securely
|
8. Wipe USB drives securely
|
||||||
|
|
||||||
See [playbook.md](playbook.md) for detailed air-gapped procedures.
|
See [playbook.md](playbook.md) for detailed air-gapped procedures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆕 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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
150
playbook.md
150
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.
|
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.
|
||||||
|
|
||||||
@@ -186,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
|
- **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
|
- **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
|
- **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
|
#### Running Tests
|
||||||
|
|
||||||
@@ -325,6 +326,141 @@ Naming uses UTC timestamps (e.g. `20260108_011830Z`):
|
|||||||
|
|
||||||
## Commands
|
## 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)
|
### fetchkey (online)
|
||||||
|
|
||||||
Download and verify a PGP public key from a URL.
|
Download and verify a PGP public key from a URL.
|
||||||
@@ -759,6 +895,16 @@ python src/pyhdwallet.py test
|
|||||||
|
|
||||||
## Changelog
|
## 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**
|
- **v1.0.5**
|
||||||
- `--unsafe-print` removed; `gen` prints mnemonic by default (debug/test behavior).
|
- `--unsafe-print` removed; `gen` prints mnemonic by default (debug/test behavior).
|
||||||
- `--output` removed; file payload is always JSON (unencrypted) or `.asc` (PGP encrypted).
|
- `--output` removed; file payload is always JSON (unencrypted) or `.asc` (PGP encrypted).
|
||||||
|
|||||||
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_*
|
||||||
@@ -1,30 +1,46 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile requirements.in
|
||||||
|
#
|
||||||
base58==2.1.1
|
base58==2.1.1
|
||||||
bip_utils==2.10.0
|
# via -r requirements.in
|
||||||
build==1.3.0
|
bip-utils==2.10.0
|
||||||
|
# via -r requirements.in
|
||||||
cbor2==5.8.0
|
cbor2==5.8.0
|
||||||
|
# via bip-utils
|
||||||
cffi==2.0.0
|
cffi==2.0.0
|
||||||
click==8.3.1
|
# via
|
||||||
|
# cryptography
|
||||||
|
# pynacl
|
||||||
coincurve==21.0.0
|
coincurve==21.0.0
|
||||||
|
# via bip-utils
|
||||||
crcmod==1.7
|
crcmod==1.7
|
||||||
|
# via bip-utils
|
||||||
cryptography==46.0.3
|
cryptography==46.0.3
|
||||||
|
# via pgpy
|
||||||
ecdsa==0.19.1
|
ecdsa==0.19.1
|
||||||
|
# via bip-utils
|
||||||
ed25519-blake2b==1.4.1
|
ed25519-blake2b==1.4.1
|
||||||
iniconfig==2.3.0
|
# via bip-utils
|
||||||
packaging==25.0
|
pgpy==0.6.0
|
||||||
PGPy==0.6.0
|
# via -r requirements.in
|
||||||
pip-chill==1.0.3
|
|
||||||
pip-tools==7.5.2
|
|
||||||
pluggy==1.6.0
|
|
||||||
py-sr25519-bindings==0.2.3
|
py-sr25519-bindings==0.2.3
|
||||||
|
# via bip-utils
|
||||||
pyasn1==0.6.1
|
pyasn1==0.6.1
|
||||||
|
# via pgpy
|
||||||
pycparser==2.23
|
pycparser==2.23
|
||||||
|
# via cffi
|
||||||
pycryptodome==3.23.0
|
pycryptodome==3.23.0
|
||||||
|
# via bip-utils
|
||||||
pycryptodomex==3.23.0
|
pycryptodomex==3.23.0
|
||||||
Pygments==2.19.2
|
# via pyzipper
|
||||||
PyNaCl==1.6.2
|
pynacl==1.6.2
|
||||||
pyproject_hooks==1.2.0
|
# via
|
||||||
pytest==9.0.2
|
# -r requirements.in
|
||||||
|
# bip-utils
|
||||||
pyzipper==0.3.6
|
pyzipper==0.3.6
|
||||||
setuptools==80.9.0
|
# via -r requirements.in
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
wheel==0.45.1
|
# via ecdsa
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
pyhdwallet v1.0.5 (Python 3.11+)
|
pyhdwallet v1.1.0 (Python 3.11+)
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
- fetchkey (online): download ASCII-armored PGP public key and print SHA256 + fingerprint
|
- 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.
|
- 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.
|
- 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).
|
- Seed recovery removed (mnemonic only).
|
||||||
- No plaintext mnemonic via CLI args.
|
- No plaintext mnemonic via CLI args.
|
||||||
- Use either:
|
- Use either:
|
||||||
@@ -274,6 +274,135 @@ def get_master_fingerprint(seed_bytes: bytes) -> str:
|
|||||||
h160 = Hash160.QuickDigest(pubkey_bytes)
|
h160 = Hash160.QuickDigest(pubkey_bytes)
|
||||||
return h160[:4].hex().upper()
|
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)
|
# PGP helpers (PGPy)
|
||||||
@@ -315,7 +444,7 @@ def pgp_fingerprint(armored: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def fetch_ascii_armored_text(url: str, timeout: int = 15) -> Tuple[str, bytes]:
|
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:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
data = resp.read()
|
data = resp.read()
|
||||||
text = data.decode("utf-8", errors="strict")
|
text = data.decode("utf-8", errors="strict")
|
||||||
@@ -610,7 +739,7 @@ def build_payload_gen(
|
|||||||
addresses: Dict[str, Any],
|
addresses: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"version": "pyhdwallet v1.0.5",
|
"version": "pyhdwallet v1.1.0",
|
||||||
"purpose": "generated mnemonic backup",
|
"purpose": "generated mnemonic backup",
|
||||||
"mnemonic": mnemonic,
|
"mnemonic": mnemonic,
|
||||||
"passphrase_used": passphrase_used,
|
"passphrase_used": passphrase_used,
|
||||||
@@ -634,7 +763,7 @@ def build_payload_recover(
|
|||||||
addresses: Dict[str, Any],
|
addresses: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
payload: Dict[str, Any] = {
|
payload: Dict[str, Any] = {
|
||||||
"version": "pyhdwallet v1.0.5",
|
"version": "pyhdwallet v1.1.0",
|
||||||
"purpose": "recovery payload",
|
"purpose": "recovery payload",
|
||||||
"master_fingerprint": fp,
|
"master_fingerprint": fp,
|
||||||
"solana_profile": sol_profile,
|
"solana_profile": sol_profile,
|
||||||
@@ -658,10 +787,56 @@ def deterministic_inner_name(is_encrypted: bool, ts: str) -> str:
|
|||||||
return f"encrypted_wallet_{ts}.asc"
|
return f"encrypted_wallet_{ts}.asc"
|
||||||
return f"test_wallet_{ts}.json"
|
return f"test_wallet_{ts}.json"
|
||||||
|
|
||||||
|
|
||||||
def deterministic_zip_name(prefix: str, ts: str) -> str:
|
def deterministic_zip_name(prefix: str, ts: str) -> str:
|
||||||
return f"{prefix}_{ts}.zip"
|
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
|
# Commands
|
||||||
@@ -881,6 +1056,160 @@ def cmd_recover(args) -> None:
|
|||||||
seed_bytes = None
|
seed_bytes = None
|
||||||
passphrase = 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:
|
def cmd_test(args) -> None:
|
||||||
with NetworkGuard("test"):
|
with NetworkGuard("test"):
|
||||||
@@ -915,8 +1244,9 @@ def cmd_test(args) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="pyhdwallet v1.0.5 - Secure HD Wallet Tool")
|
parser = argparse.ArgumentParser(description="pyhdwallet v1.1.0 - Secure HD Wallet Tool")
|
||||||
parser.add_argument("--version", action="version", version="pyhdwallet v1.0.5")
|
parser.add_argument("--version", action="version", version="pyhdwallet v1.1.0")
|
||||||
|
|
||||||
sub = parser.add_subparsers(dest="cmd")
|
sub = parser.add_subparsers(dest="cmd")
|
||||||
|
|
||||||
p_fetch = sub.add_parser("fetchkey", help="Download ASCII-armored PGP pubkey from URL (online)")
|
p_fetch = sub.add_parser("fetchkey", help="Download ASCII-armored PGP pubkey from URL (online)")
|
||||||
@@ -983,6 +1313,54 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
p_rec = sub.add_parser("recover", help="Recover addresses from mnemonic (offline)")
|
p_rec = sub.add_parser("recover", help="Recover addresses from mnemonic (offline)")
|
||||||
add_common(p_rec)
|
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 = 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("--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)")
|
g.add_argument("--mnemonic-stdin", dest="mnemonic_stdin", action="store_true", help="Read BIP39 mnemonic from stdin (non-interactive)")
|
||||||
@@ -1007,6 +1385,9 @@ def main() -> None:
|
|||||||
if args.cmd == "recover":
|
if args.cmd == "recover":
|
||||||
cmd_recover(args)
|
cmd_recover(args)
|
||||||
return
|
return
|
||||||
|
if args.cmd == "gen-child":
|
||||||
|
cmd_gen_child(args)
|
||||||
|
return
|
||||||
if args.cmd == "test":
|
if args.cmd == "test":
|
||||||
cmd_test(args)
|
cmd_test(args)
|
||||||
return
|
return
|
||||||
|
|||||||
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}"
|
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):
|
def test_cli_recover_smoke(tmp_path, vectors):
|
||||||
"""
|
"""
|
||||||
Runs the CLI in a subprocess to verify end-to-end wiring
|
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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
31
vendor/linux-aarch64/SHA256SUMS
vendored
Normal file
31
vendor/linux-aarch64/SHA256SUMS
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
|
||||||
|
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
|
||||||
|
7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 build-1.3.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
|
||||||
|
981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 click-8.3.1-py3-none-any.whl
|
||||||
|
5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
|
||||||
|
6a6b11d6c42a450359f0d7f2312cb1fe69493ae8314dc3f6674b95cefed469a2 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
|
||||||
|
75c8d36691348abdd395b22f2c2ac5dd5c153c653550e6d18da5b0e433d7ce84 ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
|
||||||
|
f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 iniconfig-2.3.0-py3-none-any.whl
|
||||||
|
29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 packaging-25.0-py3-none-any.whl
|
||||||
|
8d6f2b1a217ccefd933c7ec8bf69927e90d4630093d0af2404fe4e33703dcf0f pgpy-0.6.0-py3-none-any.whl
|
||||||
|
9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd pip-25.3-py3-none-any.whl
|
||||||
|
452a38edbcdfc333301c438c26ba00a0762d2034fe26a235d8587134453ccdb1 pip_chill-1.0.3-py2.py3-none-any.whl
|
||||||
|
2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce pip_tools-7.5.2-py3-none-any.whl
|
||||||
|
e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 pluggy-1.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
|
||||||
|
86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b pygments-2.19.2-py3-none-any.whl
|
||||||
|
26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
|
||||||
|
9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 pyproject_hooks-1.2.0-py3-none-any.whl
|
||||||
|
711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b pytest-9.0.2-py3-none-any.whl
|
||||||
|
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
|
||||||
|
062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 setuptools-80.9.0-py3-none-any.whl
|
||||||
|
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
|
||||||
|
708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 wheel-0.45.1-py3-none-any.whl
|
||||||
BIN
vendor/linux-aarch64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/base58-2.1.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/bip_utils-2.10.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/build-1.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/build-1.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-aarch64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/click-8.3.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/click-8.3.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/crcmod-1.7-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/ecdsa-0.19.1-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/iniconfig-2.3.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/packaging-25.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/packaging-25.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pgpy-0.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip-25.3-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip-25.3-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip_chill-1.0.3-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pip_tools-7.5.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pluggy-1.6.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
vendor/linux-aarch64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyasn1-0.6.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycparser-2.23-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pygments-2.19.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyproject_hooks-1.2.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pytest-9.0.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/pyzipper-0.3.6-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/setuptools-80.9.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/six-1.17.0-py2.py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
vendor/linux-aarch64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
BIN
vendor/linux-aarch64/wheel-0.45.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user