У нас имеется утилита, которая управляет билдами и деплоями.
Помимо всего прочего – в ней прописаны несколько паролей – для авторизации в базе данных (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 = [email protected] 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') ...
Ссылки по теме