Subversion Repositories pub

Compare Revisions

Ignore whitespace Rev 589 → Rev 590

/relevation/branches/1.3/CHANGELOG
2,6 → 2,8
 
1.3 (?):
- Check file magic [#230] and reject unsupported data formats
- Support the new data file format [#228]
- Added extra checks for data integrity
 
1.2.1 (2013-11-05):
- Minimal GUI Fixes:
/relevation/branches/1.3/relevation.py
55,6 → 55,9
# <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
import PBKDF2
 
USE_PYCRYPTO = True
 
106,6 → 109,18
MODE_AND='and'
MODE_OR='or'
 
# Errors
class Errors(object):
class Error(Exception):
def __str__(self):
return self.msg
class DecryptError(Error):
def __init__(self, msg = 'Failed to decrypt data. Wrong password?'):
self.msg = msg
class DataFormatError(Error):
def __init__(self, msg = 'Incorrect data format'):
self.msg = msg
 
def printe(s):
' Print to stderr '
sys.stderr.write(s+'\n')
309,46 → 324,134
printe('Configuration file (~/.relevation.conf) is not readable!')
return ( fl, pw, mode )
 
class DataReaderV1(object):
def _decrypt_compressed_data(self, key, cipher_text):
''' Decrypt cipher_text using key.
_decrypt_compressed_data(str, str) -> cleartext (gzipped xml)
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 = ord(data[-1])
for i in data[-padlen:]:
if ord(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.
'''
# Old format:
# [0:12) 12B header: "rvl" 0x00, 0x01, 0x00
# [12:28) 16B ECB encrypted CBC IV
# [28:] encrypted data
if USE_PYCRYPTO:
# Extract IV
c = AES.new(key)
iv = c.decrypt(cipher_text[12:28])
# Decrypt data, CBC mode
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)
ct = c.decrypt(cipher_text[28:])
cleardata = c.decrypt(data)
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
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
'''
# Pad password
password += (chr(0) * (32 - len(password)))
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 Errors.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 Errors.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)
# Length of data padding
# Validate padding for decompression
if not self.validate_compressed_padding(cleardata_gz):
raise Errors.DataFormatError
# Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
padlen = ord(cleardata_gz[-1])
# Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
return zlib.decompress(cleardata_gz[:-padlen], 15, 2**15)
 
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 Errors.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 Errors.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 Errors.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 Errors.DataFormatError
# Decompress
padlen = ord(cleardata_gz[-1])
return zlib.decompress(cleardata_gz[:-padlen])
 
class DataReader(object):
''' Interface to read Revelation's data files '''
def __init__(self, filename):
385,6 → 488,8
app_version = header[6:9]
if data_version == '\x01':
self._impl = DataReaderV1()
elif data_version == '\x02':
self._impl = DataReaderV2()
else:
raise IOError('File \'%s\' is in a newer, unsupported, data format' % self._filename)
def get_xml(self, password):
/relevation/branches/1.3/PBKDF2.py
0,0 → 1,297
#!/usr/bin/python
# -*- coding: ascii -*-
###########################################################################
# pbkdf2 - PKCS#5 v2.0 Password-Based Key Derivation
#
# Copyright (C) 2007-2011 Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Country of origin: Canada
#
###########################################################################
# Sample PBKDF2 usage:
# from Crypto.Cipher import AES
# from pbkdf2 import PBKDF2
# import os
#
# salt = os.urandom(8) # 64-bit salt
# key = PBKDF2("This passphrase is a secret.", salt).read(32) # 256-bit key
# iv = os.urandom(16) # 128-bit IV
# cipher = AES.new(key, AES.MODE_CBC, iv)
# ...
#
# Sample crypt() usage:
# from pbkdf2 import crypt
# pwhash = crypt("secret")
# alleged_pw = raw_input("Enter password: ")
# if pwhash == crypt(alleged_pw, pwhash):
# print "Password good"
# else:
# print "Invalid password"
#
###########################################################################
 
__version__ = "1.3"
__all__ = ['PBKDF2', 'crypt']
 
from struct import pack
from random import randint
import string
import sys
 
try:
# Use PyCrypto (if available).
from Crypto.Hash import HMAC, SHA as SHA1
except ImportError:
# PyCrypto not available. Use the Python standard library.
import hmac as HMAC
try:
from hashlib import sha1 as SHA1
except ImportError:
# hashlib not available. Use the old sha module.
import sha as SHA1
 
#
# Python 2.1 thru 3.2 compatibility
#
 
if sys.version_info[0] == 2:
_0xffffffffL = long(1) << 32
def isunicode(s):
return isinstance(s, unicode)
def isbytes(s):
return isinstance(s, str)
def isinteger(n):
return isinstance(n, (int, long))
def b(s):
return s
def binxor(a, b):
return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])
def b64encode(data, chars="+/"):
tt = string.maketrans("+/", chars)
return data.encode('base64').replace("\n", "").translate(tt)
from binascii import b2a_hex
else:
_0xffffffffL = 0xffffffff
def isunicode(s):
return isinstance(s, str)
def isbytes(s):
return isinstance(s, bytes)
def isinteger(n):
return isinstance(n, int)
def callable(obj):
return hasattr(obj, '__call__')
def b(s):
return s.encode("latin-1")
def binxor(a, b):
return bytes([x ^ y for (x, y) in zip(a, b)])
from base64 import b64encode as _b64encode
def b64encode(data, chars="+/"):
if isunicode(chars):
return _b64encode(data, chars.encode('utf-8')).decode('utf-8')
else:
return _b64encode(data, chars)
from binascii import b2a_hex as _b2a_hex
def b2a_hex(s):
return _b2a_hex(s).decode('us-ascii')
xrange = range
 
class PBKDF2(object):
"""PBKDF2.py : PKCS#5 v2.0 Password-Based Key Derivation
 
This implementation takes a passphrase and a salt (and optionally an
iteration count, a digest module, and a MAC module) and provides a
file-like object from which an arbitrarily-sized key can be read.
 
If the passphrase and/or salt are unicode objects, they are encoded as
UTF-8 before they are processed.
 
The idea behind PBKDF2 is to derive a cryptographic key from a
passphrase and a salt.
 
PBKDF2 may also be used as a strong salted password hash. The
'crypt' function is provided for that purpose.
 
Remember: Keys generated using PBKDF2 are only as strong as the
passphrases they are derived from.
"""
 
def __init__(self, passphrase, salt, iterations=1000,
digestmodule=SHA1, macmodule=HMAC):
self.__macmodule = macmodule
self.__digestmodule = digestmodule
self._setup(passphrase, salt, iterations, self._pseudorandom)
 
def _pseudorandom(self, key, msg):
"""Pseudorandom function. e.g. HMAC-SHA1"""
return self.__macmodule.new(key=key, msg=msg,
digestmod=self.__digestmodule).digest()
 
def read(self, bytes):
"""Read the specified number of key bytes."""
if self.closed:
raise ValueError("file-like object is closed")
 
size = len(self.__buf)
blocks = [self.__buf]
i = self.__blockNum
while size < bytes:
i += 1
if i > _0xffffffffL or i < 1:
# We could return "" here, but
raise OverflowError("derived key too long")
block = self.__f(i)
blocks.append(block)
size += len(block)
buf = b("").join(blocks)
retval = buf[:bytes]
self.__buf = buf[bytes:]
self.__blockNum = i
return retval
 
def __f(self, i):
# i must fit within 32 bits
assert 1 <= i <= _0xffffffffL
U = self.__prf(self.__passphrase, self.__salt + pack("!L", i))
result = U
for j in xrange(2, 1+self.__iterations):
U = self.__prf(self.__passphrase, U)
result = binxor(result, U)
return result
 
def hexread(self, octets):
"""Read the specified number of octets. Return them as hexadecimal.
 
Note that len(obj.hexread(n)) == 2*n.
"""
return b2a_hex(self.read(octets))
 
def _setup(self, passphrase, salt, iterations, prf):
# Sanity checks:
 
# passphrase and salt must be str or unicode (in the latter
# case, we convert to UTF-8)
if isunicode(passphrase):
passphrase = passphrase.encode("UTF-8")
elif not isbytes(passphrase):
raise TypeError("passphrase must be str or unicode")
if isunicode(salt):
salt = salt.encode("UTF-8")
elif not isbytes(salt):
raise TypeError("salt must be str or unicode")
 
# iterations must be an integer >= 1
if not isinteger(iterations):
raise TypeError("iterations must be an integer")
if iterations < 1:
raise ValueError("iterations must be at least 1")
 
# prf must be callable
if not callable(prf):
raise TypeError("prf must be callable")
 
self.__passphrase = passphrase
self.__salt = salt
self.__iterations = iterations
self.__prf = prf
self.__blockNum = 0
self.__buf = b("")
self.closed = False
 
def close(self):
"""Close the stream."""
if not self.closed:
del self.__passphrase
del self.__salt
del self.__iterations
del self.__prf
del self.__blockNum
del self.__buf
self.closed = True
 
def crypt(word, salt=None, iterations=None):
"""PBKDF2-based unix crypt(3) replacement.
 
The number of iterations specified in the salt overrides the 'iterations'
parameter.
 
The effective hash length is 192 bits.
"""
 
# Generate a (pseudo-)random salt if the user hasn't provided one.
if salt is None:
salt = _makesalt()
 
# salt must be a string or the us-ascii subset of unicode
if isunicode(salt):
salt = salt.encode('us-ascii').decode('us-ascii')
elif isbytes(salt):
salt = salt.decode('us-ascii')
else:
raise TypeError("salt must be a string")
 
# word must be a string or unicode (in the latter case, we convert to UTF-8)
if isunicode(word):
word = word.encode("UTF-8")
elif not isbytes(word):
raise TypeError("word must be a string or unicode")
 
# Try to extract the real salt and iteration count from the salt
if salt.startswith("$p5k2$"):
(iterations, salt, dummy) = salt.split("$")[2:5]
if iterations == "":
iterations = 400
else:
converted = int(iterations, 16)
if iterations != "%x" % converted: # lowercase hex, minimum digits
raise ValueError("Invalid salt")
iterations = converted
if not (iterations >= 1):
raise ValueError("Invalid salt")
 
# Make sure the salt matches the allowed character set
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
for ch in salt:
if ch not in allowed:
raise ValueError("Illegal character %r in salt" % (ch,))
 
if iterations is None or iterations == 400:
iterations = 400
salt = "$p5k2$$" + salt
else:
salt = "$p5k2$%x$%s" % (iterations, salt)
rawhash = PBKDF2(word, salt, iterations).read(24)
return salt + "$" + b64encode(rawhash, "./")
 
# Add crypt as a static method of the PBKDF2 class
# This makes it easier to do "from PBKDF2 import PBKDF2" and still use
# crypt.
PBKDF2.crypt = staticmethod(crypt)
 
def _makesalt():
"""Return a 48-bit pseudorandom salt for crypt().
 
This function is not suitable for generating cryptographic secrets.
"""
binarysalt = b("").join([pack("@H", randint(0, 0xffff)) for i in range(3)])
return b64encode(binarysalt, "./")
 
# vim:set ts=4 sw=4 sts=4 expandtab: