Мне нужно безопасно хранить имя пользователя и пароль в Python, каковы мои варианты?

Я пишу небольшой скрипт на Python, который будет периодически извлекать информацию из стороннего сервиса, используя комбинацию имени пользователя и пароля. Мне не нужно создавать что-то, что будет на 100% пуленепробиваемым (даже существует ли оно на 100%?), Но я хотел бы задействовать хорошую меру безопасности, поэтому, по крайней мере, кому-то потребуется много времени, чтобы взломать его.

Этот скрипт не будет иметь графического интерфейса и будет периодически запускаться cronпоэтому ввод пароля каждый раз, когда он запускается для расшифровки, не будет работать, и мне придется хранить имя пользователя и пароль либо в зашифрованном файле, либо в зашифрованном виде в базе данных SQLite, что было бы предпочтительнее, чем я буду в любом случае, используя SQLite, и мне может понадобиться изменить пароль в какой-то момент. Кроме того, я, вероятно, заверну всю программу в EXE-файл, так как на данный момент она предназначена исключительно для Windows.

Как я могу безопасно хранить комбинацию имени пользователя и пароля для периодического использования через cron работа?

8 ответов

Решение

Я рекомендую стратегию, аналогичную ssh-agent. Если вы не можете использовать ssh-agent напрямую, вы можете реализовать что-то подобное, чтобы ваш пароль оставался только в оперативной памяти. В задании cron могли быть настроены учетные данные для получения действительного пароля от агента каждый раз, когда он запускается, его можно использовать один раз и немедленно отменить ссылку, используя del заявление.

Администратор по-прежнему должен вводить пароль для запуска ssh-agent, во время загрузки или чего-то еще, но это разумный компромисс, который позволяет избежать сохранения простого текста в любом месте на диске.

Библиотека ключей Python интегрируется с CryptProtectData API на Windows (вместе с соответствующими API на Mac и Linux), который шифрует данные с учетными данными пользователя.

Простое использование:

import keyring

# the service is just a namespace for your app
service_id = 'IM_YOUR_APP!'

keyring.set_password(service_id, 'dustin', 'my secret password')
password = keyring.get_password(service_id, 'dustin') # retrieve password

Использование, если вы хотите сохранить имя пользователя в связке ключей:

import keyring

MAGIC_USERNAME_KEY = 'im_the_magic_username_key'

# the service is just a namespace for your app
service_id = 'IM_YOUR_APP!'  

username = 'dustin'

# save password
keyring.set_password(service_id, username, "password")

# optionally, abuse `set_password` to save username onto keyring
# we're just using some known magic string in the username field
keyring.set_password(service_id, MAGIC_USERNAME_KEY, username)

Позже, чтобы получить информацию о связке ключей

# again, abusing `get_password` to get the username.
# after all, the keyring is just a key-value store
username = keyring.get_password(service_id, MAGIC_USERNAME_KEY)
password = keyring.get_password(service_id, username)  

Элементы шифруются с использованием учетных данных операционной системы пользователя, поэтому другие приложения, работающие в вашей учетной записи, смогут получить доступ к паролю.

Чтобы немного скрыть эту уязвимость, вы можете каким-то образом зашифровать / запутать пароль перед его сохранением в связке ключей. Конечно, любой, кто нацелился на ваш сценарий, мог бы просто посмотреть на источник и выяснить, как дешифровать / разблокировать пароль, но вы, по крайней мере, не допустите, чтобы какое-то приложение пылесосило все пароли в хранилище и получало ваши.,

Существует несколько вариантов хранения паролей и других секретов, которые должна использовать программа Python, особенно программа, которая должна работать в фоновом режиме, где она не может просто попросить пользователя ввести пароль.

Проблемы, которых следует избегать:

  1. Проверка пароля в системе контроля версий, где его могут увидеть другие разработчики или даже публика.
  2. Другие пользователи на том же сервере читают пароль из файла конфигурации или исходного кода.
  3. Наличие пароля в исходном файле, где другие могут видеть его через плечо, пока вы редактируете его.

Вариант 1: SSH

Это не всегда вариант, но, вероятно, лучший. Ваш закрытый ключ никогда не передается по сети, SSH просто выполняет математические вычисления, чтобы доказать, что у вас есть правильный ключ.

Чтобы это работало, вам нужно следующее:

  • База данных или все, к чему вы обращаетесь, должно быть доступно по SSH. Попробуйте найти "SSH" плюс любой сервис, к которому вы обращаетесь. Например, "ssh postgresql". Если это не функция в вашей базе данных, перейдите к следующей опции.
  • Создайте учетную запись для запуска службы, которая будет выполнять вызовы в базу данных, и сгенерируйте ключ SSH.
  • Либо добавьте открытый ключ к службе, которую вы собираетесь вызывать, либо создайте локальную учетную запись на этом сервере и установите открытый ключ там.

Вариант 2: переменные среды

Этот самый простой, так что это может быть хорошим началом. Это хорошо описано в приложении " Двенадцать факторов". Основная идея заключается в том, что ваш исходный код просто извлекает пароль или другие секреты из переменных среды, а затем вы настраиваете эти переменные среды в каждой системе, в которой вы запускаете программу. Также может быть приятно, если вы используете значения по умолчанию, которые будут работать для большинства разработчиков. Вы должны сбалансировать это с тем, чтобы сделать ваше программное обеспечение "безопасным по умолчанию".

Вот пример, который извлекает сервер, имя пользователя и пароль из переменных среды.

import os

server = os.getenv('MY_APP_DB_SERVER', 'localhost')
user = os.getenv('MY_APP_DB_USER', 'myapp')
password = os.getenv('MY_APP_DB_PASSWORD', '')

# db_connect(server, user, password)

Посмотрите, как установить переменные среды в вашей операционной системе, и подумайте о запуске службы под собственной учетной записью. Таким образом, у вас нет конфиденциальных данных в переменных среды, когда вы запускаете программы под своей учетной записью. Когда вы настраиваете эти переменные среды, будьте особенно внимательны, чтобы другие пользователи не могли их прочитать. Проверьте права доступа к файлу, например. Конечно, любой пользователь с правами root сможет их прочитать, но с этим ничего не поделаешь.

Вариант 3: файлы конфигурации

Это очень похоже на переменные среды, но вы читаете секреты из текстового файла. Я все еще нахожу переменные среды более гибкими для таких вещей, как инструменты развертывания и серверы непрерывной интеграции. Если вы решите использовать файл конфигурации, Python поддерживает несколько форматов в стандартной библиотеке, таких как JSON, INI, netrc и XML. Вы также можете найти внешние пакеты, такие как PyYAML и TOML. Лично я считаю JSON и YAML наиболее простым в использовании, а YAML позволяет комментировать.

Три вещи, чтобы рассмотреть с файлами конфигурации:

  1. Где файл? Может быть, местоположение по умолчанию, как ~/.my_appи параметр командной строки для использования другого местоположения.
  2. Убедитесь, что другие пользователи не могут прочитать файл.
  3. Очевидно, не фиксируйте файл конфигурации в исходном коде. Возможно, вы захотите зафиксировать шаблон, который пользователи могут скопировать в свой домашний каталог.

Вариант 4: модуль Python

Некоторые проекты просто помещают свои секреты прямо в модуль Python.

# settings.py
db_server = 'dbhost1'
db_user = 'my_app'
db_password = 'correcthorsebatterystaple'

Затем импортируйте этот модуль, чтобы получить значения.

# my_app.py
from settings import db_server, db_user, db_password

# db_connect(server, user, password)

Одним из проектов, который использует эту технику, является Django. Очевидно, вы не должны совершать settings.py к исходному контролю, хотя вы можете захотеть зафиксировать файл с именем settings_template.py что пользователи могут копировать и изменять.

Я вижу несколько проблем с этой техникой:

  1. Разработчики могут случайно передать файл в систему контроля версий. Добавление его в .gitignore уменьшает этот риск.
  2. Часть вашего кода не находится под контролем исходного кода. Если вы дисциплинированы и ввели здесь только строки и цифры, это не будет проблемой. Если вы начнете писать здесь классы фильтров журналирования, остановитесь!

Если ваш проект уже использует эту технику, легко перейти к переменным среды. Просто переместите все значения настроек в переменные среды и измените модуль Python для чтения из этих переменных среды.

Посмотрев ответы на этот и другие вопросы, я собрал некоторый код, используя несколько предложенных методов для шифрования и скрытия секретных данных. Этот код специально предназначен для случаев, когда скрипт должен запускаться без вмешательства пользователя (если пользователь запускает его вручную, лучше всего, чтобы он вставил пароль и оставил его в памяти только в соответствии с ответом на этот вопрос). Этот метод не является сверхбезопасным; По сути, скрипт может получить доступ к секретной информации, поэтому любой, кто имеет полный системный доступ, имеет скрипт и связанные с ним файлы и может получить к ним доступ. Что это делает, id скрывает данные от случайной проверки и оставляет файлы данных в безопасности, если они проверяются по отдельности или вместе без сценария.

Моя мотивация для этого - проект, который опрашивает некоторые из моих банковских счетов для мониторинга транзакций - мне нужно, чтобы он работал в фоновом режиме без повторного ввода паролей каждую минуту или две.

Просто вставьте этот код в начало вашего скрипта, измените saltSeed, а затем используйте store() retrieve() и require() в своем коде по мере необходимости:

from getpass import getpass
from pbkdf2 import PBKDF2
from Crypto.Cipher import AES
import os
import base64
import pickle


### Settings ###

saltSeed = 'mkhgts465wef4fwtdd' # MAKE THIS YOUR OWN RANDOM STRING

PASSPHRASE_FILE = './secret.p'
SECRETSDB_FILE = './secrets'
PASSPHRASE_SIZE = 64 # 512-bit passphrase
KEY_SIZE = 32 # 256-bit key
BLOCK_SIZE = 16  # 16-bit blocks
IV_SIZE = 16 # 128-bits to initialise
SALT_SIZE = 8 # 64-bits of salt


### System Functions ###

def getSaltForKey(key):
    return PBKDF2(key, saltSeed).read(SALT_SIZE) # Salt is generated as the hash of the key with it's own salt acting like a seed value

def encrypt(plaintext, salt):
    ''' Pad plaintext, then encrypt it with a new, randomly initialised cipher. Will not preserve trailing whitespace in plaintext!'''

    # Initialise Cipher Randomly
    initVector = os.urandom(IV_SIZE)

    # Prepare cipher key:
    key = PBKDF2(passphrase, salt).read(KEY_SIZE)

    cipher = AES.new(key, AES.MODE_CBC, initVector) # Create cipher

    return initVector + cipher.encrypt(plaintext + ' '*(BLOCK_SIZE - (len(plaintext) % BLOCK_SIZE))) # Pad and encrypt

def decrypt(ciphertext, salt):
    ''' Reconstruct the cipher object and decrypt. Will not preserve trailing whitespace in the retrieved value!'''

    # Prepare cipher key:
    key = PBKDF2(passphrase, salt).read(KEY_SIZE)

    # Extract IV:
    initVector = ciphertext[:IV_SIZE]
    ciphertext = ciphertext[IV_SIZE:]

    cipher = AES.new(key, AES.MODE_CBC, initVector) # Reconstruct cipher (IV isn't needed for edecryption so is set to zeros)

    return cipher.decrypt(ciphertext).rstrip(' ') # Decrypt and depad


### User Functions ###

def store(key, value):
    ''' Sore key-value pair safely and save to disk.'''
    global db

    db[key] = encrypt(value, getSaltForKey(key))
    with open(SECRETSDB_FILE, 'w') as f:
        pickle.dump(db, f)

def retrieve(key):
    ''' Fetch key-value pair.'''
    return decrypt(db[key], getSaltForKey(key))

def require(key):
    ''' Test if key is stored, if not, prompt the user for it while hiding their input from shoulder-surfers.'''
    if not key in db: store(key, getpass('Please enter a value for "%s":' % key))


### Setup ###

# Aquire passphrase:
try:
    with open(PASSPHRASE_FILE) as f:
        passphrase = f.read()
    if len(passphrase) == 0: raise IOError
except IOError:
    with open(PASSPHRASE_FILE, 'w') as f:
        passphrase = os.urandom(PASSPHRASE_SIZE) # Random passphrase
        f.write(base64.b64encode(passphrase))

        try: os.remove(SECRETSDB_FILE) # If the passphrase has to be regenerated, then the old secrets file is irretrievable and should be removed
        except: pass
else:
    passphrase = base64.b64decode(passphrase) # Decode if loaded from already extant file

# Load or create secrets database:
try:
    with open(SECRETSDB_FILE) as f:
        db = pickle.load(f)
    if db == {}: raise IOError
except (IOError, EOFError):
    db = {}
    with open(SECRETSDB_FILE, 'w') as f:
        pickle.dump(db, f)

### Test (put your code here) ###
require('id')
require('password1')
require('password2')
print
print 'Stored Data:'
for key in db:
    print key, retrieve(key) # decode values on demand to avoid exposing the whole database in memory
    # DO STUFF

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

Я буду рад любым предложениям, критике или другим уязвимым местам, о которых может подумать каждый. Я довольно новичок в написании криптографического кода, поэтому то, что я сделал, почти наверняка можно улучшить.

Нет смысла пытаться зашифровать пароль: у человека, от которого вы пытаетесь его скрыть, есть скрипт Python, в котором будет код для его расшифровки. Самый быстрый способ получить пароль - добавить оператор печати в скрипт Python непосредственно перед тем, как он использует пароль со сторонней службой.

Поэтому сохраните пароль в виде строки в скрипте и закодируйте его в base64, чтобы просто прочитать файл, а затем назвать его днем.

Я думаю, что лучшее, что вы можете сделать, это защитить файл скрипта и систему, на которой он работает.

В основном сделайте следующее:

  • Использовать разрешения файловой системы (chmod 400)
  • Надежный пароль для учетной записи владельца в системе
  • Уменьшить возможность взлома системы (брандмауэр, отключить ненужные службы и т. Д.)
  • Удалите административные /root/sudo привилегии для тех, кому это не нужно

Я использовал Cryptography, потому что у меня были проблемы с установкой (компиляцией) других часто упоминаемых библиотек в моей системе. (Win7 x64, Python 3.5)

from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(b"password = scarybunny")
plain_text = cipher_suite.decrypt(cipher_text)

Мой сценарий выполняется в физически защищенной системе / комнате. Я шифрую учетные данные с помощью "сценария шифратора" в конфигурационном файле. А затем расшифровывать, когда мне нужно их использовать. "Сценарий шифрования" отсутствует в реальной системе, есть только зашифрованный файл конфигурации. Тот, кто анализирует код, может легко взломать шифрование, проанализировав код, но вы все равно можете скомпилировать его в EXE-файл, если это необходимо.

Операционные системы часто поддерживают защиту данных для пользователя. в случае окон это выглядит как http://msdn.microsoft.com/en-us/library/aa380261.aspx

Вы можете вызвать win32 apis из python, используя http://vermeulen.ca/python-win32api.html

Насколько я понимаю, это будет хранить данные, так что они могут быть доступны только из учетной записи, используемой для их хранения. если вы хотите редактировать данные, вы можете сделать это, написав код для извлечения, изменения и сохранения значения.

Другие вопросы по тегам