diff --git a/src/codex32/_bitcoin_core.py b/src/codex32/_bitcoin_core.py index 45031a7..5b3485b 100644 --- a/src/codex32/_bitcoin_core.py +++ b/src/codex32/_bitcoin_core.py @@ -1,6 +1,5 @@ from __future__ import annotations -import hashlib import json import shutil import string @@ -11,9 +10,7 @@ from typing import Literal from codex32._bip32 import _master_xprv_from_seed -from codex32.bech32 import _u5_to_chars, convertbits from codex32.bip93 import MasterSeed -from codex32.generation import _fingerprint_identifier class BitcoinCoreError(Exception): @@ -54,36 +51,6 @@ def parse_fingerprint(text: str) -> bytes: ) -def identifier_note(origin: str | None) -> str: - # Say what `identifier_origin` found, for an operator restoring without a record. - if origin is None: - return ( - "The backup identifier was not made from this seed. That can be normal for codex32 backups " - "made from split shares, supplied seed bytes or an explicit identifier. Bails made every " - "identifier from its seed, so for a Bails backup these are the wrong or mixed-up cards." - ) - return ( - f"The backup identifier matches this seed ({origin} rule). That rules out most mixed-up cards, " - "but not cards replaced on purpose." - ) - - -def identifier_origin(secret: MasterSeed, fingerprint: bytes) -> str | None: - """Check codex32's fingerprint or Bails' three-character seed-digest identifier.""" - identifier = secret.header.identifier - if identifier == _fingerprint_identifier(fingerprint): - return "codex32" - for name, digest in (("Bails", "ripemd160"), ("Bails alpha", "sha256")): - try: - hashed = hashlib.new(digest, secret.seed_bytes).digest() - except ValueError: - continue - derived = convertbits(hashed, 8, 5, pad=True) - if identifier[:3] == _u5_to_chars(tuple(derived[:3])): - return name - return None - - @dataclass(frozen=True) class BitcoinCore: executable: str diff --git a/src/codex32/cli.py b/src/codex32/cli.py index f41e9a7..b06d01f 100644 --- a/src/codex32/cli.py +++ b/src/codex32/cli.py @@ -14,8 +14,6 @@ BitcoinCore, BitcoinCoreError, FingerprintMismatch, - identifier_note, - identifier_origin, parse_fingerprint, ) from codex32._cli_input import ( @@ -56,6 +54,7 @@ ConfirmationResult, CreationCeremony, _confirmation, + _fingerprint_identifier, generate_master_seed, ) from codex32.wallet import master_xprv @@ -323,8 +322,20 @@ def _show_fingerprint(core: BitcoinCore, secret: MasterSeed, action: str) -> Non def _without_record(core: BitcoinCore, secret: MasterSeed) -> bool: fingerprint = core.fingerprint(secret) _print(f"\nMaster fingerprint: {fingerprint.hex().upper()}", err=True) - _print(f"Backup identifier: {secret.header.identifier.upper()}", err=True) - _print(identifier_note(identifier_origin(secret, fingerprint)), err=True) + if secret.header.threshold == 0: + # New unshared codex32 backups take their identifier from the fingerprint. + if secret.header.identifier == _fingerprint_identifier(fingerprint): + _print( + "The backup identifier matches this fingerprint, so the card is consistent with itself. " + "That does not show it is the wallet you expect.", + err=True, + ) + else: + _print( + "The backup identifier does not match this fingerprint. New unshared codex32 backups " + "take their identifier from it, so check this is the right card.", + err=True, + ) _print(NO_RECORD_WARNING, err=True) return _text("Restore without a wallet record? [y/N]", optional=True).lower() in ("y", "yes") diff --git a/tests/test_bitcoin_core.py b/tests/test_bitcoin_core.py index 4d1b3bf..05e70f3 100644 --- a/tests/test_bitcoin_core.py +++ b/tests/test_bitcoin_core.py @@ -2,7 +2,6 @@ from __future__ import annotations -import hashlib import json import re import subprocess @@ -14,12 +13,9 @@ BitcoinCore, BitcoinCoreError, FingerprintMismatch, - identifier_note, - identifier_origin, parse_fingerprint, ) from codex32.bip93 import MasterSeed, parse_codex32 -from codex32.generation import _fingerprint_identifier _parsed = parse_codex32("ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw") assert isinstance(_parsed, MasterSeed) @@ -816,44 +812,3 @@ def unused(_client: BitcoinCore, _secret: MasterSeed) -> bytes: monkeypatch.setattr(BitcoinCore, "fingerprint", unused) BitcoinCore("bitcoin-cli", "main", 300000).verify_identity(_SEED, None) - - -# Frozen from Bails' own ms32.seed_identifier for this seed: master (RIPEMD-160) and the -# June 2023 alpha (SHA-256). Bails checked three characters and kept the fourth for re-sharing. -_BAILS_SEED = bytes(range(16)) - - -@pytest.mark.parametrize( - ("identifier", "origin"), - ( - (_fingerprint_identifier(_FINGERPRINT), "codex32"), - ("d9k8", "Bails"), - ("d9kq", "Bails"), - ("hezu", "Bails alpha"), - ("test", None), - ), -) -def test_identifier_origin_names_the_rule_that_made_it(identifier: str, origin: str | None) -> None: - secret = MasterSeed.from_seed(_BAILS_SEED, identifier=identifier) - assert identifier_origin(secret, _FINGERPRINT) == origin - assert ("matches this seed" in identifier_note(origin)) is (origin is not None) - - -def test_identifier_origin_still_checks_alpha_without_ripemd160(monkeypatch: pytest.MonkeyPatch) -> None: - original_new = hashlib.new - - def without_ripemd160(name: str, data: bytes = b"") -> object: - if name == "ripemd160": - raise ValueError("unsupported hash type ripemd160") - return original_new(name, data) - - monkeypatch.setattr(hashlib, "new", without_ripemd160) - secret = MasterSeed.from_seed(_BAILS_SEED, identifier="hezu") - assert identifier_origin(secret, _FINGERPRINT) == "Bails alpha" - - -def test_identifier_note_allows_supported_nonderived_codex32_identifiers() -> None: - note = identifier_note(None) - assert "split shares" in note - assert "supplied seed bytes" in note - assert "explicit identifier" in note diff --git a/tests/test_cli.py b/tests/test_cli.py index 6a3072d..2d30fe0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2845,16 +2845,18 @@ def test_restore_without_a_record_shows_what_the_cards_say_and_asks( assert prompts[1] == "Restore without a wallet record? [y/N]" shown = capsys.readouterr().err assert shown.count(f"Master fingerprint: {fingerprint.hex().upper()}") == 1 - assert "was not made from this seed" in shown and "nothing can prove" in shown + assert "nothing can prove" in shown prompts = _record_answers(monkeypatch, "", "y") assert _RECORDED_FINGERPRINT(core, secret) is None assert prompts[1] == "Restore without a wallet record? [y/N]" + assert "does not match this fingerprint" in capsys.readouterr().err derived = MasterSeed.from_seed(secret.seed_bytes, identifier=_fingerprint_identifier(fingerprint)) _record_answers(monkeypatch, "", "yes") assert _RECORDED_FINGERPRINT(core, derived) is None - assert "matches this seed (codex32 rule)" in capsys.readouterr().err + shown = capsys.readouterr().err + assert "matches this fingerprint" in shown and "does not show it is the wallet you expect" in shown def test_wallet_restore_hides_fingerprint_until_the_record_gate(monkeypatch: pytest.MonkeyPatch) -> None: