diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..03f12d9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning:pgpy.constants +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* diff --git a/src/pyhdwallet.py b/src/pyhdwallet.py index 3162a54..74837ee 100644 --- a/src/pyhdwallet.py +++ b/src/pyhdwallet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -pyhdwallet v1.0.5 (Python 3.11+) +pyhdwallet v1.0.6 (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.0.6): - Seed recovery removed (mnemonic only). - No plaintext mnemonic via CLI args. - Use either: @@ -274,6 +274,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 +444,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 +739,7 @@ def build_payload_gen( addresses: Dict[str, Any], ) -> Dict[str, Any]: return { - "version": "pyhdwallet v1.0.5", + "version": "pyhdwallet v1.0.6", "purpose": "generated mnemonic backup", "mnemonic": mnemonic, "passphrase_used": passphrase_used, @@ -634,7 +763,7 @@ def build_payload_recover( addresses: Dict[str, Any], ) -> Dict[str, Any]: payload: Dict[str, Any] = { - "version": "pyhdwallet v1.0.5", + "version": "pyhdwallet v1.0.6", "purpose": "recovery payload", "master_fingerprint": fp, "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"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.0.6", + "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 +1056,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 +1244,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.0.6 - Secure HD Wallet Tool") + parser.add_argument("--version", action="version", version="pyhdwallet v1.0.6") + sub = parser.add_subparsers(dest="cmd") 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)") 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 +1385,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 diff --git a/tests/generate_bip85_vectors.py b/tests/generate_bip85_vectors.py new file mode 100755 index 0000000..c392dbb --- /dev/null +++ b/tests/generate_bip85_vectors.py @@ -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)) diff --git a/tests/test_vectors.py b/tests/test_vectors.py index 9c162d3..b6e1262 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -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 diff --git a/tests/vectors.json b/tests/vectors.json index cb949ce..031ed4d 100644 --- a/tests/vectors.json +++ b/tests/vectors.json @@ -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" + } ] } \ No newline at end of file