236 lines
6.8 KiB
Python
Executable file
236 lines
6.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
"""Script to populate a KeePass database with entries from a Password Store"""
|
|
|
|
import os
|
|
import sys
|
|
from getpass import getpass
|
|
|
|
import gnupg # type: ignore
|
|
import pykeepass # type: ignore
|
|
|
|
VERBOSE = bool("-v" in sys.argv or "--verbose" in sys.argv)
|
|
|
|
|
|
def main() -> None:
|
|
"""Open a KeePass database, drop the entries and import them from Pass"""
|
|
|
|
while "-v" in sys.argv:
|
|
sys.argv.remove("-v")
|
|
while "--verbose" in sys.argv:
|
|
sys.argv.remove("--verbose")
|
|
if len(sys.argv) != 2:
|
|
usage()
|
|
sys.exit(1)
|
|
db_path = sys.argv[1]
|
|
database = pykeepass.create_database(
|
|
filename=db_path, password=getpass(f"New password for {db_path}: ")
|
|
)
|
|
|
|
pass_store = PassStore()
|
|
pass_store.send_to_keepass(database)
|
|
print("Saving new KeePass database...")
|
|
database.save()
|
|
|
|
|
|
class PassStore:
|
|
"""Class to represent a Pass Store"""
|
|
|
|
def __init__(self):
|
|
self.path = os.path.expanduser(
|
|
os.environ.get("PASSWORD_STORE_DIR", "~/.password-store")
|
|
)
|
|
print(f"Loading Password store at {self.path}...")
|
|
|
|
self.groups = [PassGroup("root_group")]
|
|
|
|
for item in sorted(os.listdir(self.path)):
|
|
if item.startswith("."): # .git, .gitignore, .gpg-id, etc.
|
|
continue
|
|
if os.path.isfile(os.path.join(self.path, item)):
|
|
# append to root_group
|
|
self.groups[0].entries.append(PassEntry(os.path.join(self.path, item)))
|
|
continue
|
|
if os.path.isdir(os.path.join(self.path, item)):
|
|
group = PassGroup(os.path.join(self.path, item))
|
|
group.trickle_down()
|
|
self.groups.append(group)
|
|
continue
|
|
print(f"Unrecognized item {item}")
|
|
|
|
def print_tree(self):
|
|
"""Print a tree of the store"""
|
|
|
|
for group in self.groups:
|
|
group.print_tree()
|
|
|
|
def send_to_keepass(self, database: pykeepass.PyKeePass) -> None:
|
|
"""Send the PassStore to a KeePass database"""
|
|
|
|
print("Sending Password Store to KeePass database...")
|
|
|
|
for group in self.groups:
|
|
group.send_to_keepass(database, parent_group=None)
|
|
|
|
|
|
class PassGroup:
|
|
"""Class to represent a Pass Store directory"""
|
|
|
|
def __init__(self, path: str):
|
|
vprint(f"Got group at {path}")
|
|
self.path = path
|
|
self.name = os.path.basename(path)
|
|
self.groups: list[PassGroup] = []
|
|
self.entries: list[PassEntry] = []
|
|
|
|
def trickle_down(self):
|
|
"""Move down the group to find subgroups and entries"""
|
|
|
|
for item in sorted(os.listdir(self.path)):
|
|
if item.startswith("."): # .git, .gitignore, .gpg-id, etc.
|
|
continue
|
|
if os.path.isfile(os.path.join(self.path, item)):
|
|
self.entries.append(PassEntry(os.path.join(self.path, item)))
|
|
continue
|
|
if os.path.isdir(os.path.join(self.path, item)):
|
|
group = PassGroup(os.path.join(self.path, item))
|
|
group.trickle_down()
|
|
self.groups.append(group)
|
|
continue
|
|
print(f"Unrecognized item {item}")
|
|
|
|
def print_tree(self):
|
|
"""Print a tree of this group"""
|
|
|
|
indent = len(os.path.dirname(self.path).split("/"))
|
|
print(" " * indent + f"{self.name}:")
|
|
for entry in self.entries:
|
|
print(" " * indent + f" - {entry.name}")
|
|
for group in self.groups:
|
|
group.print_tree()
|
|
|
|
def send_to_keepass(
|
|
self, database: pykeepass.PyKeePass, parent_group: pykeepass.group.Group | None
|
|
) -> None:
|
|
"""Send the PassGroup to a KeePass database"""
|
|
|
|
if parent_group is None:
|
|
parent_group = database.root_group
|
|
|
|
vprint(f"Sending PassGroup {self.name} to {parent_group.name}...")
|
|
|
|
if self.name == "root_group":
|
|
kp_group = database.root_group
|
|
else:
|
|
kp_group = database.add_group(
|
|
destination_group=parent_group,
|
|
group_name=self.name,
|
|
)
|
|
for group in self.groups:
|
|
group.send_to_keepass(database, kp_group)
|
|
for entry in self.entries:
|
|
entry.send_to_keepass(database, kp_group)
|
|
|
|
|
|
class PassEntry:
|
|
"""Class to represent a Pass Store file"""
|
|
|
|
def __init__(self, path: str):
|
|
vprint(f"Got entry at {path}")
|
|
self.path = path
|
|
self.name = os.path.basename(path).removesuffix(".gpg")
|
|
self.group = os.path.basename(os.path.dirname(path))
|
|
self.data = self.decrypt()
|
|
self.password = self.get_password()
|
|
self.url = self.get_url()
|
|
self.username = self.get_username()
|
|
self.notes = self.get_notes()
|
|
|
|
def decrypt(self) -> str:
|
|
"""Decrypt the PassEntry and return the decrypted content"""
|
|
|
|
vprint(f"Decrypting {self.name}...")
|
|
gpg = gnupg.GPG()
|
|
decrypt = gpg.decrypt_file(self.path)
|
|
if not decrypt.ok:
|
|
print(decrypt.stderr)
|
|
sys.exit(decrypt.returncode)
|
|
|
|
data = decrypt.data.decode()
|
|
if not data.rsplit("\n", maxsplit=1)[-1]: # empty last line
|
|
data_lst = data.split("\n")
|
|
data_lst.pop(-1)
|
|
data = "\n".join(data_lst)
|
|
|
|
return data
|
|
|
|
def get_password(self) -> str:
|
|
"""Return the password from the PassEntry data"""
|
|
|
|
password = self.data.split("\n", maxsplit=1)[0]
|
|
|
|
return password
|
|
|
|
def get_url(self) -> str:
|
|
"""Return the url from the PassEntry data"""
|
|
|
|
try:
|
|
url = self.data.split("\n")[1]
|
|
except IndexError:
|
|
return ""
|
|
|
|
return url
|
|
|
|
def get_username(self) -> str:
|
|
"""Return the username from the PassEntry data"""
|
|
|
|
try:
|
|
username = self.data.split("\n")[2]
|
|
except IndexError:
|
|
return ""
|
|
|
|
return username
|
|
|
|
def get_notes(self) -> str:
|
|
"""Return notes from the PassEntry data"""
|
|
|
|
nb_lines = len(self.data.split("\n"))
|
|
if nb_lines < 4:
|
|
return ""
|
|
if nb_lines == 4:
|
|
return self.data.split("\n")[3]
|
|
|
|
return self.data
|
|
|
|
def send_to_keepass(
|
|
self, database: pykeepass.PyKeePass, kp_group: pykeepass.group.Group
|
|
) -> None:
|
|
"""Send the PassEntry to a KeePass database"""
|
|
|
|
vprint(f"Sending PassEntry {self.name} to {kp_group}...")
|
|
|
|
database.add_entry(
|
|
destination_group=kp_group,
|
|
title=self.name,
|
|
username=self.username,
|
|
password=self.password,
|
|
url=self.url,
|
|
notes=self.notes,
|
|
)
|
|
|
|
|
|
def usage():
|
|
"""Print usage"""
|
|
|
|
print(f"Usage: {sys.argv[0]} [-v|--verbose] path_to_database.kdbx", file=sys.stderr)
|
|
|
|
|
|
def vprint(*args, **kwargs):
|
|
"""Print if VERBOSE is True"""
|
|
|
|
if VERBOSE:
|
|
print(*args, **kwargs)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|