密码数据库的安全实现

信息安全 Web应用程序 密码管理 数据库
2021-08-27 05:05:10

免责声明 我不是安全专家,只是一个努力做到最好的程序员。此外,这是我在这个社区的第一篇文章,所以如果这个问题过于宽泛,我深表歉意。

情况 我的本地网络(由专业的 IT 团队管理和保护)上有许多运行简单 Web 服务器的设备,这些设备提供用于控制数据记录器、PID 控制器等的页面。这些网页目前根本没有任何形式的身份验证,如果坏人确实破坏了网络,这将是一个大问题。

我的目标是通过向服务器正在运行的应用程序添加登录页面来为这些网页添加身份验证。(可能相关,我正在使用 Python 来运行服务器。)

我的实现我有一个概念验证设计,我在其中执行以下操作:

  • 注册页面提示用户输入用户名和密码
  • 服务器使用 SHA256 对密码进行哈希处理以创建等长的哈希值
  • 使用合理的工作因子生成随机盐。
  • 哈希密码再次被哈希,这次是盐。(盐生成和哈希使用python的bcrypt)
  • 用户名、工作因子、盐和哈希存储在密码数据库中。
  • 当用户尝试登录时,输入的密码以相同的方式进行哈希处理,并与数据库中的记录进行比较。(用于比较的盐是使用输入的用户名作为密钥从密码数据库中获取的。)
  • 如果哈希值匹配,则会向用户提供一个 cookie,授予他们访问受限网页的权限。

(请注意,网页和服务器之间的所有通信都使用 POST 请求。)

问题和疑虑

  • 在同一个数据库中保存哈希、工作因子和盐似乎是一种不好的做法。
  • 这种散列方法(合理地)是否确保没有人可以确定用户密码,包括我自己?
  • 目前一切都使用http。如果所有访问都在本地网络内,是否需要 https?
  • 使用 cookie 是验证用户是否成功登录的好方法吗?
4个回答

当前认证的一般最佳实践在NIST SP 800-63-3 数字身份指南标准中,特别是在SP-63B 认证和生命周期管理中。

这些 NIST 标准对开发人员来说很容易阅读,除了告诉你要做什么之外,它还讨论了你为什么想做某些事情。(如果您需要比 NIST 标准提供的更多详细信息,我们很乐意提供帮助。)

也就是说,让我们看看您当前的身份验证系统并解决您的问题:

注册页面提示用户输入用户名和密码

注册页面

这可能打错了,您的意思是登录页面,但是如果您的网络上的任何人都可以访问注册页面,那么您需要明确定义一个单独的授权系统。例如,任何人都可以在任何在线商店注册一个帐户,但该帐户不允许您访问其网站中允许管理员更改其产品价格的部分。

服务器使用 SHA256 对密码进行哈希处理以创建等长的哈希值

此步骤不会增加任何安全性,也不会以任何有意义的方式提高系统性能,尤其是因为您使用的是 bcrypt,它无论如何都会丢弃密码的前 72 个字符之后的任何部分。

这不太可能,但是这一步可以减少熵。熵的减少在宏伟的计划中是微不足道的,并且要求用户已经在使用长且随机生成的密码,但由于这是一个不会提高安全性的额外步骤,我建议将其省略。

使用合理的工作因子生成随机盐。

我承认一开始我很困惑看到这一步。如果这是关于汽车维修的,那对我来说就像“添加了具有合理辛烷值的合成油”一样有意义。然而,在评论中透露,正在使用的特定 bcrypt 库是PyPI 托管的 bcrypt 库。

我找不到源代码(并且对使用空格来描述范围的语言过敏),但根据文档,似乎该库用于生成 bcrypt 参数的函数调用名为bcrypt.gensalt(workfactor),并且此方法本身需要工作factor 作为它的参数...扩展汽车维修的比喻,就好像有一个名为vehicle.refuel(viscosity), or的函数vehicle.changeoil(octane)

在身份验证和密码学术语中,盐是添加到明文中的随机值,使预先计算密码散列函数或密钥派生函数的输出变得不可行。盐本身并不是秘密。它唯一的优点是事先不知道。

盐的价值是以它的长度来衡量的,也称为它的熵。(密码学中的熵是一个微妙的话题,它不仅仅取决于长度,但对于任何创建盐的合理库,盐越长,它的熵就越大。)

另一方面,工作因素是特定于密钥拉伸算法的行话,例如这个 bcrypt KDF。工作因子定义了 CPU(或 GPU、FPGA 或 ASIC,当你是攻击者时)需要做多少工作,其主要特点是计算输出需要更长的时间,使用无法绕过的步骤,猜测或忽略。这似乎违反直觉;您希望您的应用程序尽可能快地运行,对吗?

好吧,一般风险是您的密码数据库将被泄露。顶级零售商一直在泄露密码,而您没有像顶级零售商那样花费数百万美元来保护应用程序,因此假设您的密码数据库最终也可能被泄露。

这里的权衡是,登录时,您的用户将不得不多等一秒钟。当攻击者破解您的密码时,他们每秒只能猜测一个密码,每个 CPU 投入到任务中。(有动机的攻击者可以采取一些捷径,但如果您只使用单轮 SHA256 而不是密钥拉伸算法,那么即使每秒 10 次猜测也比每秒数十亿次猜测要好得多。)

因此,盐分和功因数都是彼此无关的重要参数。很抱歉,您第一次接触这个概念是通过一个名字很差的函数。

哈希密码再次被哈希,这次是盐。(盐生成和哈希使用python的bcrypt)

轻微的挑剔,但这是因为我很彻底:bcrypt 的结果不是哈希,尽管很多人称它为哈希,如果你继续称它为 a,这里的每个人都会知道你在说什么哈希。结果是延长密码或派生密钥。

用户名、工作因子、盐和哈希存储在密码数据库中。

如果结果看起来像这样,那么您已经拥有了工作因子、盐和派生密钥:

$2a$08$0SN/h83Gt1jZMR6924.Kd.HaK3MyTDt/W8FCjUOtbY3Pmres5rsma

2a是算法(支持 unicode 的 bcrypt)

08工作因素。

接下来的 22 个字符 ,0SN/h83Gt1jZMR6924.Kd.是盐。

其余的是派生密钥。

如果你得到的东西与你的 bcrypt 库不同,找一个不同的库。

当用户尝试登录时,输入的密码以相同的方式进行哈希处理,并与数据库中的记录进行比较。(用于比较的盐是使用输入的用户名作为密钥从密码数据库中获取的。)

伟大的。但是,您正在使用的库已经为您完成了繁重的工作,因此请充分利用您的库。bcrypt.checkpw函数将为您提取工作因子、盐和派生密钥。然后它将运行其 KDF 并比较结果。

使用这个库函数意味着,如果您决定将来更改默认工作因数,您不必有单独的代码来处理旧派生密钥,因为bcrypt.checkpw可以自己找出所需的工作因数.

如果哈希值匹配,则会向用户提供一个 cookie,授予他们访问受限网页的权限。

从表面上看,这听起来很棒……但请确保 cookie 本身不包含授权。它应该包含一个随机生成的字符串(会话 ID),并且在访问站点时,服务器应该查看该会话 ID 并检查其自己的资源,以查看该会话 ID 是否被正确授权。过一会儿也清理会话 ID。

如果您的 cookie 包含“is_authorized=true”而不是服务器双重检查自己资源的会话 ID,那么人们可以制作自己的授权 cookie,并且永远不需要进行身份验证。


关注点:

在同一个数据库中保存哈希、工作因子和盐似乎是一种不好的做法。

盐和工作因素在任何方面都不敏感,但对于验证密码是必需的。充其量,任何隐藏盐的尝试都会导致隐匿安全,这根本不是任何类型的安全。您最好将工作因子和盐存储在派生密钥旁边,以免自己头疼,因为将它们分开不会阻止攻击者获取它们。

这种散列方法(合理地)是否确保没有人可以确定用户密码,包括我自己?

至于您作为 Web 开发人员的责任?是的。

并感谢您询问它是否合理地确保没有人可以确定密码。它们仍然可以在离线攻击中被猜到,但使用 bcrypt 等密钥拉伸算法将极大地阻碍攻击者,即使在对泄露数据库进行离线攻击的最坏情况下也是如此。

您唯一可以做得更好的是添加 2fa 并要求您的用户使用具有 24+ 个字符长的密码管理器,真正随机密码对于每个站点都是唯一的。

请注意,密码重置不应取决于第二个因素。根据 NIST 指南,它应该首先取决于设置帐户时使用的相同识别因素(并在您的应用程序知道的尽可能多的用户通信渠道上发出尽可能多的噪音)至少使用一个额外的因素,如果有的话。

对于大多数 Web 应用程序,这意味着向帐户注册期间使用的帐户发送电子邮件(不是发送到手机的短信或安全问题本身,尽管这些可能是发送电子邮件其他因素)。对于内部业务应用程序,这意味着用户必须致电 sysops 团队/帮助台(或者,更实际地,他们需要在开发人员的办公桌前停下来)。

目前一切都使用http。如果所有访问都在本地网络内,是否需要 https?

您必须在某种程度上信任您的本地网络。但是,添加 HTTPS 永远不会有坏处。如果您完全担心,请添加 HTTPS。(您问这个问题的事实意味着,是的,您很担心。所以,是的,您应该添加 HTTPS。)

出于非安全原因,添加 HTTPS 也是一个好主意,因为 Web 服务器(和浏览器)将仅通过 HTTPS 连接使用 HTTP/2(如果可用)。这可以显着加快您的网站速度。它不适用于您的情况,因为您使用的是基于 Python 脚本的 Web 服务器,它不太可能具有 HTTP/2 功能,但出于“只是”安全性之外的原因进入是一个好习惯。(在我看来,安全性是绰绰有余的理由,但有些经理需要额外的说服力。)

使用 cookie 是验证用户是否成功登录的好方法吗?

如果该 cookie 包含会话 ID 而不是身份验证信息,则该会话 ID 是随机的,并且服务器会验证该会话 ID,那么是的。这是绝大多数具有任何类型用户标识的网站的标准做法。

盐不应该是秘密的。它的目的是让每个密码都不同,这样哈希数据库就不会受到彩虹表的攻击。您提到的其他参数也不应该是秘密的。因此,您可以将所有这些信息存储在同一个数据库中,这是大多数应用程序的做法。包含散列的数据库表通常还包含盐和有关散列算法的所有其他信息。

如果您使用一种好的散列方法(使用适当的库,不要尝试自己实现散列算法!),没有人能够在不使用暴力破解的情况下恢复密码。但这并不意味着它是不可能的!弱密码仍然是弱密码,因此当然12345admin很容易被猜到。

HTTPS 在任何地方、任何时间、对每个人都很重要。攻击者不仅在远方,在国外。攻击者可能是您的同事、前同事、访客、闯入者,甚至是受感染的路由器或任何其他设备。因此,尽可能摆脱 HTTP。希望有一天 HTTP 会消失。

Cookie 可用于会话管理,但您需要注意几个细节。例如使用仅 HTTP cookie,确保 cookie 中包含的会话 ID 是真正随机且无法猜测的,决定是否需要设置过期时间(用户是否应该在一段时间不活动后退出?),等等。

我不确定您打算将此登录屏幕放在哪里,但我假设您将重写部分服务器以添加登录页面。我不太明白您所指的用 Python 编写的“服务器”是什么。

无论如何,不​​要重新发明如何存储密码。有一些方法,因为你提到了 Python,一个很好的起点可能是关于该主题的 Django 文档

这还将负责如何保持会话(例如通过 cookie)

作为有关密码存储的一般概述,请参阅Thomas Pornin 的2013 年回答您可以考虑使用Argon2作为哈希算法。

最后,您可能想阅读OWASP关于该问题的定位,其中提到(强调我的)

与密码学的大多数领域一样,需要考虑许多不同的因素,但令人高兴的是,大多数现代语言和框架都提供了内置功能来帮助存储密码,从而处理大部分复杂性

这是这里的软件工程师。我会从这个角度回答。

我的本地网络上有许多运行简单 Web 服务器的设备

您真的应该尝试看看是否可以使用 SSO(单点登录)。

在讲述一些细节之前,SSO 基本上不会在您的应用程序中存储密码。您将依赖第三方安全方,例如 Active Directory。毕竟是公司环境吗?凉爽的

如果您想走这条路,您将获得主要优势,即您不必管理重新发明安全性。处理密码并不是一件容易的事,必须为身份验证系统编写代码而不是与现有的开源框架集成是一种设计味道。

使用第三方作为身份验证源可以让您摆脱这个问题,并避免用户拥有太多密码。同样,Github世界各地都有各种 SSO 的库,因此您不必从头开始重写。

认为如果您将密码存储在每台设备的服务器上,那么每个服务器都会有一份密码(哈希)副本,并且如果用户决定更改密码或重置密码,它们将不会同步。如果您有 5 台设备……或 1000 台设备……请告诉我经验!

最简单的基于 AD 的 SSO 意味着用户在登录屏幕上键入密码,但该密码从未存储在门户中,而是发送到中央域控制器进行验证。我不会讨论权限管理。

Active Directory 只是一个示例。还有其他工具。

至于其他点

目前一切都使用http。如果所有访问都在本地网络内,是否需要 https?

用户在任何地方输入密码,都必须使用 https。有几个很好的理由:

  • 如果人们使用我提到的那个 Active Directory 密码,你就是在网上暴露它(当你登录到你的计算机时,MS Windows 不会)。因此,您正在进入一个被认为是企业级安全的系统中的漏洞
  • 通常人们会重复使用密码,所以如果有人抓住了密码,他们可以尝试在其他地方重复使用

即使在小型办公室中,也可能有不良玩家试图在 LAN 上运行 Wireshark 并嗅探密码,可能是他们经理的密码。做什么?取决于...访问工资单?????

使用 cookie 是验证用户是否成功登录的好方法吗?

Cookie 用于用户通过身份验证后的“记住我”功能。所以他们不必再次输入密码。对于这个答案的范围,我会说是的,这是一个方法。但是您必须使用 https 并保护您的应用程序免受 XSRF 攻击。我不会在这里讨论这些。Google 和 Facebook 以正确的方式使用 Cookie。如果他们使用cookies,你为什么不呢?

免责声明:我的微软没有钱。我只是在写我脑海中最简单的例子