0,0 → 1,191 |
#!/usr/bin/env python |
# -*- coding: UTF-8 -*- |
|
import getopt |
import libxml2 |
import os |
import sys |
import zlib |
from Crypto.Cipher import AES |
|
""" |
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 |
""" |
|
__author__ = 'Toni Corvera' |
__date__ = '$Date$' |
__revision__ = '$Rev$' |
__version_info__ = ( 0, 2, 0 ) |
__version__ = '.'.join(map(str, __version_info__)) |
RELEASE=True |
|
def usage(channel): |
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(' -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 search(xmldata, search, caseInsensitive=True): |
doc = libxml2.parseDoc(xmldata) |
ctxt = doc.xpathNewContext() |
commonXPath = '//revelationdata//entry[@type!="folder"]//text()' |
caseSensitiveXPath = '[contains(., "%s")]/../..' % search |
caseInsensitiveXPath = '[contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "%s")]/../..' % search |
if caseInsensitive: |
xpath = commonXPath + caseInsensitiveXPath |
else: |
xpath = commonXPath + caseSensitiveXPath |
res = ctxt.xpathEval(xpath) |
print '-> Search "%s": ' % search, |
if not len(res): |
print 'No results' |
sys.exit(80) |
print '%d matches' % len(res) |
tagnames ={ 'generic-url': 'Url:', |
'generic-username': 'Username:', |
'generic-password': 'Password:', |
'generic-email': 'Email:', |
'generic-hostname': 'Hostname:', |
} |
for x in res: |
print '- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - ' |
print '' |
for attr in x.properties: # Is it accessible directly? |
if attr.name == 'type': |
print 'Type:',attr.children |
for chld in x.children: |
n = chld.name |
val = chld.content |
if n == 'name': |
print 'Name:',val |
elif n == 'description': |
print 'Description:',val |
elif n == 'field': |
for attr in chld.properties: |
if attr.name == 'id': |
idv = attr.content |
if idv in tagnames: |
idv = tagnames[idv] |
print idv,chld.content |
print '' |
# / for chld in x.children |
nr = len(res) |
plural = '' |
if nr > 1: |
plural = 's' |
sys.stderr.write('<- (end of %d result%s for "%s")\n\n' % ( nr, plural, search )) |
doc.freeDoc() |
ctxt.xpathFreeContext() |
|
def main(): |
datafile = None |
password = None |
searches = [] |
caseInsensitive = True |
|
sys.stderr.write('Relevation v%s, (c) 2011 Toni Corvera\n\n' % __version__) |
try: |
ops, args = getopt.getopt(sys.argv[1:], 'f:p:s:0ciah', |
[ 'file=', 'password=', 'search=', 'stdin', |
'case-sensitive', 'case-insensitive', 'ask', |
'help', 'version' ]) |
except getopt.GetoptError, err: |
print str(err) |
usage(sys.stderr) |
sys.exit(os.EX_USAGE) |
if args: |
searches = 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 |
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' ): |
sys.stderr.write('File password: ') |
password = sys.stdin.readline() |
password = password[:-1] |
elif opt in ( '-s', '--search' ): |
searches.append(arg) |
elif opt in ( '-i', '--case-insensitive' ): |
caseInsensitive = True |
elif opt in ( '-c', '--case-sensitive' ): |
caseInsensitive = False |
else: |
sys.stderr.write('Unhandled option: %s\n' % opt) |
assert False, "internal error parsing options" |
if not datafile or not password: |
usage(sys.stderr) |
if not datafile: |
sys.stderr.write('Input password filename is required\n') |
if not password: |
sys.stderr.write('Password is required\n') |
sys.exit(os.EX_USAGE) |
# Encrypted data |
try: |
f = open(datafile, "rb") |
data = f.read() |
finally: |
f.close() |
# Pad password |
password += (chr(0) * (32 - len(password))) |
# Data IV |
c = AES.new(password) |
iv = c.decrypt(data[12:28]) |
# Decrypt. Decrypted data is compressed |
c = AES.new(password, AES.MODE_CBC, iv) |
cleardata_gz = c.decrypt(data[28:]) |
# 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) |
if not searches: |
print xmldata |
sys.exit(os.EX_OK) |
for s in searches: |
search(xmldata, s, caseInsensitive) |
|
if __name__ == '__main__': |
try: |
main() |
except libxml2.parserError as e: |
sys.stderr.write('XML parsing error\n') |
if not RELEASE: |
import traceback |
traceback.print_exc() |
sys.exit(os.EX_DATAERR) |
|
# 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 |