0,0 → 1,419 |
#!/usr/bin/env python |
# -*- coding: UTF-8 -*- |
|
""" |
Relevation Password Printer |
a command line interface to Revelation Password Manager. |
|
Code based on Revelation's former BTS (no longer online, not archived?): |
(ref1) code: |
http://oss.wired-networks.net/bugzilla/attachment.cgi?id=13&action=view |
(ref2) bug report: |
http://oss.wired-networks.net/bugzilla/show_bug.cgi?id=111 |
-> http://web.archive.org/http://oss.wired-networks.net/bugzilla/show_bug.cgi?id=111 |
(ref3) http://docs.python.org/library/zlib.html |
""" |
# Relevation Password Printer |
# |
# Copyright (c) 2011, Toni Corvera |
# All rights reserved. |
# |
# 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 ConfigParser |
import getopt |
from lxml import etree |
import os |
import stat |
import sys |
import zlib |
# Help py2exe in packaging lxml |
# <http://www.py2exe.org/index.cgi/WorkingWithVariousPackagesAndModules> |
import lxml._elementpath as _dummy |
import gzip # py2exe again |
|
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 |
|
__author__ = 'Toni Corvera' |
__date__ = '$Date$' |
__revision__ = '$Rev$' |
__version_info__ = ( 1, 1 ) #, 0 ) |
__version__ = '.'.join(map(str, __version_info__)) |
RELEASE=True |
|
# These are pseudo-standardized exit codes, in Linux (*NIX?) they are defined |
#+in the header </usr/include/sysexits.h> and available as properties of 'os' |
#+In windows they aren't defined at all |
|
if 'EX_OK' not in dir(os): |
# If not defined set them manually |
codes = { 'EX_OK': 0, 'EX_USAGE': 64, 'EX_DATAERR': 65, |
'EX_NOINPUT': 66, 'EX_SOFTWARE': 70, 'EX_IOERR': 74, |
} |
for (k,v) in codes.items(): |
setattr(os, k, v) |
del codes, k, v |
|
TAGNAMES ={ 'generic-url': 'Url:', |
'generic-username': 'Username:', |
'generic-password': 'Password:', |
'generic-email': 'Email:', |
'generic-hostname': 'Hostname:', |
'generic-location': 'Location:', |
'generic-code': 'Code:', |
'generic-certificate': 'Certificate:', |
'generic-database': 'Database:', |
'generic-domain': 'Domain:', |
'generic-keyfile': 'Key file:', |
'generic-pin': 'PIN', |
'generic-port': 'Port' |
} |
|
def printe(s): |
' Print to stderr ' |
sys.stderr.write(s+'\n') |
|
def printen(s): |
' Print to stderr without added newline ' |
sys.stderr.write(s) |
|
def usage(channel): |
' Print help message ' |
def p(s): |
channel.write(s) |
p('%s {-f passwordfile} {-p password | -0} [search] [search2] [...]\n' % sys.argv[0]) |
p('\nOptions:\n') |
p(' -f FILE, --file=FILE Revelation password file.\n') |
p(' -p PASS, --password=PASS Master password.\n') |
p(' -s SEARCH, --search=SEARCH Search for string.\n') |
p(' -i, --case-insensitive Case insensitive search (default).\n') |
p(' -c, --case-sensitive Case sensitive search.\n') |
p(' -a, --ask Interactively ask for password.\n') |
p(' Note it will be displayed in clear as you\n') |
p(' type it.\n') |
p(' -t TYPE, --type=TYPE Print only entries of type TYPE.\n') |
p(' With no search string, prints all entries of\n') |
p(' type TYPE.\n') |
p(' -x, --xml Dump unencrypted XML document.\n') |
p(' -0, --stdin Read password from standard input.\n') |
p(' -h, --help Print help (this message).\n') |
p(' --version Print the program\'s version information.\n') |
p('\n') |
|
def make_xpath_query(search_text=None, type_filter=None, ignore_case=True, negate_filter=False): |
''' Construct the actual XPath expression |
make_xpath_query(str, str, bool, bool) -> str |
''' |
xpath = '/revelationdata//entry' |
if type_filter: |
sign = '=' |
if negate_filter: |
sign = '!=' |
xpath = '%s[@type%s"%s"]' % ( xpath, sign, type_filter ) |
if search_text: |
xpath = xpath + '//text()' |
if ignore_case: |
xpath = '%s[contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "%s")]/../..' % ( xpath, search_text ) |
else: |
xpath = '%s[contains(., "%s")]/../..' % ( xpath, search_text ) |
return xpath |
|
def dump_all_entries(xmldata): |
' Dump all entries from xmldata, with no filter at all ' |
tree = etree.fromstring(xmldata) |
res = tree.xpath('//entry') |
return dump_result(res, 'all') |
|
def dump_entries(xmldata, search_text=None, type_filter=None, ignore_case=True, negate_filter=False): |
' Dump entries from xmldata that match criteria ' |
tree = etree.fromstring(xmldata) |
xpath = make_xpath_query(search_text, type_filter, ignore_case, negate_filter) |
try: |
res = tree.xpath(xpath) |
except etree.XPathEvalError: |
if not RELEASE: |
printe('Failed with xpath expression: %s' % xpath) |
raise |
query_desc = '' |
if search_text: |
query_desc = '"%s"' % search_text |
if type_filter: |
neg = '' |
if negate_filter: |
neg = 'not ' |
if search_text: |
query_desc = '%s (\'%s%s\' entries)' % ( query_desc, neg, type_filter ) |
else: |
query_desc = '%s%s entries' % ( neg, type_filter ) |
nr = dump_result(res, query_desc) |
return nr |
|
def print_wrapper(s): |
print s |
|
def dump_result(res, query_desc, printfn=print_wrapper): |
''' Print query results. |
dump_result(list of entries, query description) -> int |
''' |
print '-> Search %s: ' % query_desc, |
if not len(res): |
print 'No results' |
return False |
print '%d matches' % len(res) |
for x in res: |
printe('-------------------------------------------------------------------------------') |
s = '\n' |
s += 'Type: %s\n' % x.get('type') |
for chld in x.getchildren(): |
n = chld.tag |
val = chld.text |
if n == 'name': |
s += 'Name: %s\n' % val |
elif n == 'description': |
s += 'Description: %s\n' % val |
elif n == 'field': |
idv = chld.get('id') |
if idv in TAGNAMES: |
idv = TAGNAMES[idv] |
s += '%s %s\n' % ( idv, chld.text ) |
#s += '\n' |
printfn(s) |
# / for chld in x.children |
nr = len(res) |
plural = '' |
if nr > 1: |
plural = 's' |
printe('-------------------------------------------------------------------------------') |
printe('<- (end of %d result%s for {%s})\n' % ( nr, plural, query_desc )) |
return nr |
|
def world_readable(path): |
' Check if a file is readable by everyone ' |
assert os.path.exists(path) |
if sys.platform == 'win32': |
return True |
st = os.stat(path) |
return bool(st.st_mode & stat.S_IROTH) |
|
def load_config(): |
''' Load configuration file is one is found |
load_config() -> ( str file, str pass ) |
''' |
cfg = os.path.join(os.path.expanduser('~'), '.relevation.conf') |
pw = None |
fl = None |
if os.path.isfile(cfg): |
if os.access(cfg, os.R_OK): |
wr = world_readable(cfg) |
if wr and sys.platform != 'win32': |
printe('Configuration (~/.relevation.conf) is world-readable!!!') |
parser = ConfigParser.ConfigParser() |
parser.read(cfg) |
ops = parser.options('relevation') |
if 'file' in ops: |
fl = os.path.expanduser(parser.get('relevation', 'file')) |
if 'password' in ops: |
if wr: # TODO: how to check in windows? |
printe('Your password can be read by anyone!!!') |
pw = parser.get('relevation', 'password') |
else: # exists but not readable |
printe('Configuration file (~/.relevation.conf) is not readable!') |
return ( fl, pw ) |
|
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(argv): |
datafile = None |
password = None |
# values to search for |
needles = [] |
caseInsensitive = True |
# individual search: ( 'value to search', 'type of search', 'type of entry to filter' ) |
searchTypes = [] |
dump_xml = False |
|
printe('Relevation v%s, (c) 2011 Toni Corvera\n' % __version__) |
|
# ---------- OPTIONS ---------- # |
( datafile, password ) = load_config() |
try: |
# gnu_getopt requires py >= 2.3 |
ops, args = getopt.gnu_getopt(argv, 'f:p:s:0ciaht:x', |
[ 'file=', 'password=', 'search=', 'stdin', |
'case-sensitive', 'case-insensitive', 'ask', |
'help', 'version', 'type=', 'xml' ]) |
except getopt.GetoptError, err: |
print str(err) |
usage(sys.stderr) |
sys.exit(os.EX_USAGE) |
if args: |
needles = args |
|
if ( '-h', '' ) in ops or ( '--help', '' ) in ops: |
usage(sys.stdout) |
sys.exit(os.EX_OK) |
if ( '--version', '' ) in ops: |
release='' |
if not RELEASE: |
release=' [DEBUG]' |
print 'Relevation version %s%s' % ( __version__, release ) |
print 'Python version %s' % sys.version |
if USE_PYCRYPTO: |
import Crypto |
print 'PyCrypto version %s' % Crypto.__version__ |
else: |
# AFAIK cryptopy doesn't export version info |
print 'cryptopy' |
sys.exit(os.EX_OK) |
|
for opt, arg in ops: |
if opt in ( '-f', '--file' ): |
datafile = arg |
elif opt in ( '-p', '--password' ): |
password = arg |
elif opt in ( '-a', '--ask', '-0', '--stdin' ): |
if opt in ( '-a', '--ask' ): |
printen('File password: ') |
password = sys.stdin.readline() |
password = password[:-1] |
elif opt in ( '-s', '--search' ): |
needles.append(arg) |
elif opt in ( '-i', '--case-insensitive' ): |
caseInsensitive = True |
elif opt in ( '-c', '--case-sensitive' ): |
caseInsensitive = False |
elif opt in ( '-t', '--type' ): |
iarg = arg.lower() |
neg = False |
if iarg.startswith('-'): |
iarg = iarg[1:] |
neg = True |
if not iarg in ( 'creditcard', 'cryptokey', 'database', 'door', 'email', |
'folder', 'ftp', 'generic', 'phone', 'shell', 'website' ): |
printe('Warning: Type "%s" is not known by relevation.' % arg) |
searchTypes.append( ( iarg, neg ) ) |
elif opt in ( '-x', '--xml' ): |
dump_xml = True |
else: |
printe('Unhandled option: %s' % opt) |
assert False, "internal error parsing options" |
if not datafile or not password: |
usage(sys.stderr) |
if not datafile: |
printe('Input password filename is required') |
if not password: |
printe('Password is required') |
sys.exit(os.EX_USAGE) |
|
# ---------- PASSWORDS FILE DECRYPTION AND DECOMPRESSION ---------- # |
f = None |
try: |
if not os.access(datafile, os.R_OK): |
raise IOError('File \'%s\' not accessible' % datafile) |
f = open(datafile, "rb") |
# Encrypted data |
data = f.read() |
finally: |
if f: |
f.close() |
# 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) |
|
# ---------- QUERIES ---------- # |
if dump_xml: |
print xmldata |
sys.exit(os.EX_OK) |
# Multiply values to search by type of searches |
numhits = 0 |
|
if not ( needles or searchTypes ): # No search nor filters, print all |
numhits = dump_all_entries(xmldata) |
elif not searchTypes: # Simple case, all searches are text searches |
for text in needles: |
numhits += dump_entries(xmldata, text, 'folder', caseInsensitive, True) |
elif needles: # Do a search filtered for each type |
for text in needles: |
for ( sfilter, negate ) in searchTypes: |
numhits += dump_entries(xmldata, text, sfilter, caseInsensitive, |
negate_filter=negate) |
else: # Do a search only of types |
for ( sfilter, negate ) in searchTypes: |
numhits += dump_entries(xmldata, None, sfilter, negate_filter=negate) |
if numhits == 0: |
sys.exit(80) |
|
if __name__ == '__main__': |
try: |
main(sys.argv[1:]) |
except zlib.error: |
printe('Failed to decompress decrypted data. Wrong password?') |
sys.exit(os.EX_DATAERR) |
except etree.XMLSyntaxError as e: |
printe('XML parsing error') |
if not RELEASE: |
traceback.print_exc() |
sys.exit(os.EX_DATAERR) |
except IOError as e: |
if not RELEASE: |
traceback.print_exc() |
printe(str(e)) |
sys.exit(os.EX_IOERR) |
|
# vim:set ts=4 et ai fileencoding=utf-8: # |
Property changes: |
Added: svn:executable |
Added: svn:keywords |
+Rev Id Date |
\ No newline at end of property |