存储加密密钥的位置

信息安全 加密 验证 密码学 php
2021-08-28 16:26:36

我正在构建一个安全系统,它将消息存储在 Postgresql 数据库中的服务器上。消息使用 PHP 的openssl_encrypt()函数和该AES-256-CBC方法加密存储。

目前,用于此功能的密钥(我们称之为 ApplicationKey)作为文件存储在服务器上,只有 www-data 用户 (0400) 的读取权限。

但是该文件存在于服务器上......我实际上不想要的东西。因此,我构建了另一层安全性,通过 UserHash 在本地获取密钥。

新层的定义:

我没有将密钥以纯文本形式存储在服务器的文件系统上,而是为每个用户创建了一个加密版本的 ApplicationKey 并将它们存储在数据库中:

UserCipherKey = 加密(ApplicationKeyUserHash
storeInDb(UserId,UserCipherKey)

然后,该表包含来自每个用户的 ApplicationKey 的加密密码。这个用于加密的UserHash是通过以下方式构造的:

UserHash = sha256(用户密码+用户名+Blowfish)

其中UserPasswordUserName都由客户端填写,Blowfish来自服务器。此 UserHash 是用于解锁加密消息的 ApplicationKey 的密钥。

因为我不希望用户每次请求页面时都输入他/她的密码,所以我想将 UserHash 存储在某个位置,最好有时间限制,例如 30 分钟的非活动使用时间。

使用 PHP 会话听起来是正确的,因为它绑定到客户端,客户端关闭窗口-> 会话被破坏;在不活动后会话被破坏等等......但是这个会话中的数据也存储在服务器文件系统上;所以我们回到第一方......因为如果文件系统受到破坏,带有 sess_* 文件的目录将被暴露,哈希值也会......你明白我的担忧吗?

我的问题/顾虑
1 - 如果我想在本地存储UserHash,还有其他东西可以使用,那么 php-session. 使用 HTML5 的本地存储是否比 php-sessions 更好/更安全?

2 - 如果有人丢失了他/她的密码,我们需要重新设置它;做这个的最好方式是什么?

3 - 还是我做错了什么?

4个回答

据我了解您的情况:

  • 您必须在服务器上存储用户必须能够阅读的一些数据(“消息”)。
  • 服务器可能能够在某些时候读取数据,即使只是暂时的。
  • 但服务器不应永久保持此电源。如果攻击者窃取了服务器硬盘的完整副本(例如完整备份),那么他应该无法读取数据。
  • 用户,作为用户,他身边的存储容量很小;充其量,他可能会记住用户在家中的一些纸上写下的密码和/或恢复代码(但是虽然用户可以接受每天输入他的密码,但他会拒绝输入一个巨大的、胖恢复代码经常;“恢复代码”应该是一种从忘记密码中恢复的方法)。

在这些情况下,唯一可能的解决方案是进行一些基于密码的加密。事实上,服务器端存储的数据和用户密码的组合应该足以恢复数据。由于我们谈论的是 Web,所以当涉及到计算任务时,客户端是虚弱的;所以加密和解密必须在服务器上完成。无论如何,您希望在服务器上完成解密,以便任何数据格式(将消息转换为 HTML)也发生在服务器上。

您所描述的与事实相去甚远(其中“真相”是“您所希望的最好的”)。也就是说,您必须执行以下操作:

  • 对于每个用户,都有一个用户特定的对称密钥K u该密钥是在创建用户帐户时在服务器端生成的,具有强大的 PRNG,并且具有适当的长度(比如 128 位——256 位是多余的,但是,如果你需要一个大数字来吸引投资者,那就选择 256 位)。
  • 对于每个用户,“恢复代码” R uK u一起生成,具有相似的特征。如果用户忘记了他的密码,这将被使用。K u的加密,使用R u作为密钥,存储在服务器上(我们称之为F u)。R u被发送给用户,并附有打印或写下并安全存储的说明。
  • 每个用户都有一个密码P u密码的哈希值是在服务器上计算的,它使用了一个很好的密码哈希函数,例如 bcrypt。此操作需要一个用户特定的随机盐S u,它存储在服务器上。哈希值通过密钥派生函数扩展为一些位(例如,256 位)如果您只需要 256 位,那么 SHA-256 将非常适合作为 KDF。

    请注意,我们只想扩展散列输出;大多数 bcrypt 库将输出作为字符串生成,该字符串对盐哈希输出进行编码。您想要分别恢复这两个值,因为您想要存储盐,并且您想要单独使用哈希输出作为 KDF 的输入。用 PBKDF2 替换 bcrypt 可能更简单:PBKDF2 是它自己的 KDF(输出大小是可配置的),大多数 PBKDF2 实现已经分别处理盐和输出。

  • KDF 输出分为两半(例如两个 128 位值)。前半部分是Vu,是密码验证令牌服务器存储它。后半部分用作加密K u的密钥;该加密值(我们称之为E u)存储在服务器上。

因此,对于用户U,服务器存储S u(用户 salt)、F u(用户密钥K u与恢复码的加密)、V u(密码验证令牌)和E u(加密用户密钥K u与密码派生密钥)。用户U的所有“消息”都将使用密钥K u加密。

当用户登录时,他会发送他的姓名 ( U ) 和他的密码 ( P u )。使用U作为索引,服务器恢复存储的数据。使用P uS u,服务器重新计算密码哈希,然后使用 KDF 进行扩展。如果KDF输出的前半部分不等于V u,则说明用户密码错误,拒绝用户。否则,用户通过身份验证(服务器有一定的保证,客户端确实是真正的用户U),KDF 输出的后半部分可用于解密E u,产生密钥K此时,服务器知道了Ku ,可以做所有的加解密业务。

如果用户想要更改他的密码,那么将生成一个新的盐,输入新密码,再次计算哈希函数,产生一个新的验证令牌和一个新的E u然而,密钥K u没有改变,所以存储的消息不需要以任何方式被触及。

如果用户忘记了他的密码,那么恢复代码可以作为一种“备用密码”来恢复K u,此时我们又回到了“密码更改”的情况。


现在来做一些重要的说明

当服务器获知用户密钥K u时,服务器应仅将其保存在 RAM 中。你不想让K u碰到磁盘(这就是练习的重点)。因此,您应该注意以下几点:

  • PHP 会话被写入磁盘。您放入会话变量中的内容成为文件!但是你可以选择目的地;例如,您可以使用基于RAM 的文件系统,该系统将作为文件显示给 PHP ,但实际上是 RAM。或者,有一个共享内存支持,因此根本不创建文件(这可能更安全;如果攻击者劫持实时服务器,则他可以读取所有文件,包括 tmpfs 上的文件;但这样的攻击者也可以掠夺 PHP 的 RAM直接,并读取所有会话数据)。

  • 虚拟内存,又称“交换空间”,可以诱导内核将通常在 RAM 中的内容写入磁盘。交换空间未备份,但仍在写入。勤奋的攻击者会扫描您的垃圾桶以寻找废弃的旧硬盘。如果磁盘发生故障(电子板烧毁),则您无法清除其内容(至少,不容易),但攻击者可以恢复数据(通过更换板)。

    完全禁用交换可能是个好主意。只要您有足够的 RAM,无需交换即可运行;而且你真的想要有足够的内存,因为访问交换空间确实会降低性能,尤其是在 PHP 等基于 GC 的语言中。虚拟内存是这些非常好的想法之一

  • 在某种程度上,您不想放入会话变量中的内容可以作为 cookie 存储在客户端上。Cookie 并不是真正永久的(如果用户有多个设备,cookie 可能不会在他的浏览器之间共享),但它们可以从一些“安全存储”中受益——就像用户计算机上的东西一样安全。

  • 您正在谈论消息......所以当该用户未登录时,您可能想为该用户加密一些数据。在这种情况下,服务器可能不知道K u为了解决这个问题,您将需要非对称加密:使K u成为公钥/私钥对的私钥,公钥“按原样”(未加密)存储在数据库中。加密使用公钥,而解密需要私钥。

    正确地进行非对称加密有点复杂。它需要多种密码算法的组合,并且有足够的空间发生无法通过测试检测到的破坏性错误。我们强烈建议您依赖现有的格式和软件;例如用于 PHP 的 GnuPG 绑定

  • 我强烈希望客户端和服务器之间的所有通信都使用 SSL,即 HTTPS;并且任何相关的 cookie 都被标记为HttpOnly 和 Secure

您应该考虑使用 HTML5 本地存储并在客户端解密内容。这样你的服务器就永远无法访问明文数据。

也就是说,没有完美的解决方案。XSS 漏洞或服务器入侵和修改的文件(和修改的 javascript)可以获取客户端机密并将其发布到第 3 方站点。

“如何将加密的密钥存储在客户端 cookie 中?” -- 如果服务器未配置为仅用于 SSL,则服务器将有权访问每个请求,可能是 HTTP 请求(可能是图像资源)。

您不需要让服务器无法获取数据(因为它无论如何都需要它,至少在您的示例中),因此您只需要确保它不会轻易泄漏(尤其是在磁盘上)。

因此,两个可能的存储位置将是服务器的 RAM/内存(但我不知道如何在 PHP 中执行此操作)或可以使材料过期的外部进程(例如memcached ),对服务器进行身份验证。

如果您将密钥存储在memcached实例中,您还可以使用只有 Web 服务器知道的秘密对其进行加密,这样您就不必信任memcached操作员。memcached 有一个很好的 PHP API,它通常可能是一个很好的会话存储。

对我来说它非常简单:) 例如,在用户表上创建一个名为 Key 的额外列,您将在那里存储使用哈希密码加密的密钥,基本上您将需要正确的密码来解密加密的密钥,我使用了什么以前,如果您想非常安全,则将散列密码与您的用户的 IP 地址或可能的地址连接起来,这是无法猜测的,即使有人可以访问您拥有用户的数据库,也永远不会可以访问加密的加密密钥 :) 以及您在另一个数据库中拥有的数据。祝你好运 。