J'ai besoin de stocker en toute sécurité un nom d'utilisateur et un mot de passe en Python, quelles sont mes options?


94

J'écris un petit script Python qui extraira périodiquement des informations d'un service tiers à l'aide d'un combo nom d'utilisateur et mot de passe. Je n'ai pas besoin de créer quelque chose qui soit à 100% à l'épreuve des balles (existe-t-il même à 100%?), Mais j'aimerais impliquer une bonne mesure de sécurité, donc au moins il faudrait beaucoup de temps pour que quelqu'un le casse.

Ce script n'aura pas d'interface graphique et sera exécuté périodiquement par cron, donc entrer un mot de passe chaque fois qu'il est exécuté pour décrypter les choses ne fonctionnera pas vraiment, et je devrai stocker le nom d'utilisateur et le mot de passe dans un fichier crypté ou crypté dans une base de données SQLite, ce qui serait préférable car j'utiliserai de toute façon SQLite, et je devrai peut- être modifier le mot de passe à un moment donné. De plus, je vais probablement envelopper tout le programme dans un EXE, car il est exclusivement pour Windows à ce stade.

Comment puis-je stocker en toute sécurité le combo nom d'utilisateur et mot de passe à utiliser périodiquement via un crontravail?


Réponses:


18

Je recommande une stratégie similaire à ssh-agent . Si vous ne pouvez pas utiliser ssh-agent directement, vous pouvez implémenter quelque chose comme ça, de sorte que votre mot de passe ne soit conservé que dans la RAM. Le travail cron peut avoir configuré des informations d'identification pour obtenir le mot de passe réel de l'agent à chaque exécution, l'utiliser une fois et le dé-référencer immédiatement à l'aide de l' delinstruction.

L'administrateur doit encore entrer le mot de passe pour démarrer ssh-agent, au démarrage ou autre, mais c'est un compromis raisonnable qui évite d'avoir un mot de passe en texte brut stocké n'importe où sur le disque.


2
+1, cela a beaucoup de sens. Je pourrais toujours créer une interface utilisateur pour cela qui demande essentiellement à l'utilisateur son mot de passe au démarrage, de cette façon, il n'est jamais stocké sur le disque et est à l'abri des regards indiscrets.
Naftuli Kay

54

La bibliothèque de trousseaux de clés python s'intègre à l' CryptProtectDataAPI sur Windows (avec les API pertinentes sur Mac et Linux) qui crypte les données avec les informations de connexion de l'utilisateur.

Utilisation 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

Utilisation si vous souhaitez stocker le nom d'utilisateur sur le trousseau de clés:

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)

Plus tard pour obtenir vos informations du trousseau de clés

# 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)  

Les éléments sont chiffrés avec les informations d'identification du système d'exploitation de l'utilisateur, de sorte que d'autres applications exécutées dans votre compte d'utilisateur pourraient accéder au mot de passe.

Pour masquer un peu cette vulnérabilité, vous pouvez chiffrer / masquer le mot de passe d'une manière ou d'une autre avant de le stocker sur le trousseau de clés. Bien sûr, toute personne qui ciblait votre script pourrait simplement regarder la source et trouver comment déchiffrer / désobfusquer le mot de passe, mais vous empêcheriez au moins certaines applications d'aspirer tous les mots de passe dans le coffre-fort et d'obtenir le vôtre également. .


Comment le nom d'utilisateur doit-il être stocké? Est -ce que le keyringsoutien à la fois récupérer le nom d' utilisateur et mot de passe?
Stevoisiak

1
@DustinWyatt Utilisation intelligente de get_passwordpour le nom d'utilisateur. Bien que, je pense que vous devriez commencer la réponse avec l'exemple simplifié original de keyring.set_password()etkeyring.get_password()
Stevoisiak

keyringne fait pas partie de la bibliothèque standard python
Ciasto piekarz

@Ciastopiekarz Quelque chose à propos de la réponse vous a amené à croire qu'elle faisait partie de la bibliothèque standard?
Dustin Wyatt

25

Après avoir examiné les réponses à cette question et à des questions connexes, j'ai rassemblé du code en utilisant quelques-unes des méthodes suggérées pour crypter et masquer les données secrètes. Ce code est spécifiquement pour lorsque le script doit s'exécuter sans intervention de l'utilisateur (si l'utilisateur le démarre manuellement, il est préférable de lui faire entrer le mot de passe et de le conserver uniquement en mémoire comme le suggère la réponse à cette question). Cette méthode n'est pas super sécurisée; fondamentalement, le script peut accéder aux informations secrètes afin que toute personne disposant d'un accès complet au système ait le script et ses fichiers associés et puisse y accéder. Ce que cela fait id obscurcit les données de l'inspection occasionnelle et laisse les fichiers de données eux-mêmes sécurisés s'ils sont examinés individuellement ou ensemble sans le script.

Ma motivation pour cela est un projet qui interroge certains de mes comptes bancaires pour surveiller les transactions - j'en ai besoin pour fonctionner en arrière-plan sans que je ressaisisse les mots de passe toutes les minutes ou deux.

Collez simplement ce code en haut de votre script, modifiez le saltSeed puis utilisez store () retrieve () et require () dans votre code si nécessaire:

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 sécurité de cette méthode serait considérablement améliorée si les autorisations du système d'exploitation étaient définies sur les fichiers secrets pour permettre uniquement au script lui-même de les lire, et si le script lui-même était compilé et marqué comme exécutable uniquement (non lisible). Une partie de cela pourrait être automatisée, mais je n'ai pas dérangé. Cela nécessiterait probablement de configurer un utilisateur pour le script et d'exécuter le script en tant que cet utilisateur (et de définir la propriété des fichiers du script sur cet utilisateur).

J'adorerais toutes les suggestions, critiques ou autres points de vulnérabilité auxquels tout le monde peut penser. Je suis assez nouveau dans l'écriture de code cryptographique, donc ce que j'ai fait pourrait presque certainement être amélioré.


22

Il existe quelques options pour stocker les mots de passe et autres secrets qu'un programme Python doit utiliser, en particulier un programme qui doit s'exécuter en arrière-plan où il ne peut pas simplement demander à l'utilisateur de taper le mot de passe.

Problèmes à éviter:

  1. Vérification du mot de passe dans le contrôle de code source où d'autres développeurs ou même le public peuvent le voir.
  2. D'autres utilisateurs sur le même serveur lisent le mot de passe à partir d'un fichier de configuration ou d'un code source.
  3. Avoir le mot de passe dans un fichier source où les autres peuvent le voir par-dessus votre épaule pendant que vous le modifiez.

Option 1: SSH

Ce n'est pas toujours une option, mais c'est probablement la meilleure. Votre clé privée n'est jamais transmise sur le réseau, SSH exécute simplement des calculs mathématiques pour prouver que vous avez la bonne clé.

Pour le faire fonctionner, vous avez besoin des éléments suivants:

  • La base de données ou tout ce à quoi vous accédez doit être accessible par SSH. Essayez de rechercher «SSH» ainsi que le service auquel vous accédez. Par exemple, "ssh postgresql" . Si ce n'est pas une fonctionnalité de votre base de données, passez à l'option suivante.
  • Créez un compte pour exécuter le service qui effectuera des appels à la base de données et générera une clé SSH .
  • Ajoutez la clé publique au service que vous allez appeler ou créez un compte local sur ce serveur et installez-y la clé publique.

Option 2: Variables d'environnement

Celui-ci est le plus simple, donc ce pourrait être un bon point de départ. Il est bien décrit dans l'application Twelve Factor . L'idée de base est que votre code source extrait simplement le mot de passe ou d'autres secrets des variables d'environnement, puis vous configurez ces variables d'environnement sur chaque système sur lequel vous exécutez le programme. Cela peut également être une bonne idée si vous utilisez des valeurs par défaut qui fonctionneront pour la plupart des développeurs. Vous devez équilibrer cela avec le fait de rendre votre logiciel «sécurisé par défaut».

Voici un exemple qui extrait le serveur, le nom d'utilisateur et le mot de passe des variables d'environnement.

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)

Recherchez comment définir les variables d'environnement dans votre système d'exploitation et envisagez d'exécuter le service sous son propre compte. De cette façon, vous n'avez pas de données sensibles dans les variables d'environnement lorsque vous exécutez des programmes dans votre propre compte. Lorsque vous configurez ces variables d'environnement, veillez à ce que les autres utilisateurs ne puissent pas les lire. Vérifiez les autorisations de fichier, par exemple. Bien sûr, tous les utilisateurs avec l'autorisation root pourront les lire, mais cela ne peut pas être aidé.

Option 3: fichiers de configuration

Ceci est très similaire aux variables d'environnement, mais vous lisez les secrets à partir d'un fichier texte. Je trouve toujours les variables d'environnement plus flexibles pour des choses comme les outils de déploiement et les serveurs d'intégration continue. Si vous décidez d'utiliser un fichier de configuration, Python prend en charge plusieurs formats dans la bibliothèque standard, tels que JSON , INI , netrc et XML . Vous pouvez également trouver des packages externes comme PyYAML et TOML . Personnellement, je trouve JSON et YAML les plus simples à utiliser, et YAML autorise les commentaires.

Trois choses à considérer avec les fichiers de configuration:

  1. Où est le fichier? Peut-être un emplacement par défaut comme ~/.my_app, et une option de ligne de commande pour utiliser un emplacement différent.
  2. Assurez-vous que les autres utilisateurs ne peuvent pas lire le fichier.
  3. Évidemment, ne validez pas le fichier de configuration dans le code source. Vous souhaiterez peut-être valider un modèle que les utilisateurs peuvent copier dans leur répertoire de base.

Option 4: module Python

Certains projets mettent simplement leurs secrets directement dans un module Python.

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

Importez ensuite ce module pour obtenir les valeurs.

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

db_connect(db_server, db_user, db_password)

Un projet qui utilise cette technique est Django . Évidemment, vous ne devriez pas vous engager settings.pydans le contrôle de code source, bien que vous souhaitiez peut-être valider un fichier appelé settings_template.pyque les utilisateurs peuvent copier et modifier.

Je vois quelques problèmes avec cette technique:

  1. Les développeurs peuvent commettre accidentellement le fichier dans le contrôle de code source. L'ajouter à .gitignoreréduit ce risque.
  2. Une partie de votre code n'est pas sous contrôle de code source. Si vous êtes discipliné et ne mettez que des chaînes et des nombres ici, ce ne sera pas un problème. Si vous commencez à écrire des classes de filtres de journalisation ici, arrêtez!

Si votre projet utilise déjà cette technique, il est facile de passer aux variables d'environnement. Déplacez simplement toutes les valeurs de paramètre vers des variables d'environnement et changez le module Python pour lire à partir de ces variables d'environnement.


Bonjour. Si votre projet utilise déjà cette technique, il est facile de passer aux variables d'environnement. Je sais comment définir manuellement les variables d'environnement dans Windows 10, mais je peux y accéder à partir de mon code python en utilisant os.getenv(). Comment faire cela si le code est partagé? Si le code est téléchargé par un autre développeur, comment doit-il / elle s'assurer que les variables d'environnement sont déjà définies pour lui?
a_sid

J'essaie de transmettre une valeur par défaut raisonnable à os.getenv(), @a_sid, de sorte que le code fonctionnera au moins pour un utilisateur qui n'a pas défini les variables d'environnement. S'il n'y a pas de bonne valeur par défaut, générez une erreur claire lorsque vous obtenez None. Sinon, mettez des commentaires clairs dans le fichier de paramètres. Si j'ai mal compris quelque chose, je vous suggère de poser une question distincte.
Don Kirkby le

8

Il ne sert à rien d'essayer de chiffrer le mot de passe: la personne à qui vous essayez de le cacher a le script Python, qui aura le code pour le déchiffrer. Le moyen le plus rapide d'obtenir le mot de passe sera d'ajouter une instruction d'impression au script Python juste avant qu'il n'utilise le mot de passe avec le service tiers.

Donc, stockez le mot de passe sous forme de chaîne dans le script et encodez-le en base64 pour que la simple lecture du fichier ne soit pas suffisante, puis appelez-le un jour.


J'aurai besoin de modifier périodiquement le nom d'utilisateur et le mot de passe et je vais envelopper le tout dans un EXE pour Windoze; J'ai modifié le message pour refléter cela. Dois-je simplement le baser64 partout où je le stocke?
Naftuli Kay

Je reconnais que "crypter" le mot de passe n'aide pas, puisque le mot de passe en texte brut doit de toute façon être obtenu de manière automatisée et doit donc pouvoir être obtenu à partir de tout ce qui est stocké. Mais il existe des approches viables.
wberry

Je pensais avoir reconnu votre nom, vous étiez sur le panel des débutants et des experts sur TalkPython, en tant que débutant, votre message a vraiment résonné avec moi, merci!
Manakin

6

Je pense que le mieux que vous puissiez faire est de protéger le fichier de script et le système sur lequel il fonctionne.

En gros, procédez comme suit:

  • Utiliser les autorisations du système de fichiers (chmod 400)
  • Mot de passe fort pour le compte du propriétaire sur le système
  • Réduire la capacité du système à être compromis (pare-feu, désactiver les services inutiles, etc.)
  • Supprimez les privilèges administratifs / root / sudo pour ceux qui n'en ont pas besoin

Malheureusement, c'est Windows, je vais l'envelopper dans un EXE et je devrai changer le mot de passe de temps en temps, donc le codage en dur ne sera pas une option.
Naftuli Kay

1
Windows dispose toujours des autorisations du système de fichiers. Stockez le mot de passe dans un fichier externe et supprimez l'accès de tout le monde à l'exception du vôtre. Vous devez probablement également supprimer leurs privilèges administratifs.
Corey D

Oui, l'utilisation des autorisations est la seule option de sécurité fiable ici. Évidemment, tout administrateur pourra toujours accéder aux données (au moins sur Windows / les distributions Linux habituelles) mais alors c'est une bataille déjà perdue.
Voo

C'est vrai. Lorsque le décryptage du mot de passe est automatisé, c'est aussi bien que d'avoir un mot de passe en texte brut. La vraie sécurité réside dans le verrouillage du compte utilisateur avec accès. Le mieux que l'on puisse faire est de donner des autorisations en lecture seule uniquement à ce compte d'utilisateur. Créez éventuellement un utilisateur spécial, spécifiquement et uniquement pour ce service.
Sepero

1

Les systèmes d'exploitation prennent souvent en charge la sécurisation des données de l'utilisateur. dans le cas de Windows, il semble que ce soit http://msdn.microsoft.com/en-us/library/aa380261.aspx

vous pouvez appeler des apis win32 à partir de python en utilisant http://vermeulen.ca/python-win32api.html

pour autant que je sache, cela stockera les données afin qu'elles ne soient accessibles qu'à partir du compte utilisé pour les stocker. si vous souhaitez modifier les données, vous pouvez le faire en écrivant du code pour extraire, modifier et enregistrer la valeur.


Cela me semble être le meilleur choix, mais je pense que cette réponse est bien trop incomplète pour l'accepter, étant donné qu'elle manque d'exemples réels.
ArtOfWarfare

1
Il y a quelques exemples d'utilisation de ces fonctions en Python ici: stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare

1

J'ai utilisé Cryptography parce que j'avais des problèmes pour installer (compiler) d'autres bibliothèques couramment mentionnées sur mon système. (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)

Mon script s'exécute dans un système / une salle physiquement sécurisé. Je crypte les informations d'identification avec un "script de chiffrement" dans un fichier de configuration. Et puis décrypter quand j'ai besoin de les utiliser. Le "script de chiffrement" n'est pas sur le système réel, seul le fichier de configuration chiffré l'est. Quelqu'un qui analyse le code peut facilement casser le cryptage en analysant le code, mais vous pouvez toujours le compiler dans un EXE si nécessaire.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.