我需要在Python中安全地存储用户名和密码,我有哪些选择?


96

我正在编写一个小的Python脚本,该脚本将使用用户名和密码组合定期从第三方服务中获取信息。我不需要创建100%防弹的东西(甚至存在100%的东西吗?),但是我想采用一种很好的安全措施,因此至少有人要花很长时间才能破解它。

该脚本没有GUI,将由定期运行cron,因此每次运行解密内容时都不会真正输入密码,因此我必须将用户名和密码存储在加密文件或加密文件中在SQLite数据库中,这将是更好的选择,因为无论如何我都将使用SQLite,并且可能需要在某个时候编辑密码。另外,我可能会将整个程序包装在EXE中,因为这时它仅适用于Windows。

如何安全地存储通过cron作业定期使用的用户名和密码组合?


Answers:


19

我推荐一种类似于ssh-agent的策略。如果您不能直接使用ssh-agent,则可以实现类似的方法,以便密码仅保存在RAM中。cron作业可能已经配置了凭据,以便在每次运行时从代理获取实际密码,一次使用,然后立即使用该del语句取消引用。

管理员仍必须在启动时或在启动时输入密码以启动ssh-agent,但这是一个合理的折衷方案,可以避免将纯文本密码存储在磁盘上的任何位置。


2
+1,这很有意义。我总是可以为其构建一个UI,该UI本质上要求用户在启动时输入密码,这样它就永远不会存储在磁盘上,并且不会被撬开。
Naftuli Kay 2011年

54

蟒蛇钥匙圈库集成了CryptProtectData在Windows API(以及Mac和Linux相关的API)进行加密与用户的登录凭据数据。

简单用法:

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)  

使用用户的操作系统凭据对项目进行加密,因此,以您的用户帐户运行的其他应用程序将能够访问密码。

为了稍微掩盖该漏洞,您可以在将密码存储在密钥环上之前以某种方式对密码进行加密/混淆。当然,任何以您的脚本为目标的人都只能查看源代码,并弄清楚如何对密码进行解密/取消混淆,但是您至少要防止某些应用程序清除库中的所有密码并获取您的密码。 。


用户名应如何存储?是否keyring支持检索用户名和密码?
Stevoisiak

1
@DustinWyatt巧妙使用get_password用户名。虽然,我想你应该开始回答与原来的简化例子 keyring.set_password()keyring.get_password()
Stevoisiak

keyring不是python标准库的一部分
Ciasto piekarz

@Ciastopiekarz关于答案的某些事情是否使您相信它是标准库的一部分?
达斯汀·怀亚特

是否可以keyring安全地从日志和内存后记中清除密码?
凯布曼

26

在查看了有关此问题和相关问题的答案之后,我使用一些建议的加密和隐藏秘密数据的方法整理了一些代码。此代码专门用于脚本必须在没有用户干预的情况下运行的情况(如果用户手动启动该脚本,则最好将其放入密码中,并仅将其保存在内存中,以解决此问题)。这种方法不是超级安全的。从根本上讲,脚本可以访问机密信息,因此具有完全系统访问权限的任何人都可以使用脚本及其关联文件,并且可以访问它们。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权限以仅允许脚本本身读取它们,并且脚本本身被编译并标记为仅可执行文件(不可读),则该方法的安全性将得到显着提高。其中一些可以自动化,但是我没有打扰。可能需要为该脚本设置一个用户,然后以该用户身份运行该脚本(并将脚本文件的所有权设置给该用户)。

我喜欢任何人都能想到的任何建议,批评或其他弱点。我对编写加密代码非常陌生,因此我所做的工作几乎可以肯定会得到改善。


25

有一些选项可以存储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会在标准库中支持几种格式,例如JSONINInetrcXML。您还可以找到外部软件包,例如PyYAMLTOML。就个人而言,我发现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(db_server, db_user, db_password)

使用此技术的一个项目是Django。显然,settings.py尽管您可能想要提交一个名为settings_template.py用户可以复制和修改的文件,但您不应提交到源代码管理。

我发现此技术存在一些问题:

  1. 开发人员可能会不小心将文件提交给源代码管理。添加它可以.gitignore降低这种风险。
  2. 您的某些代码不受源代码控制。如果您训练有素,只在这里输入字符串和数字,那将不是问题。如果您在此处开始编写日志记录过滤器类,请停止!

如果您的项目已使用此技术,则很容易过渡到环境变量。只需将所有设置值移至环境变量,然后更改Python模块以从这些环境变量中读取。


你好。如果您的项目已使用此技术,则很容易过渡到环境变量。 我知道如何在Windows 10中手动设置环境变量,但是可以使用来从我的python代码访问它们os.getenv()。如果代码被共享,我们应该怎么做?如果代码是由另一个开发人员下载的,那么他/她应该如何确保已经为他设置了环境变量?
a_sid

我尝试将合理的默认值传递给os.getenv()@a_sid,因此该代码至少将针对尚未设置环境变量的用户运行。如果没有好的默认值,则在获得时会引发一个明显的错误None。除此之外,在设置文件中添加清晰的注释。如果我误解了一些东西,建议您再问一个问题。
Don Kirkby

7

尝试加密密码没有多大意义:您要隐藏密码的人拥有Python脚本,该脚本将具有解密密码。最快的获取密码的方法是在将Python脚本与第三方服务一起使用密码之前,将打印语句添加到Python脚本中。

因此,将密码作为字符串存储在脚本中,并对base64进行编码,这样仅读取文件就不够,然后每天调用它。


我需要定期编辑用户名和密码,并将整个内容包装在WINDOWS的EXE中;我已经修改了帖子以反映这一点。我应该在最终存储它的地方将它简单地设置为base64吗?
Naftuli Kay 2011年

我同意“加密”密码无济于事,因为无论如何都必须以自动方式获取纯文本密码,因此必须可以从存储的任何内容中获取密码。但是有可行的方法。
wberry 2011年

以为我认出了您的名字,您在TalkPython的初学者和专家面板上,作为一个初学者,您的信息真的引起了我的共鸣,谢谢!
侏儒鸟

7

我认为您所能做的最好就是保护脚本文件及其正在运行的系统。

基本上执行以下操作:

  • 使用文件系统权限(chmod 400)
  • 系统上所有者帐户的强密码
  • 降低系统受到破坏的能力(防火墙,禁用不需要的服务等)
  • 删除不需要的管理员/ root / sudo特权

不幸的是,它是Windows,我将其包装在EXE中,并且需要经常更改密码,因此硬编码将不是一种选择。
Naftuli Kay 2011年

1
Windows仍然具有文件系统权限。将密码存储在外部文件中,并删除除您自己之外的所有用户的访问权限。您可能还必须删除其管理权限。
Corey D

是的,使用权限是这里唯一可靠的安全选项。显然,任何管理员仍然可以访问数据(至少在Windows /通常的Linux发行版上),但这已经是一场失败的战斗。
Voo

这是真的。如果密码解密是自动进行的,那与拥有纯文本密码一样好。真正的安全性在于锁定具有访问权限的用户帐户。最好的办法是仅对该用户帐户授予只读权限。可能会创建一个特殊用户,专门针对该服务。
Sepero 2014年

1

操作系统通常支持为用户保护数据。对于Windows,它看起来像是http://msdn.microsoft.com/en-us/library/aa380261.aspx

您可以使用http://vermeulen.ca/python-win32api.html从python调用win32 api

据我了解,这将存储数据,以便只能从用于存储它的帐户访问它。如果要编辑数据,可以通过编写代码来提取,更改和保存值来进行。


对我来说,这似乎是最好的选择,但是我认为这个答案太不完整,无法接受,因为它缺少任何实际的例子。
ArtOfWarfare 2015年

1
这里有一些在Python中使用这些功能的示例:stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare 2015年

1

我使用密码术是因为我在系统上安装(编译)其他常用库时遇到了麻烦。(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。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.