diff --git a/README.md b/README.md index e87e0556..fa90443d 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,15 @@ Reference implementation of BIP-0093 (codex32): checksummed, SSSS-aware BIP32 se This repository implements the codex32 string format described by BIP-0093. It provides encoding/decoding, regular/long codex32 checksums, CRC padding for base conversions, -Shamir secret sharing scheme (SSSS) interpolation helpers and helpers to build codex32 strings from seed bytes. +Shamir secret sharing scheme (SSSS) interpolation helpers and helpers to build codex32 secrets from seed bytes. +Shares are never encoded from bytes: generate them from random characters or by interpolation. ## Features - Encode/decode codex32 data via `from_string` and `from_unchecksummed_string`. -- Regular checksum (13 chars) and long checksum (15 chars) support. -- Construct codex32 strings from raw seed bytes via `from_seed`. -- CRC-based default padding scheme for `from_seed`. -- `from_seed` requires an explicit four-character identifier in its prefix. -- Interpolate/recover shares via `interpolate_at`. +- Regular and long codex32 checksum support. +- Construct codex32 secrets (share index `s`) from raw seed bytes via `from_seed`, with CRC padding by default. +- Interpolate shares recover secrets via `interpolate_at`. - Parse codex32 strings and access parts via properties. -- Mutate codex32 strings by reassigning `is_upper`, `hrp`, `k`, `ident`, `share_idx`, `data`, and `pad_val`. -- Contains module and tests for Bech32/Bech32m and segwit addresses. ## Security Caution: This is reference code. Verify carefully before using with real funds. @@ -47,7 +44,7 @@ from codex32 import Codex32String # Create from seed bytes s = Codex32String.from_seed( bytes.fromhex('ffeeddccbbaa99887766554433221100'), - "ms13cashs", # prefix string with explicit identifier (HRP + '1' + header) + "ms13cashs", # prefix string, (HRP + '1' + header) 0 # padding value (default "CRC", otherwise integer) ) print(s.s) # codex32 string @@ -62,8 +59,7 @@ print(a.payload) # payload part print(a.checksum) # checksum part print(len(a)) # length of the codex32 string print(a.is_upper) # case is upper True/False -print(s.data.hex()) # raw seed bytes as hex -print(a.pad_val) # padding value integer, (MSB first) +print(s.data().hex()) # master seed bytes as hex ("ms" secrets only) @@ -80,15 +76,6 @@ print(derived_share_d.s) e = Codex32String.from_string("cl", "cl10lueasd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanvrktzhlhusz") print(e.ident) print(e.s) - -# Relabel a Codex32String object -e.ident = "cln2" -print(e.ident) -print(e.s) - -# Uppercase a Codex32String object (for encoding in QR codes or handwriting) -e.is_upper = True -print(e.s) ``` ## Tests diff --git a/pyproject.toml b/pyproject.toml index 6ad33728..901a1b84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codex32" -version = "0.6.2" +version = "0.7.0" authors = [ { name = "Ben Westgate", email = "benwestgate@protonmail.com" }, ] diff --git a/src/codex32/bech32.py b/src/codex32/bech32.py index 3e734e93..a401d04c 100644 --- a/src/codex32/bech32.py +++ b/src/codex32/bech32.py @@ -101,7 +101,8 @@ def u5_parse(bech: str) -> tuple[str, list[int]]: raise InvalidCase("mixed upper/lower case bech32 string") if (pos := (bech := bech.lower()).rfind("1")) < 1: raise MissingHrp("empty HRP") if not pos else SeparatorNotFound("'1' not found") - hrp = bech[:pos] + if len(hrp := bech[:pos]) > 83: + raise InvalidLength(f"HRP has {len(hrp)} characters, the maximum is 83") data = chars_to_u5(bech[pos + 1 :]) return hrp, data @@ -111,13 +112,14 @@ def u5_decode(bech: str, encodings: list[Checksum]) -> tuple[str, list[int], Che hrp, data = u5_parse(bech) e = MissingEncoding("no encoding or encodings were passed") for spec in encodings: - if len(hrp) <= (datlen := len(bech) - 1 - spec.cs_len): - if datlen in (c := spec.coverage): + explen = len(bech32_hrp_expand(hrp)) + len(data) + if spec.cs_len <= len(data): + if explen in (c := spec.coverage): if spec.verify(bech32_hrp_expand(hrp) + data): return hrp, data[: -spec.cs_len], spec e = InvalidChecksum(f"{spec.kind} checksum invalid for hrp and data") if not isinstance(e, InvalidChecksum): - e = InvalidLength(f"{datlen} chars {spec.kind} reqs {min(c)}..{max(c)}") + e = InvalidLength(f"{explen} expanded, {spec.kind}: {min(c)}-{max(c)}") if not isinstance(e, (InvalidLength, InvalidChecksum)): e = MissingChecksum(f"{spec.kind}: {len(data)} data chars < {spec.cs_len}") raise e diff --git a/src/codex32/bip93.py b/src/codex32/bip93.py index 4fb4234d..51cab2fd 100644 --- a/src/codex32/bip93.py +++ b/src/codex32/bip93.py @@ -29,6 +29,7 @@ from codex32.bech32 import ( CHARSET, + InvalidLength, chars_to_u5, convertbits, u5_to_chars, @@ -39,11 +40,6 @@ from codex32.checksums import CODEX32, CODEX32_LONG from codex32.errors import CodexError -HRP_CODES = { - "ms": 0, # BIP-0032 master seed - "cl": 1, # CLN HSM secret -} # Registry: https://github.com/satoshilabs/slips/blob/master/slip-0173.md#uses-of-codex32 -IDX_ORDER = "sacdefghjklmnpqrstuvwxyz023456789" # Canonical BIP93 share indices alphabetical order BECH32_INV = [ 0, 1, @@ -83,9 +79,6 @@ # pylint: disable=missing-class-docstring -class IdNotLength4(CodexError): ... - - class InvalidThreshold(CodexError): ... @@ -110,9 +103,6 @@ class RepeatedIndex(CodexError): ... class ThresholdNotPassed(CodexError): ... -class InvalidSeedLength(CodexError): ... - - def bech32_mul(a, b): """Multiply two Bech32 values.""" res = 0 @@ -137,8 +127,24 @@ def bech32_lagrange(pts, x): def codex32_decode(codex): - """Validate a codex32 string, and determine HRP and data.""" - return u5_decode(codex, [CODEX32_LONG, CODEX32]) + """Validate a codex32 string's checksum and header, and determine HRP and data.""" + hrp, data, spec = u5_decode(codex, [CODEX32_LONG, CODEX32]) + if len(header := u5_to_chars(data[:6])) < 6: + raise MismatchedLength(f"'{header}' header too short: {len(data)} < 6") + if not (k := header[0]).isdigit(): + raise InvalidThreshold(f"threshold parameter '{k}' must be a digit") + if k == "0" and (idx := header[5]) != "s": + raise InvalidShareIndex(f"share index '{idx}' must be 's' when k='0'") + return hrp, data, spec + + +def ms32_verify(s: str): + """Validate a codex32-encoded master seed or share, and determine HRP and data.""" + if (hrp := u5_parse(s)[0]) != "ms": + raise MismatchedHrp(f"{hrp} != ms") + if len(s) not in (48, 54, 61, 67, 74, 127): # 16-32 (by 4) or 64 byte seeds + raise InvalidLength("'ms' strings must be 48, 54, 61, 67, 74 or 127 chars") + return codex32_decode(s) def codex32_interpolate(strings, x): @@ -155,33 +161,27 @@ def codex32_interpolate(strings, x): def codex32_encode(hrp: str, data): """Compute a codex32 string given HRP and data values.""" - spec = CODEX32_LONG if len(hrp) + len(data) > 80 else CODEX32 + spec = CODEX32_LONG if 2 * len(hrp) + 1 + len(data) + 13 > 93 else CODEX32 return u5_encode(hrp, data, spec) -def decode(hrp: str, s: str, pad_val: int | str = "any"): - """Decode a codex32 string, and determine header, seed, and padding.""" - hrpgot, data, _ = codex32_decode(s) - if hrpgot != hrp: - raise MismatchedHrp(f"{hrpgot} != {hrp}") - if len(header := u5_to_chars(data[:6])) < 6: - raise MismatchedLength(f"'{header}' header too short: {len(data)} < 6") - if not (k := header[0]).isdigit(): - raise InvalidThreshold(f"threshold parameter '{k}' must be a digit") - if k == "0" and (idx := header[5]) != "s": - raise InvalidShareIndex(f"share index '{idx}' must be 's' when k='0'") +def decode(s: str, pad_val: int | str = "any"): + """Decode a codex32-encoded master seed, and determine header, seed, and padding.""" + data = ms32_verify(s)[1] + if (header := u5_to_chars(data[:6]))[5] != "s": + raise InvalidShareIndex(f"only master seeds ('s') have bytes, got '{header}'") decoded = convertbits(data[6:], 5, 8, False, pad_val) - if hrp == "ms" and (not 16 <= (msl := len(decoded)) <= 64 or msl % 4): - raise InvalidSeedLength(f"Master seeds must be in 16..20..64 bytes, got {msl}") pad = data[-1] % (1 << ((len(data[6:]) * 5) % 8)) return header, bytes(decoded), pad if pad_val == "any" else pad_val -def encode(hrp: str, header: str, seed: bytes, pad_val: int | str = "CRC"): - """Encode a codex32 string given HRP, header, seed, and padding.""" +def encode(header: str, seed: bytes, pad_val: int | str = "CRC"): + """Encode a codex32-encoded master seed given header, seed, and padding.""" + if header[5:] != "s": + raise InvalidShareIndex(f"only secrets ('s') encode bytes, got '{header}'") u5_payload = convertbits(seed, 8, 5, True, pad_val) - ret = codex32_encode(hrp, chars_to_u5(header) + u5_payload) - if len(header) != 6 or (header, seed, pad_val) != decode(hrp, ret, pad_val): + ret = codex32_encode("ms", chars_to_u5(header) + u5_payload) + if len(header) != 6 or (header, seed, pad_val) != decode(ret, pad_val): raise MismatchedLength(f"'{header}' header must be 6 chars, got {len(header)}") return ret @@ -189,24 +189,27 @@ def encode(hrp: str, header: str, seed: bytes, pad_val: int | str = "CRC"): class Codex32String: """Class representing a codex32 string.""" + __slots__ = ("s", "is_upper", "hrp", "payload", "k", "ident", "share_idx") + + def __setattr__(self, name, value): + if hasattr(self, name): + raise AttributeError(f"{name} is read-only") + super().__setattr__(name, value) + + def __delattr__(self, name): + raise AttributeError(f"{name} is read-only") + def __init__(self, s: str) -> None: """Initialize Codex32String from a codex32 string.""" - self.is_upper = s.isupper() - self.hrp = codex32_decode(s)[0] - header, self.data, self.pad_val = decode(self.hrp, s) + self.s, self.is_upper = s, s.isupper() + verify = ms32_verify if u5_parse(s)[0] == "ms" else codex32_decode + self.hrp, data, _ = verify(s) + header, self.payload = u5_to_chars(data[:6]), u5_to_chars(data[6:]) self.k, self.ident, self.share_idx = header[0], header[1:5], header[5] - @property - def payload(self) -> str: - """Return the payload part of the codex32 string.""" - return u5_to_chars(convertbits(self.data, 8, 5, True, self.pad_val)) - - @property - def s(self) -> str: - """Return the full codex32 string.""" - header = self.k + self.ident + self.share_idx - ret = encode(self.hrp, header, self.data, self.pad_val) - return ret.upper() if ret and self.is_upper else ret + def data(self) -> bytes: + """Return the master seed bytes of a codex32-encoded master seed.""" + return decode(self.s)[1] def __str__(self) -> str: return self.s @@ -263,12 +266,9 @@ def interpolate_at( def from_seed( cls, data: bytes, prefix: str, pad_val: int | str = "CRC" ) -> "Codex32String": - """Create Codex32String from seed bytes and a prefix with an identifier.""" + """Create a master seed secret given prefix (ms1, k, ident) and seed bytes.""" hrp, data_part = u5_parse(prefix) - header = u5_to_chars(data_part) - k = "0" if not header else header[:1] - ident = header[1 : max(5, len(header) - 1)] - if len(ident) != 4: - raise IdNotLength4(f"prefix must contain a four-character identifier, got {len(ident)}") - share_idx = "s" if not header[5:] else header[5:6] - return cls(encode(hrp, k + ident + share_idx, data, pad_val)) + if hrp != "ms": + raise MismatchedHrp(f"{hrp} != ms") + header = u5_to_chars(data_part) + ("s" if len(data_part) == 5 else "") + return cls(encode(header, data, pad_val)) diff --git a/src/codex32/checksums.py b/src/codex32/checksums.py index a133fd61..499b2b3c 100644 --- a/src/codex32/checksums.py +++ b/src/codex32/checksums.py @@ -1,5 +1,3 @@ -# Bech32/Bech32m constants in this file are derived from work by: -# Copyright (c) 2017, 2020 Pieter Wuille, MIT License # codex32 constants in this file are derived from work by: # Author: Leon Olsson Curr and Pearlwort Sneed # License: BSD-3-Clause @@ -25,11 +23,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -"""Checksum specs and utilities for Bech32, codex32, and CRC variants.""" +"""Checksum specs and utilities for codex32 and CRC variants.""" # Generators are the reduction polynomials for polymod computations -BECH32_GEN = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] CODEX32_GEN = [ 0x19DC500CE73FDE210, 0x1BFAE00DEF77FE529, @@ -44,8 +41,6 @@ 0x0C577EAECCF1990D13C, 0x1887F74F8DC71B10651, ] -BECH32_CONST = 1 -BECH32M_CONST = 0x2BC830A3 CODEX32_CONST = 0x10CE0795C2FD1E62A CODEX32_LONG_CONST = 0x43381E570BF4798AB26 @@ -82,10 +77,8 @@ def create(self, values): codex32_long_spec = (CODEX32_LONG_GEN, 15, CODEX32_LONG_CONST) # detects 8 errors -CODEX32_LONG = Checksum("Long codex32", codex32_long_spec, (81, 1008)) -CODEX32 = Checksum("codex32", (CODEX32_GEN, 13, CODEX32_CONST), (0, 80)) -BECH32 = Checksum("Bech32", (BECH32_GEN, 6, BECH32_CONST), (0, 83)) # detects 4 errors -BECH32M = Checksum("Bech32m", (BECH32_GEN, 6, BECH32M_CONST), (0, 83)) +CODEX32_LONG = Checksum("Long codex32", codex32_long_spec, (96, 1023)) # expanded +CODEX32 = Checksum("codex32", (CODEX32_GEN, 13, CODEX32_CONST), (0, 93)) CRC1 = Checksum("CRC1", ([1], 1, 0), (0, 0)) CRC2 = Checksum("CRC2", ([3], 2, 0), (0, 1)) # detects 2 errors CRC3 = Checksum("CRC3", ([3], 3, 0), (0, 4)) diff --git a/src/codex32/segwit_addr.py b/src/codex32/segwit_addr.py deleted file mode 100644 index f970b009..00000000 --- a/src/codex32/segwit_addr.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2017, 2020 Pieter Wuille -# Copyright (c) 2026 Ben Westgate -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -"""Reference implementation for Bech32/Bech32m and segwit addresses.""" - -from enum import Enum - -from codex32.errors import CodexError -from codex32.bech32 import u5_encode, u5_decode, convertbits -from codex32.checksums import BECH32, BECH32M - - -class Encoding(Enum): - """Enumeration type to list the various supported encodings.""" - - BECH32 = 1 - BECH32M = 2 - - -def bech32_encode(hrp, data, spec): - """Compute a Bech32 string given HRP and data values.""" - return u5_encode(hrp, data, BECH32 if spec == Encoding.BECH32 else BECH32M) - - -def bech32_decode(bech: str): - """Validate a Bech32/Bech32m string, and determine HRP and data.""" - try: - hrp, data, spec = u5_decode(bech, [BECH32, BECH32M]) - if spec is None: - return (None, None, None) - return hrp, data, Encoding.BECH32 if spec == BECH32 else Encoding.BECH32M - except CodexError: - return (None, None, None) - - -def decode(hrp: str, addr): - """Decode a segwit address.""" - hrpgot, data, spec = bech32_decode(addr) - if hrpgot != hrp or not data: - return (None, None) - try: - decoded = convertbits(data[1:], 5, 8, False) - except CodexError: - decoded = None - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) - if data[0] > 16: - return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - if ( - data[0] == 0 - and spec != Encoding.BECH32 - or data[0] != 0 - and spec != Encoding.BECH32M - ): - return (None, None) - return (data[0], decoded) - - -def encode(hrp, witver, witprog): - """Encode a segwit address.""" - spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5, True, 0), spec) - if decode(hrp, ret) == (None, None): - return None - return ret diff --git a/tests/data/bip93_vectors.py b/tests/data/bip93_vectors.py index 2f03743e..0f914eeb 100644 --- a/tests/data/bip93_vectors.py +++ b/tests/data/bip93_vectors.py @@ -66,20 +66,33 @@ "secret_hex": "dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9", } -VECTOR_6 = { +VECTORS_6_7_8 = [ # (master seed hex, codex32 secret) + ("000102030405060708090a0b0c0d0e0f10111213", "ms10seedsqqqsyqcyq5rqwzqfpg9scrgwpugpzysn9vaqzzvs20xnl"), + ("202122232425262728292a2b2c2d2e2f3031323334353637", "ms10seedsyqsjygeyy5nzw2pf9g4jctfw9ucrzv3nxs6nvdau84gz0632s0xs"), + ("404142434445464748494a4b4c4d4e4f505152535455565758595a5b", "ms10seedsgpq5ys6yg4rywjzfff95cn2wfag9z5jn2324v46ct9d9hrcduqw8c3lccl"), +] + +VECTOR_9 = { "codex32_luea": "cl10lueasd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanvrktzhlhusz", - "ident_cln2": "cln2", "codex32_cln2": "cl10cln2sd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn9lcvcu7cez4s", "codex32_peev": "cl10peevst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqsjhsks4laxts8q", + "ms_peev": "ms10peevst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqstxmpzl24l6e0d", +} + +VECTOR_10 = { + "share_a": "test_vector12spana320zyxwvutsrqpnmlkjhgfedca320zyxwvutsrqpnmlkjhgfedca304ppsqh4l7v3dh", + "share_c": "test_vector12spancacdefghjklmnpqrstuvwxyz023acdefghjklmnpqrstuvwxyz023jxmjy7q9xl7d3ul", + "derived_d": "test_vector12spandll4f8jlh4e5vdvuldlfxu2jhdnll4f8jlh4e5vdvuldlfxu2jhdnx3pe3yqzdl7p30y", + "secret_s": "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5kdf37v5qm2l7l3az", } +VECTOR_11 = "a83characterlongcodex32humanreadablepartforsecretsharewiththedigit1andthelettersbio10keyss9mfjjk25y05e3nq" + VALID_CODEX32 = [ "A12UEL5LLGCHJ4UJCQVHG", "a12uel5llgchj4ujcqvhg", - "a74characterlonghumanreadablepartcontainingnumber1andexcludedcharactersbio15tttgsdupy3h58nvmja", "abcdef13qpzry9x8gf2tvdw0s3jn54khce6mua7lclc606q3t75r4", - "1199999999999999999999999999999999999999999999999999999999999999999999999999999997f7ekwq8dq7tm", "split12checkupstagehandshakeupstreamerranterredcaperred75pe8uz2kh9ey", "?13zyfclf624rkvjcl35t", ] @@ -87,9 +100,7 @@ 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", "?17v59aaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2uwygewx4t4gps0", ] @@ -97,8 +108,6 @@ " 12fauxxpgjxu9gyqhql4", # HRP character out of range "\x7f" + "12fauxxk7kd7xqlns9mj", # HRP character out of range "\x80" + "12fauxxgqp5ecwf5kzg3", # HRP character out of range - # overall max length exceeded - "a75characterslonghumanreadablepartcontainingnumber1andexcludedcharactersbio12fauxxau7wnkdhzp90r", "x12fauxbhf2k7v7ay7ua5", # Invalid data character "li12fauxxz4pdg55uwav3", # Too short checksum "de12fauxxrmt7mj886swl" + "\xff", # Invalid character in checksum @@ -111,8 +120,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 @@ -153,21 +160,19 @@ ] INVALID_MASTER_SEED_ENC = [ - ("MS", "0FAUXS", bytes(16)), # Invalid uppercase - ("MS", "0fauxs", bytes(16)), # Invalid mixed case - ("ms", "0FAUXS", bytes(16)), # Invalid mixed case - ("", "0fauxs", bytes(16)), # Invalid empty HRP - ("ms", "", b"\xd0" * 32), # Invalid empty header - ("ms", "0faux", b"\x80" * 32), # Invalid missing share idx - ("ms", "0", b"\x80" * 32), # Invalid missing identifier and share idx - ("ms", "0fauxxxs", bytes(16)), # Invalid identifier length - ("ms", "fauxxs", bytes(32)), # Invalid threshold - ("ms", "0fauxx", bytes(64)), # Invalid share idx - ("ms", "0fauxs", bytes(15)), # Invalid seed length (<16 bytes) - ("ms", "0fauxs", bytes(65)), # Invalid seed length (>64 bytes) - ("ms", "0fauxs", bytes(17)), # Invalid seed length (non-multiple of 4 bytes) - ("ms", "0fauxs", bytes(18)), # Invalid seed length (non-multiple of 4 bytes) - ("ms", "0fauxs", bytes(19)), # Invalid seed length (non-multiple of 4 bytes) + ("0FAUXS", bytes(16)), # Invalid mixed case + ("", b"\xd0" * 32), # Invalid empty header + ("0faux", b"\x80" * 32), # Invalid missing share idx + ("0", b"\x80" * 32), # Invalid missing identifier and share idx + ("0fauxxxs", bytes(16)), # Invalid identifier length + ("fauxxs", bytes(32)), # Invalid threshold + ("0fauxx", bytes(64)), # Invalid share idx + ("2fauxa", bytes(16)), # Invalid share idx: shares are not encoded from bytes + ("0fauxs", bytes(15)), # Invalid seed length (<16 bytes) + ("0fauxs", bytes(65)), # Invalid seed length (>64 bytes) + ("0fauxs", bytes(17)), # Invalid seed length (non-multiple of 4 bytes) + ("0fauxs", bytes(18)), # Invalid seed length (non-multiple of 4 bytes) + ("0fauxs", bytes(19)), # Invalid seed length (non-multiple of 4 bytes) ] @@ -244,6 +249,21 @@ "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxh9d0fhnvfyx3x", ] +VALID_NOT_MASTER_SEED = [ + "m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxx8t28z74x8hs4l", + "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxh9d0fhnvfyx3x", +] + +INVALID_HRP_VECTORS = [ + # expanded codeword lengths 94 and 95 + "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5kp0f0tlr2cxh0t", + "test_vector12spans6xqguzttxkeqnjsjzv4jv3nz5k6xqguzttxkeqnjsjzv4jv3nz5c2m7mxmk8s5qecp", + # checksum computed with the uppercase form of the HRP + "CL10PEEVST6CQH0WU7P5SSJYF4Z4EZ42KS9JLT3ZNEJU9UUYPR2HDDAK6TLQS3J5AYX4Y08079", + # HRP longer than 83 characters + "a84characterslongcodex32humanreadablepartforsecretsharewiththedigit1andthelettersbio10keyss9hqxu3pxsnkdkth", +] + BAD_CASES = [ "Ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", "mS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", diff --git a/tests/test_bech32.py b/tests/test_bech32.py deleted file mode 100644 index 7c9bc763..00000000 --- a/tests/test_bech32.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/python3 - -# Copyright (c) 2017 Pieter Wuille -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -"""Reference tests for segwit adresses""" - -import binascii -import unittest -from codex32 import segwit_addr - - -def segwit_scriptpubkey(witver, witprog): - """Construct a Segwit scriptPubKey for a given witness program.""" - return bytes([witver + 0x50 if witver else 0, len(witprog)] + witprog) - - -VALID_BECH32 = [ - "A12UEL5L", - "a12uel5l", - "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", - "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", - "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", - "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", - "?1ezyfcl", -] - -VALID_BECH32M = [ - "A1LQFN3A", - "a1lqfn3a", - "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6", - "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx", - "11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8", - "split1checkupstagehandshakeupstreamerranterredcaperredlc445v", - "?1v759aa", -] - -INVALID_BECH32 = [ - " 1nwldj5", # HRP character out of range - "\x7f" + "1axkwrx", # HRP character out of range - "\x80" + "1eym55h", # HRP character out of range - # overall max length exceeded - "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", - "pzry9x0s0muk", # No separator character - "1pzry9x0s0muk", # Empty HRP - "x1b4n0q5v", # Invalid data character - "li1dgmt3", # Too short checksum - "de1lg7wt" + "\xff", # Invalid character in checksum - "A1G7SGD8", # checksum calculated with uppercase form of HRP - "10a06t8", # empty HRP - "1qzzfhee", # empty HRP -] - -INVALID_BECH32M = [ - " 1xj0phk", # HRP character out of range - "\x7f" + "1g6xzxy", # HRP character out of range - "\x80" + "1vctc34", # HRP character out of range - # overall max length exceeded - "an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4", - "qyrz8wqd2c9m", # No separator character - "1qyrz8wqd2c9m", # Empty HRP - "y1b0jsk6g", # Invalid data character - "lt1igcx5c0", # Invalid data character - "in1muywd", # Too short checksum - "mm1crxm3i", # Invalid character in checksum - "au1s5cgom", # Invalid character in checksum - "M1VUXWEZ", # Checksum calculated with uppercase form of HRP - "16plkw9", # Empty HRP - "1p2gdwpf", # Empty HRP -] - -VALID_ADDRESS = [ - [ - "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", - "0014751e76e8199196d454941c45d1b3a323f1433bd6", - ], - [ - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", - "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", - ], - [ - "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", - "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", - ], - ["BC1SW50QGDZ25J", "6002751e"], - ["bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"], - [ - "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", - "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", - ], - [ - "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", - "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", - ], - [ - "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", - "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - ], -] - -INVALID_ADDRESS = [ - # Invalid HRP - "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut", - # Invalid checksum algorithm (bech32 instead of bech32m) - "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd", - # Invalid checksum algorithm (bech32 instead of bech32m) - "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf", - # Invalid checksum algorithm (bech32 instead of bech32m) - "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL", - # Invalid checksum algorithm (bech32m instead of bech32) - "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", - # Invalid checksum algorithm (bech32m instead of bech32) - "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47", - # Invalid character in checksum - "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4", - # Invalid witness version - "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R", - # Invalid program length (1 byte) - "bc1pw5dgrnzv", - # Invalid program length (41 bytes) - "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav", - # Invalid program length for witness version 0 (per BIP141) - "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", - # Mixed case - "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq", - # More than 4 padding bits - "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf", - # Non-zero padding in 8-to-5 conversion - "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j", - # Empty data section - "bc1gmk9yu", -] - -INVALID_ADDRESS_ENC = [ - ("BC", 0, 20), - ("bc", 0, 21), - ("bc", 17, 32), - ("bc", 1, 1), - ("bc", 16, 41), -] - - -class TestSegwitAddress(unittest.TestCase): - """Unit test class for segwit addressess.""" - - def test_valid_checksum(self): - """Test checksum creation and validation.""" - for spec in segwit_addr.Encoding: - tests = ( - VALID_BECH32 if spec == segwit_addr.Encoding.BECH32 else VALID_BECH32M - ) - for test in tests: - hrp, _, dspec = segwit_addr.bech32_decode(test) - self.assertTrue(hrp is not None and dspec == spec) - pos = test.rfind("1") - test = test[: pos + 1] + chr(ord(test[pos + 1]) ^ 1) + test[pos + 2 :] - hrp, _, dspec = segwit_addr.bech32_decode(test) - self.assertIsNone(hrp) - - def test_invalid_checksum(self): - """Test validation of invalid checksums.""" - for spec in segwit_addr.Encoding: - tests = ( - INVALID_BECH32 - if spec == segwit_addr.Encoding.BECH32 - else INVALID_BECH32M - ) - for test in tests: - hrp, _, dspec = segwit_addr.bech32_decode(test) - self.assertTrue(hrp is None or dspec != spec) - - def test_valid_address(self): - """Test whether valid addresses decode to the correct output.""" - for address, hexscript in VALID_ADDRESS: - hrp = "bc" - witver, witprog = segwit_addr.decode(hrp, address) - if witver is None: - hrp = "tb" - witver, witprog = segwit_addr.decode(hrp, address) - self.assertIsNotNone(witver, address) - scriptpubkey = segwit_scriptpubkey(witver, witprog) - self.assertEqual(scriptpubkey, binascii.unhexlify(hexscript)) - addr = segwit_addr.encode(hrp, witver, witprog) - self.assertEqual(address.lower(), addr) - - def test_invalid_address(self): - """Test whether invalid addresses fail to decode.""" - for test in INVALID_ADDRESS: - witver, _ = segwit_addr.decode("bc", test) - self.assertIsNone(witver) - witver, _ = segwit_addr.decode("tb", test) - self.assertIsNone(witver) - - def test_invalid_address_enc(self): - """Test whether address encoding fails on invalid input.""" - for hrp, version, length in INVALID_ADDRESS_ENC: - code = segwit_addr.encode(hrp, version, [0] * length) - self.assertIsNone(code) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_bip93.py b/tests/test_bip93.py index f466fecf..dc278366 100644 --- a/tests/test_bip93.py +++ b/tests/test_bip93.py @@ -1,7 +1,5 @@ # tests/test_bip93.py """Tests for BIP-93 codex32 implementation.""" -import inspect - import pytest from data.bip93_vectors import ( VECTOR_1, @@ -9,7 +7,12 @@ VECTOR_3, VECTOR_4, VECTOR_5, - VECTOR_6, + VECTORS_6_7_8, + VECTOR_9, + VECTOR_10, + VECTOR_11, + VALID_NOT_MASTER_SEED, + INVALID_HRP_VECTORS, VALID_CODEX32, VALID_CODEX32_LONG, INVALID_CODEX32, @@ -26,8 +29,6 @@ ) from codex32.bip93 import ( Codex32String, - IdNotLength4, - InvalidSeedLength, MismatchedHrp, MismatchedLength, codex32_decode, @@ -49,6 +50,28 @@ from codex32.checksums import CODEX32, CODEX32_LONG +@pytest.mark.parametrize( + "name", ("s", "is_upper", "hrp", "payload", "k", "ident", "share_idx") +) +def test_codex32_string_read_only_attributes(name): + """Stored metadata cannot be reassigned or deleted after parsing.""" + share = Codex32String(VECTOR_1["secret_s"]) + original = str(share) + with pytest.raises(AttributeError): + setattr(share, name, "changed") + with pytest.raises(AttributeError): + delattr(share, name) + assert str(share) == original + + +def test_codex32_string_slots(): + """Instances cannot acquire arbitrary attributes or an instance dict.""" + share = Codex32String(VECTOR_1["secret_s"]) + with pytest.raises(AttributeError): + setattr(share, "unexpected", "value") + assert not hasattr(share, "__dict__") + + def test_parts(): """Test Vector 1: parse a codex32 string into parts""" s = Codex32String(VECTOR_1["secret_s"]) @@ -59,16 +82,7 @@ def test_parts(): assert s.ident == VECTOR_1["identifier"] assert s.payload == VECTOR_1["payload"] assert s.checksum == VECTOR_1["checksum"] - assert s.data.hex() == VECTOR_1["secret_hex"] - - -def test_from_seed_requires_explicit_identifier(): - """Seed encoding must not infer an identifier or import BIP32.""" - seed = bytes.fromhex(VECTOR_3["secret_hex"]) - prefix = inspect.signature(Codex32String.from_seed).parameters["prefix"] - assert prefix.default is inspect.Parameter.empty - with pytest.raises(IdNotLength4, match="four-character identifier"): - Codex32String.from_seed(seed, "ms10") + assert s.data().hex() == VECTOR_1["secret_hex"] def test_derive_and_recover(): @@ -76,11 +90,13 @@ def test_derive_and_recover(): a = Codex32String(VECTOR_2["share_A"]) c = Codex32String(VECTOR_2["share_C"]) # interpolation target is 'D' (uppercase as inputs are uppercase) + with pytest.raises(InvalidShareIndex): # shares have no seed bytes + a.data() d = Codex32String.interpolate_at([a, c], "D") assert str(d) == VECTOR_2["derived_D"] s = Codex32String.interpolate_at([a, c], "S") assert str(s) == VECTOR_2["secret_S"] - assert s.data.hex() == VECTOR_2["secret_hex"] + assert s.data().hex() == VECTOR_2["secret_hex"] def test_from_seed_and_interpolate_3_of_5(): @@ -108,7 +124,7 @@ def test_from_seed_and_alternates(): # confirm all 16 encodings decode to same master data s = Codex32String.from_seed(seed, "ms10leet", pad_val) assert str(s) == VECTOR_4[f"secret_s_alternate_{pad_val}"] - assert s.data.hex() == VECTOR_4["secret_hex"] + assert s.data().hex() == VECTOR_4["secret_hex"] def test_long_string(): @@ -123,22 +139,41 @@ def test_long_string(): ) assert s.checksum == VECTOR_5["checksum"] assert str(s) == VECTOR_5["secret_s"] - assert s.data.hex() == VECTOR_5["secret_hex"] + assert s.data().hex() == VECTOR_5["secret_hex"] long_str = VECTOR_5["secret_s"] long_seed = Codex32String(long_str) - assert long_seed.data.hex() == VECTOR_5["secret_hex"] + assert long_seed.data().hex() == VECTOR_5["secret_hex"] + + +def test_master_seed_sizes(): + """Test Vectors 6, 7 and 8: 160-, 192- and 224-bit master seeds.""" + for seed_hex, secret in VECTORS_6_7_8: + assert Codex32String(secret).data().hex() == seed_hex def test_alternate_hrp(): - """Test Vector 6: codex32 strings with "cl" HRP.""" - c0 = Codex32String(VECTOR_6["codex32_luea"]) - assert str(c0) == VECTOR_6["codex32_luea"] - c0.ident = VECTOR_6["ident_cln2"] - assert str(c0) == VECTOR_6["codex32_cln2"] - c1 = Codex32String(VECTOR_6["codex32_cln2"]) - assert str(c1) == VECTOR_6["codex32_cln2"] - c2 = Codex32String.from_string("cl", VECTOR_6["codex32_peev"]) - assert str(c2) == VECTOR_6["codex32_peev"] + """Test Vector 9: codex32 strings with "cl" HRP.""" + c0 = Codex32String(VECTOR_9["codex32_luea"]) + assert str(c0) == VECTOR_9["codex32_luea"] + c1 = Codex32String(VECTOR_9["codex32_cln2"]) + assert str(c1) == VECTOR_9["codex32_cln2"] + c2 = Codex32String.from_string("cl", VECTOR_9["codex32_peev"]) + assert str(c2) == VECTOR_9["codex32_peev"] + assert Codex32String(VECTOR_9["ms_peev"]).payload == c2.payload + for s in VALID_NOT_MASTER_SEED: + assert str(Codex32String(s)) == s + + +def test_long_hrp(): + """Test Vectors 10 and 11: long HRPs select the checksum by expanded length.""" + a, c = Codex32String(VECTOR_10["share_a"]), Codex32String(VECTOR_10["share_c"]) + assert len(a.checksum) == 15 + assert str(Codex32String.interpolate_at([a, c], "d")) == VECTOR_10["derived_d"] + assert str(Codex32String.interpolate_at([a, c], "s")) == VECTOR_10["secret_s"] + assert len(Codex32String(VECTOR_11).hrp) == 83 + for s in INVALID_HRP_VECTORS: + with pytest.raises((InvalidLength, InvalidChecksum)): + Codex32String(s) def test_valid_codex32(): @@ -174,7 +209,6 @@ def test_invalid_master_seed(): MismatchedHrp, MismatchedLength, InvalidChecksum, - InvalidSeedLength, InvalidLength, InvalidChar, InvalidThreshold, @@ -182,26 +216,26 @@ def test_invalid_master_seed(): IncompleteGroup, ) ): - decode("ms", test) + decode(test) def test_invalid_master_seed_enc(): """Test whether master seed encoding fails on invalid input.""" - for hrp, header, data in INVALID_MASTER_SEED_ENC: + for header, data in INVALID_MASTER_SEED_ENC: with pytest.raises( ( MissingHrp, MismatchedLength, - InvalidSeedLength, InvalidChar, InvalidCase, InvalidDataValue, InvalidThreshold, InvalidShareIndex, + InvalidLength, AssertionError, ) ): - encode(hrp, header, data) + encode(header, data) def test_bad_checksums(): @@ -215,7 +249,7 @@ def test_wrong_checksums_or_length(): """Test strings with wrong checksums or lengths.""" for chk in WRONG_CHECKSUMS: with pytest.raises( - (InvalidLength, InvalidSeedLength, IncompleteGroup, InvalidChecksum) + (InvalidLength, IncompleteGroup, InvalidChecksum) ): Codex32String(chk) @@ -223,7 +257,7 @@ def test_wrong_checksums_or_length(): def test_invalid_length(): """Test strings with invalid lengths.""" for chk in INVALID_LENGTHS: - with pytest.raises((InvalidLength, InvalidSeedLength, IncompleteGroup)): + with pytest.raises((InvalidLength, IncompleteGroup)): Codex32String(chk) diff --git a/tests/test_roundtrip_interpolated.py b/tests/test_roundtrip_interpolated.py deleted file mode 100644 index 6c6dfbef..00000000 --- a/tests/test_roundtrip_interpolated.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Test for round-trip encoding/decoding and recovery via interpolation.""" - -from codex32 import Codex32String, decode - - -def test_round_trip_recovery(): - """Test round-trip encoding/decoding and recovery via interpolation.""" - # secret share from seed - s = Codex32String.from_seed( - bytes.fromhex("68f14219957131d21b615271058437e8"), "ms13k00ls" - ) - assert s.s == "ms13k00lsdrc5yxv4wycayxmp2fcstpphaq55a60p9hfds9t" - a = Codex32String.from_seed( - bytes.fromhex("641be1cb12c97ede1c6bad8edf067760"), "ms13k00la" - ) - assert a.s == "ms13k00lavsd7rjcje9ldu8rt4k8d7pnhvppyrt5gpff9wwl" - c = Codex32String.from_seed( - bytes.fromhex("61b3c4052f7a31dc2b425c843a13c9b4"), "ms13k00lc" - ) - assert c.s == "ms13k00lcvxeugpf00gcac26ztjzr5y7fknx9cw72l8md0xn" - # derive next share via interpolation - d = Codex32String.interpolate_at([s, a, c], "d") - assert d.s == "ms13k00ldp4v5nw8lph96x47mjxzgwjexehw766s4dmj6qx8" - - # now round-trip d share ('d' is derived via interpolation, NOT via 'from_seed') - dd = Codex32String.from_seed(d.data, "ms13k00ld", d.pad_val) - assert dd.s == d.s - - e = Codex32String.interpolate_at([s, a, c], "e") - f = Codex32String.interpolate_at([s, a, c], "f") - assert e.s == "ms13k00lezuknydaaygk5u20zs4fm736vj6zlcrjhtduanyk" - assert f.s == "ms13k00lf0ehe53zsu6vrxcjjh9v7wzsa8vd9pjk28l8zavw" - - # recover from shares, use 'd' without round-trip - rec_s = Codex32String.interpolate_at([a, c, d], "s") - # recover from shares, use 'd' after round-trip - rec_ss = Codex32String.interpolate_at([a, c, dd], "s") - # confirm recovered secrets and padding match original - assert decode(s.hrp, rec_s.s, "CRC") == ("3k00ls", s.data, "CRC") - assert decode(s.hrp, rec_ss.s, "CRC") == ("3k00ls", s.data, "CRC")