Python: библиотка PyCrypto — шифрование файла

Автор: | 09/28/2015
 

PythonУ нас имеется утилита, которая управляет билдами и деплоями.

Помимо всего прочего — в ней прописаны несколько паролей — для авторизации в базе данных (Apache Cassandra) и для отправки почтовых уведомлений.

Хранились они в plaintext виде, прямо в коде утилиты.

Для решения этой задачи — был написан отдельный класс, который умеет шифровать и дешифровать файл, в котором хранятся логины и пароли (при этом его нешифрованная версия не хранится в репозитории) и который возвращает необходимые данные другим методам утилиты.

Данные из расшированного файла считываются с помощью ConfigParser, а что бы по умолчанию избежать создания файла на диске после его расшифровки — он передаётся модулю ConfigParser-у в виде «memory file«, с помощью модуля StringIO.

Если же файл требуется расшифровать и сохранить на диск для редактирования — в основном скрипте утилиты предусморена отдельная опция.

Отличная статья по PyCrypto есть тут>>>.

Сам скрипт:

#!/usr/bin/env python

import os

import StringIO
import hashlib

from Crypto import Random
from Crypto.Cipher import AES

from lib.shared import ConfigParser


class RDSCryptor(object):

    """Can be done with https://pypi.python.org/pypi/keyring#using-keyring module"""

    def __init__(self, rdsmanager_local_path):

        """Used to define key for symmetric encryption.

        key : byte string
        The secret key to use in the symmetric cipher.
        It must be 16 (*AES-128*), 24 (*AES-192*), or 32 (*AES-256*) bytes long.

        See pydoc Crypto.Cipher.AES.new for more details.

        passfile_enc - encrypted file;
        passfile_clear - decrypted file, to view, edit etc."""

        self.key = hashlib.sha256('somepass').digest()

        # encrypted file, MUST be stored in repository
        self.passfile_enc = os.path.join(rdsmanager_local_path, 'conf', 'credentials.txt.enc')

        # decrypted file, to view, edit etc
        # usually must NOT be stored on disc and/or in repository
        self.passfile_clear = os.path.join(rdsmanager_local_path, 'conf', 'credentials.txt')

    def pad(self, s):

        """Pad message blocks before encrypt.
        Electronic codebook and cipher-block chaining (CBC) mode are examples of block cipher mode of operation.
        Block cipher modes for symmetric-key encryption algorithms require plain text input
        that is a multiple of the block size, so messages may have to be padded to bring them to this length."""

        return s + b"" * (AES.block_size - len(s) % AES.block_size)

    def encrypt(self, message):

        """Encrypt with AES in CBC mode:
        https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
        using randomly generated IV
        https://en.wikipedia.org/wiki/Initialization_vector"""

        message = self.pad(message)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)

        return iv + cipher.encrypt(message)

    def decrypt(self, ciphertext):

        iv = ciphertext[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        plaintext = cipher.decrypt(ciphertext[AES.block_size:])

        return plaintext.rstrip(b"")

    def encrypt_file(self):

        with open(self.passfile_clear, 'rb') as fo:
            plaintext = fo.read()
        enc = self.encrypt(plaintext)

        with open(self.passfile_enc, 'wb') as fo:
            fo.write(enc)

    def decrypt_file(self, return_type=None):

        """By default decrypt_file() must be called from  get_credentials()
        with return_type = 'mem'. If so - file will NOT be created on the disk - data will be passed to ConfigParser()
        using StringIO() as "memory file".

        If decrypt_file() called from RDSmanager with rds --decrypt option - it will create new file conf/credentials.txt
        with decrypted data from credentials.txt.enc."""

        with open(self.passfile_enc, 'rb') as fo:
            ciphertext = fo.read()

        __dec = self.decrypt(ciphertext)

        if return_type == 'mem':
            return __dec
        else:
            with open(self.passfile_clear, 'wb') as fo:
                fo.write(__dec)

    def get_credentials(self, section, option):

        return_type = 'mem'

        buf = StringIO.StringIO(self.decrypt_file(return_type))

        config = ConfigParser.ConfigParser()
        config.readfp(buf)

        return config.get(section, option)

Сам файл с данными является простым ini-документом, который выглядит так:

d:RDSrdsmanager>type confcredentials.txt
[cloudlibrary]
clc_user = user
clc_password = pass

[kantar_smtp]
smtpconnect_user = user@domain.com
smtpconnect_password = pass

Использование класса для авторизации другими методами:

...
    # Cloudlibrary access data
    crypto = RDSCryptor(rdsmanager_local_path)

    clc_user = crypto.get_credentials('cloudlibrary', 'clc_user')
    clc_password = crypto.get_credentials('cloudlibrary', 'clc_password')

    # Sendmail credentials
    smtpconnect_user = crypto.get_credentials('kantar_smtp', 'smtpconnect_user')
    smtpconnect_password = crypto.get_credentials('kantar_smtp', 'smtpconnect_password')
...
...
        # set of defaults, if variables not found
        defaults = {'base_url': 'https://www.dev.domain.com/cloudlibrary/',
                    'user': self.clc_user,
                    'password': self.clc_password, }
...
user = defaults.get('user')
password = defaults.get('password')
...

Ссылки по теме

http://eli.thegreenplace.net

https://en.wikipedia.org

https://en.wikipedia.org