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

I need to securely store a username and password in Python, what are my options?

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

这个脚本没有图形用户界面,将由cron定期运行,因此每次运行时输入密码来解密东西都不会真正起作用,我必须将用户名和密码存储在加密文件中或加密在sqlite数据库中,这是更好的选择,因为无论如何我都将使用sqlite,我可能需要编辑密码。此外,我可能会将整个程序包装在一个exe中,因为此时它是专门针对Windows的。

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


python keyring库与windows上的CryptProtectDataAPI(以及Mac和Linux上的相关API)集成,后者使用用户的登录凭证对数据进行加密。

简单用法:

1
2
3
4
5
6
7
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

如果要在钥匙圈上存储用户名,请使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

稍后从钥匙圈中获取信息

1
2
3
4
# 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)

项目使用用户的操作系统凭据加密,因此在您的用户帐户中运行的其他应用程序可以访问密码。

为了稍微掩盖这个漏洞,您可以在将密码存储到密匙环之前以某种方式加密/混淆密码。当然,任何以脚本为目标的人都可以查看源代码并找出如何解密/取消混淆密码,但至少可以防止某些应用程序清空保险库中的所有密码并获取您的密码。


在查看了这个问题和相关问题的答案之后,我使用一些建议的加密和隐藏秘密数据的方法来整理一些代码。此代码专门用于脚本必须在无需用户干预的情况下运行时(如果用户手动启动它,最好将其放入密码中,并按照此问题的答案将其保存在内存中)。这种方法不是超级安全的;从根本上讲,脚本可以访问机密信息,这样任何具有完全系统访问权限的人都可以访问脚本及其相关文件。这样做的目的是为了从临时检查中隐藏数据,如果单独检查数据文件,或者在没有脚本的情况下一起检查数据文件,那么这些数据文件本身就是安全的。

我之所以这样做,是因为这个项目会对我的一些银行账户进行投票,以监控交易——我需要它在后台运行,而不需要每隔一两分钟重新输入密码。

只需将此代码粘贴到脚本顶部,更改saltseed,然后根据需要在代码中使用store()retrieve()和require():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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

如果在机密文件上设置操作系统权限,只允许脚本本身读取它们,并且脚本本身被编译并标记为仅可执行(不可读),则此方法的安全性将显著提高。其中一些可能是自动化的,但我没有打扰。它可能需要为脚本设置一个用户,并以该用户的身份运行脚本(并将脚本文件的所有权设置为该用户)。

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


我建议使用类似于ssh代理的策略。如果不能直接使用ssh代理,可以实现类似的功能,这样您的密码就只保存在RAM中。cron作业可以配置凭证,以便在每次运行时从代理获取实际密码,使用一次,并使用del语句立即取消引用。

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


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

基本上执行以下操作:

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


有几个选项可以存储密码和其他python程序需要使用的秘密,特别是需要在后台运行的程序,它不能要求用户输入密码。好的。

应避免的问题:好的。

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

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

    要使其正常工作,您需要:好的。

    • ssh需要访问数据库或您正在访问的任何内容。尝试搜索"ssh"以及您正在访问的任何服务。例如,"ssh postgresql"。如果这不是数据库中的功能,请转到下一个选项。
    • 创建一个帐户来运行将调用数据库的服务,并生成一个ssh密钥。
    • 要么将公钥添加到要调用的服务中,要么在该服务器上创建本地帐户,然后在那里安装公钥。

    选项2:环境变量

    这个是最简单的,所以它可能是一个好的开始。它在12因子应用程序中描述得很好。基本思想是,源代码只是从环境变量中提取密码或其他机密,然后在运行程序的每个系统上配置这些环境变量。如果您使用对大多数开发人员都有效的默认值,这也是一个不错的选择。你必须平衡这与使你的软件"默认安全"的关系。好的。

    下面是一个从环境变量中提取服务器、用户名和密码的示例。好的。

    1
    2
    3
    4
    5
    6
    7
    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)

    查找如何在操作系统中设置环境变量,并考虑在自己的帐户下运行服务。这样,当您在自己的帐户中运行程序时,环境变量中就没有敏感数据。当您设置这些环境变量时,要特别注意其他用户不能读取它们。例如,检查文件权限。当然,任何具有根权限的用户都可以读取它们,但这是无济于事的。好的。选项3:配置文件

    这与环境变量非常相似,但您可以从文本文件中读取机密。我仍然发现环境变量对于诸如部署工具和持续集成服务器之类的东西更为灵活。如果您决定使用一个配置文件,python支持标准库中的几种格式,如json、ini、netrc和xml。您还可以找到外部包,如pyyaml和toml。我个人认为json和yaml最简单,yaml允许评论。好的。

    配置文件需要考虑三件事:好的。

  • 文件在哪里?可能是像~/.my_app这样的默认位置,以及使用其他位置的命令行选项。
  • 确保其他用户无法读取该文件。
  • 显然,不要将配置文件提交到源代码。您可能需要提交一个模板,用户可以将其复制到主目录。
  • 选项4:python模块

    有些项目只是把它们的秘密直接放到了一个Python模块中。好的。

    1
    2
    3
    4
    # settings.py
    db_server = 'dbhost1'
    db_user = 'my_app'
    db_password = 'correcthorsebatterystaple'

    然后导入该模块以获取值。好的。

    1
    2
    3
    4
    # 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的文件,用户可以复制和修改该文件。好的。

    我看到这个技术的一些问题:好的。

  • 开发人员可能会意外地将文件提交到源代码管理。添加到.gitignore中可以降低这种风险。
  • 有些代码不受源代码管理。如果你纪律严明,只在这里设置字符串和数字,那就不成问题了。如果您开始在这里编写日志过滤器类,请停止!
  • 如果您的项目已经使用了这种技术,那么很容易转换为环境变量。只需将所有设置值移动到环境变量,并更改python模块以从这些环境变量中读取。好的。好啊.


    尝试加密密码没有多大意义:您试图隐藏密码的人有一个python脚本,该脚本将具有解密密码的代码。获取密码的最快方法是在第三方服务使用密码之前向python脚本添加一个print语句。

    所以在脚本中将密码存储为一个字符串,base64对其进行编码,这样仅仅读取文件就不够了,然后调用它一天。


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

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

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


    我使用密码学是因为在系统上安装(编译)其他常见的库时遇到问题。(win7 x64,python 3.5)

    1
    2
    3
    4
    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。