summaryrefslogtreecommitdiff
path: root/contrib/revelation2pass.py
diff options
context:
space:
mode:
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)
nohover-highlight'> Signed-off-by: Lars Hjemli <hjemli@gmail.com> 2006-12-10Add caching infrastructureLars Hjemli9-28/+353 This enables internal caching of page output. Page requests are split into four groups: 1) repo listing (front page) 2) repo summary 3) repo pages w/symbolic references in query string 4) repo pages w/constant sha1's in query string Each group has a TTL specified in minutes. When a page is requested, a cached filename is stat(2)'ed and st_mtime is compared to time(2). If TTL has expired (or the file didn't exist), the cached file is regenerated. When generating a cached file, locking is used to avoid parallell processing of the request. If multiple processes tries to aquire the same lock, the ones who fail to get the lock serves the (expired) cached file. If the cached file don't exist, the process instead calls sched_yield(2) before restarting the request processing. Signed-off-by: Lars Hjemli <hjemli@gmail.com>