126 votes

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

Je suis en train d'écrire un petit script Python script qui va périodiquement extraire des informations d'un service tiers en utilisant une combinaison de nom d'utilisateur et de mot de passe. Je n'ai pas besoin de créer quelque chose qui soit 100% à l'épreuve des balles (est-ce que 100% existe ?), mais j'aimerais impliquer une bonne mesure de sécurité pour qu'au moins il faille beaucoup de temps à quelqu'un pour le casser.

Ce script n'aura pas d'interface graphique et sera exécuté périodiquement par cron Je devrai donc stocker le nom d'utilisateur et le mot de passe soit dans un fichier crypté, soit dans une base de données SQLite, ce qui serait préférable car j'utiliserai SQLite de toute façon, et je n'ai pas besoin d'un mot de passe pour le décrypter. pourrait Il est nécessaire de modifier le mot de passe à un moment ou à un autre. De plus, je vais probablement envelopper tout le programme dans un EXE, puisqu'il est exclusivement destiné à Windows à ce stade.

Comment puis-je stocker en toute sécurité la combinaison du nom d'utilisateur et du mot de passe qui sera utilisée périodiquement par le biais d'un cron emploi ?

76voto

Dustin Wyatt Points 576

En bibliothèque de trousseau python s'intègre à la CryptProtectData sous Windows (ainsi que les API correspondantes sous Mac et Linux) qui crypte les données à l'aide des identifiants 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 :

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, vous obtiendrez vos informations à partir du porte-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 cryptés avec les informations d'identification du système d'exploitation de l'utilisateur, de sorte que d'autres applications fonctionnant sous votre compte d'utilisateur pourraient accéder au mot de passe.

Pour masquer un peu cette vulnérabilité, vous pourriez crypter/obfusquer le mot de passe d'une manière ou d'une autre avant de le stocker sur le porte-clés. Bien sûr, toute personne ciblant votre script serait capable de regarder la source et de trouver comment désencrypter/décrypter le mot de passe, mais vous empêcheriez au moins une application d'aspirer tous les mots de passe dans le coffre-fort et d'obtenir le vôtre également.

46voto

Don Kirkby Points 12671

Il existe plusieurs 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 et qui ne peut pas simplement demander à l'utilisateur de taper le mot de passe.

Problèmes à éviter :

  1. Vérifier le mot de passe dans le contrôle de 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. Le fait d'avoir le mot de passe dans un fichier source où d'autres personnes peuvent le voir par-dessus votre épaule pendant que vous le modifiez.

Option 1 : SSH

Ce n'est pas toujours possible, mais c'est probablement la meilleure solution. Votre clé privée n'est jamais transmise sur le réseau, SSH se contente d'effectuer des calculs mathématiques pour prouver que vous avez la bonne clé.

Pour que cela fonctionne, vous devez disposer des éléments suivants :

  • La base de données ou tout autre élément auquel vous accédez doit être accessible par SSH. Essayez de rechercher "SSH" plus le service auquel vous accédez. Par exemple, "ssh postgresql" . Si cette fonctionnalité n'est pas présente dans votre base de données, passez à l'option suivante.
  • Créer un compte pour exécuter le service qui fera des appels à la base de données, et générer une clé SSH .
  • Vous pouvez soit ajouter la clé publique au service que vous allez appeler, soit créer un compte local sur ce serveur et y installer la clé publique.

Option 2 : Variables d'environnement

Celle-ci est la plus simple et peut donc constituer un bon point de départ. Il est bien décrit dans le L'application Twelve Factor . L'idée de base est que votre code source extrait le mot de passe ou d'autres secrets des variables d'environnement, puis vous configurez ces variables d'environnement sur chaque système où vous exécutez le programme. Il peut également être intéressant d'utiliser des valeurs par défaut qui conviendront à la plupart des développeurs. Vous devez trouver un équilibre entre cela et le fait de rendre votre logiciel "sûr 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)

Cherchez à savoir 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'aurez pas de données sensibles dans les variables d'environnement lorsque vous exécutez des programmes sous votre propre compte. Lorsque vous définissez ces variables d'environnement, veillez à ce que les autres utilisateurs ne puissent pas les lire. Vérifiez les autorisations de fichiers, par exemple. Bien sûr, tous les utilisateurs disposant de l'autorisation Root pourront les lire, mais il n'y a rien à faire. Si vous utilisez systemd, regardez le fichier unité de service et veillez à utiliser EnvironmentFile au lieu de Environment pour tous les secrets. Environment peuvent être consultées par n'importe quel utilisateur disposant d'un systemctl show .

Option 3 : Fichiers de configuration

C'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 supporte plusieurs formats dans la bibliothèque standard, comme JSON , INI , netrc y XML . Vous pouvez également trouver des paquets externes comme PyYAML y TOML . Personnellement, je trouve que JSON et YAML sont les plus simples à utiliser, et YAML permet les commentaires.

Trois éléments à prendre en compte pour les fichiers de configuration :

  1. Où se trouve 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. Il est évident que le fichier de configuration ne doit pas être intégré au code source. Vous pourriez vouloir livrer un modèle que les utilisateurs peuvent copier dans leur répertoire personnel.

Option 4 : Module Python

Certains projets intègrent leurs secrets 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 . Il est évident que vous ne devez pas vous engager settings.py dans le contrôle de source, bien que vous puissiez vouloir livrer un fichier appelé settings_template.py que les utilisateurs peuvent copier et modifier.

Je vois quelques problèmes avec cette technique :

  1. Les développeurs peuvent accidentellement livrer le fichier au contrôle de source. En l'ajoutant à .gitignore réduit ce risque.
  2. Une partie de votre code n'est pas sous contrôle de source. Si vous êtes discipliné et ne mettez que des chaînes de caractères et des nombres, cela ne posera pas de 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. Il suffit de déplacer toutes les valeurs de réglage vers des variables d'environnement et de modifier le module Python pour qu'il lise ces variables d'environnement.

28voto

drodgers Points 441

Après avoir consulté les réponses à cette question et à d'autres questions connexes, j'ai mis au point un code utilisant quelques-unes des méthodes suggérées pour le cryptage et l'occultation des données secrètes. Ce code est spécialement conçu pour les cas où le script doit s'exécuter sans intervention de l'utilisateur (si l'utilisateur le lance manuellement, il est préférable de lui demander d'entrer le mot de passe et de ne le conserver qu'en mémoire, comme le suggère la réponse à cette question). Cette méthode n'est pas très sûre ; fondamentalement, le script peut accéder à l'information secrète, de sorte que toute personne ayant un accès complet au système possède le script et ses fichiers associés et peut y accéder. Cela a pour effet d'obscurcir les données à l'abri d'une inspection occasionnelle et de laisser les fichiers de données eux-mêmes en sécurité 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'ai besoin qu'il fonctionne en arrière-plan sans que j'aie à ressaisir les mots de passe toutes les minutes ou toutes les deux minutes.

Il suffit de coller ce code au début de votre script, de changer le saltSeed et d'utiliser 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 permissions os é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). Certaines de ces opérations pourraient être automatisées, mais je ne m'en suis pas donné la peine. Il faudrait probablement créer un utilisateur pour le script et exécuter le script en tant qu'utilisateur (et attribuer la propriété des fichiers du script à cet utilisateur).

J'aimerais recevoir des suggestions, des critiques ou d'autres points de vulnérabilité. Je suis assez novice en matière d'écriture de code cryptographique et ce que j'ai fait pourrait très certainement être amélioré.

21voto

wberry Points 6068

Je recommande une stratégie similaire à ssh-agent . Si vous ne pouvez pas utiliser ssh-agent directement, vous pouvez implémenter quelque chose de similaire, de sorte que votre mot de passe ne soit conservé qu'en mémoire vive. Le travail cron pourrait avoir configuré les informations d'identification pour obtenir le mot de passe réel de l'agent à chaque fois qu'il s'exécute, l'utiliser une fois, et le déréférencer immédiatement en utilisant l'option del déclaration.

L'administrateur doit toujours saisir le mot de passe pour lancer ssh-agent, au moment du démarrage ou autre, mais il s'agit d'un compromis raisonnable qui évite d'avoir un mot de passe en clair stocké n'importe où sur le disque.

10voto

Ned Batchelder Points 128913

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

Il faut donc stocker le mot de passe sous forme de chaîne dans le script, et l'encoder en base64 pour que la simple lecture du fichier ne suffise pas, puis s'arrêter là.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X