diff --git a/src/codex32/bech32.py b/src/codex32/bech32.py index 59befc7..3cb14a8 100644 --- a/src/codex32/bech32.py +++ b/src/codex32/bech32.py @@ -88,6 +88,8 @@ def bech32_decode(value: str, spec: _Checksum | None = None) -> tuple[str, list[ raise MissingSeparator("No separator (1) was found.") if separator == 0: raise MissingSeparator("The application prefix before 1 is missing.") + if separator > 83: + raise InvalidLength(f"human-readable part exceeds 83 characters ({separator})") lowered = value.lower() hrp = lowered[:separator] data = _chars_to_u5(lowered[separator + 1 :], separator + 2) diff --git a/src/codex32/correction.py b/src/codex32/correction.py index 9abd345..63fe348 100644 --- a/src/codex32/correction.py +++ b/src/codex32/correction.py @@ -768,6 +768,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/data/bip93_vectors.py b/tests/data/bip93_vectors.py index d27d9b0..0ce348f 100644 --- a/tests/data/bip93_vectors.py +++ b/tests/data/bip93_vectors.py @@ -79,6 +79,31 @@ "codex32_peev": "cl10peevst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqsjhsks4laxts8q", } +# BIP-93 generalized-HRP draft, BenWestgate/bips PR #2 @ 01374bf. +BIP93_GENERAL_HRP = { + "share_a": "test_vector12spana320zyxwvutsrqpnmlkjhgfedca320zyxwvutsrqpnmlkjhgfedca304ppsqh4l7v3dh", + "share_c": "test_vector12spancacdefghjklmnpqrstuvwxyz023acdefghjklmnpqrstuvwxyz023jxmjy7q9xl7d3ul", + "derived_d": "test_vector12spandll4f8jlh4e5vdvuldlfxu2jhdnll4f8jlh4e5vdvuldlfxu2jhdnx3pe3yqzdl7p30y", + "secret_s": "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5kdf37v5qm2l7l3az", + "valid_83": ( + "a83characterlongcodex32humanreadablepartforsecretsharewiththedigit1andthelettersbio" + "10keyss9mfjjk25y05e3nq" + ), + "invalid_gap_regular": ( + "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5kp0f0tlr2cxh0t" + ), + "invalid_gap_long": ( + "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5c2m7mxmk8s5qecp" + ), + "invalid_uppercase_hrp_checksum": ( + "CL10PEEVST6CQH0WU7P5SSJYF4Z4EZ42KS9JLT3ZNEJU9UUYPR2HDDAK6TLQS3J5AYX4Y08079" + ), + "invalid_84": ( + "a84characterslongcodex32humanreadablepartforsecretsharewiththedigit1andthelettersbio" + "10keyss9hqxu3pxsnkdkth" + ), +} + VALID_CODEX32 = [ "A12UEL5LLGCHJ4UJCQVHG", @@ -93,7 +118,6 @@ VALID_CODEX32_LONG = [ "A12UEL5LQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQV3RR8ZLCK96GTC3", "a12uel5lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv3rr8zlck96gtc3", - "a1002characterlonghumanreadablepartthatcontainsthenumber1,theexcludedcharactersbio,andeveryus-asciicharacterin[33-126]!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~15ttgtscr3gvktxamm8mzt", "abcdef12l7aum6echk45nj3s0wdvt2fg8x9yrzpql7aum6echk45nj3s0wdvt2fg8x9yrzpql7aum6echk45nj3s0wdvt2fg8x9yrzpqp9evrmhc52umqew", "1177777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777fn0jxg9gc35xwa8", "split13checkupstagehandshakeupstreamerranterredcaperredscatteredsusurrantplunderedqsp5ws8r2klm66l", @@ -117,8 +141,6 @@ + "12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx836hdd09mhkhkhx", # HRP character out of range "\x80" + "12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxlhf5ywnkmk4r3tc", # HRP character out of range - # overall max length exceeded - "a1003characterslonghumanreadablepartthatcontainsthenumber1,theexcludedcharactersbio,andeveryus-asciicharacterin[33-126]!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~12fauxxru38cppmlpu0t6l", "y12bfauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxt3y5fewy4gnw2hs", # Invalid data character "lt12ifauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxjxd0ehq868vm3zl", # Invalid data character "in12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvgegljrsvs5w9q", # Too short checksum diff --git a/tests/test_correction_bch.py b/tests/test_correction_bch.py index a8edc80..cd09961 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 9111a7d..0cf4f80 100644 --- a/tests/test_generic_hrp.py +++ b/tests/test_generic_hrp.py @@ -7,6 +7,7 @@ import pytest from _codex32_oracle import oracle_encode +from data.bip93_vectors import BIP93_GENERAL_HRP from codex32 import ( CorrectionContext, @@ -18,7 +19,7 @@ recover_secret, ) from codex32.cli import main, ms_main -from codex32.errors import MismatchedHrp, MismatchedProfile +from codex32.errors import InvalidChecksum, InvalidLength, MismatchedHrp, MismatchedProfile from tools._wallet_test_vectors import STUB_FINGERPRINT UNKNOWN = { @@ -178,3 +179,33 @@ def fingerprint(_secret: object) -> bytes: status, output, error = _invoke(main, ["share", "d", "--plain"], basis) assert (status, output.strip(), error) == (0, UNKNOWN["short"]["D"], "") assert _invoke(ms_main, ["share", "d", "--plain"], basis)[0] == 2 + + +def test_bip93_generalized_hrp_share_vectors() -> None: + share_a = parse_codex32(BIP93_GENERAL_HRP["share_a"]) + share_c = parse_codex32(BIP93_GENERAL_HRP["share_c"]) + assert share_a.hrp == share_c.hrp == "test_vector" + assert recover_secret([share_a, share_c]).text == BIP93_GENERAL_HRP["secret_s"] + assert derive_share([share_a, share_c], "d").text == BIP93_GENERAL_HRP["derived_d"] + + +def test_bip93_generalized_hrp_boundary_vectors() -> None: + valid = BIP93_GENERAL_HRP["valid_83"] + invalid = BIP93_GENERAL_HRP["invalid_84"] + artifact = parse_codex32(valid) + + assert len(artifact.hrp) == 83 + assert artifact.hrp == valid.rsplit("1", 1)[0] + with pytest.raises(InvalidLength): + parse_codex32(invalid) + + +@pytest.mark.parametrize("key", ("invalid_gap_regular", "invalid_gap_long")) +def test_bip93_generalized_hrp_gap_vectors_are_invalid(key: str) -> None: + with pytest.raises(InvalidLength): + parse_codex32(BIP93_GENERAL_HRP[key]) + + +def test_bip93_generalized_hrp_checksum_uses_lowercase_hrp() -> None: + with pytest.raises(InvalidChecksum): + parse_codex32(BIP93_GENERAL_HRP["invalid_uppercase_hrp_checksum"])