Skip to content

Commit 6dd7828

Browse files
authored
correct: Redact edit characters (#132)
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. Exclude 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. Fixes #129
1 parent 1eed32c commit 6dd7828

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

‎src/codex32/correction.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"""Fixed BCH correction derived from PR #70, with reverse-indexed coordinates."""
2323

2424
from collections.abc import Callable, Sequence
25-
from dataclasses import dataclass, replace
25+
from dataclasses import dataclass, field, replace
2626
from functools import cache, lru_cache
2727
from math import comb
2828
from time import monotonic
@@ -85,8 +85,8 @@ def __init__(
8585
class CorrectionEdit:
8686
kind: Literal["substitution", "erasure", "insertion", "deletion", "transposition"]
8787
reverse_index: int
88-
observed: str
89-
replacement: str
88+
observed: str = field(repr=False)
89+
replacement: str = field(repr=False)
9090

9191

9292
@dataclass(frozen=True, slots=True)

‎tests/test_public_api.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@
1111
from data.bip93_vectors import VECTOR_2
1212

1313
import codex32
14-
from codex32 import CorrectionCandidate, Header, MasterSeed, Share, parse_codex32
14+
from codex32 import (
15+
CorrectionCandidate,
16+
CorrectionContext,
17+
Header,
18+
MasterSeed,
19+
Profile,
20+
Share,
21+
correct,
22+
parse_codex32,
23+
)
1524
from codex32.errors import InvalidIdentifier, InvalidShareIndex, InvalidThreshold
1625

1726

@@ -95,6 +104,16 @@ def test_artifact_default_rendering_does_not_disclose_recovery_text() -> None:
95104
assert repr(share) == "Share(<redacted>)"
96105

97106

107+
def test_correction_edits_do_not_render_recovery_characters() -> None:
108+
text = VECTOR_2["share_A"]
109+
candidate = correct(CorrectionContext(Profile.MS), text[:9] + "?" * 8 + text[17:])[0]
110+
rendered = repr(candidate)
111+
112+
assert len(candidate.edits) == 8
113+
assert all(f"'{edit.replacement}'" not in rendered for edit in candidate.edits)
114+
assert "observed" not in rendered and "replacement" not in rendered
115+
116+
98117
def test_master_seed_default_rendering_does_not_disclose_seed_material() -> None:
99118
secret = MasterSeed.from_seed(bytes(range(16)), identifier="test")
100119

0 commit comments

Comments
 (0)