diff --git a/src/codex32/_cli_input.py b/src/codex32/_cli_input.py index 74a4364d..d25e5309 100644 --- a/src/codex32/_cli_input.py +++ b/src/codex32/_cli_input.py @@ -29,11 +29,6 @@ ExistingTargetIndex, InvalidChecksum, InvalidLength, - MismatchedIdentifier, - MismatchedPayloadLength, - MismatchedProfile, - MismatchedThreshold, - SecretInRecoverySet, ) from codex32.profiles import Profile, _optional_profile_rules, _profile_rules from codex32.profiles.ms32 import ( @@ -318,7 +313,7 @@ def _parse(value: str, profiles: tuple[Profile, ...] | None) -> Artifact: try: artifact = parse_codex32(value) except CodexError as error: - message = _FRIENDLY_SET_ERRORS.get(type(error), str(error)) + message = str(error) if isinstance(error, InvalidChecksum): # Parsing has already checked the container and generic length. # Explain an unsupported profile length without validating input @@ -688,17 +683,6 @@ def allowed(candidate: CorrectionCandidate) -> bool: return accepted -_FRIENDLY_SET_ERRORS: dict[type[Exception], str] = { - InvalidChecksum: "The checksum does not match.", - MismatchedProfile: "These strings are for different applications.", - MismatchedThreshold: "These strings require different numbers of shares.", - MismatchedIdentifier: "These strings have different identifiers.", - MismatchedPayloadLength: "These strings have different lengths.", - DuplicateShareIndex: "That share index was already entered.", - SecretInRecoverySet: "Enter ordinary shares rather than the shared secret.", -} - - def _interactive( *, basis: bool, @@ -774,7 +758,7 @@ def allowed(candidate: CorrectionCandidate) -> bool: try: validate(artifact) except CodexError as error: - _stderr(f"Rejected: {_FRIENDLY_SET_ERRORS.get(type(error), str(error))}\n") + _stderr(f"Rejected: {error}\n") duplicate = isinstance(error, (DuplicateShareIndex, ExistingTargetIndex)) prefill = "" if duplicate else _retry_text(entered, prefix) continue diff --git a/src/codex32/bip93.py b/src/codex32/bip93.py index 238f7dd2..632d81eb 100644 --- a/src/codex32/bip93.py +++ b/src/codex32/bip93.py @@ -103,7 +103,7 @@ def _decode_codex32(text: str) -> tuple[str, _ProfileRules | None, tuple[int, .. body = tuple(encoded[: -checksum.length]) Header._from_symbols(body[:6]) if not bech32_verify_checksum(hrp, encoded, checksum): - raise InvalidChecksum(f"invalid {checksum.kind} checksum") + raise InvalidChecksum("The checksum does not match.") profile_rules = _optional_profile_rules(hrp) if profile_rules is not None: profile_rules.validate_text_length(len(text)) @@ -278,7 +278,7 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b first = copied[0] threshold = first.header.threshold if threshold not in range(2, 10): - raise MismatchedThreshold("linear sharing requires threshold 2 through 9") + raise MismatchedThreshold("Only strings with a threshold of 2 through 9 can be combined.") if len(copied) > threshold or (require_exact and len(copied) != threshold): raise WrongShareCount(f"threshold is {threshold}, but {len(copied)} artifacts were supplied") first_tail, checksum_length, encoded_length = _artifact_tail(first) @@ -286,11 +286,11 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b indices = [first.header.index] for item in copied[1:]: if item.hrp != first.hrp: - raise MismatchedHrp(f"{first.hrp} and {item.hrp} cannot be combined") + raise MismatchedHrp("These strings are for different applications.") if item.header.threshold != threshold: - raise MismatchedThreshold("share thresholds do not match") + raise MismatchedThreshold("These strings require different numbers of shares.") if item.header.identifier != first.header.identifier: - raise MismatchedIdentifier("share identifiers do not match") + raise MismatchedIdentifier("These strings have different identifiers.") tail, item_checksum_length, item_encoded_length = _artifact_tail(item) item_shape = ( len(item.payload_symbols), @@ -298,11 +298,11 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b item_encoded_length, ) if item_shape != (len(first.payload_symbols), checksum_length, encoded_length): - raise MismatchedPayloadLength("share payload/checksum lengths do not match") + raise MismatchedPayloadLength("These strings have different lengths.") tails.append(tail) indices.append(item.header.index) if len(set(indices)) != len(indices): - raise DuplicateShareIndex("share indices must be distinct") + raise DuplicateShareIndex("That share index was already entered.") return _ShareSet( copied, first.hrp, @@ -317,7 +317,7 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b def _validate_recovery_prefix(shares: Sequence[Share | Secret]) -> None: share_set = _validate_share_set(shares, require_exact=False) if any(isinstance(item, Secret) for item in share_set.artifacts): - raise SecretInRecoverySet("recovery accepts ordinary shares only") + raise SecretInRecoverySet("Enter ordinary shares rather than the shared secret.") def _validate_basis_prefix(basis: Sequence[Share | Secret]) -> None: @@ -342,7 +342,7 @@ def recover_secret(shares: Sequence[Share]) -> Secret: """Recover S from exactly k compatible, distinct ordinary shares.""" share_set = _validate_share_set(shares) if any(isinstance(item, Secret) for item in share_set.artifacts): - raise SecretInRecoverySet("recovery accepts ordinary shares only") + raise SecretInRecoverySet("Enter ordinary shares rather than the shared secret.") recovered = _interpolate_tail(share_set, "s") if not isinstance(recovered, Secret): raise InvalidShareSet("interpolation did not produce a secret") diff --git a/tests/test_bip93.py b/tests/test_bip93.py index a57b1f05..314e4b62 100644 --- a/tests/test_bip93.py +++ b/tests/test_bip93.py @@ -106,8 +106,9 @@ def test_vector_6_core_lightning_examples_are_immutable() -> None: @pytest.mark.parametrize("value", BAD_CHECKSUMS) def test_bad_checksums_are_rejected(value: str) -> None: - with pytest.raises(InvalidChecksum): + with pytest.raises(InvalidChecksum) as error: parse_codex32(value) + assert str(error.value) == "The checksum does not match." @pytest.mark.parametrize("value", BAD_CASES) diff --git a/tests/test_sharing.py b/tests/test_sharing.py index ab6e7742..da6566fe 100644 --- a/tests/test_sharing.py +++ b/tests/test_sharing.py @@ -112,28 +112,35 @@ def test_every_threshold_recovers_exactly_k_shares(threshold: int) -> None: def test_share_set_mismatches_have_distinct_errors() -> None: ms_secret, ms_masks = _ms_basis() opaque = parse_codex32(oracle_encode("xy", "2testa" + "q" * len(ms_secret.payload_symbols))) - with pytest.raises(MismatchedProfile): + with pytest.raises(MismatchedProfile) as error: derive_share([ms_secret, opaque], "c") # type: ignore[list-item] + assert str(error.value) == "These strings are for different applications." threshold_three = parse_codex32(oracle_encode("ms", "3testc" + "q" * len(ms_secret.payload_symbols))) - with pytest.raises(MismatchedThreshold): + with pytest.raises(MismatchedThreshold) as error: derive_share([ms_secret, threshold_three], "d") # type: ignore[list-item] + assert str(error.value) == "These strings require different numbers of shares." other_id = parse_codex32(oracle_encode("ms", "2namec" + "q" * len(ms_secret.payload_symbols))) - with pytest.raises(MismatchedIdentifier): + with pytest.raises(MismatchedIdentifier) as error: derive_share([ms_secret, other_id], "d") # type: ignore[list-item] + assert str(error.value) == "These strings have different identifiers." longer = MasterSeed.from_seed(bytes(20), identifier="test", threshold=2) longer_mask = parse_codex32(oracle_encode("ms", "2testc" + "q" * len(longer.payload_symbols))) - with pytest.raises(MismatchedPayloadLength): + with pytest.raises(MismatchedPayloadLength) as error: derive_share([ms_secret, longer_mask], "d") # type: ignore[list-item] + assert str(error.value) == "These strings have different lengths." - with pytest.raises(DuplicateShareIndex): + with pytest.raises(DuplicateShareIndex) as error: recover_secret([ms_masks[0], ms_masks[0]]) # type: ignore[list-item] - with pytest.raises(SecretInRecoverySet): + assert str(error.value) == "That share index was already entered." + with pytest.raises(SecretInRecoverySet) as error: recover_secret([ms_secret, ms_masks[0]]) # type: ignore[list-item] - with pytest.raises(MismatchedThreshold): + assert str(error.value) == "Enter ordinary shares rather than the shared secret." + with pytest.raises(MismatchedThreshold) as error: recover_secret([MasterSeed.from_seed(bytes(16), identifier="test")]) # type: ignore[list-item] + assert str(error.value) == "Only strings with a threshold of 2 through 9 can be combined." with pytest.raises(TypeError): recover_secret([VECTOR_2["share_A"], VECTOR_2["share_C"]]) # type: ignore[list-item] with pytest.raises(TypeError):