12,10 → 12,11 |
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 |
(ref4) http://pymotw.com/2/getpass/ |
""" |
# Relevation Password Printer |
# |
# Copyright (c) 2011, Toni Corvera |
# Copyright (c) 2011,2012,2013 Toni Corvera |
# All rights reserved. |
# |
# Redistribution and use in source and binary forms, with or without |
41,9 → 42,11 |
|
import ConfigParser |
import getopt |
import getpass |
from lxml import etree |
import os |
import stat |
import string |
import sys |
import zlib |
# Help py2exe in packaging lxml |
61,13 → 64,13 |
from crypto.cipher import rijndael, cbc |
from crypto.cipher.base import noPadding |
except ImportError: |
sys.stderr.write('Either PyCrypto or cryptopy are required\n') |
sys.stderr.write('Either PyCrypto or cryptopy is required\n') |
raise |
|
__author__ = 'Toni Corvera' |
__date__ = '$Date$' |
__revision__ = '$Rev$' |
__version_info__ = ( 1, 1 ) #, 0 ) |
__version_info__ = ( 1, 2 ) #, 0 ) |
__version__ = '.'.join(map(str, __version_info__)) |
RELEASE=True |
|
98,15 → 101,13 |
'generic-pin': 'PIN', |
'generic-port': 'Port' |
} |
MODE_AND='and' |
MODE_OR='or' |
|
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): |
113,6 → 114,8 |
channel.write(s) |
p('%s {-f passwordfile} {-p password | -0} [search] [search2] [...]\n' % sys.argv[0]) |
p('\nOptions:\n') |
# Reference: 80 characters |
# ------------------------------------------------------------------------------- |
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') |
124,6 → 127,10 |
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(' -A, --and When multiple search terms are used, use an AND\n') |
p(' operator to combine them.\n') |
p(' -O, --or When multiple search terms are used, use an OR\n') |
p(' operator to combine them.\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') |
133,6 → 140,11 |
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 |
or |
make_xpath_query(list, str, bool, bool) -> str |
|
Passing a list as the second argument implies combining its elements |
in the search (AND) |
''' |
xpath = '/revelationdata//entry' |
if type_filter: |
140,12 → 152,29 |
if negate_filter: |
sign = '!=' |
xpath = '%s[@type%s"%s"]' % ( xpath, sign, type_filter ) |
if type_filter != 'folder': |
# Avoid printing folders since all their children are printed |
# alongside |
xpath += '[@type!="folder"]' |
if search_text: |
xpath = xpath + '//text()' |
if ignore_case: |
xpath = '%s[contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "%s")]/../..' % ( xpath, search_text ) |
#xpath = xpath + '//text()' |
needles = [] |
if type(search_text) == str: |
needles = [ search_text, ] |
else: |
xpath = '%s[contains(., "%s")]/../..' % ( xpath, search_text ) |
needles = search_text |
selector = '' |
for search in needles: |
if ignore_case: |
# must pass lowercase to actually be case insensitive |
search = string.lower(search) |
# XPath 2.0 has lower-case, upper-case, matches(..., -i) etc. |
selector += '//text()[contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "%s")]/../..' % search |
else: |
selector += '//text()[contains(., "%s")]/../..' % search |
xpath = '%s%s' % ( xpath, selector ) |
if not RELEASE: |
printe("> Xpath: %s\n" % xpath) |
return xpath |
|
def dump_all_entries(xmldata): |
155,7 → 184,11 |
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 ' |
''' Dump entries from xmldata that match criteria |
dump_entries(str, str, str, bool, bool) -> int |
or |
dump_entries(str, list, str, bool, bool) -> int |
''' |
tree = etree.fromstring(xmldata) |
xpath = make_xpath_query(search_text, type_filter, ignore_case, negate_filter) |
try: |
178,10 → 211,19 |
nr = dump_result(res, query_desc) |
return nr |
|
def print_wrapper(s): |
def dump_single_result(typeName, name, descr, notes, fields): |
printe('-------------------------------------------------------------------------------') |
s = '\n' |
s += 'Type: %s\n' % typeName |
s += 'Name: %s\n' % name |
s += 'Description: %s\n' % descr |
s += 'Notes: %s\n' % notes |
for field in fields: |
s += '%s %s\n' % field # field, value |
#s += '\n' |
print s |
|
def dump_result(res, query_desc, printfn=print_wrapper): |
def dump_result(res, query_desc, dumpfn=dump_single_result): |
''' Print query results. |
dump_result(list of entries, query description) -> int |
''' |
191,23 → 233,32 |
return False |
print '%d matches' % len(res) |
for x in res: |
printe('-------------------------------------------------------------------------------') |
s = '\n' |
s += 'Type: %s\n' % x.get('type') |
typeName = x.get('type') |
name = None |
descr = None |
fields = [] |
notes = None |
for chld in x.getchildren(): |
n = chld.tag |
val = chld.text |
if val is None: |
val = '' |
if n == 'name': |
s += 'Name: %s\n' % val |
name = val |
elif n == 'description': |
s += 'Description: %s\n' % val |
descr = 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) |
val = chld.text |
if val is None: |
val = '' |
# Maintain order => list |
fields += [ ( idv, val ), ] |
elif n == 'notes': |
notes = val |
dumpfn(typeName, name, descr, notes, fields) |
# / for chld in x.children |
nr = len(res) |
plural = '' |
232,6 → 283,7 |
cfg = os.path.join(os.path.expanduser('~'), '.relevation.conf') |
pw = None |
fl = None |
mode = MODE_OR |
if os.path.isfile(cfg): |
if os.access(cfg, os.R_OK): |
wr = world_readable(cfg) |
246,9 → 298,14 |
if wr: # TODO: how to check in windows? |
printe('Your password can be read by anyone!!!') |
pw = parser.get('relevation', 'password') |
if 'mode' in ops: |
mode = parser.get('relevation', 'mode') |
if mode not in [ MODE_AND, MODE_OR ]: |
printe('Warning: Unknown mode \'%s\' set in configuration' % mode) |
mode=MODE_OR |
else: # exists but not readable |
printe('Configuration file (~/.relevation.conf) is not readable!') |
return ( fl, pw ) |
return ( fl, pw, mode ) |
|
def decrypt_gz(key, cipher_text): |
''' Decrypt cipher_text using key. |
282,17 → 339,19 |
# individual search: ( 'value to search', 'type of search', 'type of entry to filter' ) |
searchTypes = [] |
dump_xml = False |
mode = None |
|
printe('Relevation v%s, (c) 2011 Toni Corvera\n' % __version__) |
printe('Relevation v%s, (c) 2011-2013 Toni Corvera\n' % __version__) |
|
# ---------- OPTIONS ---------- # |
( datafile, password ) = load_config() |
( datafile, password, mode ) = load_config() |
try: |
# gnu_getopt requires py >= 2.3 |
ops, args = getopt.gnu_getopt(argv, 'f:p:s:0ciaht:x', |
ops, args = getopt.gnu_getopt(argv, 'f:p:s:0ciaht:xAO', |
[ 'file=', 'password=', 'search=', 'stdin', |
'case-sensitive', 'case-insensitive', 'ask', |
'help', 'version', 'type=', 'xml' ]) |
'help', 'version', 'type=', 'xml', |
'and', 'or' ]) |
except getopt.GetoptError, err: |
print str(err) |
usage(sys.stderr) |
323,10 → 382,16 |
elif opt in ( '-p', '--password' ): |
password = arg |
elif opt in ( '-a', '--ask', '-0', '--stdin' ): |
prompt = '' |
if opt in ( '-a', '--ask' ): |
printen('File password: ') |
password = sys.stdin.readline() |
password = password[:-1] |
prompt = 'File password: ' |
# see [ref4] |
if sys.stdin.isatty(): |
password = getpass.getpass(prompt=prompt, stream=sys.stderr) |
else: |
# Not a terminal, getpass won't work |
password = sys.stdin.readline(); |
password = password[:-1] # XXX: would .rstrip() be safe enough? |
elif opt in ( '-s', '--search' ): |
needles.append(arg) |
elif opt in ( '-i', '--case-insensitive' ): |
345,6 → 410,10 |
searchTypes.append( ( iarg, neg ) ) |
elif opt in ( '-x', '--xml' ): |
dump_xml = True |
elif opt in ( '-A', '--and' ): |
mode = MODE_AND |
elif opt in ( '-O', '--or' ): |
mode = MODE_OR |
else: |
printe('Unhandled option: %s' % opt) |
assert False, "internal error parsing options" |
386,13 → 455,23 |
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: |
if mode == MODE_OR: |
for text in needles: |
numhits += dump_entries(xmldata, text, 'folder', caseInsensitive, True) |
else: |
assert mode == MODE_AND, "Unknown boolean operation mode" |
numhits += dump_entries(xmldata, needles, 'folder', caseInsensitive, True) |
elif needles: |
if mode == MODE_OR: # 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 combined search, filter for each type |
assert mode == MODE_AND, "Unknown boolean operation mode" |
for ( sfilter, negate ) in searchTypes: |
numhits += dump_entries(xmldata, text, sfilter, caseInsensitive, |
negate_filter=negate) |
numhits += dump_entries(xmldata, needles, 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) |