Commits

Mikel Olasagasti Uranga committed c9cdb73

Implement a new more secure datahandler using PBKDF2.
Added src/lib/PBKDF2.py from Dwayne C. Litzenberger

Thanks to marcan@marcansoft.com for the advices.

Comments (0)

Files changed (3)

src/lib/Makefile.am

 	entry.py \
 	io.py \
 	ui.py \
-	util.py
+	util.py \
+	PBKDF2.py	
 
 nodist_librevelation_PYTHON	= config.py
 CLEANFILES			= config.py

src/lib/PBKDF2.py

+#!/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:

src/lib/datahandler/rvl.py

 #
 #
 # Copyright (c) 2003-2006 Erik Grinaker
+# Copyright (c) 2012 Mikel Olasagasti
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 import base
 from revelation import config, data, entry, util
 from revelation.bundle import luks
+from revelation.PBKDF2 import PBKDF2
 
 import os, re, StringIO, struct, xml.dom.minidom, zlib
 
 		header = "rvl\x00"		# magic string
 		header += "\x02"		# data version
 		header += "\x00"		# separator
-		header += "\x00\x05\x00"	# application version
+		header += "\x00\x04\x07"	# application version
 		header += "\x00\x00\x00"	# separator
 
 		return header
 		if password is None:
 			raise base.PasswordError
 		
-		# 256 bit salt
-		salt = os.urandom(32)
-		h = hashlib.sha256()
-		h.update(password)
-		h.update(salt)
-		key = h.digest()
-
-		# 10.000 hash cycles are widely accepted to be safe
-		# Maybe the iteration could be stored in the header as well to provide
-		# configurability in a future version
-		for i in range(0,10000):
-			key = hashlib.sha256(key).digest()
-
+		# 64-bit salt
+		salt = os.urandom(8)
+		
+		# 256-bit key
+		key = PBKDF2(password, salt, iterations=12000).read(32)
+		
 		# generate XML
 		data = RevelationXML.export_data(self, entrystore)
 
 
 		data += chr(padlen) * padlen
 
-		# generate an initial vector for the CBC encryption
-		iv = util.random_string(16)
+		# 128-bit IV
+		iv = os.urandom(16)
 
-		# encrypt data
-		AES.block_size = 16
-		AES.key_size = 32
-
-		data = AES.new(key, AES.MODE_CBC, iv).encrypt(data)
+		data = AES.new(key, AES.MODE_CBC, iv).encrypt(hashlib.sha256(data).digest() + data)
 
 		# encrypt the iv, and prepend it to the data with a header and the used salt
-		data = self.__generate_header() + salt + AES.new(key).encrypt(iv) + data
+		data = self.__generate_header() + salt + iv + data
 
 		return data
 
 		if dataversion != 2:
 			raise base.VersionError
 		
-		# Fetch the used 256 bit salt
-		salt = input[12:44]
-
-		# Calculate the needed key
-		h = hashlib.sha256()
-		h.update(password)
-		h.update(salt)
-		key = h.digest()
-
-		# 10.000 hash cycles are widely accepted to be safe
-		# Maybe the iteration could be stored in the header as well to provide
-		# configurability in a future version
-		for i in range(0,10000):
-			key = hashlib.sha256(key).digest()
-
-		# fetch and decrypt the initial vector for CBC decryption
-		AES.block_size = 16
-		AES.key_size = 32
-
-		cipher = AES.new(key)
-		iv = cipher.decrypt(input[44:60])
-
+		# Fetch the used 64 bit salt
+		salt = input[12:20]
+		iv = input[20:36]
+		key = PBKDF2(password, salt, iterations=12000).read(32)
 		# decrypt the data
-		input = input[60:]
+		input = input[36:]
 
 		if len(input) % 16 != 0:
 			raise base.FormatError
 
 		cipher = AES.new(key, AES.MODE_CBC, iv)
 		input = cipher.decrypt(input)
+		hash256 = input[0:32]
+		data = input[32:]
+
+		if hash256 != hashlib.sha256(data).digest():
+			raise base.PasswordError
 
 		# decompress data
-		padlen = ord(input[-1])
-		for i in input[-padlen:]:
+		padlen = ord(data[-1])
+		for i in data[-padlen:]:
 			if ord(i) != padlen:
-				raise base.PasswordError
+				raise base.FormatError
 
-		input = zlib.decompress(input[0:-padlen])
+		data = zlib.decompress(data[0:-padlen])
 
 		# check and import data
-		if input.strip()[:5] != "<?xml":
-			raise base.PasswordError
+		if data.strip()[:5] != "<?xml":
+			raise base.FormatError
 
-		entrystore = RevelationXML.import_data(self, input)
+		entrystore = RevelationXML.import_data(self, data)
 
 		return entrystore