pass2keepass/pass2keepass.py

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()