【问题标题】:Safely storing encrypted credentials in django在 django 中安全地存储加密凭据
【发布时间】:2012-10-05 22:54:24
【问题描述】:

我正在开发一个 python/django 应用程序,其中包括将数据同步到各种其他服务,包括 samba 共享、ssh(scp) 服务器、Google 应用程序等。因此,它需要存储凭据以访问这些服务。我认为将它们存储为未加密字段将是一个坏主意,因为 SQL 注入攻击可以检索凭据。所以我需要在存储之前加密凭据 - 有没有可靠的库来实现这一点?

一旦凭据被加密,它们需要在可用之前被解密。我的应用有两个用例:

  • 一个是交互式的 - 在这种情况下,用户将提供密码来解锁凭据。
  • 另一个是自动同步 - 这是由 cron 作业或类似任务启动的。我应该将密码保存在哪里以最大程度地降低此处被利用的风险?

或者我应该采取不同的方法来解决这个问题吗?

【问题讨论】:

  • 这正是我的问题。很难找到这个而不是数百个“我如何正确地加盐/哈希密码”问题......呃。

标签: python django security encryption credentials


【解决方案1】:

也许您可以依赖多用户方案,通过创建:

  • 运行 Django 的用户(例如 django)没有有权访问凭据
  • 拥有这些权限的用户(例如sync)。

他们都可以在django 组中,以允许他们访问应用程序。之后,制作一个脚本(例如,一个 Django 命令,例如 manage.py sync-external)来同步您想要的内容。

这样,django 用户将有权访问应用程序和同步脚本,但不能凭据,因为只有 sync 用户可以。如果有人试图在没有凭据的情况下运行该脚本,那当然会导致错误。

在我看来,依赖 Linux 权限模型是一个“好主意”,但我不是安全专家,所以请记住这一点。如果有人对以上内容有什么要说的,请不要犹豫!

【讨论】:

    【解决方案2】:

    首先在服务器上存储足以登录多个系统的凭据看起来像是一场噩梦。无论采用何种加密方式,服务器上的泄露代码都会泄露它们。

    您应该只存储执行任务(即文件同步)所需的凭据。对于服务器,您应该考虑使用像 RSync 这样的同步服务器,对于 Google,使用像 OAuth 这样的协议等。这样,如果您的服务器受到攻击,这只会泄露数据而不是系统访问权限。

    接下来就是加密这些凭据。对于密码学,我建议你使用PYCrypto。

    对于您将在密码学中使用的所有随机数,通过 Crypto.Random(或其他一些强大的方法)生成它们,以确保它们足够强大。

    您不应使用相同的密钥加密不同的凭据。我推荐的方法是这样的:

    1. 您的服务器应该有它的主机密 M(派生自 /dev/random)。将其存储在 root 拥有的文件中,并且只有 root 可以读取。
    2. 当您的服务器以 root 权限启动时,它会将文件读入内存,而在为客户端提供服务之前会放弃它的权限。这是网络服务器和其他恶魔的正常做法。
    3. 当您要编写新凭证(或更新现有凭证)时,会生成一个随机块 S。取前半部分,计算哈希K=H(S1,M)。那将是您的加密密钥。
    4. 使用 CBC 模式加密您的数据。取 S2 中的初始化向量 (IV)。
    5. 将 S 与加密数据一起存储。

    当您需要解密时,只需取出 S 创建 K 并使用相同的 IV 解密。

    对于哈希,我建议使用 SHA1,对于加密 - AES。哈希和对称密码足够快,因此使用更大的密钥大小不会受到伤害。

    这个方案在某些地方有点过头了,但这不会有什么坏处。

    但请再次记住,存储凭据的最佳方式不是存储凭据,并且在必要时使用允许您完成任务的最低权限的凭据。

    【讨论】:

      【解决方案3】:

      我有同样的问题,过去几天一直在研究这个问题。 @Rostislav 提出的解决方案相当不错,但它不完整且有点过时。

      关于算法层

      首先,有一个新的密码学库,恰如其分地称为Cryptography。使用这个库而不是 PyCrypto 的原因有很多,但吸引我的主要原因是:

      • 核心目标是让您无法自爆。例如,它没有 像 MD2 这样严重过时的哈希算法。
      • 拥有强大的机构支持
      • 500,000 次测试,在各种平台上持续集成!
      • 他们的文档网站有更好的 SSL 配置(near-perfect A+ score 而不是 a mediocre B rating)
      • 他们制定了漏洞披露政策。

      您可以阅读有关the reasons for creating the new library on LWN 的更多信息。

      其次,另一个答案建议使用 SHA1 作为加密密钥。 SHA1 is dangerously weak and getting weaker。 SHA1 的替代品是 SHA2,最重要的是,您应该使用bcrypt 或PBKDF2 对哈希进行加盐并拉伸它。加盐作为防止rainbow tables 的保护很重要,而拉伸是防止暴力破解的重要保护。

      (Bcrypt 测试较少,但旨在使用大量内存,而 PBKDF2 设计速度较慢,NIST 推荐。在我的实现中,我使用 PBKDF2。如果您想了解更多差异,read this。 )

      对于 CBC 模式下的 AES 加密,应该使用 128 位密钥,如上所述 - 这没有改变,尽管它现在已汇总到规范 called Fernet。初始化向量将在此库中自动为您生成,因此您可以放心地忘记它。

      关于密钥生成和存储层

      其他答案非常正确,建议您需要仔细考虑密钥处理并尽可能选择 OAuth 之类的东西。但是假设这是不可能的(它不在我的实现中),您有两个用例:Cron 作业和交互式。

      cron 作业用例归结为您需要将密钥保存在安全的地方并使用它来运行 cron 作业。我没有研究过这个,所以我不会在这里发表意见。我认为有很多好方法可以做到这一点,但我不知道最简单的方法。

      对于交互式用例,您需要做的是收集用户密码,使用该密码生成密钥,然后使用该密钥解密存储的凭据。

      带回家

      我将使用 Cryptography 库执行上述所有操作:

      from cryptography.fernet import Fernet
      from cryptography.hazmat.primitives.hashes import SHA256
      from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
      from cryptography.hazmat.backends import default_backend
      
      secret = "Some secret"
      
      # Generate a salt for use in the PBKDF2 hash
      salt = base64.b64encode(os.urandom(12))  # Recommended method from cryptography.io
      # Set up the hashing algo
      kdf = PBKDF2HMAC(
          algorithm=SHA256(),
          length=32,
          salt=str(salt),
          iterations=100000,  # This stretches the hash against brute forcing
          backend=default_backend(),  # Typically this is OpenSSL
      )
      # Derive a binary hash and encode it with base 64 encoding
      hashed_pwd = base64.b64encode(kdf.derive(user_pwd))
      
      # Set up AES in CBC mode using the hash as the key
      f = Fernet(hashed_pwd)
      encrypted_secret = f.encrypt(secret)
      
      # Store the safe inputs in the DB, but do NOT include a hash of the 
      # user's password, as that is the key to the encryption! Only store 
      # the salt, the algo and the number of iterations.
      db.store(
          user='some-user', 
          secret=encrypted_secret,
          algo='pbkdf2_sha256', 
          iterations='100000', 
          salt=salt
      )
      

      然后解密如下:

      # Get the data back from your database
      encrypted_secret, algo, iterations, salt = db.get('some-user')
      
      # Set up the Key Derivation Formula (PBKDF2)
      kdf = PBKDF2HMAC(
          algorithm=SHA256(),
          length=32,
          salt=str(salt),
          iterations=int(iterations),
          backend=default_backend(),
      )
      # Generate the key from the user's password
      key = base64.b64encode(kdf.derive(user_pwd))
      
      # Set up the AES encryption again, using the key
      f = Fernet(key)
      
      # Decrypt the secret!
      secret = f.decrypt(encrypted_secret)
      print("  Your secret is: %s" % secret)
      

      攻击?

      假设您的数据库已泄露到 Internet。攻击者能做什么?好吧,我们用于加密的密钥采用了用户加盐密码的第 100,000 个 SHA256 哈希值。我们将盐和我们的加密算法存储在您的数据库中。因此,攻击者必须:

      • 尝试暴力破解哈希:将盐与所有可能的密码结合起来,并对其进行 100,000 次哈希。取该哈希并尝试将其作为解密密钥。攻击者必须进行 100,000 次哈希才能尝试一个密码。这基本上是不可能的。
      • 直接尝试所有可能的哈希作为解密密钥。这基本上是不可能的。
      • 尝试使用预计算哈希的彩虹表?不,在涉及随机盐时不会。

      我认为这非常可靠。

      不过,还有一件事需要考虑。 PBKDF2 的设计速度很慢。它需要大量的 CPU 时间。这意味着如果用户有办法生成 PBKDF2 哈希值,那么您将面临 DDOS 攻击。做好准备。

      后记

      所有这一切,我认为有些图书馆会为你做一些事情。谷歌搜索django encrypted field之类的东西。我不能对这些实现做出任何承诺,但也许你会了解其他人是如何做到这一点的。

      【讨论】:

        猜你喜欢
        • 2020-04-06
        • 1970-01-01
        • 2013-11-19
        • 2021-10-31
        • 1970-01-01
        • 2012-10-20
        • 1970-01-01
        • 1970-01-01
        • 2012-06-14
        相关资源
        最近更新 更多