mirror of
https://codeberg.org/privacy1st/paper-secret
synced 2024-12-22 23:56:05 +01:00
init
This commit is contained in:
commit
125f5c968f
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/venv
|
||||||
|
/.idea
|
||||||
|
**/__pycache__
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
**/*.egg-info
|
24
.run/depaper.run.xml
Normal file
24
.run/depaper.run.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="depaper" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
|
||||||
|
<module name="paper-secret" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="$USER_HOME$/venv-310/bin/python" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/paper_secret/depaper.py" />
|
||||||
|
<option name="PARAMETERS" value="secret.txt.split-text.txt" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
24
.run/enpaper.run.xml
Normal file
24
.run/enpaper.run.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="enpaper" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
|
||||||
|
<module name="paper-secret" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="$USER_HOME$/venv-310/bin/python" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/paper_secret/enpaper.py" />
|
||||||
|
<option name="PARAMETERS" value="secret.txt" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
121
LICENSE
Normal file
121
LICENSE
Normal file
@ -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.
|
58
README.md
Normal file
58
README.md
Normal file
@ -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
|
8
pyproject.toml
Normal file
8
pyproject.toml
Normal file
@ -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"
|
0
requirements.txt
Normal file
0
requirements.txt
Normal file
36
setup.cfg
Normal file
36
setup.cfg
Normal file
@ -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
|
0
src/paper_secret/__init__.py
Normal file
0
src/paper_secret/__init__.py
Normal file
56
src/paper_secret/depaper.py
Executable file
56
src/paper_secret/depaper.py
Executable file
@ -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()
|
92
src/paper_secret/enpaper.py
Executable file
92
src/paper_secret/enpaper.py
Executable file
@ -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()
|
32
src/paper_secret/util.py
Normal file
32
src/paper_secret/util.py
Normal file
@ -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}'
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user