Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Script to merge conflicted Keepass Classic .xml exports in a single .xml file
from xml.dom import minidom
from collections import namedtuple
import os
PwEntry = namedtuple("PwEntry", [
"group",
"groupTree",
"title",
"username",
"url",
"password",
"notes",
"uuid",
"image",
"creationtime",
"lastmodtime",
"lastaccesstime",
"expiretime",
"expireFlag"
])
def xmlToPwEntry(xmlEntry):
keys = [
"group",
"title",
"username",
"url",
"password",
"notes",
"uuid",
"image",
"creationtime",
"lastmodtime",
"lastaccesstime",
"expiretime"
]
values = {
key: xmlEntry.getElementsByTagName(key)[0].firstChild.wholeText
if xmlEntry.getElementsByTagName(key)[0].hasChildNodes() else ""
for key in keys
}
# special field, it's an attribute of `//pwlist/pwentry/group`
values["groupTree"] = xmlEntry.getElementsByTagName("group")[0].getAttribute("tree")
# special field, it's an attribute of `//pwlist/pwentry/expiretime`
values["expireFlag"] = xmlEntry.getElementsByTagName("expiretime")[0].getAttribute("expires")
return PwEntry(**values)
def extractEntries(xmlFile, passwordCache):
with open(xmlFile) as src:
doc = minidom.parse(src)
rawEntries = doc.getElementsByTagName("pwentry")
for entry in map(xmlToPwEntry, rawEntries):
try:
passwordCache[entry.title].append(entry)
except KeyError:
passwordCache[entry.title] = [entry]
def pwEntryWithChangedTitle(pwEntry, newTitle):
internalKeyValues = pwEntry._asdict()
internalKeyValues["title"] = newTitle
return PwEntry(**internalKeyValues)
def removeDuplicates(passwordCache):
result = []
for title in passwordCache:
entries = passwordCache[title]
entries.sort(key=lambda it: it.uuid)
entries_ = []
for index, entry in enumerate(entries):
if index == 0 or (entry.uuid != entries[index-1].uuid):
entries_.append(entry)
if len(entries_) == 1:
result.append(entries_[0])
else:
print("WARNING: found multiple keys for title {}, writing them with a numbered suffix".format(title))
for index, entry_ in enumerate(entries_):
new_entry = pwEntryWithChangedTitle(entry_, title + " - {}".format(index))
result.append(new_entry)
return result
def pwEntryToXml(pwEntry):
def createFieldEntry(pwEntry, fieldName, attributes=None):
if not attributes: attributes = []
pwAsDict = pwEntry._asdict()
xmlField = minidom.Element(fieldName)
if pwAsDict[fieldName]:
# if you have nothing to put there, just don't create
# an empty text node. that's why Text.replaceWholeText("")
# complains. TIL.
xmlFieldText = minidom.Text()
xmlFieldText.replaceWholeText(pwAsDict[fieldName])
xmlField.appendChild(xmlFieldText)
for attr_key, attr_alias in attributes:
xmlField.setAttribute(attr_alias, pwAsDict[attr_key])
return xmlField
fullEntry = minidom.Element("pwentry")
fullEntry.appendChild(createFieldEntry(pwEntry, "group", [("groupTree", "tree")]))
fullEntry.appendChild(createFieldEntry(pwEntry, "title"))
fullEntry.appendChild(createFieldEntry(pwEntry, "username"))
fullEntry.appendChild(createFieldEntry(pwEntry, "url"))
fullEntry.appendChild(createFieldEntry(pwEntry, "password"))
fullEntry.appendChild(createFieldEntry(pwEntry, "notes"))
fullEntry.appendChild(createFieldEntry(pwEntry, "uuid"))
fullEntry.appendChild(createFieldEntry(pwEntry, "image"))
fullEntry.appendChild(createFieldEntry(pwEntry, "creationtime"))
fullEntry.appendChild(createFieldEntry(pwEntry, "lastmodtime"))
fullEntry.appendChild(createFieldEntry(pwEntry, "lastaccesstime"))
fullEntry.appendChild(createFieldEntry(pwEntry, "expiretime", [("expireFlag", "expires")]))
return fullEntry
def convertEntriesToXml(pwEntries):
outdoc = minidom.Document()
pwdListElement = minidom.Element("pwdlist")
for xmlEntry in map(pwEntryToXml, pwEntries):
pwdListElement.appendChild(xmlEntry)
outdoc.appendChild(pwdListElement)
return outdoc.toprettyxml()
def main(args):
# extract entries from XML dumps of Keepass Classic (keepass1) databases
passwordEntries = dict()
for path, _, files in os.walk(args.folder):
for file_ in (f_ for f_ in files if f_.endswith(".kdb.xml")):
fullpath = os.path.join(path, file_)
extractEntries(fullpath, passwordEntries)
pwdEntriesToWrite = removeDuplicates(passwordEntries)
with open("merge.xml", "w") as out:
out.write(convertEntriesToXml(pwdEntriesToWrite))
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("folder")
main(parser.parse_args())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.