Subversion Repositories pub

Compare Revisions

No changes between revisions

Ignore whitespace Rev 695 → Rev 696

/relevation/trunk/relevation.spec.in
File deleted
Property changes:
Deleted: svn:keywords
-Rev Id Date
\ No newline at end of property
/relevation/trunk/CHANGELOG
1,7 → 1,8
$Date$
 
1.3.1 (2020-02-11):
- Port to Python3
- Refactor into a package [#235]
- Include minimalistic GUI into package
 
1.3 (2014-05-24):
- Check file magic [#230] and reject unsupported data formats
/relevation/trunk/GNUmakefile
25,6 → 25,8
ifeq ($(findstring dist-packages,$(PYTHONROOT)),dist-packages)
INSTALL_LAYOUT=--install-layout=deb
endif
# Set to something from the command-line to allow packaging of pre-releases
force:=
 
all: $(PKG).1
@echo
40,6 → 42,8
@echo "# distclean"
@echo "# testman Displays compiled manpage"
@echo "# test_min_python Find min. Python version required"
@echo "# "
@echo "# append force=1 to allow packaging pre-releases"
@echo "########################################################"
@echo
 
109,6 → 113,10
setup.py: devtools/setup.py.in
sed 's/@VERSION@/$(VERSION)/g' < $< > $@
 
egg: setup.py
cat $< | sed -e 's/scripts=/py_modules=["__main__"],scripts=/' \
-e 's/from distutils.core/from setuptools/' | python - bdist_egg
 
$(PKG).1: manpage_source.sgml
docbook-to-man $< > $@
 
120,7 → 128,7
 
is_release:
# Only allowed if RELEASE
bash devtools/relver.bash | grep -q -v 'DEBUG'
test -n "$(force)" || bash devtools/relver.bash | grep -q -v 'DEBUG'
 
package_copy:
@# Make a temporary copy to package
127,7 → 135,7
-mkdir $(PKGVER)
tar c . $(TAR_EXCLUDES) | ( cd $(PKGVER) && tar x )
 
$(PKG).spec: $(PKG).spec.in
$(PKG).spec: devtools/$(PKG).spec.in
test -n "$(VERSION)" # Version (=$(VERSION)) must be defined
@echo "[creating $@]"
@cat "$<" | sed -e 's/@VERSION@/$(VERSION)/g' \
/relevation/trunk/devtools/devenv.bash
0,0 → 1,23
#!/bin/bash
#
# This script sets up environment for development
# $Id$
#
pp="$(realpath "$(dirname "$0")"/../src)"
 
echo "#"
echo "# PYTHONPATH=\"$pp\""
echo "#"
echo "# alias: rel -> src/relevation/__main__.py"
echo "# alias: gui -> src/relevation/gui.py"
echo "#"
 
env PYTHONPATH="$pp" bash --rcfile <(cat <<EOF
[[ -f /etc/bash.bashrc ]] && source /etc/bash.bashrc
[[ -f ~/.bashrc ]] && source ~/.bashrc
PS1="[RLV]\\u@\\h:\\w\\$ "
alias rel='python -m relevation'
alias gui='python -m relevation.gui'
EOF
)
 
Property changes:
Added: svn:executable
+*
\ No newline at end of property
Added: svn:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/devtools/relevation.spec.in
0,0 → 1,86
# $Id$
# See http://fedoraproject.org/wiki/Packaging:Python
# Downstream specfile can be found at
# http://pkgs.fedoraproject.org/cgit/relevation.git/
 
%if 0%{?rhel} && 0%{?rhel} <= 6
%{!?__python2: %global __python2 /usr/bin/python2}
%{!?python2_sitelib: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
%{!?python2_sitearch: %global python2_sitearch %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")}
%endif
 
Summary: Command-line search for Revelation Password Manager files
Name: relevation
Version: @VERSION@
Release: 1.pon%{?dist}
License: BSD
Packager: @PACKAGER@
Group: Applications/Text
URL: http://p.outlyer.net/relevation/
Source: http://p.outlyer.net/relevation/files/relevation-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
BuildArch: noarch
BuildRequires: python3-devel, make
Requires: python3-libxml2
Requires: python3-crypto
Requires: python3-lxml
 
%description
Relevation is a tool to retrieve passwords stored in a password file in the
format used by Revelation, from the command-line instead of through a GUI.
 
%prep
%setup -q
 
%build
 
%install
rm -rf %{buildroot}
make install prefix=%{_prefix} DESTDIR=%{buildroot} INSTALL_LAYOUT=
# We include the extra tools as %%doc, remove from here
rm -rf %{buildroot}%{_prefix}/share/doc/relevation/extra/
 
%clean
rm -rf %{buildroot}
 
%files
%defattr(-,root,root,-)
%doc CHANGELOG LICENSE
%doc devtools/*.py
%{_bindir}/relevation
%{_mandir}/man1/relevation.1*
%{python2_sitelib}/relevation/
%{python2_sitelib}/relevation-*.egg-info
 
%changelog
* Tue 11 Feb 2020 Toni Corvera <outlyer@gmail.com> 1.3.1-1.pon
- Updated to use Python 3
 
* Fri May 23 2014 Toni Corvera <outlyer@gmail.com> 1.3-1.pon
- Handle installation of new module
 
* Wed Oct 30 2013 Toni Corvera <outlyer@gmail.com> 1.2.1-1.pon
- Integrated spec from Fedora into upstream source
 
* Sun Aug 04 2013 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.1-6
- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild
 
* Thu Feb 14 2013 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.1-5
- Rebuilt for https://fedoraproject.org/wiki/Fedora_19_Mass_Rebuild
 
* Sat Jul 21 2012 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.1-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild
 
* Thu Mar 29 2012 Matthias Saou <matthias@saou.eu> 1.1-3
- Add missing python-lxml requirement (#807335).
 
* Mon Mar 26 2012 Matthias Saou <matthias@saou.eu> 1.1-2
- Fix URL vs. email in spec changelog.
 
* Mon Jul 18 2011 Matthias Saou <matthias@saou.eu> 1.1-1
- Update to 1.1.
- New extra tools, remove the "make install"ed ones, include them as %%doc.
 
* Mon Jul 4 2011 Matthias Saou <matthias@saou.eu> 1.0-1
- Initial RPM release.
 
Property changes:
Added: svn:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/devtools/relver.bash
5,7 → 5,7
# This scripts avoid having the same runtime requirements for package
# creation
 
r="$(dirname "$0")/../src/relevation.py"
r="$(dirname "$0")/../src/relevation/__init__.py"
 
rel=$(grep '^RELEASE' $r | tail -1)
verinfo=$(grep '^__version_info__' $r | head -1)
/relevation/trunk/devtools/setup.py.in
17,7 → 17,9
def run(self):
distutils.command.install_scripts.install_scripts.run(self)
for script in self.get_outputs():
if script.endswith(".py"):
if script.endswith('.bash'):
shutil.move(script, script[:-5])
elif script.endswith('.py'):
shutil.move(script, script[:-3])
 
setup(name='relevation',
33,12 → 35,12
license='BSD',
package_dir={'': 'src'},
packages=['relevation'],
scripts=['src/relevation.py'],
scripts=['src/relevation.bash'],
cmdclass = {"install_scripts": my_install}, # hook
data_files=[
('share/man/man1', ['relevation.1']),
#('share/doc/relevation', ['CHANGELOG']),
('share/doc/relevation/extra', ['src/gui.py','devtools/checkpw.py','devtools/genpw.py']),
('share/doc/relevation/extra', ['devtools/checkpw.py','devtools/genpw.py']),
]
)
 
/relevation/trunk/src/gui.py
File deleted
Property changes:
Deleted: svn:executable
-*
\ No newline at end of property
Deleted: svn:keywords
-Rev Id Date
\ No newline at end of property
/relevation/trunk/src/relevation.py
File deleted
Property changes:
Deleted: svn:executable
Deleted: svn:keywords
-Rev Id Date
\ No newline at end of property
/relevation/trunk/src/relevation/__init__.py
0,0 → 1,46
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
Relevation Password Printer
a command line interface to Revelation Password Manager.
 
$Id$
"""
# Relevation Password Printer
#
# Copyright (c) 2011,2012,2013,2014 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.
 
__author__ = 'Toni Corvera'
__date__ = '$Date$'
__revision__ = '$Rev$'
 
RELEASE=not True
__version_info__ = ( 1, 3, 1 ) # Note: For x.y.0, only x and y are kept
if not RELEASE:
import traceback
__version_info__ += ( '0-pre1', )
__version__ = '.'.join(map(str, __version_info__))
 
# vim:set ts=4 et ai fileencoding=utf-8: #
Property changes:
Added: svn:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/src/relevation/__main__.py
0,0 → 1,700
#!/usr/bin/env python3
# -*- 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
(ref4) http://pymotw.com/2/getpass/
 
$Id$
'''
# Relevation Password Printer
#
# Copyright (c) 2011,2012,2013,2014,2020 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.
 
__date__ = '$Date$'
__revision__ = '$Rev$'
 
import configparser
import getopt
import getpass
from lxml import etree
import locale
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
import hashlib # required by newer format
# PBKDF2 stolen from Revelation
from relevation import PBKDF2, __version__, __author__, RELEASE
 
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 is required\n')
raise
 
RELEASE=True
__author__ = 'Toni Corvera'
__date__ = '$Date$'
__revision__ = '$Rev$'
__version_info__ = ( 1, 3, 1 ) #, 0 ) # Note: For x.y.0, only x and y are kept
if not RELEASE:
import traceback
__version_info__ += ( '0-pre1', )
__version__ = '.'.join(map(str, __version_info__))
 
# 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 list(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'
}
MODE_AND='and'
MODE_OR='or'
 
# Errors
class RlvError(Exception):
def __str__(self):
return self.msg
class DecryptError(RlvError):
exitCode = os.EX_DATAERR
def __init__(self, msg = 'Failed to decrypt data. Wrong password?'):
self.msg = msg
class DecompressError(RlvError):
exitCode = os.EX_DATAERR
def __init__(self, msg = 'Failed to decompress data.'):
self.msg = msg
class DataFormatError(RlvError):
exitCode = os.EX_DATAERR
def __init__(self, msg = 'Incorrect data format'):
self.msg = msg
class DataVersionError(RlvError):
exitCode = os.EX_DATAERR
def __init__(self, msg = 'Data format version not supported'):
self.msg = msg
 
def printe(s):
' Print to stderr '
sys.stderr.write(s+'\n')
 
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')
# 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')
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(' -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')
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
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:
sign = '='
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()'
needles = []
if type(search_text) == list:
needles = search_text
else:
# FIXME: Used for OR's
assert type(search_text) == str or type(search_text) == str
needles = [ search_text, ]
selector = ''
for search in needles:
if ignore_case:
# must pass lowercase to actually be case insensitive
search = search.lower()
# 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):
' 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
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:
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 dump_single_result(typeName, name, descr, notes, fields):
''' dump_single_result(str, unicode, unicode, list) -> None '''
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
try:
# sys.stdout.encoding will be None if piped
#print(s.encode(sys.stdout.encoding or locale.getpreferredencoding()))
print(s)
except UnicodeEncodeError:
# XXX: In Python 2 there were unicode conversions, no longer needed in Python 3
# TODO: This branch shouldn't be entered anymore, but needs testing
# E.g. console in ASCII ($ LC_ALL=C relevation)
# TODO: Flag for notification
#printe("WARNING: The console doesn't have a compatible encoding, falling back to UTF-8")
print(s.encode('utf8'))
 
def dump_result(res, query_desc, dumpfn=dump_single_result):
''' Print query results.
dump_result(list of entries, query description) -> int
'''
def u8(s):
''' Return a unicode version of s, whether it's already a unicode
string or a str encoded in UTF-8
'''
# Note the XML is in UTF-8, and that the extracted fields will be
# either type
if type(s) == str:
return s
return str(s.decode('utf8'))
 
print('-> Search %s: ' % query_desc, end=' ')
if not len(res):
print('No results')
return False
print('%d matches' % len(res))
for x in res:
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 = ''
val = u8(val)
if n == 'name':
name = val
elif n == 'description':
descr = val
elif n == 'field':
idv = chld.get('id')
if idv in TAGNAMES:
idv = TAGNAMES[idv]
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 = ''
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 if one is found
load_config() -> ( str file, str pass )
'''
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)
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')
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, mode )
 
class _DataReaderBase(object):
' Common methods for reading data files '
def validate_compressed_padding(self, data):
''' Checks that the gzip-compressed 'data' is padded correctly.
validate_compressed_padding(str) -> bool
'''
padlen = data[-1] # XXX: While on Python 2 we were using ord(data[-1]), ord(i), etc.
for i in data[-padlen:]:
if i != padlen:
return False
return True
def validate_cipher_length(self, data):
''' Checks that encrypted 'data' has an appropriate length.
validate_cipher_length(str) -> bool
Encrypted data length must be a multiple of 16
'''
return ( len(data) % 16 == 0 )
def _aes_decrypt_ecb(self, key, data):
''' Decrypt AES cipher text in ECB mode
_aes_decrypt_ecb(str, str) -> str
This function will use the underlying, available, cipher module.
'''
if USE_PYCRYPTO:
c = AES.new(key)
cleardata = c.decrypt(data)
else:
c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
cleardata = c.decrypt(data)
return cleardata
def _aes_decrypt_cbc(self, key, iv, data):
''' Decrypt AES cipher text in CBC mode
_aes_decrypt_ecb(str, str, str) -> str
This function will use the underlying, available, cipher module.
'''
if USE_PYCRYPTO:
c = AES.new(key, AES.MODE_CBC, iv)
cleardata = c.decrypt(data)
else:
bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
c = cbc.CBC(bc, padding=noPadding())
cleardata = c.decrypt(data, iv=iv)
return cleardata
def get_xml(self, data, password):
''' Extract the XML contents from the encrypted and compressed input.
get_xml(str, str) -> str
'''
pass
 
class DataReaderV1(_DataReaderBase):
''' Data reading for Revelation data files in the original format.
Old format header:
[0:12) 12B header: "rvl" 0x00, 0x01, 0x00
[12:28) 16B ECB encrypted IV (for CBC-encrypted data)
[28:] CBC encrypted data
'''
def _decrypt_compressed_data(self, password, cipher_text):
''' Decrypt cipher_text using password.
_decrypt_compressed_data(str, str) -> cleartext (gzipped xml)
'''
# Minimum length of header
if len(cipher_text) < 28:
raise DataFormatError
# Key <= Padded password
key = password
key += (chr(0) * (32 - len(password)))
# Extract IV
iv = self._aes_decrypt_ecb(key, cipher_text[12:28])
# Skip IV
cipher_text = cipher_text[28:]
# Input strings for decrypt must be a multiple of 16 in length
if not self.validate_cipher_length(cipher_text):
raise DataFormatError
# Decrypt data, CBC mode
return self._aes_decrypt_cbc(key, iv, cipher_text)
 
def get_xml(self, data, password):
# Decrypt. Decrypted data is compressed
cleardata_gz = self._decrypt_compressed_data(password, data)
# Validate padding for decompression
if not self.validate_compressed_padding(cleardata_gz):
raise DataFormatError
# Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
padlen = cleardata_gz[-1]
try:
# Note data is encoded in UTF-8 but not decoded yet (because
# the XML parser is too easy to choke in that case)
# http://lxml.de/parsing.html#python-unicode-strings
return zlib.decompress(cleardata_gz[:-padlen], 15, 2**15)
except zlib.error:
raise DecompressError
 
class DataReaderV2(_DataReaderBase):
''' Data reading for Revelation data files in the new format.
New format header:
[0:12) 12B header: "rvl" 0x00, 0x02, 0x00
[12:20) 8B salt
[20:36) 16B IV (for CBC-encrypted data)
[36:] CBC encrypted data
The encryption key is derived from the password and salt through the
PBKDF2 module.
'''
def _decrypt_compressed_data(self, password, cipher_text):
# Minimum length of header
if len(cipher_text) < 36:
raise DataFormatError
salt = cipher_text[12:20]
iv = cipher_text[20:36]
key = PBKDF2.PBKDF2(password, salt, iterations=12000).read(32)
# Skip encryption header
cipher_text = cipher_text[36:]
if not self.validate_cipher_length(cipher_text):
raise DataFormatError
# Decrypt data (CBC)
decrypted = self._aes_decrypt_cbc(key, iv, cipher_text)
sha256hash = decrypted[0:32]
# Skip hash. decrypted <= Decrypted, Compressed data
decrypted = decrypted[32:]
# Validate hash
if sha256hash != hashlib.sha256(decrypted).digest():
raise DecryptError
return decrypted
 
def get_xml(self, data, password):
# Decrypt...
cleardata_gz = self._decrypt_compressed_data(password, data)
# Validate padding for decompression
if not self.validate_compressed_padding(cleardata_gz):
raise DataFormatError
# Decompress
padlen = cleardata_gz[-1]
try:
return zlib.decompress(cleardata_gz[:-padlen])
except zlib.error:
raise DecompressError
 
class DataReader(object):
''' Interface to read Revelation's data files '''
def __init__(self, filename):
''' Loads file data and checks file format and data version for compatibility
DataReader(str)
 
raises IOError If 'filename' not readable
raises DataFormatError If 'filename' not in Revelation format
raises DataVersionError If filename not in a supported data format version
'''
self._impl = None
self._data = None
self._filename = filename
f = None
try:
if not os.access(filename, os.R_OK):
raise IOError('File \'%s\' not accessible' % filename)
f = open(filename, "rb")
# Encrypted data
self._data = f.read()
finally:
if f:
f.close()
self._check_header()
def _check_header(self):
''' Checks the file header for compatibility.
_check_header() -> None
 
raises DataFormatError If the file isn't a Revelation data file
raises DataVersionError If the data format is in an unsupported version
'''
header = self._data[0:12]
magic = header[0:4]
if magic != b"rvl\x00":
raise DataFormatError
data_version = header[4]
app_version = header[6:9]
if data_version == 0x01: #'\x01':
self._impl = DataReaderV1()
elif data_version == 0x02: #'\x02':
self._impl = DataReaderV2()
else:
raise DataVersionError
def get_xml(self, password):
''' Decrypt and decompress file data
get_xml(str) -> str
'''
return self._impl.get_xml(self._data, password)
 
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
mode = None
 
printe('Relevation v%s, (c) 2011-2020 Toni Corvera\n' % __version__)
 
# ---------- OPTIONS ---------- #
( datafile, password, mode ) = load_config()
try:
# gnu_getopt requires py >= 2.3
ops, args = getopt.gnu_getopt(argv, 'f:p:s:0ciaht:xAO',
[ 'file=', 'password=', 'search=', 'stdin',
'case-sensitive', 'case-insensitive', 'ask',
'help', 'version', 'type=', 'xml',
'and', 'or' ])
except getopt.GetoptError as 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' ):
prompt = ''
if opt in ( '-a', '--ask' ):
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' ):
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
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"
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 ---------- #
xmldata = DataReader(datafile).get_xml(password)
# ---------- 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
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, 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)
if numhits == 0:
sys.exit(80)
 
if __name__ == '__main__':
try:
main(sys.argv[1:])
except RlvError as e:
printe('Error: %s' % e.msg)
if not RELEASE:
traceback.print_exc()
sys.exit(e.exitCode)
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:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/src/relevation/gui.py
0,0 → 1,194
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
 
"""
Relevation Password Printer
a command line interface to Revelation Password Manager.
 
Simplistic Graphical User Interface.
This GUI is mainly intended to be used in systems where command-lines
are less common, like Windows.
 
$Id$
"""
# Relevation Password Printer
#
# Copyright (c) 2011, 2013, 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 sys
import re
import Tkinter as tk
from Tkinter import Frame, Button, Entry, Listbox, Scrollbar, Label, Radiobutton, StringVar
 
import relevation.__main__ as rlv
 
__author__ = 'Toni Corvera'
__date__ = '$Date$'
__revision__ = '$Rev$'
__version_info__ = ( 1, 2, 1 )
__version__ = '.'.join(map(str, __version_info__))
 
old_fn = rlv.dump_result
 
def append_result(typeName, name, descr, notes, fields):
global gui
gui.lst.insert(tk.END, name)
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
gui.items.append(s)
 
def dump_result_override(res, query_desc):
return old_fn(res, query_desc, append_result)
 
rlv.dump_result = dump_result_override
 
class ResultDialog:
def __init__(self, parent, result):
top = self.top = tk.Toplevel(parent)
 
self.value = tk.Text(top)
self.value.insert(tk.END, result)
self.value.config(state=tk.DISABLED)
self.value.pack()
b = Button(top, text='OK', command=self.ok)
b.pack(pady=5)
 
top.bind('<Return>', lambda event: self.ok())
top.focus_set()
 
def ok(self):
self.top.destroy()
 
class GUI(object):
def do_find(self):
global rootw
search = self.search_text.get()
self.lst.delete(0, tk.END)
self.items = []
mode = '-O'
args = sys.argv[1:] + [ '-s', ]
if self.mode.get() == rlv.MODE_AND:
mode = '-A'
search = search.split(' ')
args += search
args += [ mode, ]
else:
args += [ search, mode ]
print args
try:
rlv.main(args)
except SystemExit:
# No matches -> Exit with 80
gui.lst.insert(tk.END, '<No matches>')
self.items.append('<No passwords matched search>')
 
def display(self):
global rootw
selected = self.lst.curselection()
if not selected:
return
selected = int(selected[0])
item = self.items[selected]
print item
dlg = ResultDialog(rootw, item)
rootw.wait_window(dlg.top)
 
def __init__(self, master=None):
self.master = master
frame = Frame(master)
frame.pack(expand=1, fill=tk.BOTH)
self.items = []
self.frame = frame
#top = master
top = frame.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1, pad=5)
frame.rowconfigure(0, weight=1, pad=5)
frame.columnconfigure(0, weight=1)
 
# Avoid printing to stderr
def ignoreme(s):
pass
rlv.printe = ignoreme
FILL = tk.N+tk.S+tk.E+tk.W
BTNROW = 3
MODEROW = 2
RESROW = 1
# Populate
self.search_text = Entry(self.frame)
#self.search_text.pack({'expand': 1, 'side': 'top'})
self.search_text.grid(row=0, column=0, columnspan=4, padx=5, sticky=tk.N+tk.E+tk.W)
self.search_text.bind('<Return>', lambda event: self.do_find())
self.quit = Button(self.frame, text='Quit', fg='red', command=frame.quit)
#self.quit.pack(side=tk.LEFT)
self.quit.grid(row=BTNROW, column=0, padx=10)
self.search = Button(self.frame, text='Search', command=self.do_find)
#self.search.pack(side=tk.RIGHT)
self.search.grid(row=BTNROW, column=2, padx=5)
 
self.mode = StringVar()
 
mode = rlv.load_config()[2]
self.mode.set(mode)
 
rbO = Radiobutton(self.frame, text="OR/Literal", variable=self.mode, value=rlv.MODE_OR)
rbA = Radiobutton(self.frame, text="AND", variable=self.mode, value=rlv.MODE_AND)
rbO.grid(row=MODEROW, column=0)
rbA.grid(row=MODEROW, column=1)
self.view = Button(self.frame, text='View', command=self.display)
#self.view.pack(side=tk.RIGHT)
self.view.grid(row=BTNROW, column=1)
 
## FIXME
scrollbar = Scrollbar(self.frame, orient=tk.VERTICAL)
scrollbar.grid(row=RESROW, column=4, sticky=FILL)
self.lst = Listbox(self.frame)
#self.lst.pack()
self.lst.grid(row=RESROW, column=0, columnspan=3, sticky=FILL)
self.lst.bind('<Double-Button-1>', lambda event: self.display())
 
self.lst.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=self.lst.yview)
 
self.search_text.focus_set()
 
if __name__ == '__main__':
rootw = tk.Tk()
rootw.title('Relevation search v' + __version__)
gui = GUI(master=rootw)
rootw.mainloop()
#rootw.destroy()
 
# vim:set ts=4 et ai fileencoding=utf-8: #
Property changes:
Added: svn:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/src/relevation.bash
0,0 → 1,4
#!/bin/sh
 
exec python3 -m relevation "$@"
 
Property changes:
Added: svn:executable
+*
\ No newline at end of property
Added: svn:keywords
+Rev Id Date
\ No newline at end of property
/relevation/trunk/.
Property changes:
Modified: svn:mergeinfo
Merged /relevation/branches/1.3.1:r617-695