tally
shamir's secret sharing over gf(256).
git clone https://owenrusk.dev/tally.git
commit 24930610342c02da53780dd5aef52c76fb7e58da author Owen Rusk <owen@papermothgames.com> date 2024-06-11 20:12:41 -0500
gf256: add, mul, div via log tables start of a small shamir library, so the studio's recovery codes never sit whole in one place, or with one person.
| .gitignore | +5 | -0 |
| tally/__init__.py | +0 | -0 |
| tally/gf256.py | +49 | -0 |
| tests/__init__.py | +0 | -0 |
| tests/test_gf256.py | +50 | -0 |
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47584ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.egg-info/ +build/ +dist/ +.venv/ diff --git a/tally/__init__.py b/tally/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tally/gf256.py b/tally/gf256.py new file mode 100644 index 0000000..98e31e5 --- /dev/null +++ b/tally/gf256.py @@ -0,0 +1,49 @@ +# arithmetic in gf(2^8), reduced by the aes polynomial x^8 + x^4 + x^3 + x + 1. +# addition is xor. multiplication goes through log tables built from the generator 3. + +POLY = 0x11B + +# EXP runs twice round so LOG[a] + LOG[b] never needs a mod +EXP = [0] * 510 +LOG = [0] * 256 + + +def _build() -> None: + x = 1 + for i in range(255): + EXP[i] = x + LOG[x] = i + # x * 3 is x * 2 + x + x2 = x << 1 + if x2 & 0x100: + x2 ^= POLY + x = x2 ^ x + for i in range(255, 510): + EXP[i] = EXP[i - 255] + + +_build() + + +def add(a: int, b: int) -> int: + return a ^ b + + +def mul(a: int, b: int) -> int: + if a == 0 or b == 0: + return 0 + return EXP[LOG[a] + LOG[b]] + + +def inv(a: int) -> int: + if a == 0: + raise ZeroDivisionError("0 has no inverse in gf(256)") + return EXP[255 - LOG[a]] + + +def div(a: int, b: int) -> int: + if b == 0: + raise ZeroDivisionError("division by 0 in gf(256)") + if a == 0: + return 0 + return EXP[LOG[a] + 255 - LOG[b]] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_gf256.py b/tests/test_gf256.py new file mode 100644 index 0000000..c8a935a --- /dev/null +++ b/tests/test_gf256.py @@ -0,0 +1,50 @@ +import unittest + +from tally import gf256 + + +def slow_mul(a: int, b: int) -> int: + # shift and add, reducing as we go + out = 0 + while b: + if b & 1: + out ^= a + a <<= 1 + if a & 0x100: + a ^= gf256.POLY + b >>= 1 + return out + + +class FieldTest(unittest.TestCase): + def test_mul_matches_shift_and_add(self) -> None: + for a in range(256): + for b in range(256): + self.assertEqual(gf256.mul(a, b), slow_mul(a, b), (a, b)) + + def test_exp_hits_every_nonzero_element(self) -> None: + self.assertEqual(sorted(gf256.EXP[:255]), list(range(1, 256))) + + def test_every_nonzero_has_an_inverse(self) -> None: + for a in range(1, 256): + self.assertEqual(gf256.mul(a, gf256.inv(a)), 1, a) + + def test_div_undoes_mul(self) -> None: + for a in range(256): + for b in range(1, 256): + self.assertEqual(gf256.div(gf256.mul(a, b), b), a, (a, b)) + + def test_add_is_its_own_inverse(self) -> None: + for a in range(256): + self.assertEqual(gf256.add(a, a), 0) + + def test_zero(self) -> None: + with self.assertRaises(ZeroDivisionError): + gf256.inv(0) + with self.assertRaises(ZeroDivisionError): + gf256.div(1, 0) + self.assertEqual(gf256.div(0, 7), 0) + + +if __name__ == "__main__": + unittest.main()