From f64da6f9d932118db25969f059ac3a7b65b0c3ac Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Mon, 5 Oct 2026 01:29:48 -0500 Subject: [PATCH] correct: Redact edit characters CorrectionEdit was a plain dataclass, so repr() of a correction candidate disclosed each edit's observed and replacement characters. The replacement can be true recovery text.\n\nExclude those two fields from dataclass repr while preserving explicit field access, equality, hashing, and correction behavior. Add a regression that repairs eight erasures and verifies the candidate representation does not reveal the restored characters.\n\nFixes #129 --- src/codex32/correction.py | 6 +++--- tests/test_public_api.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/codex32/correction.py b/src/codex32/correction.py index 9abd345..5df1a1c 100644 --- a/src/codex32/correction.py +++ b/src/codex32/correction.py @@ -22,7 +22,7 @@ """Fixed BCH correction derived from PR #70, with reverse-indexed coordinates.""" from collections.abc import Callable, Sequence -from dataclasses import dataclass, replace +from dataclasses import dataclass, field, replace from functools import cache, lru_cache from math import comb from time import monotonic @@ -85,8 +85,8 @@ def __init__( class CorrectionEdit: kind: Literal["substitution", "erasure", "insertion", "deletion", "transposition"] reverse_index: int - observed: str - replacement: str + observed: str = field(repr=False) + replacement: str = field(repr=False) @dataclass(frozen=True, slots=True) diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 09d66d4..1901802 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -11,7 +11,16 @@ from data.bip93_vectors import VECTOR_2 import codex32 -from codex32 import CorrectionCandidate, Header, MasterSeed, Share, parse_codex32 +from codex32 import ( + CorrectionCandidate, + CorrectionContext, + Header, + MasterSeed, + Profile, + Share, + correct, + parse_codex32, +) from codex32.errors import InvalidIdentifier, InvalidShareIndex, InvalidThreshold @@ -95,6 +104,16 @@ def test_artifact_default_rendering_does_not_disclose_recovery_text() -> None: assert repr(share) == "Share()" +def test_correction_edits_do_not_render_recovery_characters() -> None: + text = VECTOR_2["share_A"] + candidate = correct(CorrectionContext(Profile.MS), text[:9] + "?" * 8 + text[17:])[0] + rendered = repr(candidate) + + assert len(candidate.edits) == 8 + assert all(f"'{edit.replacement}'" not in rendered for edit in candidate.edits) + assert "observed" not in rendered and "replacement" not in rendered + + def test_master_seed_default_rendering_does_not_disclose_seed_material() -> None: secret = MasterSeed.from_seed(bytes(range(16)), identifier="test")