aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/contrib/revelation2pass.py
diff options
context:
space:
mode:
authorEmanuele Aina <emanuele.aina@collabora.com>2013-01-13 18:25:21 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2013-01-18 16:17:12 +0100
commita9f34015d3121cfcdfdae3bba69f25a7460520b5 (patch)
tree2512cca8b049929940c8d5e687208e70f2165e41 /contrib/revelation2pass.py
parentKed Password Manager import script from Antoine Beaupré. (diff)
downloadpassword-store-a9f34015d3121cfcdfdae3bba69f25a7460520b5.tar.xz
password-store-a9f34015d3121cfcdfdae3bba69f25a7460520b5.zip
Script to import from the Revelation password manager
http://revelation.olasagasti.info/
Diffstat (limited to 'contrib/revelation2pass.py')
-rwxr-xr-xcontrib/revelation2pass.py172
1 files changed, 172 insertions, 0 deletions
diff --git a/contrib/revelation2pass.py b/contrib/revelation2pass.py
new file mode 100755
index 0000000..f04c1a8
--- /dev/null
+++ b/contrib/revelation2pass.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 Emanuele Aina <em@nerd.ocracy.org>. All Rights Reserved.
+# Copyright (C) 2011 Toni Corvera. All Rights Reserved.
+# This file is licensed under the BSD 2-clause license:
+# http://www.opensource.org/licenses/BSD-2-Clause
+#
+# Import script for the Revelation password manager:
+# http://revelation.olasagasti.info/
+# Heavily based on the Relevation command line tool:
+# http://p.outlyer.net/relevation/
+
+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)