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