0

After using Keepass for several years, an idea came to me that would be nice to implement. There are over 300 records in my database, and it would be nice to know which of those records are still good. Setting the expires flag on the records would help remind me to check if records are valid and potentially encourage me to change my password. Is there a way to look through the entire database and do the following to each record?

  1. Check if the expires flag is set.
  2. If it is not set, add X number of years to the creation (or modification) time.
  3. Set the expires flag.
  4. Set the expires time to the value calculated in step 2.
  5. Continue to the next record in the database.

A reply was received from Paul saying, "Not with KeePass, but you can use KPScript and some PowerShell." He included a link to #1318 Change Expiration Date from List, but my inexperience with the tools he recommended has only left me with more questions. Could anyone provide further assistance? (1)

Addendum

After some further research, the ability to export the database in a XML format was discovered. Would it be easier to parse the XML file, alter the data in that form, and then create a new database by importing the results? If using the suggestions already made would be too difficult, XML processing could potentially be an easier path to take.

2 Answers 2

0

Yes, it is possible to set the “expires” time based on “creation” time. By using a program, the process can be automated. Change the global constants (KPSCRIPT, DATABASE, and PASSWORD) to values corresponding to your system before running. In this particular example, the expiry time is set to be five years after the last modification date. The program below was adapted from Paul's answer.

#! /usr/bin/env python3
import datetime
import subprocess
import uuid


KPSCRIPT = r'C:\Program Files (x86)\KeePass Password Safe 2\KPScript.exe'
DATABASE = r'C:\Users\Stephen Paul Chappel\Documents\Database.kdbx'
PASSWORD = r'password'


def main():
    """Get KeePass entries, check each one, and change if required."""
    for line in ps(KPSCRIPT, '-c:ListEntries', DATABASE, '-pw:' + PASSWORD):
        key, value, *pair = line.split(': ', 1) + [None]
        if pair:
            if key == 'UUID':
                reference = uuid.UUID(value)
            elif key == 'TLM':
                tlm = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
            elif key == 'EXP':
                if not {'False': False, 'True': True}[value]:
                    # Add 5 years to the last modification
                    # time for the expiry date and format.
                    te = tlm.replace(tlm.year + 5)
                    te = te.strftime('%Y-%m-%dT%H:%M:%S')
                    ps(
                        KPSCRIPT,
                        '-c:EditEntry',
                        DATABASE,
                        '-pw:' + PASSWORD,
                        '-refx-UUID:' + reference.hex,
                        '-setx-Expires:True',
                        '-setx-ExpiryTime:' + te
                    )
                del reference, tlm


def ps(*args):
    """Provide an interface for writing commands like those in PowerShell."""
    return subprocess.run(
        args,
        timeout=1,
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        universal_newlines=True
    ).stdout.splitlines()


if __name__ == '__main__':
    main()
0

If you want an OOP approach, you might try using the following prototype:

#! /usr/bin/env python3
from datetime import datetime
from pathlib import Path
from subprocess import run, CalledProcessError
from sys import intern
from uuid import UUID

DATABASE = Path(r'C:\Users\Stephen Chappell\Documents\Accounts Live.kdbx')
PASSWORD = r'password'


def main():
    """Get KeePass entries, check each one, and change if required."""
    db = KPScript(DATABASE, PASSWORD)
    for entry in filter(needs_expiration, db.list_entries()):
        print('Updating:', {key: entry['S'][key] for key in
                            'Title UserName Password URL'.split()})
        # Add 5 years to the last modification
        # time for the expiry date and format.
        last_modified = entry['TLM']
        expiry_time = last_modified.replace(last_modified.year + 5)
        db.edit_entry(entry['UUID'], True, expiry_time)


def needs_expiration(entry):
    """Check if an entry needs its expiration set."""
    return not entry['EXP'] and \
        entry['GRPN'] not in {'Recycle Bin', 'Templates'}


def _field_to_datetime(value):
    """Convert a timestamp into a datetime object."""
    return datetime.strptime(value, KPScript.DATETIME_FORMAT)


class KPScript:
    """Reference: https://keepass.info/help/v2_dev/scr_sc_index.html"""

    EXE = Path(r'C:\Program Files\KeePass Password Safe 2\KPScript.exe')
    TIMEOUT = 2
    DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
    FIELD_TYPES = {
        'UUID': UUID,
        'GRPU': UUID,
        'GRPN': intern,
        'S': lambda s: s,
        'TC': _field_to_datetime,
        'TLM': _field_to_datetime,
        'TE': _field_to_datetime,
        'EXP': {'True': True, 'False': False}.__getitem__
    }

    def __init__(self, db_path, password=None, keyfile=None):
        """Initialize the attributes of a KPScript instance."""
        self.__db_path = db_path
        self.__password = password
        self.__keyfile = keyfile

    def __do_command(self, name, kwargs=None):
        """Help run whatever command is specified by name."""
        args = [self.EXE, f'-c:{name}', self.__db_path]
        if self.__password is not None:
            args.append(f'-pw:{self.__password}')
        if self.__keyfile is not None:
            args.append(f'-keyfile:{self.__keyfile}')
        if kwargs is not None:
            for key, value in kwargs.items():
                args.append(f'-{key}:{value}')
        try:
            completed_process = run(
                args,
                capture_output=True,
                timeout=self.TIMEOUT,
                check=True,
                text=True
            )
        except CalledProcessError as error:
            print('Error:', error.stdout)
            raise error
        else:
            return completed_process.stdout.splitlines()

    def list_groups(self):
        """This command lists all groups in a format that easily
        machine-readable."""
        ...

    def list_entries(self):
        """This command lists all entries in a format that easily
        machine-readable."""

        def parse_line():
            nonlocal line_key, line_value
            try:
                new_line_key, new_line_value = line.split(': ', 1)
                if new_line_key not in self.FIELD_TYPES.keys() | {'OK'}:
                    raise ValueError()
            except ValueError:
                append_value()
            else:
                line_key, line_value = new_line_key, new_line_value
                if line_key == 'S':
                    parse_s()
                else:
                    entry[line_key] = line_value

        def parse_s():
            nonlocal s_key, s_value
            try:
                s_key, s_value = line_value.split(' = ', 1)
            except ValueError:
                raise SyntaxError(line_value)
            else:
                s_field = entry.setdefault('S', {})
                s_field[s_key] = s_value

        def append_value():
            if line_key == 'S':
                entry['S'][s_key] += '\n' + line
            else:
                entry[line_key] += '\n' + line

        def complete_entry():
            return entry.keys() == self.FIELD_TYPES.keys()

        def transform():
            for key, value in self.FIELD_TYPES.items():
                entry[key] = value(entry[key])
            return entry

        entry = {}
        line_key = line_value = ''
        s_key = s_value = ''
        for line in self.__do_command('ListEntries'):
            if line:
                parse_line()
            elif complete_entry():
                yield transform()
                entry = {}
            else:
                append_value()
        if len(entry) != 1 or 'OK' not in entry:
            raise ValueError('last entry is invalid')

    def get_entry_string(self):
        """Retrieves the value of an entry string field."""
        ...

    def add_entry(self):
        """This command adds an entry to the database."""
        ...

    def edit_entry(self, uuid, expires=None, expiry_time=None):
        """This command edits existing entries."""
        kwargs = {'refx-UUID': uuid.hex}
        if expires is not None:
            kwargs['setx-Expires'] = str(bool(expires)).lower()
        if expiry_time is not None:
            kwargs['setx-ExpiryTime'] = expiry_time.strftime(
                self.DATETIME_FORMAT)
        self.__do_command('EditEntry', kwargs)

    def move_entry(self):
        """This command moves one or more existing entries."""
        ...

    def delete_entry(self):
        """This command deletes one or more existing entries."""
        ...

    def delete_all_entries(self):
        """This command deletes all entries (in all subgroups)."""
        ...

    def import_(self):
        """This command imports a file into the database."""
        ...

    def export(self):
        """This command exports (parts of) the database."""
        ...

    def sync(self):
        """This command synchronizes the database with another one."""
        ...

    def change_master_key(self):
        """This command changes the master key of the database."""
        ...

    def detach_bins(self):
        """This command saves all entry attachments (into the directory of the
        database) and removes them from the database."""
        ...

    def gen_pw(self):
        """Generates passwords."""
        ...

    def estimate_quality(self):
        """Estimates the quality (in bits) of the password specified via the
        -text: parameter."""
        ...


if __name__ == '__main__':
    main()

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .