Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions src/codex32/_bitcoin_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import hashlib
import json
import shutil
import string
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions src/codex32/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
BitcoinCore,
BitcoinCoreError,
FingerprintMismatch,
identifier_note,
identifier_origin,
parse_fingerprint,
)
from codex32._cli_input import (
Expand Down Expand Up @@ -56,6 +54,7 @@
ConfirmationResult,
CreationCeremony,
_confirmation,
_fingerprint_identifier,
generate_master_seed,
)
from codex32.wallet import master_xprv
Expand Down Expand Up @@ -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 "

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unshared secret may use a custom identifier. A nonmatch is inconclusive; please say so instead of implying the card is wrong.

"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")

Expand Down
45 changes: 0 additions & 45 deletions tests/test_bitcoin_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import hashlib
import json
import re
import subprocess
Expand All @@ -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)
Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading