# This module can load passwords from external sources, such as files or
# environment variables or KWallet (and no, *not* libsecret -- as much as I
# want to use the nifty gi-based libsecret API...).
#
# It deals with strings (the lowest common denominator), but it's really meant
# to be used for storing Base64-encoded keys and not raw passwords.

import dbus
import os
import re
import subprocess

class KWalletClient():
    app_id = "org.eu.nullroute.Secretsd"
    folder = "Passwords"

    def __init__(self):
        self.bus = dbus.SessionBus()
        self.mgr = self.bus.get_object("org.kde.kwalletd5", "/modules/kwalletd5")
        self.mgr = dbus.Interface(self.mgr, "org.kde.KWallet")

    def __enter__(self):
        self.wallet = self.mgr.localWallet()
        self.handle = self.mgr.open(self.wallet, 0, self.app_id)
        return self

    def __exit__(self, *argv):
        self.mgr.disconnectApplication(self.wallet, self.app_id)

    def get_password(self, name):
        if self.mgr.hasEntry(self.handle, self.folder, name, self.app_id):
            return str(self.mgr.readPassword(self.handle, self.folder, name, self.app_id))
        else:
            raise KeyError(name)

    def set_password(self, name, value):
        self.mgr.writePassword(self.handle, self.folder, name, value, self.app_id)

def _parse_specifier(source):
    m = re.match(r"^(\w+):(.*)", source)
    if m:
        return m.groups()
    else:
        # Too easy to end up saving keys in a file named 'kwallet'...
        #return "file", source
        raise ValueError("key location must be specified as 'type:rest'")

def load_ext_key(source):
    kind, rest = _parse_specifier(source)
    if kind == "env":
        return os.environ[rest]
    elif kind == "exec":
        res = subprocess.run(rest, shell=True,
                                   stdin=subprocess.DEVNULL,
                                   stdout=subprocess.PIPE,
                                   check=True)
        return res.stdout.decode().strip()
    elif kind == "file":
        with open(rest, "r") as fh:
            return fh.read().strip()
    elif kind == "kwallet":
        with KWalletClient() as kw:
            return kw.get_password(rest or "secretsd master key")
    elif kind == "libsecret":
        raise ValueError("cannot load external key from myself")
    else:
        raise ValueError("unknown external key source %r" % kind)

def store_ext_key(source, key):
    kind, rest = _parse_specifier(source)
    if kind == "env":
        raise ValueError("environment is volatile storage, cannot store keys there")
    elif kind == "exec":
        # XXX: Should there be a way to distinguish whether the command is
        # invoked for load vs store, or should I just remove storing to exec:
        # because this is an advanced operation and the user can just emulate
        # it with a temporary file?
        res = subprocess.run(rest, shell=True,
                                   input=key.encode(),
                                   check=True)
    elif kind == "file":
        with open(rest, "w", opener=lambda p, f: os.open(p, f, 0o400)) as fh:
            fh.write(key)
    elif kind == "kwallet":
        with KWalletClient() as kw:
            kw.set_password(rest or "secretsd master key", key)
    elif kind == "libsecret":
        raise ValueError("cannot store external key in myself")
    else:
        raise ValueError("unknown external key source %r" % kind)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("specifier")
    parser.add_argument("--store-key", metavar="KEY", help="store the specified key (as text)")
    args = parser.parse_args()

    arg = args.specifier
    if key := args.store_key:
        print(f"Storing key {key!r} to {arg!r}")
        store_ext_key(arg, key)
    print(f"Retrieving key from {arg!r}")
    key = load_ext_key(arg)
    print(f"The key is {key!r}")
