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

我正在编写一个小的 Python 脚本,它将定期使用用户名和密码组合从第三方服务中提取信息。我不需要创造出100% 防弹的东西(100% 真的存在吗?)但我希望能有一个很好的安全措施,所以至少需要很长时间,有人打破它。

这个脚本没有图形用户界面,会由 cron定期运行,所以每次运行时输入密码解密都不会真正起作用,我必须将用户名和密码存储在加密的文件中,或者存储在 SQLite 数据库中,这样会更好,因为我无论如何都会使用 SQLite,而且 也许吧需要在某个时候编辑密码。此外,我可能会将整个程序包装在一个 EXE 中,因为此时它只适用于 Windows。

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

134133 次浏览

试图加密密码没有多大意义: 您试图隐藏密码的人有 Python 脚本,而 Python 脚本将提供解密密码。获取密码的最快方法是在 Python 脚本在第三方服务中使用密码之前向其添加 print 语句。

因此,将密码作为字符串存储在脚本中,然后 base64对其进行编码,这样仅仅读取文件是不够的,然后就可以结束了。

我认为最好的办法就是保护脚本文件和运行它的系统。

Basically do the following:

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

操作系统通常支持为用户保护数据。在窗户的情况下,它看起来像 http://msdn.microsoft.com/en-us/library/aa380261.aspx

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

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

我推荐一种类似于 Ssh-agent的策略。如果不能直接使用 ssh-agent,可以实现类似的功能,这样密码就只能保存在 RAM 中。Cron 作业可以将凭据配置为在每次运行时从代理获取实际密码,使用一次,然后立即使用 del语句取消引用。

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

在查看了这个问题和相关问题的答案之后,我使用一些建议的加密和模糊秘密数据的方法来组合了一些代码。这段代码专门用于在不需要用户干预的情况下运行脚本(如果用户手动启动脚本,最好让他们输入密码,并按照这个问题的答案建议将密码保存在内存中)。这种方法并不是超级安全的; 从根本上说,脚本可以访问秘密信息,因此任何具有完全系统访问权限的人都可以访问脚本及其相关文件,并且可以访问它们。如果单独检查数据文件,或者在没有脚本的情况下一起检查数据文件,那么这样做会使数据不易被随意检查,并使数据文件本身保持安全。

我的动机是这是一个项目,轮询我的一些银行帐户,以监测交易-我需要它在后台运行,而不是我重新输入密码每一分钟或两分钟。

只需将这段代码粘贴到脚本的顶部,更改 saltSeed,然后根据需要在代码中使用 store ()撷取()和 request () :

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 权限设置为只允许脚本本身读取秘密文件,并且脚本本身被编译并标记为只可执行(不可读) ,那么这种方法的安全性将得到显著提高。其中一些可以自动化,但我没有麻烦。它可能需要为该脚本设置一个用户,并以该用户的身份运行该脚本(并将该脚本文件的所有权设置为该用户)。

我喜欢任何人能想到的任何建议、批评或其他弱点。我对编写加密代码还是个新手,所以我所做的几乎可以肯定是可以改进的。

The Python keyring 库 integrates with the CryptProtectData API on Windows (along with relevant API's on Mac and Linux) which encrypts data with the user's logon credentials.

简单用法:

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

如果要在 keyring 上存储用户名,使用方法:

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 之前以某种方式加密/混淆密码。当然,任何针对你的脚本的人都可以查看源代码并找出如何解密或解除密码混淆,但是你至少可以阻止一些应用程序清空保险库中的所有密码并获取你的密码。

我使用 密码学是因为我在系统上安装(编译)其他常用的库时遇到了麻烦

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)

My script is running in a physically secure system/room. I encrypt credentials with an "encrypter script" to a config file. And then decrypt when I need to use them. “加密脚本”不在真正的系统上,只有加密的配置文件是。分析代码的人可以很容易地通过分析代码来破解加密,但是如果必要的话,您仍然可以将其编译成 EXE。

Python 程序需要使用一些存储密码和其他秘密的选项,特别是需要在后台运行的程序,因为它不能要求用户键入密码。

Problems to avoid:

  1. 将密码签入源代码管理,其他开发人员甚至公众都可以看到。
  2. 同一服务器上的其他用户从配置文件或源代码中读取密码。
  3. 将密码保存在源文件中,当您编辑它时,其他人可以从您的肩膀上看到它。

选择1: SSH

这并不总是一个选项,但它可能是最好的。您的私钥永远不会通过网络传输,SSH 只是运行数学计算来证明您拥有正确的密钥。

为了让它发挥作用,你需要以下几点:

  • 数据库或您正在访问的任何东西都需要由 SSH 访问。尝试搜索“ SSH”以及您正在访问的任何服务。例如,“ ssh postgregql”。如果这不是数据库中的一个特性,请转到下一个选项。
  • 创建一个帐户以运行将对数据库进行调用的服务,以及 生成一个 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)

Look up how to set environment variables in your operating system, and consider running the service under its own account. That way you don't have sensitive data in environment variables when you run programs in your own account. When you do set up those environment variables, take extra care that other users can't read them. Check file permissions, for example. Of course any users with root permission will be able to read them, but that can't be helped. If you're using systemd, look at the 服务单位, and be careful to use EnvironmentFile instead of Environment for any secrets. Environment values can be viewed by any user with systemctl show.

选项3: 配置文件

This is very similar to the environment variables, but you read the secrets from a text file. I still find the environment variables more flexible for things like deployment tools and continuous integration servers. If you decide to use a configuration file, Python supports several formats in the standard library, like JSON, INI, netrc, and XML. You can also find external packages like PyYAML and TOML. Personally, I find JSON and YAML the simplest to use, and YAML allows comments.

配置文件需要考虑以下三点:

  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)

使用这种技术的一个项目是 姜戈。显然,您不应该将 settings.py提交给源代码管理,尽管您可能希望提交一个名为 settings_template.py的文件,用户可以复制和修改该文件。

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

  1. Developers might accidentally commit the file to source control. Adding it to .gitignore reduces that risk.
  2. Some of your code is not under source control. If you're disciplined and only put strings and numbers in here, that won't be a problem. If you start writing logging filter classes in here, stop!

如果您的项目已经使用了这种技术,那么很容易转换到环境变量。只需将所有设置值移动到环境变量,并更改 Python 模块以从这些环境变量中读取。