password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

revelation2pass.py (6223B)


      1 #!/usr/bin/env python
      2 # -*- coding: utf-8 -*-
      3 #
      4 # Copyright (C) 2013 Emanuele Aina <em@nerd.ocracy.org>. All Rights Reserved.
      5 # Copyright (C) 2011 Toni Corvera. All Rights Reserved.
      6 # This file is licensed under the BSD 2-clause license:
      7 # http://www.opensource.org/licenses/BSD-2-Clause
      8 #
      9 # Import script for the Revelation password manager:
     10 # http://revelation.olasagasti.info/
     11 # Heavily based on the Relevation command line tool:
     12 # http://p.outlyer.net/relevation/
     13 
     14 import os, sys, argparse, zlib, getpass, traceback
     15 from subprocess import Popen, PIPE, STDOUT, CalledProcessError
     16 from collections import OrderedDict
     17 try:
     18     from lxml import etree
     19 except ImportError:
     20     from xml.etree import ElementTree as etree
     21 
     22 USE_PYCRYPTO = True
     23 try:
     24     from Crypto.Cipher import AES
     25 except ImportError:
     26     USE_PYCRYPTO = False
     27     try:
     28         from crypto.cipher import rijndael, cbc
     29         from crypto.cipher.base import noPadding
     30     except ImportError:
     31         sys.stderr.write('Either PyCrypto or cryptopy are required\n')
     32         raise
     33 
     34 def path_for(element, path=None):
     35     """ Generate path name from elements name and current path """
     36     name = element.find('name').text
     37     name = name.replace('/', '-').replace('\\', '-')
     38     path = path if path else ''
     39     return os.path.join(path, name)
     40 
     41 def format_password_data(data):
     42     """ Format the secret data that will be handed to Pass in multi-line mode:
     43     $password
     44     $fieldname: $fielddata
     45     ...
     46      $multi_line_notes_with_leading_spaces"""
     47     password = data.pop('password', None) or ''
     48     ret = password + '\n'
     49     notes = data.pop('notes', None)
     50     for label, text in data.iteritems():
     51         ret += label + ': ' + text + '\n'
     52     if notes:
     53         ret += ' ' + notes.replace('\n', '\n ').strip() + '\n'
     54     return ret
     55 
     56 def password_data(element):
     57     """ Return password data and additional info if available from
     58     password entry element. """
     59     data = OrderedDict()
     60     try:
     61         data['password'] = element.find('field[@id="generic-password"]').text
     62     except AttributeError:
     63         data['password'] = None
     64     data['type'] = element.attrib['type']
     65     for field in element.findall('field'):
     66         field_id = field.attrib['id']
     67         if field_id == 'generic-password':
     68             continue
     69         if field.text is not None:
     70             data[field_id] = field.text
     71     for tag in ('description', 'notes'):
     72         field = element.find(tag)
     73         if field is not None and field.text:
     74             data[tag] = field.text
     75     return format_password_data(data)
     76 
     77 
     78 def import_entry(element, path=None, verbose=0):
     79     """ Import new password entry to password-store using pass insert
     80     command """
     81     cmd = ['pass', 'insert', '--multiline', '--force', path_for(element, path)]
     82     if verbose:
     83         print 'cmd:\n ' + ' '.join(cmd)
     84     proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
     85     stdin = password_data(element).encode('utf8')
     86     if verbose:
     87         print 'input:\n ' + stdin.replace('\n', '\n ').strip()
     88     stdout, _ = proc.communicate(stdin)
     89     retcode = proc.poll()
     90     if retcode:
     91         raise CalledProcessError(retcode, cmd, output=stdout)
     92 
     93 def import_folder(element, path=None, verbose=0):
     94     path = path_for(element, path)
     95     import_subentries(element, path, verbose)
     96 
     97 def import_subentries(element, path=None, verbose=0):
     98     """ Import all sub entries of the current folder element """
     99     for entry in element.findall('entry'):
    100         if entry.attrib['type'] == 'folder':
    101             import_folder(entry, path, verbose)
    102         else:
    103             import_entry(entry, path, verbose)
    104 
    105 def decrypt_gz(key, cipher_text):
    106     ''' Decrypt cipher_text using key.
    107     decrypt(str, str) -> cleartext (gzipped xml)
    108 
    109     This function will use the underlying, available, cipher module.
    110     '''
    111     if USE_PYCRYPTO:
    112         # Extract IV
    113         c = AES.new(key)
    114         iv = c.decrypt(cipher_text[12:28])
    115         # Decrypt data, CBC mode
    116         c = AES.new(key, AES.MODE_CBC, iv)
    117         ct = c.decrypt(cipher_text[28:])
    118     else:
    119         # Extract IV
    120         c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
    121         iv = c.decrypt(cipher_text[12:28])
    122         # Decrypt data, CBC mode
    123         bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
    124         c = cbc.CBC(bc, padding=noPadding())
    125         ct = c.decrypt(cipher_text[28:], iv=iv)
    126     return ct
    127 
    128 def main(datafile, verbose=False, xml=False):
    129     f = None
    130     with open(datafile, "rb") as f:
    131         # Encrypted data
    132         data = f.read()
    133     if xml:
    134         xmldata = data
    135     else:
    136         password = getpass.getpass()
    137         # Pad password
    138         password += (chr(0) * (32 - len(password)))
    139         # Decrypt. Decrypted data is compressed
    140         cleardata_gz = decrypt_gz(password, data)
    141         # Length of data padding
    142         padlen = ord(cleardata_gz[-1])
    143         # Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
    144         xmldata = zlib.decompress(cleardata_gz[:-padlen], 15, 2**15)
    145     root = etree.fromstring(xmldata)
    146     import_subentries(root, verbose=verbose)
    147 
    148 if __name__ == '__main__':
    149     parser = argparse.ArgumentParser()
    150     parser.add_argument('-x', '--xml', help='read plain XML file', action='store_true')
    151     parser.add_argument('--verbose', '-v', action='count')
    152     parser.add_argument('FILE', help="the file storing the Revelation passwords")
    153     args = parser.parse_args()
    154 
    155     def err(s):
    156         sys.stderr.write(s+'\n')
    157 
    158     try:
    159         main(args.FILE, verbose=args.verbose, xml=args.xml)
    160     except KeyboardInterrupt:
    161         if args.verbose:
    162             traceback.print_exc()
    163             err(str(e))
    164     except zlib.error:
    165         err('Failed to decompress decrypted data. Wrong password?')
    166         sys.exit(os.EX_DATAERR)
    167     except CalledProcessError as e:
    168         if args.verbose:
    169             traceback.print_exc()
    170             print 'output:\n ' + e.output.replace('\n', '\n ').strip()
    171         else:
    172             err('CalledProcessError: ' + str(e))
    173         sys.exit(os.EX_IOERR)
    174     except IOError as e:
    175         if args.verbose:
    176             traceback.print_exc()
    177         else:
    178             err('IOError: ' + str(e))
    179         sys.exit(os.EX_IOERR)