Skip to content

Commit

Permalink
Implement new CryptedFileKeyring.
Browse files Browse the repository at this point in the history
CryptedFileKeyring now uses PBKDF2 to derive the key from the user's password
and a random hash. The IV is chosen randomly as well. All the stored passwords
are encrypted at once. A method to convert the old format to the new one is
provided.

The user's password is not limited to 32 characters anymore.
  • Loading branch information
Sebastian Ramacher committed Jun 2, 2012
1 parent 2a20171 commit 162f2ed
Showing 1 changed file with 139 additions and 61 deletions.
200 changes: 139 additions & 61 deletions keyring/backend.py
Expand Up @@ -28,6 +28,11 @@ def abstractmethod(funcobj):
def abstractproperty(funcobj):
return property(funcobj)

try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO

_KEYRING_SETTING = 'keyring-setting'
_CRYPTED_PASSWORD = 'crypted-password'
_BLOCK_SIZE = 32
Expand Down Expand Up @@ -435,13 +440,14 @@ class CryptedFileKeyring(BasicFileKeyring):
"""PyCrypto File Keyring"""

filename = 'crypted_pass.cfg'
crypted_password = None

def supported(self):
"""Applicable for all platforms, but not recommend"
"""
try:
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Random import get_random_bytes
status = 0
except ImportError:
status = -1
Expand All @@ -458,92 +464,164 @@ def _init_file(self):
"""

password = None
while 1:
if not password:
password = self._getpass("Please set a password for your new keyring")
password2 = self._getpass('Password (again): ')
if password != password2:
sys.stderr.write("Error: Your passwords didn't match\n")
password = None
continue
while password is None:
password = self._getpass("Please set a password for your new keyring")
password2 = self._getpass('Password (again): ')
if password != password2:
sys.stderr.write("Error: Your passwords didn't match\n")
password = None
continue
if '' == password.strip():
# forbid the blank password
sys.stderr.write("Error: blank passwords aren't allowed.\n")
password = None
continue
if len(password) > _BLOCK_SIZE:
# block size of AES is less than 32
sys.stderr.write("Error: password can't be longer than 32.\n")
password = None
continue
break

# hash the password
import crypt
self.crypted_password = crypt.crypt(password, password)

# write down the initialization
config = ConfigParser.RawConfigParser()
config.add_section(_KEYRING_SETTING)
config.set(_KEYRING_SETTING, _CRYPTED_PASSWORD, self.crypted_password)
self._write_config(config, password)

config_file = open(self.file_path,'w')
config.write(config_file)
def _create_cipher(self, password, salt, IV):
"""Create the cipher object to encrypt or decrypt the keyring.
"""

if config_file:
config_file.close()
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Cipher import AES
pw = PBKDF2(password, salt, dkLen=_BLOCK_SIZE)
return AES.new(pw[:_BLOCK_SIZE], AES.MODE_CFB, IV)

def _check_file(self):
"""Check if the password file has been init properly.
def _write_config(self, config, keyring_password):
"""Write the keyring with the given password.
"""
if os.path.exists(self.file_path):
config = ConfigParser.RawConfigParser()
config.read(self.file_path)
try:
self.crypted_password = config.get(_KEYRING_SETTING,
_CRYPTED_PASSWORD)
return self.crypted_password.strip() != ''
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
pass
return False

def _auth(self, password):
"""Return if the password can open the keyring.
config_file = StringIO()
config.write(config_file)
config_file.seek(0)

from Crypto.Random import get_random_bytes
salt = get_random_bytes(_BLOCK_SIZE)
from Crypto.Cipher import AES
IV = get_random_bytes(AES.block_size)
cipher = self._create_cipher(keyring_password, salt, IV)

if not os.path.isdir(os.path.dirname(self.file_path)):
os.makeidrs(os.path.dirname(self.file_path))

encrypted_config_file = open(self.file_path, 'w')
encrypted_config_file.write((salt + IV).encode('base64'))
encrypted_config_file.write(cipher.encrypt(config_file.read()).encode('base64'))
encrypted_config_file.close()

def _convert_old_keyring(self, keyring_password=None):
"""Convert keyring to new format.
"""

config_file = open(self.file_path, 'r')
config = ConfigParser.RawConfigParser()
config.readfp(config_file)
config_file.close()

if keyring_password is None:
keyring_password = self._getpass("Please input your password for the keyring: ")

import crypt
return crypt.crypt(password, password) == self.crypted_password
hashed = crypt.crypt(keyring_password, keyring_password)
if config.get(_KEYRING_SETTING, _CRYPTED_PASSWORD) != hashed:
sys.stderr.write("Wrong password for the keyring.\n")
raise ValueError("Wrong password")

from Crypto.Cipher import AES
password = keyring_password + (_BLOCK_SIZE - len(keyring_password) % _BLOCK_SIZE) * _PADDING

config.remove_option(_KEYRING_SETTING, _CRYPTED_PASSWORD)
for section in config.sections():
for opt in config.options(section):
cipher = AES.new(password, AES.MODE_CFB, '\0' * AES.block_size)
p = config.get(section, opt).decode()
p = cipher.decrypt(p.decode('base64'))
config.set(section, opt, p)

def _init_crypter(self):
"""Init the crypter(using the password of the keyring).
self._write_config(config, keyring_password)
return (config, keyring_password)


def _read_config(self, keyring_password=None):
"""Read the keyring.
"""
# check the password file
if not self._check_file():

# load the passwords from the file
if not os.path.exists(self.file_path):
self._init_file()

password = self._getpass("Please input your password for the keyring")
encrypted_config_file = open(self.file_path, 'r')
salt = encrypted_config_file.readline()
if salt[0] == '[':
encrypted_config_file.close()
return self._convert_old_keyring(keyring_password)

if not self._auth(password):
data = salt.decode('base64')
salt = data[:_BLOCK_SIZE]
IV = data[_BLOCK_SIZE:]
data = encrypted_config_file.read().decode('base64')
encrypted_config_file.close()

if keyring_password is None:
keyring_password = self._getpass("Please input your password for the keyring: ")
cipher = self._create_cipher(keyring_password, salt, IV)

config_file = StringIO(cipher.decrypt(data))
config = ConfigParser.RawConfigParser()
try:
config.readfp(config_file)
except ConfigParser.Error:
sys.stderr.write("Wrong password for the keyring.\n")
raise ValueError("Wrong password")
return (config, keyring_password)

# init the cipher with the password
from Crypto.Cipher import AES
# pad to _BLOCK_SIZE bytes
password = password + (_BLOCK_SIZE - len(password) % _BLOCK_SIZE) * \
_PADDING
return AES.new(password, AES.MODE_CFB)
def get_password(self, service, username):
"""Read the password from the file.
"""

def encrypt(self, password):
"""Encrypt the given password using the pycryto.
self._relocate_file()
service = escape_for_ini(service)
username = escape_for_ini(username)

# load the passwords from the file
if not os.path.exists(self.file_path):
self._init_file()

config, keyring_password = self._read_config()

# fetch the password
try:
password = config.get(service, username)
password = password.decode('utf-8')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
password = None
return password

def set_password(self, service, username, password):
"""Save the password in the file.
"""
crypter = self._init_crypter()
return crypter.encrypt(password)

self._relocate_file()
service = escape_for_ini(service)
username = escape_for_ini(username)

config, keyring_password = self._read_config()

password = password.encode('utf-8')
# write the modification
if not config.has_section(service):
config.add_section(service)
config.set(service, username, password)

self._write_config(config, keyring_password)

def encrypt(self, password):
raise NotImplementedError()

def decrypt(self, password_encrypted):
"""Decrypt the given password using the pycryto.
"""
crypter = self._init_crypter()
return crypter.decrypt(password_encrypted)
raise NotImplementedError()


class Win32CryptoKeyring(BasicFileKeyring):
Expand Down

0 comments on commit 162f2ed

Please sign in to comment.