Going from Revelation to KeePassX

I’ve decided to ditch revelation. I’ve used it for several years to manage my passwords but development seems to have stalled. The author’s website is currently inaccessible. So it is over for me. Read on for the method I used to convert my database of passwords.

I’ve switched to KeePassX because it seems better supported and has the features I want. Wzzrd produced a script to perform the conversion. His script is in Ruby, a language I’m not particularly interested in. Moreover, his script did not quite do what I wanted so I converted his script to python and adapted it to my needs. I’m attaching the script to this post: revelation-to-keepassx Anybody wanting to use this script should be warned that it is throwaway code and that I implemented just as much as I needed.

The way I converted my data was:

1. Export the data from revelation into an unencrypted XML file. (It is advisable to export it to a file in volatile memory like /dev/shm. This minimizes the risk of leaving unencrypted data in free blocks after the conversion is done.)

2. I ran my script without options:

$ revelation-to-keepassx /dev/shm/revelation-export.xml > /dev/shm/keepassx.xml

(Note that I save the result in /dev/shm again.)

This resulted in the output:

revelation-to-keepassx: ERROR: stripped following ids: generic-keyfile, generic-location, generic-certificate, creditcard-cardtype

I checked my input file to verify that I would not lose anything important if <field> elements with the above ids were to be stripped from the output instead of converted.

3. I reran the script as above but with the “-i” option which tells the script to ignore the stripped ids.

4. I imported the resulting file into KeePassX.

I’ve inspected the final database and found no evident problem. To be on the safe side I used gpg to encrypt the XML file I exported from revelation so that even if I eventually run into trouble and revelation is not easily installable, I can visually inspect the XML file.

22 thoughts on “Going from Revelation to KeePassX

  1. Maxim

    Interesting. I switched my main scripting / programming to Python (for professional reasons) a couple months ago. I had planned to rewrite the script in Python if I had some time, but you beat me to it. Maybe I’ll take a look sometime and see if I can expand on it a bit.

    Reply
    1. Louis-Dominique Post author

      You can sure use my script if you want to produce something more sophisticated in python. Usual caveats: it is throwaway code so not very robust and it was designed to cater to my needs.

      Reply
  2. trunc

    I found this error

    python revelation-to-keepassx.py revel.xml > keepass.xml -i
    Traceback (most recent call last):
    File “revelation-to-keepassx.py”, line 124, in
    parse_folder(old_root, new_root, 0)
    File “revelation-to-keepassx.py”, line 62, in parse_folder
    parse_folder(item, element, group_depth + 1)
    File “revelation-to-keepassx.py”, line 73, in parse_folder
    ElementTree.SubElement(element, “icon”).text = str(from_type_to_icon[item.get(“type”)])
    KeyError: ‘ftp’

    Reply
    1. Louis-Dominique Post author

      This is not surprising. Here is the key passage from my original post:

      Anybody wanting to use this script should be warned that it is throwaway code and that I implemented just as much as I needed.

      I did not need to support the “ftp” type so I did not implement conversion for it. I encourage you to modify the code to suit your needs.

      Reply
  3. Pingback: Миграция с revelation на KeePassX : Дмитрий Петров

  4. Ashley

    Just ran across this: Need to convert to Keepass so I can use jbiKeePass on iPhone. Worked a treat even though I’ve never had to hack a python script before. Many thanks for the script.

    Reply
  5. Oleg

    Sadly in my case script worked pretty much worse han expected, so I hacked it further and:
    — It knows about “Shell”, “FTP” and “email” fields
    — It gracefully skips unfilled “Expiry date” and some other fields
    — It supports some more FTP / Shell / email / Credit card specific fields.

    Anyway, “I implemented just as much as I needed” too :).

    So if the original author’s script fails and you know little about programming, try it:

    http://pastebin.com/f221151ff

    P/S to the author: thank you for a nice piece of Python. I bet some day I’ll learn it

    Reply
  6. andrewm

    Hi, I just wanted to say that I came across your script and have successfully converted my revelation password database. Many thanks for taking the time to write this, and also to the other contributors.

    Reply
  7. Sebastian Schlegel

    Thank you for your script! Maybe meanwhile, there have been some changes to revelation, so I simply put your parse_folder() function into a try…except block and just repaired the missing values by hand. I must admit that this is the lazy man solution 😉

    Reply
  8. Chris

    Hi!

    Thanks for the script – it was very helpful.

    Here is (assuming your blog’s code tags can deal with Python indenting!) a minor update to jendrek’s version to add support for the ‘Notes’ field, and to fix a bug that causes the title field of database entries to get clobbered by the database name.

    #!/usr/bin/python
    
    import optparse
    import logging
    import os
    import sys
    import time
    from xml.etree import ElementTree
    
    progname = os.path.basename(sys.argv[0])
    version = "0.1.1"
    
    parser = optparse.OptionParser(usage="%prog input")
    logging.basicConfig(level=logging.INFO,
                        format=progname + ": %(levelname)s: %(message)s")
    parser.add_option("-i", "--ignore-stripped", 
                      action="store_true", 
                      dest="ignore_stripped", 
                      help="ignore the stripped ids")
    
    (options, args) = parser.parse_args()
    
    class FatalError(Exception):
        pass
    
    def error(msg):
        logging.error(msg)
        raise FatalError
    
    stripped_ids = {}
    
    from_type_to_icon = {
    "email": 19,
    "ftp": 27,
    "shell": 30,
    "creditcard": 9,
    "cryptokey": 29,
    "door": 52,
    "folder": 48,
    "generic": 0,
    "phone": 68,
    "website": 1,
    "database":43
    }
    
    try:
        if len(args) != 1:
            error("requires one argument")
    
        old_filename = args[0]
    
        old_doc = ElementTree.ElementTree(file=old_filename)
        old_root = old_doc.getroot()
        new_root = ElementTree.Element("database")
        new_doc = ElementTree.ElementTree(new_root)
    
        def parse_folder(old_element, new_parent, group_depth):
            description = None
            desc_append = ""
            for item in old_element.findall("*"):
                if item.tag == "entry":
                    if item.get("type") == "folder":
                        element = ElementTree.SubElement(new_parent, "group")
                        ElementTree.SubElement(element, "icon").text = "48"
                        element.tail = "\n"
                        parse_folder(item, element, group_depth + 1)
                    else:
                        if group_depth == 0:
                            fake_group = ElementTree.SubElement(new_parent, "group")
                            ElementTree.SubElement(fake_group, "icon").text = "48"
                            ElementTree.SubElement(fake_group, "title").text = "Misc"                        
                            fake_group.tail = "\n"
                            new_parent = fake_group
                            group_depth += 1
    
                        element = ElementTree.SubElement(new_parent, "entry")
                        ElementTree.SubElement(element, "icon").text = str(from_type_to_icon[item.get("type")])
                        element.tail = "\n"
                        parse_folder(item, element, group_depth)
                else:
                    element = new_parent
                    if item.tag == "name":
                        ElementTree.SubElement(element, "title").text = item.text
                    elif item.tag == "updated":
                        lastmod_time = time.strftime("%Y-%m-%dT%H:%M:%S", 
                                                     time.localtime(int(item.text)))
                        ElementTree.SubElement(element, "lastmod").text = lastmod_time
                        ElementTree.SubElement(element, "lastaccess").text = lastmod_time
                        ElementTree.SubElement(element, "creation").text = lastmod_time
                    elif item.tag == "field":
                        id = item.get("id")
                        if id == "generic-username":
                            ElementTree.SubElement(element, "username").text = item.text
                        elif id == "generic-password":
                            ElementTree.SubElement(element, "password").text = item.text
                        elif id in ("generic-hostname", "generic-url"):
                            ElementTree.SubElement(element, "url").text = item.text
    
                        elif id == "generic-email":
                            if item.text is not None:
                                desc_append += "e-mail: " + item.text + "\n"
                        elif id == "generic-port":
                            if item.text is not None:
                                desc_append += "Port: " + item.text + "\n"
                        elif id == "generic-domain":
                            if item.text is not None:
                                ElementTree.SubElement(element, "url").text = item.text
                        elif id == "creditcard-cardtype":
                            if item.text is not None:
                                desc_append += "Type: " + item.text + "\n"
    
    
                        elif id == "creditcard-cardnumber":
                            if item.text is not None:
                                desc_append += "Card Number: " + item.text + "\n"
                        elif id == "creditcard-expirydate":
                            if item.text is not None:
                                desc_append += "Card Expiry: " + item.text + "\n"
                        elif id == "creditcard-ccv":
                            if item.text is not None:
                                desc_append += "Card CCV: " + item.text + "\n"
                        elif id == "generic-pin":
                            if item.text is not None:
                                desc_append += "PIN: " + item.text + "\n"
                        elif id == "phone-phonenumber":
                            ElementTree.SubElement(element, "password").text = item.text                        
                        elif id == "generic-code":
                            ElementTree.SubElement(element, "password").text = item.text
                        elif id in ("creditcard-cardtype", "generic-location", "generic-certificate", "generic-keyfile"):
                            stripped_ids[id] = 1
    		    elif id == "generic-database":
                            if item.text is not None:
    			    desc_append += "Database: " + item.text + "\n"
    
                        else:
                            error("unexpected field id: " + id)
                    elif item.tag == "description":
                        description = item.text
                    elif item.tag == "notes":
                        if item.text is not None:
                            desc_append += item.text
                    else:
                        error("unexpected tag: " + item.tag)
            if description is not None or len(desc_append) > 0:
                if len(desc_append) == 0:
                    ElementTree.SubElement(element, "comment").text = description
                elif description is None:
                    ElementTree.SubElement(element, "comment").text = desc_append
                else:
                    ElementTree.SubElement(element, "comment").text = description + "\n" + desc_append
                    
                
        parse_folder(old_root, new_root, 0)
    
        if len(stripped_ids) and not options.ignore_stripped:
            error("stripped following ids: " + ", ".join(stripped_ids.keys()))
    
        sys.stdout.write("\n")
        new_doc.write(sys.stdout)
    except FatalError:
        pass
    
    Reply
  9. Uwe

    As Fedora decided to throw out revelation in Fedora 31, I stumbled over your blog post and the various helpful comments here. After playing around with the scripts above I finally found out that the current keepass (I tested version 2.42.1) can directly import keepass xml files. I’m just writing this here for any other users who has the same issue and want’s to save some time 🙂

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *