Necesito almacenar de forma segura un nombre de usuario y una contraseña en Python, ¿cuáles son mis opciones? [cerrado]
Estoy escribiendo un pequeño script de Python que periódicamente extraerá información de un servicio de terceros utilizando una combinación de nombre de usuario y contraseña. No necesito crear algo que sea 100% a prueba de balas (¿existe el 100%?), pero me gustaría incluir una buena medida de seguridad para que, como mínimo, alguien tardara mucho tiempo en romperlo.
Este script no tendrá una GUI y lo ejecutará periódicamente cron
, por lo que ingresar una contraseña cada vez que se ejecuta para descifrar las cosas no funcionará realmente, y tendré que almacenar el nombre de usuario y la contraseña en un archivo cifrado o encriptado. en una base de datos SQLite, lo cual sería preferible ya que usaré SQLite de todos modos y es posible que necesite editar la contraseña en algún momento. Además, probablemente empaquetaré todo el programa en un EXE, ya que en este momento es exclusivo para Windows.
¿Cómo puedo almacenar de forma segura la combinación de nombre de usuario y contraseña para utilizarla periódicamente a través de un cron
trabajo?
La biblioteca de llaveros de Python se integra con la CryptProtectData
API en Windows (junto con las API relevantes en Mac y Linux) que cifra los datos con las credenciales de inicio de sesión del usuario.
Uso sencillo:
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
Uso si desea almacenar el nombre de usuario en el llavero:
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)
Más tarde para obtener tu información del llavero.
# 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)
Los elementos se cifran con las credenciales del sistema operativo del usuario, por lo que otras aplicaciones que se ejecutan en su cuenta de usuario podrán acceder a la contraseña.
Para ocultar un poco esa vulnerabilidad, puede cifrar/ofuscar la contraseña de alguna manera antes de almacenarla en el llavero. Por supuesto, cualquiera que estuviera apuntando a su script podría simplemente mirar la fuente y descubrir cómo desencriptar/desofuscar la contraseña, pero al menos evitaría que alguna aplicación absorbiera todas las contraseñas en la bóveda y obtuviera la suya también. .
Hay algunas opciones para almacenar contraseñas y otros secretos que un programa Python necesita usar, particularmente un programa que necesita ejecutarse en segundo plano donde no puede simplemente pedirle al usuario que ingrese la contraseña.
Problemas a evitar:
- Verificar la contraseña en el control de fuente donde otros desarrolladores o incluso el público puedan verla.
- Otros usuarios en el mismo servidor leen la contraseña de un archivo de configuración o código fuente.
- Tener la contraseña en un archivo fuente donde otros puedan verla por encima de su hombro mientras la edita.
Opción 1: SSH
Esta no siempre es una opción, pero probablemente sea la mejor. Su clave privada nunca se transmite a través de la red, SSH simplemente ejecuta cálculos matemáticos para demostrar que tiene la clave correcta.
Para que funcione, necesita lo siguiente:
- La base de datos o cualquier cosa a la que esté accediendo debe ser accesible mediante SSH. Intente buscar "SSH" más cualquier servicio al que esté accediendo. Por ejemplo, "ssh postgresql" . Si esta no es una característica de su base de datos, pase a la siguiente opción.
- Cree una cuenta para ejecutar el servicio que realizará llamadas a la base de datos y generará una clave SSH .
- Agregue la clave pública al servicio al que va a llamar o cree una cuenta local en ese servidor e instale la clave pública allí.
Opción 2: variables de entorno
Éste es el más sencillo, por lo que podría ser un buen punto de partida. Está bien descrito en la aplicación Twelve Factor . La idea básica es que su código fuente simplemente extraiga la contraseña u otros secretos de las variables de entorno y luego configure esas variables de entorno en cada sistema donde ejecute el programa. También podría ser un buen toque si utiliza valores predeterminados que funcionen para la mayoría de los desarrolladores. Tienes que equilibrar eso con hacer que tu software sea "seguro por defecto".
A continuación se muestra un ejemplo que extrae el servidor, el nombre de usuario y la contraseña de las variables de entorno.
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)
Busque cómo configurar variables de entorno en su sistema operativo y considere ejecutar el servicio con su propia cuenta. De esa manera, no tendrá datos confidenciales en las variables de entorno cuando ejecute programas en su propia cuenta. Cuando configure esas variables de entorno, tenga especial cuidado de que otros usuarios no puedan leerlas. Verifique los permisos de archivos, por ejemplo. Por supuesto, cualquier usuario con permiso de root podrá leerlos, pero eso no se puede evitar. Si está usando systemd, mire la unidad de servicio y tenga cuidado de usarla EnvironmentFile
en lugar de Environment
para cualquier secreto. Environment
Los valores pueden ser vistos por cualquier usuario con systemctl show
.
Opción 3: Archivos de configuración
Esto es muy similar a las variables de entorno, pero lees los secretos de un archivo de texto. Sigo encontrando que las variables de entorno son más flexibles para cosas como herramientas de implementación y servidores de integración continua. Si decide utilizar un archivo de configuración, Python admite varios formatos en la biblioteca estándar, como JSON , INI , netrc y XML . También puedes encontrar paquetes externos como PyYAML y TOML . Personalmente, encuentro que JSON y YAML son los más sencillos de usar, y YAML permite comentarios.
Tres cosas a considerar con los archivos de configuración:
- ¿Dónde está el archivo? Tal vez una ubicación predeterminada como
~/.my_app
y una opción de línea de comandos para usar una ubicación diferente. - Asegúrese de que otros usuarios no puedan leer el archivo.
- Obviamente, no envíe el archivo de configuración al código fuente. Es posible que desee enviar una plantilla que los usuarios puedan copiar en su directorio de inicio.
Opción 4: Módulo Python
Algunos proyectos simplemente ponen sus secretos directamente en un módulo de Python.
# settings.py
db_server = 'dbhost1'
db_user = 'my_app'
db_password = 'correcthorsebatterystaple'
Luego importe ese módulo para obtener los valores.
# my_app.py
from settings import db_server, db_user, db_password
db_connect(db_server, db_user, db_password)
Un proyecto que utiliza esta técnica es Django . Obviamente, no debería comprometerse settings.py
con el control de código fuente, aunque es posible que desee comprometer un archivo llamado settings_template.py
que los usuarios puedan copiar y modificar.
Veo algunos problemas con esta técnica:
- Los desarrolladores podrían enviar accidentalmente el archivo al control de código fuente. Agregarlo
.gitignore
reduce ese riesgo. - Parte de su código no está bajo control de código fuente. Si eres disciplinado y solo pones cadenas y números aquí, eso no será un problema. Si comienza a escribir clases de filtro de registro aquí, ¡deténgase!
Si su proyecto ya utiliza esta técnica, es fácil realizar la transición a variables de entorno. Simplemente mueva todos los valores de configuración a las variables de entorno y cambie el módulo de Python para leer esas variables de entorno.
Después de analizar las respuestas a esta y otras preguntas relacionadas, he elaborado un código utilizando algunos de los métodos sugeridos para cifrar y ocultar datos secretos. Este código es específicamente para cuando el script debe ejecutarse sin la intervención del usuario (si el usuario lo inicia manualmente, es mejor que ingrese la contraseña y solo la mantenga en la memoria como sugiere la respuesta a esta pregunta). Este método no es súper seguro; Fundamentalmente, el script puede acceder a la información secreta para que cualquiera que tenga acceso total al sistema tenga el script y sus archivos asociados y pueda acceder a ellos. Lo que esto hace es ocultar los datos de una inspección casual y deja los archivos de datos seguros si se examinan individualmente o juntos sin el script.
Mi motivación para esto es un proyecto que sondea algunas de mis cuentas bancarias para monitorear las transacciones; necesito que se ejecute en segundo plano sin tener que volver a ingresar contraseñas cada minuto o dos.
Simplemente pegue este código en la parte superior de su script, cambie saltSeed y luego use store() retrieve() y require() en su código según sea necesario:
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
La seguridad de este método mejoraría significativamente si los permisos del sistema operativo se establecieran en los archivos secretos para permitir que solo el script los leyera, y si el script en sí se compilara y marcara como ejecutable únicamente (no legible). Algo de eso podría automatizarse, pero no me he molestado. Probablemente requeriría configurar un usuario para el script y ejecutarlo como ese usuario (y establecer la propiedad de los archivos del script para ese usuario).
Me encantaría recibir sugerencias, críticas u otros puntos de vulnerabilidad que cualquiera pueda imaginar. Soy bastante nuevo en la escritura de código criptográfico, por lo que es casi seguro que lo que he hecho podría mejorarse.
Recomiendo una estrategia similar a ssh-agent . Si no puede usar ssh-agent directamente, puede implementar algo similar, de modo que su contraseña solo se guarde en la RAM. El trabajo cron podría haber configurado credenciales para obtener la contraseña real del agente cada vez que se ejecuta, usarla una vez y eliminar la referencia inmediatamente usando la del
declaración.
El administrador aún tiene que ingresar la contraseña para iniciar ssh-agent, en el momento del arranque o lo que sea, pero este es un compromiso razonable que evita tener una contraseña de texto plano almacenada en cualquier lugar del disco.