owenrusk.dev

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()