bean spiller
This commit is contained in:
@@ -22,10 +22,7 @@ pipx: ## Install all packages with pipx
|
|||||||
for module in *; do if [ -f $$module/setup.py ]; then pipx install $$module; fi; done
|
for module in *; do if [ -f $$module/setup.py ]; then pipx install $$module; fi; done
|
||||||
|
|
||||||
format: ## Reformat packages with black
|
format: ## Reformat packages with black
|
||||||
black SimpleWebPage/
|
for module in *; do if [ -f $$module/setup.py ]; then black $$module/; fi; done
|
||||||
black TSVFilter/
|
|
||||||
black markslider/
|
|
||||||
black ffmpeg-parser/
|
|
||||||
|
|
||||||
tar-SimpleWebPage: clean ## Create package for SimpleWebPage
|
tar-SimpleWebPage: clean ## Create package for SimpleWebPage
|
||||||
tar czf SimpleWebPage.tgz SimpleWebPage/
|
tar czf SimpleWebPage.tgz SimpleWebPage/
|
||||||
|
|||||||
@@ -1056,8 +1056,7 @@ def get_footer(readme, show_wget=True):
|
|||||||
return """</tbody></table>{README}
|
return """</tbody></table>{README}
|
||||||
</div>{WGET_STR}
|
</div>{WGET_STR}
|
||||||
</body></html>""".format(
|
</body></html>""".format(
|
||||||
README=readme,
|
README=readme, WGET_STR=wget_str
|
||||||
WGET_STR=wget_str
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
25
py-packages/spiller/setup.py
Normal file
25
py-packages/spiller/setup.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def version_reader(path):
|
||||||
|
for line in open(path, "rt").read(1024).split("\n"):
|
||||||
|
if line.startswith("__version__"):
|
||||||
|
return line.split("=")[1].strip().replace('"', "")
|
||||||
|
|
||||||
|
|
||||||
|
version = version_reader(os.path.join("spiller", "__init__.py"))
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="spiller",
|
||||||
|
packages=["spiller"],
|
||||||
|
version=version,
|
||||||
|
description="Very simple password storage, that encrypts with GPG cmdline tool.",
|
||||||
|
author="Ville Rantanen",
|
||||||
|
author_email="ville.q.rantanen@gmail.com",
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"spill = spiller.spiller:main",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
2
py-packages/spiller/spiller/__init__.py
Normal file
2
py-packages/spiller/spiller/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__version__ = "0.1"
|
||||||
|
from spiller.spiller import retrieve, store, list_storage
|
||||||
220
py-packages/spiller/spiller/spiller.py
Executable file
220
py-packages/spiller/spiller/spiller.py
Executable file
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import random
|
||||||
|
import stat
|
||||||
|
import string
|
||||||
|
|
||||||
|
JSON = os.getenv(
|
||||||
|
"SPILLER_STORAGE", os.path.expanduser("~/.config/spiller/storage.json")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_opts():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="spill",
|
||||||
|
description="""Key/value storage that uses JSON and GPG as backend.
|
||||||
|
Values are encrypted symmetrically with the key provided, or a random string is generated. Storage file can be changed with
|
||||||
|
SPILLER_STORAGE env variable. Encryption key can be passed from a variable SPILLER_KEY instead of a switch.""",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Command")
|
||||||
|
set_parser = subparsers.add_parser("set")
|
||||||
|
get_parser = subparsers.add_parser("get")
|
||||||
|
del_parser = subparsers.add_parser("list")
|
||||||
|
del_parser = subparsers.add_parser("del")
|
||||||
|
|
||||||
|
set_parser.add_argument(
|
||||||
|
"name",
|
||||||
|
action="store",
|
||||||
|
help="Name of secret",
|
||||||
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"data",
|
||||||
|
action="store",
|
||||||
|
help="Data to store",
|
||||||
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"--plain", action="store_true", default=False, help="Do not encrypt"
|
||||||
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"--key",
|
||||||
|
action="store",
|
||||||
|
default=os.getenv("SPILLER_KEY", None),
|
||||||
|
help="Encryption key",
|
||||||
|
)
|
||||||
|
get_parser.add_argument(
|
||||||
|
"name",
|
||||||
|
action="store",
|
||||||
|
help="Name of secret",
|
||||||
|
)
|
||||||
|
get_parser.add_argument(
|
||||||
|
"--key",
|
||||||
|
action="store",
|
||||||
|
default=os.getenv("SPILLER_KEY", None),
|
||||||
|
help="Decryption key",
|
||||||
|
)
|
||||||
|
del_parser.add_argument(
|
||||||
|
"name",
|
||||||
|
action="store",
|
||||||
|
help="Name of secret to delete",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def list_storage():
|
||||||
|
"""
|
||||||
|
Get list of keys in the secret storage
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[List]: List of name and encryption method
|
||||||
|
"""
|
||||||
|
|
||||||
|
storage = load_storage()
|
||||||
|
names = []
|
||||||
|
for name in sorted(storage.keys()):
|
||||||
|
names.append([name, storage[name]["encryption"]])
|
||||||
|
names.sort(key=lambda x: x[1])
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def load_storage():
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(JSON, "rt") as fp:
|
||||||
|
return json.load(fp)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_storage(storage):
|
||||||
|
|
||||||
|
if not os.path.exists(JSON):
|
||||||
|
os.makedirs(os.path.dirname(JSON), exist_ok=True)
|
||||||
|
with open(JSON, "wt") as fp:
|
||||||
|
json.dump(storage, fp, indent=2)
|
||||||
|
try:
|
||||||
|
os.chmod(JSON, stat.S_IWUSR | stat.S_IREAD)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def del_storage(name):
|
||||||
|
|
||||||
|
storage = load_storage()
|
||||||
|
del storage[name]
|
||||||
|
save_storage(storage)
|
||||||
|
print("Deleted " + name)
|
||||||
|
|
||||||
|
|
||||||
|
def store(name, data, key, plain):
|
||||||
|
"""
|
||||||
|
Store key to secrets storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the secret.
|
||||||
|
data (str): Data to encrypt.
|
||||||
|
key (str): Encryption key. If None, randomly generate the key
|
||||||
|
plain (bool): If set, stores the data as is, without encryption
|
||||||
|
Returns:
|
||||||
|
str: Key used to encrypt, useful if it was generated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entry = {"encryption": "gpg", "data": data}
|
||||||
|
storage = load_storage()
|
||||||
|
if plain:
|
||||||
|
entry["encryption"] = "none"
|
||||||
|
else:
|
||||||
|
if key == None:
|
||||||
|
key = get_random_key()
|
||||||
|
print("Random key: " + key)
|
||||||
|
entry["data"] = encrypt(data, key)
|
||||||
|
|
||||||
|
storage[name] = entry
|
||||||
|
save_storage(storage)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve(name, key=None):
|
||||||
|
"""
|
||||||
|
Retrieve a secret from storage
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the secret.
|
||||||
|
key (str): Encryption key, if any required.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Decrypted secret
|
||||||
|
"""
|
||||||
|
|
||||||
|
storage = load_storage()
|
||||||
|
entry = storage[name]
|
||||||
|
if entry["encryption"] == "none":
|
||||||
|
return entry["data"]
|
||||||
|
return decrypt(entry["data"], key)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(data, key):
|
||||||
|
|
||||||
|
p = subprocess.run(
|
||||||
|
["gpg", "-a", "--symmetric", "--batch", "--passphrase-fd", "0"],
|
||||||
|
input=f"{key}\n{data}".encode(),
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
encrypted = p.stdout.decode()
|
||||||
|
if encrypted == "":
|
||||||
|
print("Encrypt failed!", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return encrypted
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(encrypted, key):
|
||||||
|
|
||||||
|
if key == None:
|
||||||
|
print("Requires --key!", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
p = subprocess.run(
|
||||||
|
["gpg", "-d", "--batch", "--passphrase-fd", "0"],
|
||||||
|
input=f"{key}\n{encrypted}".encode(),
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
data = p.stdout.decode()
|
||||||
|
if data == "":
|
||||||
|
print("Decrypt failed!", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_key():
|
||||||
|
|
||||||
|
return "-".join(
|
||||||
|
[
|
||||||
|
"".join(
|
||||||
|
[random.choice(string.ascii_letters + string.digits) for x in range(8)]
|
||||||
|
)
|
||||||
|
for x in range(5)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
opts = get_opts()
|
||||||
|
if opts.command == "set":
|
||||||
|
store(opts.name, opts.data, opts.key, opts.plain)
|
||||||
|
if opts.command == "get":
|
||||||
|
print(retrieve(opts.name, opts.key))
|
||||||
|
if opts.command == "list":
|
||||||
|
names = list_storage()
|
||||||
|
names.insert(0, ["Name", "Encryption"])
|
||||||
|
padlen = max([len(x[0]) for x in names])
|
||||||
|
for row in names:
|
||||||
|
print(("{:" + str(padlen) + "} {}").format(*row))
|
||||||
|
if opts.command == "del":
|
||||||
|
del_storage(opts.name)
|
||||||
Reference in New Issue
Block a user