Revelation2KeePass - import from Revelation into KeePassMobile password manager

Motivation: I need to convert sensitive data stored in a Revelation database into a KeePass compatible format I can import it into the password manager I use on my mobile phone.

Active development of Revelation stopped in 2007. I was already using the application at that time and stayed with it due to the lack of compelling alternatives. Now, KeePass 2.0 - being a .NET based program - can be run on Linux using mono but looks rather ugly; KeePassX has not been updated to support v2.0 password database format but that is not a problem since KeePassMobile also uses v1.0.

I have been managing my passwords, pin codes, credit card numbers and other credentials with Revelation for a couple of years now. It is an open source GTK/GNOME program that stores this sensitive data encrypted, protected by a master key, and after opening the password vault with the master key, it provides an clean interface to view, extend and maintain my credentials. Since I started traveling frequently on business, form time to time I find myself in a situation that I need these or that sensitive data but the circumstances are not optimal for booting up my laptop. Storing those credentials on my mobile - of course in a properly encrypted format would come handy in such cases, and as the mobile phones today support Java ME, I searched the internet for open source midlets.

I have found two candidates: One called keepassj2me which has a longer history so it might be more stable, but is definitely less polished and provides very basic funtionality when compared with the - at that time - very young but already promising KeePassMobile. Both software use the v1.0 data format of KeePass Password Safe. On Linux, this format could be handled by the QT based KeePassX. Unfortunately, nor Revelation neither provide import/export functionality for the formats of each other, so I was looking for existing third party solutions...

Search for existing solutions, patching

I have found a python script here that choked during the test run... Fixed it, so I could successfully convert the XML exported from Revelation into another XML that could be digested by KeePassX. After importing it into KeePassX and issuing some minor adjustments on the key database, I uploaded the key db to my mobile via BueTooth. Me happy.

I have issued to following changes to the script fished from the net:


*** ./revelation-to-keepassx	2009-08-20 15:57:19.000000000 +0200
--- ./revelation-to-keepassx_mod	2009-07-06 01:19:04.000000000 +0200
***************
*** 37,41 ****
  "generic": 0,
  "phone": 68,
! "website": 1 
  }
  
--- 37,43 ----
  "generic": 0,
  "phone": 68,
! "website": 1,
! "email" : 1,
! "shell" : 0 
  }
  
***************
*** 93,101 ****
                          ElementTree.SubElement(element, "url").text = item.text
                      elif id == "creditcard-cardnumber":
!                         desc_append += "Card Number: " + item.text + "\n"
                      elif id == "creditcard-expirydate":
!                         desc_append += "Card Expiry: " + item.text + "\n"
                      elif id == "creditcard-ccv":
!                         desc_append += "Card CCV: " + item.text + "\n"
                      elif id == "generic-pin":
                          if item.text is not None:
--- 95,103 ----
                          ElementTree.SubElement(element, "url").text = item.text
                      elif id == "creditcard-cardnumber":
!                         desc_append += "Card Number: " + str(item.text) + "\n"
                      elif id == "creditcard-expirydate":
!                         desc_append += "Card Expiry: " + str(item.text) + "\n"
                      elif id == "creditcard-ccv":
!                         desc_append += "Card CCV: " + str(item.text) + "\n"
                      elif id == "generic-pin":
                          if item.text is not None:
***************
*** 105,109 ****
                      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
                      else:
--- 107,111 ----
                      elif id == "generic-code":
                          ElementTree.SubElement(element, "password").text = item.text
!                     elif id in ("creditcard-cardtype", "generic-location", "generic-certificate", "generic-keyfile", "generic-email", "generic-domain"):
                          stripped_ids[id] = 1
                      else:

“Is your code ever completely without stain and flaw?” demanded Master Foo.

After a few weeks I still had the feeling that something was missing... Those minor manual adjustments I had to manually execute on each import/export cycle I did not like. Further, if any url, password, username or description field contained characters that need special handling in case of XML (& ' " < >, or also some non-ASCII Hungarian characters under circumstances) then the script would generate an errorous file for me. This is not cool. I was also dissatisfied with the way it handled (stripped) additional information found in the Revelation file. Last but not least I believe that for converting one XML into another neither Python nor Java are the ideal tools, but (surprise) XSLT which was invented to serve this particular purpose. Below follows the XSLT:


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" doctype-system="KEEPASSX-DATABASE" encoding="us-ascii"/>

<xsl:variable name="map" select="document('map.xml')" />
<xsl:variable name="nl"><xsl:text>
</xsl:text></xsl:variable>

<xsl:template match="/revelationdata">
  <database>
    <xsl:if test="entry[@type!='folder']">
      <group>
        <icon>
          <xsl:value-of select="$map//toplevel/icon" />
        </icon>
        <title>
          <xsl:value-of select="$map//toplevel/label" />
        </title>
        <xsl:apply-templates select="entry[@type!='folder']"/>
      </group>
    </xsl:if>
    <xsl:apply-templates select="entry[@type='folder']"/>
  </database>
</xsl:template>

<xsl:template match="entry[@type!='folder']">
  <xsl:variable name="type" select="@type" />
  <entry>
    <icon><xsl:value-of select="$map//type-map/icon[@for=$type]" /></icon>
    <title><xsl:value-of select="name" /></title>
    <lastmod></lastmod>
    <lastaccess></lastaccess>
    <creation></creation>
    <url>
      <xsl:call-template name="multivalue">
        <xsl:with-param name="nodes" select="field[@id=$map//field-map/field[text()='url']/@for]" />
      </xsl:call-template>
    </url>
    <username>
      <xsl:call-template name="multivalue">
        <xsl:with-param name="nodes" select="field[@id=$map//field-map/field[text()='username']/@for]" />
      </xsl:call-template>
    </username>
    <password>
      <xsl:call-template name="multivalue">
        <xsl:with-param name="nodes" select="field[@id=$map//field-map/field[text()='password']/@for]" />
      </xsl:call-template>
    </password>
    <comment>
      <xsl:if test="description/text()">
        <xsl:value-of select="concat(description,$nl)" />
      </xsl:if>
      <xsl:for-each select="field">
        <xsl:variable name="id" select="@id"/>
        <xsl:if test="not($map//field-map/field[@for=$id])">
          <xsl:value-of select="concat(@id, ': ', text(), $nl)" />
        </xsl:if>
      </xsl:for-each>
    </comment>
  </entry>
</xsl:template>

<xsl:template match="entry[@type='folder']">
  <group>
    <icon>48</icon>
    <title>
      <xsl:value-of select="name" />
    </title>
    <comment>
      <xsl:value-of select="description" />
    </comment>
    <xsl:apply-templates select="entry" />
  </group>
</xsl:template>
<!-- renders $nodes to a string, eliminating duplicate values and using $separator to separate the unique values -->
<!-- pseudocode: result = nodelist.unique().join($separator) -->
<xsl:template name="multivalue">
  <xsl:param name="nodes" select="/.."/>
  <xsl:param name="separator" select="'|'"/>
  <xsl:param name="distinct" select="/.."/>
  <xsl:choose>
    <xsl:when test="$nodes">
      <xsl:call-template name="multivalue">
        <xsl:with-param name="distinct" select="$distinct | $nodes[1][not(. = $distinct)]"/>
        <xsl:with-param name="nodes" select="$nodes[position() > 1]"/>
        <xsl:with-param name="separator" select="$separator"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$distinct[position() = 1]" />
      <xsl:if test="count($distinct) > 1">
        <xsl:for-each select="$distinct[position() != 1]">
          <xsl:value-of select="concat($separator, .)" />
        </xsl:for-each>
      </xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

</xsl:stylesheet>

The logic and configuration of a program is best kept well separated, and, to favor those who are not confident with XSLT editing, the settings of the transformation are stored in a separate XML file, that has to be dropped next to the stylesheet (into the same folder). This file defines, which Revelation fields are mapped to which properties of the KeePass entry - other fields are appended to the comment field, one key-value pair per line. The config file also dictated which Revelation entry type should receive which KeePass icon. (The KeePass v1.0 format does not support entries of multiple types: all entries consist of the username, password, url, comment tuple, plus an icon). The KeePass format, in contrast with Revelation also does not allow top level entries, each entry has to be put into a group/container. The config file also defines in which KeePass group any top level Revelation entries should be put - it additionally support setting a custom icon for this 'top level' group.

I have implemented the transformation gic in a way that only non-folder top level items are put into the special containter. Top level Revelation folders are transformed to top level containers (groups) in the KeePass file. This has saved me one click per entry when using my KeePass application on my mobile...


<?xml version="1.0" encoding="UTF-8"?>
<revelation-to-keepassx>
  <field-map>
    <field for="generic-username">username</field>
    <field for="generic-email">username</field>
    <field for="generic-password">password</field>
    <field for="generic-pin">password</field>
    <field for="generic-url">url</field>
    <field for="generic-hostname">url</field>
  </field-map>
  <type-map>
    <icon for="creditcard">9</icon>
    <icon for="cryptokey">29</icon>
    <icon for="door">51</icon>
    <icon for="folder">48</icon>
    <icon for="generic">0</icon>
    <icon for="phone">68</icon>
    <icon for="website">1</icon>
    <icon for="email">19</icon>
    <icon for="shell">30</icon>
  </type-map>
  <toplevel>
    <icon>50</icon>
    <label>TopLevel</label>
  </toplevel>
</revelation-to-keepassx>
© 2003-2020 lithium.io7.org
Content on this site is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.