Skip to content
Snippets Groups Projects
revelation2pass.py 5.82 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/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)