commit 125f5c968fb34ea79a3042dbfbcf90d5adb38877 Author: Daniel Langbein Date: Fri Sep 2 11:27:50 2022 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a71b09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/venv +/.idea +**/__pycache__ +/build +/dist +**/*.egg-info \ No newline at end of file diff --git a/.run/depaper.run.xml b/.run/depaper.run.xml new file mode 100644 index 0000000..2fd17a2 --- /dev/null +++ b/.run/depaper.run.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/.run/enpaper.run.xml b/.run/enpaper.run.xml new file mode 100644 index 0000000..7e48560 --- /dev/null +++ b/.run/enpaper.run.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5abe4e3 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Paper-Secret + +Shamir Secret Sharing on paper using gfshare. + +## Installation + +`gfshare` is required to split and merge the secret. +See `man gfshare` for an explanation of Shamir Secret Sharing in gf(2**8). + +```shell +sudo pacman -S --needed libgfshare +``` + +`qrencode` and `imagemagick` (`convert`) are required to create and merge QR-codes during the split process. +One can set the according parameters of `split_encode` to `False` to skip this step. + +```shell +sudo pacman -S --needed qrencode imagemagick +``` + +`enscript` is required to create a PDF containing the split secret in text form. +One can set the according parameters of `split_encode` to `False` to skip this step. + +```shell +sudo pacman -S --needed enscript +``` + +## Usage + +Create a secret: + +```shell +cat > secret.txt +``` + +Split the secret into 5 lines: + +```shell +./enpaper.py secret.txt +``` + +Manually delete up to 2 of the 5 lines in `secret.txt_txt.txt`. + +Then recreate the secret: + +```shell +./depaper.py secret.txt.split-text.txt +``` + +Print the secret: + +```shell +cat secret.txt.split-text.txt.merged.txt +``` + +## Notes + +* https://en.wikipedia.org/Shamir's_Secret_Sharing diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c035a4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +# https://packaging.python.org/tutorials/packaging-projects/#creating-pyproject-toml + +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dd048a0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +; setup.cfg is the configuration file for setuptools. +; https://packaging.python.org/tutorials/packaging-projects/#configuring-metadata + +[metadata] +name = paper-secret +version = 0.1.0 +author = Daniel Langbein +author_email = daniel@systemli.org +description = Shamir Secret Sharing on paper using gfshare +long_description = file: README.md +long_description_content_type = text/markdown +url = https://codeberg.org/privacy1st/paper-secret +project_urls = + Bug Tracker = https://codeberg.org/privacy1st/paper-secret/issues + +; https://pypi.org/classifiers/ +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python :: 3 + License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication + Operating System :: Unix + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.10 + +[options.packages.find] +where = src + +[options.entry_points] +; https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html +console_scripts= + enpaper = paper_secret.enpaper:main + depaper = paper_secret.depaper:main diff --git a/src/paper_secret/__init__.py b/src/paper_secret/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/paper_secret/depaper.py b/src/paper_secret/depaper.py new file mode 100755 index 0000000..17199dd --- /dev/null +++ b/src/paper_secret/depaper.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import base64 +import tempfile +from pathlib import Path +import sys +import paper_secret.util as util + + +def main(): + assert len(sys.argv) == 2, 'Expected one argument' + merge_decode(Path(sys.argv[1])) + + +def merge_decode(src: Path) -> Path: + """ + :param src: A file with at least k non-empty lines. + :return: Path to file containing reconstructed secret. + """ + assert src.exists(), str(src) + assert src.is_file(), str(src) + + content = src.read_text('UTF-8') + # All non-empty lines + lines = [line.strip() for line in content.splitlines() + if len(line.strip()) > 0] + assert len(lines) >= 2 + + merged = src.parent.joinpath(src.name + '.merged.txt') + assert not merged.exists() + + with tempfile.TemporaryDirectory() as tmpdirname: + tmpdir = Path(tmpdirname) + + parts = [] + for line in lines: + assert len(line) >= 4 + + encoded_suffix: str + encoded_part: str + encoded_suffix, encoded_part = line[0:3], line[3:] + + binary_part = base64.b64decode(encoded_part.encode('UTF-8')) + + part = tmpdir.joinpath('.' + encoded_suffix) + part.write_bytes(binary_part) + parts.append(part) + + command = ['gfcombine', '-o', str(merged)] + [str(part) for part in parts] + util.execute_stdin_capture(command) + + return merged + + +if __name__ == '__main__': + main() diff --git a/src/paper_secret/enpaper.py b/src/paper_secret/enpaper.py new file mode 100755 index 0000000..dce7e34 --- /dev/null +++ b/src/paper_secret/enpaper.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import base64 +import tempfile +from pathlib import Path +import sys +import paper_secret.util as util + + +def main(): + assert len(sys.argv) == 2, 'Expected one argument' + split_encode(Path(sys.argv[1])) + + +def split_encode( + secret: Path, + k: int = 3, + n: int = 5, + create_qr_codes: bool = True, + merge_qr_codes: bool = True, + create_text_pdf: bool = True +) -> list[Path]: + """ + Creates a file ending in `_txt.txt`. This file consists of n lines. + k of these n lines are enough to reconstruct the secret. + + The same content is also stored in a `.pdf` file. + The lines are split up into n blocks of grouped lines. + k of these n blocks are enough to reconstruct the secret. + + Each line is also available as QR-code inside a `.pdf` file. + + :param secret: File containing a secret. + :param k: Threshold for recombination. + :param n: Number of shares to generate + :return: List of created files. + """ + assert secret.exists(), str(secret) + assert secret.is_file(), str(secret) + + txt_file = secret.parent.joinpath(secret.name + '.split-text.txt') + qr_pdf = secret.parent.joinpath(secret.name + '.split-QR.pdf') + txt_pdf = secret.parent.joinpath(secret.name + '.split-text.pdf') + assert not txt_file.exists() and not qr_pdf.exists() and not txt_pdf.exists() + + with tempfile.TemporaryDirectory() as tmpdirname: + tmpdir = Path(tmpdirname) + tmpdirfile = tmpdir.joinpath('_') + + # Creates n files _.xxx + # One can reconstruct secret based on k out of n files + util.execute_stdin_capture(['gfsplit', '-n', str(k), '-m', str(n), str(secret), str(tmpdirfile)]) + + qrcodes = [] + lines = '' + for part in tmpdir.iterdir(): + binary_part = part.read_bytes() + encoded_part = base64.b64encode(binary_part).decode('UTF-8') + encoded_suffix = part.suffix[1:] + + suffix_and_content = encoded_suffix + encoded_part + lines = lines + suffix_and_content + '\n' + + encoded = part.parent.joinpath(part.name + '.txt') + encoded.write_text(suffix_and_content) + + if create_qr_codes: + qrcode = part.parent.joinpath(part.name + '.png') + qrcodes.append(qrcode) + util.execute_stdin_capture(['qrencode', '-o', str(qrcode), '-s', '10'], + suffix_and_content.encode('UTF-8')) + + txt_file.write_text(lines) + + if create_text_pdf: + command = ['enscript', '-B', '--margins=24:24:', '-o', str(txt_pdf), '-f', 'Courier@12/12'] + util.execute_stdin_capture(command, lines.replace('\n', '\n\n\n').encode('UTF-8')) + + if create_qr_codes and merge_qr_codes: + command = ['convert'] + [str(qrcode) for qrcode in qrcodes] + [str(qr_pdf)] + util.execute_stdin_capture(command) + + files = [txt_file] + if create_text_pdf: + files.append(txt_pdf) + if create_qr_codes and merge_qr_codes: + files.append(qr_pdf) + return files + + +if __name__ == '__main__': + main() diff --git a/src/paper_secret/util.py b/src/paper_secret/util.py new file mode 100644 index 0000000..6784d95 --- /dev/null +++ b/src/paper_secret/util.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import subprocess +import sys +from pathlib import Path + + +def execute_stdin_capture(command: list[str], stdin: bytes = b'', cwd: Path = Path()): + """ + Executes the given `command`, passes `stdin` into it and returns its stdout. + + :raises Exception: In case of non-zero exit code. + This exception includes the exit_code, stdout and stderr in its message. + """ + + # If a Linux shell's locale is en_GB.UTF-8, the output will be encoded to UTF-8. + encoding = 'UTF-8' + assert sys.stdout.encoding.upper() == encoding + + completed = subprocess.run( + command, + capture_output=True, + cwd=cwd, + input=stdin, + ) + if completed.returncode != 0: + raise Exception( + f'command: {command}\n' + f'exit_code: {completed.returncode}\n' + f'stdout: {completed.stdout}\n' + f'stderr: {completed.stderr}' + )