Necesito almacenar de forma segura un nombre de usuario y una contraseña en Python, ¿cuáles son mis opciones? [cerrado]

95

Estoy escribiendo un pequeño script de Python que periódicamente extraerá información de un servicio de terceros usando 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 involucrar una buena medida de seguridad, por lo que al menos tomaría mucho tiempo para que alguien lo rompa.

Este script no tendrá una GUI y lo ejecutará periódicamente cron, por lo que ingresar una contraseña cada vez que se ejecute para descifrar cosas no funcionará realmente, y tendré que almacenar el nombre de usuario y la contraseña en un archivo cifrado o cifrado en una base de datos SQLite, que sería preferible ya que de todos modos usaré SQLite, y es posible que necesite editar la contraseña en algún momento. Además, probablemente incluiré todo el programa en un archivo EXE, ya que es exclusivamente para Windows en este momento.

¿Cómo puedo almacenar de forma segura la combinación de nombre de usuario y contraseña para usarla periódicamente en un crontrabajo?

Naftuli Kay
fuente
Véase también : stackoverflow.com/questions/157938
dreftymac

Respuestas:

19

Recomiendo una estrategia similar a ssh-agent . Si no puede usar ssh-agent directamente, puede implementar algo así, de modo que su contraseña solo se mantenga 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 de inmediato mediante la deldeclaració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.

wberry
fuente
2
+1, eso tiene mucho sentido. Siempre podría crear una interfaz de usuario para ella que esencialmente le pide al usuario su contraseña en el arranque, de esa manera nunca se almacena en el disco y está a salvo de miradas indiscretas.
Naftuli Kay
55

La biblioteca de llaveros de Python se integra con la CryptProtectDataAPI en Windows (junto con las API relevantes en Mac y Linux) que encripta los datos con las credenciales de inicio de sesión del usuario.

Uso simple:

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 su 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 están encriptados con las credenciales del sistema operativo del usuario, por lo que otras aplicaciones que se ejecutan en su cuenta de usuario podrían acceder a la contraseña.

Para ocultar un poco esa vulnerabilidad, puede cifrar / ocultar la contraseña de alguna manera antes de almacenarla en el llavero. Por supuesto, cualquiera que tuviera como objetivo su secuencia de comandos solo podría mirar la fuente y descubrir cómo desencriptar / desencriptar la contraseña, pero al menos evitaría que alguna aplicación elimine todas las contraseñas en la bóveda y obtenga la suya también. .

Dustin Wyatt
fuente
¿Cómo se debe almacenar el nombre de usuario? ¿ keyringAdmite recuperar tanto el nombre de usuario como la contraseña?
Stevoisiak
1
@DustinWyatt Uso inteligente de get_passwordpara el nombre de usuario. Aunque, creo que debería comenzar la respuesta con el ejemplo original simplificado de keyring.set_password()ykeyring.get_password()
Stevoisiak
keyringno es parte de la biblioteca estándar de Python
Ciasto piekarz
@Ciastopiekarz ¿Algo de la respuesta te llevó a creer que era parte de la biblioteca estándar?
Dustin Wyatt
¿ keyringElimina con seguridad la contraseña de los registros y las contraseñas de la memoria?
Kebman
26

Después de analizar las respuestas a esta y otras preguntas relacionadas, he reunido un código utilizando algunos de los métodos sugeridos para encriptar 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 guarde en la memoria como sugiere la respuesta a esta pregunta). Este método no es muy seguro; Básicamente, el script puede acceder a la información secreta para que cualquier persona que tenga acceso completo al sistema tenga el script y sus archivos asociados y pueda acceder a ellos. Lo que hace esto id oculta 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 las contraseñas cada minuto o dos.

Simplemente pegue este código en la parte superior de su secuencia de comandos, 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 el propio script solo los lea, y si el propio script se compiló y marcó como ejecutable solamente (no legible). Algo de eso podría automatizarse, pero no me he molestado. Probablemente requeriría configurar un usuario para el script y ejecutar el script como ese usuario (y establecer la propiedad de los archivos del script para ese usuario).

Me encantaría cualquier sugerencia, crítica u otros puntos de vulnerabilidad que cualquiera pueda pensar. 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.

drodgers
fuente
26

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 escriba la contraseña.

Problemas a evitar:

  1. Verificando la contraseña en el control de fuente donde otros desarrolladores o incluso el público pueden verla.
  2. Otros usuarios en el mismo servidor leyendo la contraseña de un archivo de configuración o código fuente.
  3. Tener la contraseña en un archivo de origen donde otros puedan verla por encima del 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 solo ejecuta cálculos matemáticos para demostrar que tiene la clave correcta.

Para que funcione, necesita lo siguiente:

  • La base de datos o lo que sea que esté accediendo debe ser accesible por 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 hará 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

Este es el más simple, por lo que podría ser un buen lugar para comenzar. Se describe bien en la aplicación Twelve Factor . La idea básica es que su código fuente simplemente extrae la contraseña u otros secretos de las variables de entorno, y luego configura esas variables de entorno en cada sistema donde ejecuta el programa. También podría ser un buen toque si usa valores predeterminados que funcionarán para la mayoría de los desarrolladores. Debe equilibrar eso con hacer que su 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 establecer 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. Compruebe los permisos de los 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 usar en EnvironmentFilelugar de Environmentpara cualquier secreto. EnvironmentLos 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 lee los secretos de un archivo de texto. Todavía encuentro las variables de entorno 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 puede encontrar paquetes externos como PyYAML y TOML . Personalmente, encuentro que JSON y YAML son los más simples de usar, y YAML permite comentarios.

Tres cosas a considerar con los archivos de configuración:

  1. ¿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.
  2. Asegúrese de que otros usuarios no puedan leer el archivo.
  3. Obviamente, no confí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 debe comprometerse settings.pycon el control de código fuente, aunque es posible que desee enviar un archivo llamado settings_template.pyque los usuarios pueden copiar y modificar.

Veo algunos problemas con esta técnica:

  1. Los desarrolladores pueden enviar accidentalmente el archivo al control de código fuente. Agregarlo .gitignorereduce ese riesgo.
  2. Parte de su código no está bajo control de 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 las 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.

Don Kirkby
fuente
Hola. Si su proyecto ya utiliza esta técnica, es fácil realizar la transición a las variables de entorno. Sé cómo configurar las variables de entorno en Windows 10 manualmente, pero puedo acceder a ellas desde mi código Python usando os.getenv(). ¿Cómo deberíamos hacer esto si se comparte el código? Si otro desarrollador descarga el código, ¿cómo debe asegurarse de que las variables de entorno ya estén configuradas para él?
a_sid
Intento pasar un valor predeterminado razonable a os.getenv()@a_sid, por lo que el código al menos se ejecutará para un usuario que no haya configurado las variables de entorno. Si no hay un buen valor predeterminado, genere un error claro cuando obtenga None. Aparte de eso, ponga comentarios claros en el archivo de configuración. Si entendí mal algo, le sugiero que haga una pregunta por separado.
Don Kirkby
8

No tiene mucho sentido tratar de cifrar la contraseña: la persona a la que intentas ocultarla tiene el script Python, que tendrá el código para descifrarlo. La forma más rápida de obtener la contraseña será agregar una declaración de impresión al script de Python justo antes de que use la contraseña con el servicio de terceros.

Por lo tanto, guarde la contraseña como una cadena en el script y codifíquela en base64 para que solo leer el archivo no sea suficiente, luego llámelo un día.

Ned Batchelder
fuente
Necesitaré editar el nombre de usuario y la contraseña periódicamente y estaré envolviendo todo en un EXE para Windoze; He editado la publicación para reflejar esto. ¿Debería simplemente basarlo en donde sea que termine almacenándolo?
Naftuli Kay
Estoy de acuerdo en que "cifrar" la contraseña no ayuda, ya que la contraseña de texto plano debe obtenerse de todos modos de forma automatizada y, por lo tanto, debe poder obtenerse de lo que esté almacenado. Pero hay enfoques viables.
wberry
Aunque reconocí tu nombre, estabas en el panel de principiantes y expertos en TalkPython, como principiante, tu mensaje realmente resonó conmigo, ¡gracias!
Manakin
7

Creo que lo mejor que puede hacer es proteger el archivo de secuencia de comandos y el sistema en el que se ejecuta.

Básicamente, haz lo siguiente:

  • Usar permisos del sistema de archivos (chmod 400)
  • Contraseña segura para la cuenta del propietario en el sistema
  • Reducir la posibilidad de que el sistema se vea comprometido (cortafuegos, deshabilitar servicios innecesarios, etc.)
  • Elimine los privilegios administrativos / root / sudo para aquellos que no los necesitan
Corey D
fuente
Desafortunadamente, es Windows, lo incluiré en un archivo EXE, y necesitaré cambiar la contraseña de vez en cuando, por lo que codificarla no será una opción.
Naftuli Kay
1
Windows todavía tiene permisos del sistema de archivos. Guarde la contraseña en un archivo externo y elimine el acceso de todos, excepto el suyo. Probablemente también tenga que eliminar sus privilegios administrativos.
Corey D
Sí, el uso de permisos es la única opción de seguridad confiable aquí. Obviamente, cualquier administrador aún podrá acceder a los datos (al menos en Windows / distribuciones habituales de Linux) pero esa es una batalla ya perdida.
Voo
Es verdad. Cuando el descifrado de contraseñas está automatizado, es tan bueno como tener una contraseña de texto sin formato. La verdadera seguridad está en bloquear la cuenta de usuario con acceso. Lo mejor que se puede hacer es otorgar permisos de solo lectura solo a esa cuenta de usuario. Posiblemente cree un usuario especial, específicamente y solo para ese servicio.
Sepero
1

Los sistemas operativos a menudo tienen soporte para proteger los datos del usuario. en el caso de Windows, parece que es http://msdn.microsoft.com/en-us/library/aa380261.aspx

puede llamar a win32 apis desde python usando http://vermeulen.ca/python-win32api.html

Por lo que tengo entendido, esto almacenará los datos para que solo se pueda acceder a ellos desde la cuenta utilizada para almacenarlos. si desea editar los datos, puede hacerlo escribiendo código para extraer, cambiar y guardar el valor.

Andrew Cooke
fuente
Esta me parece la mejor opción, pero siento que esta respuesta es demasiado incompleta para aceptarla, dado que carece de ejemplos reales.
ArtOfWarfare
1
Hay algunos ejemplos para usar estas funciones en Python aquí: stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare
1

Solía criptografía porque tenía problemas de instalar (compilación) otras bibliotecas comúnmente mencionados en mi sistema. (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)

Mi script se está ejecutando en un sistema / sala físicamente seguro. Cifro las credenciales con un "script de cifrado" en un archivo de configuración. Y luego descifrar cuando necesito usarlos. El "script de cifrado" no está en el sistema real, solo el archivo de configuración cifrado. Alguien que analiza el código puede romper fácilmente el cifrado analizando el código, pero aún puede compilarlo en un EXE si es necesario.

KRBA
fuente