Minimal plaintext password store https://simonrepp.com/sicuit/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

158 lines
5.7 KiB

# YOU ARE USING THIS SCRIPT ENTIRELY AT YOUR OWN RISK.
# BACKUP YOUR SICUIT (RESPECTIVELY ALSO PASS) DATA BEFORE PROCEEDING.
#
# Python 3.6+ and the 'enolib' package (pip install enolib) are required.
#
# This script will:
# 1. ask for your explicit confirmation to overwrite ~/.password-store/.gpg-id if it already exists
# 2. create ~/.password-store/.gpg-id based on the GPG IDs that ~/.sicuit.eno.gpg is encrypted with
# 3. read ~/.sicuit.eno.gpg and export all entries to ~/.password-store/
# 4. ask for your explicit confirmation before overwriting any entry that already exists
# TODO: Export associated comments, best-effort style
import enolib, re, subprocess, sys
from os import path
from pathlib import Path
PASS_GPG_ID_PATH = path.expanduser("~/.password-store/.gpg-id")
PASS_STORE_DIR = path.expanduser("~/.password-store")
SICUIT_STORE_PATH = path.expanduser("~/.sicuit.eno.gpg")
def escape_slashes(string):
escaped = string.replace("/", "_")
if escaped == string:
return string
else:
print(f"[sicuit > pass] WARNING: Escaped \"{string}\" to \"{escaped}\".")
return escaped
def export_entry(file_path, file_content, gpg_ids):
extended_path = f"{file_path}.gpg"
if path.exists(extended_path):
while True:
choice = input(f"[sicuit > pass] ACTION REQUIRED: {extended_path} exists, overwrite? (y/N): ").lower()
if not choice or choice == "n":
return
if choice == "y":
Path(extended_path).unlink(missing_ok=True)
break
gpg_encrypt_command = ["gpg", "--encrypt", "--output", extended_path]
for id in gpg_ids:
gpg_encrypt_command.extend(["--recipient", id])
subprocess.run(
gpg_encrypt_command,
input=file_content,
stderr=subprocess.DEVNULL,
universal_newlines=True
)
def get_sicuit_store_gpg_ids():
gpg_list_packets = subprocess.run(
["gpg", "--list-packets", SICUIT_STORE_PATH],
stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL,
universal_newlines=True
)
list_packets_lines = gpg_list_packets.stderr.splitlines()
gpg_ids = []
for index, line in enumerate(list_packets_lines):
if index > 0 and list_packets_lines[index - 1].startswith("gpg: encrypted with"):
id = re.search(r"<(.+)>", line).group(1)
gpg_ids.append(id)
return gpg_ids
def write_pass_files(gpg_ids):
gpg_process = subprocess.run(
["gpg", "--decrypt", SICUIT_STORE_PATH],
stderr=subprocess.DEVNULL,
stdout=subprocess.PIPE,
universal_newlines=True
)
store = enolib.parse(gpg_process.stdout)
def recursive_export(base_path, element):
# Ensure directory exists
Path(base_path).mkdir(exist_ok=True)
for child in element.elements():
if child.yields_section():
section = child.to_section()
dir_path = path.join(base_path, escape_slashes(section.string_key()))
recursive_export(dir_path, section)
elif child.yields_field():
field = child.to_field()
file_path = path.join(base_path, escape_slashes(field.string_key()))
content = f"{field.required_string_value()}\n\n{field.optional_string_comment()}".strip()
export_entry(file_path, content, gpg_ids)
elif child.yields_fieldset():
fieldset = child.to_fieldset()
dir_path = path.join(base_path, escape_slashes(fieldset.string_key()))
Path(dir_path).mkdir(exist_ok=True)
for entry in fieldset.entries():
file_path = path.join(dir_path, escape_slashes(entry.string_key()))
content = f"{entry.optional_string_value()}\n\n{entry.optional_string_comment()}".strip()
export_entry(file_path, content, gpg_ids)
elif child.yields_list():
list = child.to_list()
comment = list.optional_string_comment()
if comment:
file_path = path.join(base_path, f"{escape_slashes(list.string_key())} (comment)")
export_entry(file_path, comment, gpg_ids)
for index, item in enumerate(list.items()):
file_path = path.join(base_path, f"{escape_slashes(list.string_key())} ({index + 1})")
content = f"{item.optional_string_value()}\n\n{item.optional_string_comment()}".strip()
export_entry(file_path, content, gpg_ids)
elif child.yields_empty():
empty = child.to_empty()
file_path = path.join(base_path, escape_slashes(empty.string_key()))
comment = empty.optional_string_comment()
export_entry(file_path, f"\n\n{comment}" if comment else "", gpg_ids)
recursive_export(PASS_STORE_DIR, store)
def write_pass_gpg_id_file(gpg_ids):
if path.exists(PASS_GPG_ID_PATH):
while True:
choice = input(f"[sicuit > pass] ACTION REQUIRED: {PASS_GPG_ID_PATH} exists, overwrite? (y/N): ").lower()
if not choice or choice == "n":
return False
elif choice == "y":
break
with open(PASS_GPG_ID_PATH, "w") as file:
file.write("".join(map(lambda id: f"{id}\n", gpg_ids)))
return True
gpg_ids = get_sicuit_store_gpg_ids()
if not gpg_ids:
sys.exit(f"[sicuit > pass] ERROR: The GPG IDs used for encrypting {SICUIT_STORE_PATH} could not be determined")
Path(PASS_STORE_DIR).mkdir(exist_ok=True) # ensure ~/.password-store exists
if write_pass_gpg_id_file(gpg_ids):
write_pass_files(gpg_ids)
print(f"[sicuit > pass] SUCCESS: All done, enjoy pass.")