diff --git a/src/codex32/bip93.py b/src/codex32/bip93.py index 02afad7..0b77fc7 100644 --- a/src/codex32/bip93.py +++ b/src/codex32/bip93.py @@ -93,6 +93,8 @@ def _checksum_for_encoded_length(hrp: str, encoded_length: int) -> _Checksum: def _decode_codex32(text: str) -> tuple[str, _ProfileRules | None, tuple[int, ...], _Checksum]: hrp, encoded = bech32_decode(text) + if len(hrp) > 83: + raise InvalidLength(f"human-readable part exceeds 83 characters ({len(hrp)})") if len(hrp) + 1 + len(encoded) < 21: raise InvalidLength("codex32 string must contain at least 21 characters") checksum = _checksum_for_encoded_length(hrp, len(encoded)) diff --git a/src/codex32/correction.py b/src/codex32/correction.py index 4a1a561..111d5b0 100644 --- a/src/codex32/correction.py +++ b/src/codex32/correction.py @@ -763,6 +763,8 @@ def _validate_context(context: CorrectionContext) -> None: _validate_single_case_ascii(context.hrp) if context.hrp.lower() != context.hrp: raise ValueError("hrp must be a normalized application prefix") + if len(context.hrp) > 83: + raise ValueError(f"human-readable part exceeds 83 characters ({len(context.hrp)})") length = context.expected_length if length is not None: if isinstance(length, bool) or not isinstance(length, int): diff --git a/tests/test_correction_bch.py b/tests/test_correction_bch.py index 49b8800..9e47b6c 100644 --- a/tests/test_correction_bch.py +++ b/tests/test_correction_bch.py @@ -217,6 +217,7 @@ def test_public_context_constrains_length_prefix_and_used_indices() -> None: CorrectionContext(Profile.MS, excluded_indices=["a"]), # type: ignore[arg-type] CorrectionContext(Profile.MS, excluded_indices=("s",)), CorrectionContext(Profile.MS, excluded_indices=("a", "A")), + CorrectionContext("z" * 84, expected_length=106), ), ) def test_malformed_public_context_is_rejected(context: CorrectionContext) -> None: diff --git a/tests/test_generic_hrp.py b/tests/test_generic_hrp.py index 341e504..9245cb1 100644 --- a/tests/test_generic_hrp.py +++ b/tests/test_generic_hrp.py @@ -18,7 +18,7 @@ recover_secret, ) from codex32.cli import main, ms_main -from codex32.errors import MismatchedHrp, MismatchedProfile +from codex32.errors import InvalidLength, MismatchedHrp, MismatchedProfile UNKNOWN = { "short": { @@ -50,6 +50,14 @@ def test_opaque_hrp_parse_complete_recover_and_derive(vector: dict[str, str]) -> assert derive_share([secret, a], "d").text == vector["D"].upper() +def test_hrp_over_83_characters_is_rejected_at_the_bip173_limit() -> None: + within_limit = oracle_encode("z" * 83, "0testsq") + over_limit = oracle_encode("z" * 84, "0testsq") + assert parse_codex32(within_limit).hrp == "z" * 83 + with pytest.raises(InvalidLength): + parse_codex32(over_limit) + + def test_sharing_compares_normalized_hrp_and_keeps_compatibility_error_name() -> None: assert MismatchedProfile is MismatchedHrp zz = parse_codex32(UNKNOWN["short"]["S"])