From 06c26dc0bbcd28ec8e52958f1f2fd4b8f15608ac Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Sun, 7 Dec 2025 16:12:53 +0100 Subject: [PATCH 1/3] add round-trip for share derived via interpolation --- tests/test_roundtrip_interpolated.py | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/test_roundtrip_interpolated.py diff --git a/tests/test_roundtrip_interpolated.py b/tests/test_roundtrip_interpolated.py new file mode 100644 index 0000000..13fd330 --- /dev/null +++ b/tests/test_roundtrip_interpolated.py @@ -0,0 +1,41 @@ +from src.codex32.codex32 import Codex32String + + +# secret share from seed +s = Codex32String.from_seed(bytes.fromhex("68f14219957131d21b615271058437e8"), "ms13k00ls") +assert s.s == "ms13k00lsdrc5yxv4wycayxmp2fcstppharks8z0r84pf3uj" + +# derive 'a' via proposed BIP-85 +a = Codex32String.from_seed(bytes.fromhex("641be1cb12c97ede1c6bad8edf067760"), "ms13k00la") +assert a.s == "ms13k00lavsd7rjcje9ldu8rt4k8d7pnhvppyrt5gpff9wwl" + +# derive 'c' via proposed BIP-85 +c = Codex32String.from_seed(bytes.fromhex("61b3c4052f7a31dc2b425c843a13c9b4"), "ms13k00lc") +assert c.s == "ms13k00lcvxeugpf00gcac26ztjzr5y7fkjl7fx7nx7ykhkr" + +# derive next share via interpolation +d = Codex32String.interpolate_at([s, a, c], "d") +assert d.s == "ms13k00ldp4v5nw8lph96x47mjxzgwjexe44p32swkq99e0w" + +# now round-trip d share ('d' is derived via interpolation, NOT via 'from_seed') +dd = Codex32String.from_seed(d.data, "ms13k00ld") +# they are NOT equal after round-trip - seem we miss padding at interpolation level +assert dd.s != d.s # FAIL (should equal) + +# irrelevant +# e = Codex32String.interpolate_at([s, a, c], "e") +# assert e.s == "ms13k00lezuknydaaygk5u20zs4fm736vj909mdj6xqp8pc2" +# +# f = Codex32String.interpolate_at([s, a, c], "f") +# assert f.s == "ms13k00lf0ehe53zsu6vrxcjjh9v7wzsa83mqfvku3fd8kem" + +# recover from shares, use 'd' without round-trip +rec_s = Codex32String.interpolate_at([a, d, c], "s") +# recover from shares, use 'd' after round-trip +rec_ss = Codex32String.interpolate_at([a, dd, c], "s") + +print(" s:", s.data.hex()) +print(" rec_s:", rec_s.data.hex()) +print("rec_ss:", rec_ss.data.hex()) +assert s.data == rec_s.data +assert s.data == rec_ss.data # FAIL From dd4767a91c5a1c62661c0addf85f789463ef454d Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Sun, 28 Dec 2025 16:05:20 +0100 Subject: [PATCH 2/3] recover_with_bytes --- tests/test_roundtrip_interpolated.py | 110 +++++++++++++++++---------- 1 file changed, 69 insertions(+), 41 deletions(-) diff --git a/tests/test_roundtrip_interpolated.py b/tests/test_roundtrip_interpolated.py index 13fd330..e5bc414 100644 --- a/tests/test_roundtrip_interpolated.py +++ b/tests/test_roundtrip_interpolated.py @@ -1,41 +1,69 @@ -from src.codex32.codex32 import Codex32String - - -# secret share from seed -s = Codex32String.from_seed(bytes.fromhex("68f14219957131d21b615271058437e8"), "ms13k00ls") -assert s.s == "ms13k00lsdrc5yxv4wycayxmp2fcstppharks8z0r84pf3uj" - -# derive 'a' via proposed BIP-85 -a = Codex32String.from_seed(bytes.fromhex("641be1cb12c97ede1c6bad8edf067760"), "ms13k00la") -assert a.s == "ms13k00lavsd7rjcje9ldu8rt4k8d7pnhvppyrt5gpff9wwl" - -# derive 'c' via proposed BIP-85 -c = Codex32String.from_seed(bytes.fromhex("61b3c4052f7a31dc2b425c843a13c9b4"), "ms13k00lc") -assert c.s == "ms13k00lcvxeugpf00gcac26ztjzr5y7fkjl7fx7nx7ykhkr" - -# derive next share via interpolation -d = Codex32String.interpolate_at([s, a, c], "d") -assert d.s == "ms13k00ldp4v5nw8lph96x47mjxzgwjexe44p32swkq99e0w" - -# now round-trip d share ('d' is derived via interpolation, NOT via 'from_seed') -dd = Codex32String.from_seed(d.data, "ms13k00ld") -# they are NOT equal after round-trip - seem we miss padding at interpolation level -assert dd.s != d.s # FAIL (should equal) - -# irrelevant -# e = Codex32String.interpolate_at([s, a, c], "e") -# assert e.s == "ms13k00lezuknydaaygk5u20zs4fm736vj909mdj6xqp8pc2" -# -# f = Codex32String.interpolate_at([s, a, c], "f") -# assert f.s == "ms13k00lf0ehe53zsu6vrxcjjh9v7wzsa83mqfvku3fd8kem" - -# recover from shares, use 'd' without round-trip -rec_s = Codex32String.interpolate_at([a, d, c], "s") -# recover from shares, use 'd' after round-trip -rec_ss = Codex32String.interpolate_at([a, dd, c], "s") - -print(" s:", s.data.hex()) -print(" rec_s:", rec_s.data.hex()) -print("rec_ss:", rec_ss.data.hex()) -assert s.data == rec_s.data -assert s.data == rec_ss.data # FAIL +from itertools import product +from src.codex32.codex32 import Codex32String, IDX_ORDER + + +def recover_with_bytes(hrp, header_str, share_idx_list, share_bytes_list, target="s"): + """Recover codex32 secret from share bytes assuming default padding.""" + pad_len = (5 - (len(share_bytes_list[0]) * 8) % 5) % 5 + default_padded_shares = IDX_ORDER[1 : 1 + len(share_idx_list)] + pad_candidates = product(range(1 << pad_len), repeat=len(share_idx_list)) + for pad_vals in pad_candidates: + given_shares = [] + for idx, data, pad_val in zip(share_idx_list, share_bytes_list, pad_vals): + given_shares.append(Codex32String.from_seed(data, hrp + '1' + header_str + idx, pad_val)) + for share_idx in default_padded_shares: + initial_share = Codex32String.interpolate_at(given_shares, share_idx) + round_tripped_initial_share = Codex32String.from_seed(initial_share.data, hrp + "1" + header_str + share_idx) + if str(round_tripped_initial_share) != str(initial_share): + break + return Codex32String.interpolate_at(given_shares, target) + +def test_round_trip_recovery(): + # secret share from seed + s = Codex32String.from_seed(bytes.fromhex("68f14219957131d21b615271058437e8"), "ms13k00ls") + assert s.s == "ms13k00lsdrc5yxv4wycayxmp2fcstppharks8z0r84pf3uj" + + # derive 'a' via proposed BIP-85 + a = Codex32String.from_seed(bytes.fromhex("641be1cb12c97ede1c6bad8edf067760"), "ms13k00la") + assert a.s == "ms13k00lavsd7rjcje9ldu8rt4k8d7pnhvppyrt5gpff9wwl" + + # derive 'c' via proposed BIP-85 + c = Codex32String.from_seed(bytes.fromhex("61b3c4052f7a31dc2b425c843a13c9b4"), "ms13k00lc") + assert c.s == "ms13k00lcvxeugpf00gcac26ztjzr5y7fkjl7fx7nx7ykhkr" + + # derive next share via interpolation + d = Codex32String.interpolate_at([s, a, c], "d") + assert d.s == "ms13k00ldp4v5nw8lph96x47mjxzgwjexe44p32swkq99e0w" + + # now round-trip d share ('d' is derived via interpolation, NOT via 'from_seed') + dd = Codex32String.from_seed(d.data, "ms13k00ld") + # they are NOT equal after round-trip - seem we miss padding at interpolation level + # assert dd.s == d.s # FAIL (should equal) + + # irrelevant + # e = Codex32String.interpolate_at([s, a, c], "e") + # assert e.s == "ms13k00lezuknydaaygk5u20zs4fm736vj909mdj6xqp8pc2" + # + # f = Codex32String.interpolate_at([s, a, c], "f") + # assert f.s == "ms13k00lf0ehe53zsu6vrxcjjh9v7wzsa83mqfvku3fd8kem" + + # recover from shares, use 'd' without round-trip + rec_s = Codex32String.interpolate_at([a, d, c], "s") + # recover from shares, use 'd' after round-trip + rec_ss = Codex32String.interpolate_at([a, dd, c], "s") + + + xx = recover_with_bytes("ms", "3k00l", ["a", "d", "c"], [a.data, d.data, c.data]) + # recover with round-trip D share + yy = recover_with_bytes("ms", "3k00l", ["a", "d", "c"], [a.data, dd.data, c.data]) + + print(" s:", s.data.hex()) + print(" rec_s:", rec_s.data.hex()) + print("rec_ss:", rec_ss.data.hex()) + print("rec_xx:", xx.data.hex()) + print("rec_yy:", yy.data.hex()) + assert s.data == rec_s.data + assert s.data == yy.data # FAIL + assert s.data == xx.data # FAIL + assert s.data == rec_ss.data # FAIL + From 725552e4da75eda380b41a802db8849b112f919b Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Mon, 29 Dec 2025 15:45:02 +0100 Subject: [PATCH 3/3] include 's' in 'default_padded_shares' --- tests/test_roundtrip_interpolated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_roundtrip_interpolated.py b/tests/test_roundtrip_interpolated.py index e5bc414..4748282 100644 --- a/tests/test_roundtrip_interpolated.py +++ b/tests/test_roundtrip_interpolated.py @@ -5,7 +5,7 @@ def recover_with_bytes(hrp, header_str, share_idx_list, share_bytes_list, target="s"): """Recover codex32 secret from share bytes assuming default padding.""" pad_len = (5 - (len(share_bytes_list[0]) * 8) % 5) % 5 - default_padded_shares = IDX_ORDER[1 : 1 + len(share_idx_list)] + default_padded_shares = IDX_ORDER[: len(share_idx_list)] pad_candidates = product(range(1 << pad_len), repeat=len(share_idx_list)) for pad_vals in pad_candidates: given_shares = []