owenrusk.dev

tally

shamir's secret sharing over gf(256).

git clone https://owenrusk.dev/tally.git

commit c5a86b704d0ff77fd00cc7db5b80005c7f10b5f5
parent 894350c029ce52829bb62903262720c00ac1ce86
author Owen Rusk <owen@papermothgames.com>
date   2024-07-08 20:48:13 -0500
cli: tally split and tally join
pyproject.toml+4-1
tally/__main__.py+5-0
tally/cli.py+55-0
diff --git a/pyproject.toml b/pyproject.toml
index e53c9fe..19bb596 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,9 +4,12 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "tally"
-version = "0.1.0"
+version = "0.2.0"
 description = "shamir's secret sharing over gf(256)"
 requires-python = ">=3.12"
 
+[project.scripts]
+tally = "tally.cli:main"
+
 [tool.setuptools]
 packages = ["tally"]
diff --git a/tally/__main__.py b/tally/__main__.py
new file mode 100644
index 0000000..dd8a8c9
--- /dev/null
+++ b/tally/__main__.py
@@ -0,0 +1,5 @@
+import sys
+
+from .cli import main
+
+sys.exit(main())
diff --git a/tally/cli.py b/tally/cli.py
new file mode 100644
index 0000000..2becc55
--- /dev/null
+++ b/tally/cli.py
@@ -0,0 +1,55 @@
+import argparse
+import sys
+
+from . import codec, shamir
+from .codec import ShareError
+
+
+def cmd_split(args: argparse.Namespace) -> int:
+    if args.secret is not None:
+        secret = args.secret.encode()
+    elif args.file in (None, "-"):
+        secret = sys.stdin.buffer.read()
+    else:
+        with open(args.file, "rb") as f:
+            secret = f.read()
+    split_id = codec.new_id()
+    for x, data in shamir.split(secret, args.k, args.n):
+        print(codec.encode(codec.Share(split_id, args.k, x, data)))
+    return 0
+
+
+def cmd_join(args: argparse.Namespace) -> int:
+    lines = args.shares or [line for line in sys.stdin if line.strip()]
+    shares = [codec.decode(line) for line in lines]
+    if len({s.id for s in shares}) > 1:
+        raise ShareError("these shares come from different splits")
+    k = shares[0].k
+    if len(shares) < k:
+        raise ShareError(f"need {k} shares, got {len(shares)}")
+    secret = shamir.combine([(s.x, s.data) for s in shares[:k]])
+    print(secret.decode())
+    return 0
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser(prog="tally", description="split a secret into shares, or join them back")
+    sub = parser.add_subparsers(dest="command", required=True)
+
+    p = sub.add_parser("split", help="split a secret into n shares")
+    p.add_argument("-k", type=int, required=True, help="shares needed to rebuild it")
+    p.add_argument("-n", type=int, required=True, help="shares to make")
+    p.add_argument("-s", "--secret", help="the secret itself, instead of a file")
+    p.add_argument("file", nargs="?", help="file to read the secret from (default: stdin)")
+    p.set_defaults(func=cmd_split)
+
+    p = sub.add_parser("join", help="rebuild a secret from k shares")
+    p.add_argument("shares", nargs="*", help="shares (default: one per line on stdin)")
+    p.set_defaults(func=cmd_join)
+
+    args = parser.parse_args(argv)
+    try:
+        return args.func(args)
+    except ShareError as e:
+        print(f"tally: {e}", file=sys.stderr)
+        return 1