diff --git a/app/config/settings.py b/app/config/settings.py index cf95017..33a7019 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -1,18 +1,26 @@ from functools import lru_cache - -from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic import BaseModel from pathlib import Path + import yaml +from pydantic import BaseModel +from pydantic_settings import BaseSettings, SettingsConfigDict + class AppConfig(BaseModel): host: str = "127.0.0.1" port: int = 8000 reload: bool = True + +class KeePassConfig(BaseModel): + file: str = "database.kdbx" + secret: Path | str = "/run/secrets/kp_secret" + + class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="KARL_", env_nested_delimiter="__") app: AppConfig = AppConfig() + kp: KeePassConfig = KeePassConfig() @classmethod def from_yaml(cls, path: Path | str = "config/config.yaml") -> "Settings": @@ -23,6 +31,7 @@ class Settings(BaseSettings): data = yaml.safe_load(fh) or {} return cls(**data) + @lru_cache def get_settings() -> Settings: return Settings.from_yaml() diff --git a/app/services/passwd.py b/app/model/__init__.py similarity index 100% rename from app/services/passwd.py rename to app/model/__init__.py diff --git a/app/model/passwords.py b/app/model/passwords.py new file mode 100644 index 0000000..d795d2c --- /dev/null +++ b/app/model/passwords.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass, field +from typing import Type + +# TODO: unnecessary? + +@dataclass +class PathItem: + name: str + t: Type + +@dataclass +class Path: + path: list[PathItem] = field(default_factory=list) + + def append(self, name, t): + self.path.append(PathItem(name, t)) + + def __str__(self): + return "/".join([i.name for i in self.path]) + + +@dataclass +class Group: + name: str + passwords: list["Password"] = field(default_factory=list) + parent: "Group|None" = None + + @property + def path(self): + if self.parent is None: + new_path = Path() + new_path.append(self.name, type(self)) + return new_path + return self.parent.path.append(self.name, type(self)) + + +@dataclass +class Password: + name: str + group: Group + + @property + def path(self): + return self.group.path.append(self.name, type(self)) + +class UnencryptedPassword(Password): + def __init__(self, name: str, value: str, group: Group): + super().__init__(name, group) + self.value = value diff --git a/app/services/passwords.py b/app/services/passwords.py new file mode 100644 index 0000000..9dca423 --- /dev/null +++ b/app/services/passwords.py @@ -0,0 +1,38 @@ +import os.path + +from pykeepass import PyKeePass, create_database, Group + + +class Passwords: + def __init__(self): + from app.config import get_settings + settings = get_settings() + + with open(settings.kp.secret, "r") as fh: + secret = fh.read() + + self._kp_org = self.__get_or_create_store(settings.kp.file, secret) + self._kp = self.__get_lock(settings.kp.file, secret) + + @staticmethod + def __get_or_create_store(path, passwd) -> PyKeePass: + if os.path.exists(path): + return PyKeePass( + path, + password=passwd, + ) + return create_database(path, passwd) + + @staticmethod + def __get_lock(path, passwd) -> PyKeePass: + lock_path = path + ".lock" + import shutil + shutil.copyfile(path, lock_path) + return Passwords.__get_or_create_store(lock_path, passwd) + + @property + def store(self): + return self._kp.root_group + + def save(self, group: Group): + pass diff --git a/app/services/system.py b/app/services/system.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/services/system.py @@ -0,0 +1 @@ + diff --git a/app/util/__init__.py b/app/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/util/dicts.py b/app/util/dicts.py new file mode 100644 index 0000000..faf7f65 --- /dev/null +++ b/app/util/dicts.py @@ -0,0 +1,11 @@ +from types import SimpleNamespace + + +class NestedNamespace(SimpleNamespace): + def __init__(self, dictionary, **kwargs): + super().__init__(**kwargs) + for key, value in dictionary.items(): + if isinstance(value, dict): + self.__setattr__(key, NestedNamespace(value)) + else: + self.__setattr__(key, value) diff --git a/config/config.yaml b/config/config.yaml index 8afdd9a..5b47197 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -2,3 +2,6 @@ app: host: "127.0.0.1" port: 8000 reload: true +kp: + file: "config/kp.kdbx" + secret: "config/secret.txt" diff --git a/pyproject.toml b/pyproject.toml index c7db9ab..0f8ebcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "jinja2>=3.1.4", "pydantic-settings>=2.4.0", "pyyaml>=6.0.2", + "pykeepass>=4.1.1.post1" ] [project.optional-dependencies]