Jason A. Donenfeld wrote:

> We're not much of a github project -- more of the mailing list patch
> kind of workflow. But thanks a bunch for the script -- I'd be happy to
> merge it into the contrib dir.

Patch attached.

> Can you relicense it as GPLv2+?

I'd prefer to keep the current licensing, the code reading the
Revelation store is mostly unchanged from the original Relevation code
and I'd prefer to be able to share eventual fixes both ways. Since it's
a contrib script and the licensing is compatible with the GPLv2+ I hope
this won't be a big issue.

-- 
Emanuele Aina
✪ http://nerd.ocracy.org/em/[email protected]
>From 72ac28f8085b3ecd09291908703cf64a058f3f10 Mon Sep 17 00:00:00 2001
From: Emanuele Aina <[email protected]>
Date: Sun, 13 Jan 2013 18:25:21 +0100
Subject: [PATCH] Script to import from the Revelation password manager

http://revelation.olasagasti.info/
---
 contrib/revelation2pass.py |  192 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 192 insertions(+)
 create mode 100755 contrib/revelation2pass.py

diff --git a/contrib/revelation2pass.py b/contrib/revelation2pass.py
new file mode 100755
index 0000000..8a818ec
--- /dev/null
+++ b/contrib/revelation2pass.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Import script for the Revelation password manager to Pass
+# http://revelation.olasagasti.info/
+# http://zx2c4.com/projects/password-store/
+#
+# Copyright © 2013 Emanuele Aina <[email protected]>
+#
+# Heavily based on the Relevation command line tool by Toni Corvera.
+# http://p.outlyer.net/relevation/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os, sys, argparse, zlib, getpass, traceback
+from subprocess import Popen, PIPE, STDOUT, CalledProcessError
+from collections import OrderedDict
+try:
+    from lxml import etree
+except ImportError:
+    from xml.etree import ElementTree as etree
+
+USE_PYCRYPTO = True
+try:
+    from Crypto.Cipher import AES
+except ImportError:
+    USE_PYCRYPTO = False
+    try:
+        from crypto.cipher import rijndael, cbc
+        from crypto.cipher.base import noPadding
+    except ImportError:
+        sys.stderr.write('Either PyCrypto or cryptopy are required\n')
+        raise
+
+def path_for(element, path=None):
+    """ Generate path name from elements name and current path """
+    name = element.find('name').text
+    name = name.replace('/', '-').replace('\\', '-')
+    path = path if path else ''
+    return os.path.join(path, name)
+
+def format_password_data(data):
+    """ Format the secret data that will be handed to Pass in multi-line mode:
+    $password
+    $fieldname: $fielddata
+    ...
+     $multi_line_notes_with_leading_spaces"""
+    password = data.pop('password', None) or ''
+    ret = password + '\n'
+    notes = data.pop('notes', None)
+    for label, text in data.iteritems():
+        ret += label + ': ' + text + '\n'
+    if notes:
+        ret += ' ' + notes.replace('\n', '\n ').strip() + '\n'
+    return ret
+
+def password_data(element):
+    """ Return password data and additional info if available from
+    password entry element. """
+    data = OrderedDict()
+    data['password'] = element.find('field[@id="generic-password"]').text
+    data['type'] = element.attrib['type']
+    for field in element.findall('field'):
+        field_id = field.attrib['id']
+        if field_id == 'generic-password':
+            continue
+        if field.text is not None:
+            data[field_id] = field.text
+    for tag in ('description', 'notes'):
+        field = element.find(tag)
+        if field is not None and field.text:
+            data[tag] = field.text
+    return format_password_data(data)
+
+
+def import_entry(element, path=None, verbose=0):
+    """ Import new password entry to password-store using pass insert
+    command """
+    cmd = ['pass', 'insert', '--multiline', '--force', path_for(element, path)]
+    if verbose:
+        print 'cmd:\n ' + ' '.join(cmd)
+    proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+    stdin = password_data(element).encode('utf8')
+    if verbose:
+        print 'input:\n ' + stdin.replace('\n', '\n ').strip()
+    stdout, _ = proc.communicate(stdin)
+    retcode = proc.poll()
+    if retcode:
+        raise CalledProcessError(retcode, cmd, output=stdout)
+
+def import_folder(element, path=None, verbose=0):
+    path = path_for(element, path)
+    import_subentries(element, path, verbose)
+
+def import_subentries(element, path=None, verbose=0):
+    """ Import all sub entries of the current folder element """
+    for entry in element.findall('entry'):
+        if entry.attrib['type'] == 'folder':
+            import_folder(entry, path, verbose)
+        else:
+            import_entry(entry, path, verbose)
+
+def decrypt_gz(key, cipher_text):
+    ''' Decrypt cipher_text using key.
+    decrypt(str, str) -> cleartext (gzipped xml)
+
+    This function will use the underlying, available, cipher module.
+    '''
+    if USE_PYCRYPTO:
+        # Extract IV
+        c = AES.new(key)
+        iv = c.decrypt(cipher_text[12:28])
+        # Decrypt data, CBC mode
+        c = AES.new(key, AES.MODE_CBC, iv)
+        ct = c.decrypt(cipher_text[28:])
+    else:
+        # Extract IV
+        c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
+        iv = c.decrypt(cipher_text[12:28])
+        # Decrypt data, CBC mode
+        bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
+        c = cbc.CBC(bc, padding=noPadding())
+        ct = c.decrypt(cipher_text[28:], iv=iv)
+    return ct
+
+def main(datafile, verbose=False):
+    f = None
+    with open(datafile, "rb") as f:
+        # Encrypted data
+        data = f.read()
+    password = getpass.getpass()
+    # Pad password
+    password += (chr(0) * (32 - len(password)))
+    # Decrypt. Decrypted data is compressed
+    cleardata_gz = decrypt_gz(password, data)
+    # Length of data padding
+    padlen = ord(cleardata_gz[-1])
+    # Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
+    xmldata = zlib.decompress(cleardata_gz[:-padlen], 15, 2**15)
+    root = etree.fromstring(xmldata)
+    import_subentries(root, verbose=verbose)
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--verbose', '-v', action='count')
+    parser.add_argument('FILE', help="the file storing the Revelation passwords")
+    args = parser.parse_args()
+
+    def err(s):
+        sys.stderr.write(s+'\n')
+
+    try:
+        main(args.FILE, verbose=args.verbose)
+    except KeyboardInterrupt:
+        if args.verbose:
+            traceback.print_exc()
+            err(str(e))
+    except zlib.error:
+        err('Failed to decompress decrypted data. Wrong password?')
+        sys.exit(os.EX_DATAERR)
+    except CalledProcessError as e:
+        if args.verbose:
+            traceback.print_exc()
+            print 'output:\n ' + e.output.replace('\n', '\n ').strip()
+        else:
+            err('CalledProcessError: ' + str(e))
+        sys.exit(os.EX_IOERR)
+    except IOError as e:
+        if args.verbose:
+            traceback.print_exc()
+        else:
+            err('IOError: ' + str(e))
+        sys.exit(os.EX_IOERR)
-- 
1.7.10.4

_______________________________________________
Password-Store mailing list
[email protected]
http://lists.zx2c4.com/listinfo.cgi/password-store-zx2c4.com

Reply via email to