怎样使用 PostgreSQL 安全地存储密码

介绍

处理用户帐户的应用程序需要在允许(授权)用户在应用程序内执行不同任务之前对用户进行身份验证(建立身份)。 密码是一种经过时间考验的常用方法,用于对用户进行身份验证。 在典型的基于密码的身份验证中,用户凭证由登录 ID(用户名、电子邮件等)和密码组成。 这些凭据存储在数据库中。 对于每次登录尝试,用户输入的凭据都会与数据库中存储的凭据进行比较。

在数据库中存储用户凭据时,切勿(永远)将密码存储为纯文本(未加密的可读文本)。 明文的对立面是密文。

  1. 所有的计算机系统都是可以破解的。 不幸的是,如果您的服务器受到威胁并且攻击者甚至获得了对数据库(或数据库转储)的只读访问权限,他们可以获得用户(包括特权用户)的登录凭据。

  2. 用户通常会忽略安全最佳实践,并为多项服务使用相同的密码。 在您的服务上公开用户的密码可能会导致该用户的帐户在其他服务上受到损害。

  3. 如果服务提供商可以读取密码,则用户可以责怪服务提供商以防发生意外情况。 此外,不道德的开发人员和系统管理员可能会滥用用户密码。

  4. 许多国家和超国家法规(例如 GDPR)规定以明文形式存储密码是非法的。

范围

本文的范围仅限于安全地存储用户密码。 本文不涉及 PostgreSQL 中的一般数据加密,也不涉及加密数据库凭据(即,用于登录数据库本身的密码)或连接。

先决条件

要从本指南中获益,必须事先接触过 PostgreSQL。 为了测试示例,假设您已经在独立服务器上运行 PostgreSQL,或者作为 PostgreSQL 的 Vultr 托管数据库实例运行。

熟悉密码学的基本概念(如加密和散列)很有帮助,但不是强制性的。

请注意,本指南中的所有代码示例都是 SQL 语句。 另请注意,SQL 语句中的文本字符串用单引号括起来。

第一原则 – 密码散列

如果用户密码不应以明文形式存储,则需要以加密形式存储。 但是,加密算法有相应的解密算法,用于恢复加密数据。 加密的目的是暂时混淆数据并在需要时将其解密回原始数据。 但这在密码的情况下是不可取的。 存储易于解密的加密密码没有任何额外好处。 因此,密码不是加密的而是散列的。

A hash 是从输入字符串生成的随机字符串。 生成散列的算法称为散列函数。 计算输入字符串的哈希值很容易,但几乎不可能计算给定哈希值的字符串的值。 散列被认为是一种单向计算。

因此,针对散列密码的最实用的攻击向量是暴力破解。 暴力破解尝试通常基于字典。 总体思路是从最常用密码的列表(字典)开始,依次计算每个可能密码的哈希值,直到找到匹配项。

散列是一项计算密集型任务; 这使得暴力破解变得困难。 此外,密码散列函数利用了诸如 按键拉伸 进一步减缓暴力破解尝试。 一个 example 一个关键的扩展技术是重复散列一个字符串(首先计算散列,然后计算散列的散列,等等)。

在 PostgreSQL 中散列密码

pgcrypto 扩大

在 PostgreSQL 中, pgcrypto 扩展具有散列密码的必要功能。 自从 pgcrypto 是内置扩展,您无需为其下载或安装任何其他软件。 启用扩展:

CREATE EXTENSION pgcrypto ;

哈希函数总是为给定的输入字符串生成相同的哈希值。 这导致两个主要问题:

  1. 如果两个用户有相同的密码,那么他们的哈希值是相同的。 最好所有散列都是唯一的。

  2. 暴力破解在计算上是昂贵的。 因此,攻击者通常使用 彩虹桌 而不是暴力破解。 这些表包含常用密码的预计算哈希值。

这两个问题都通过使用两个输入值计算哈希来解决——输入字符串(密码)和盐。 salt 是一个伪随机生成的字符串,有助于确保哈希值的唯一性。 即使两个用户有相同的密码,他们的加盐值也会不同。 因此,所有哈希值都是唯一的。

gen_salt() 功能

盐是使用 gen_salt() 功能。 通常,此函数接受一个参数 – type. 语法是:

-- pseudocode

gen_salt(type) ;

type 参数表示用于散列的加密算法的类型。 它可以采用四个可能值之一:

  • des – 用于数据加密标准 (DES) 算法

  • xdes – 用于扩展 DES 算法

  • md5 – 用于 MD5 消息摘要算法

  • bf – 对于 Blowfish 算法

为了 example,使用扩展 DES 算法生成盐:

SELECT gen_salt('xdes') ;

同样,要使用 MD5 算法生成盐:

SELECT gen_salt('md5') ;

请注意,salt 还编码有关用于散列的加密算法类型的信息,以及(如果适用)散列参数。 尝试重复生成相同类型的盐; 观察相同类型的盐遵循一致的模式。

存储新密码

当用户创建新密码(或更改其现有密码)时,数据库需要存储密码(的哈希值)。 哈希是使用 crypt() 功能。

crypt() 功能

crypt() 功能是基于 Unix地穴库. 它根据以下参数生成哈希:

  1. 密码字符串

  2. 盐值

的语法 crypt() 功能是:

-- pseudocode

crypt(password_string, salt_string) ;

salt_string 参数是使用生成的 gen_salt() 功能。 为了 example使用 md5 计算散列的算法:

-- pseudocode

SELECT crypt(password_string, gen_salt('md5')) ;

无盐散列

为了模拟使用 crypt() 没有随机盐的函数,使用常量字符串, salt, 为盐。 生成密码的哈希值, supersecurepassword:

SELECT crypt('supersecurepassword', 'salt') ;

以上命令生成密文 saUkChKIZTKFs. 非随机盐(或无盐)会导致可预测的哈希值,并使系统容易受到彩虹表攻击。 因此,有必要使用随机盐。

使用随机盐进行密码散列

为密码字符串生成哈希 supersecurepassword 使用,为了 example、MD5算法:

SELECT crypt('supersecurepassword', gen_salt('md5')) ;

复制上述命令生成的哈希值。 您将在下一节中使用它。

检查输入的密码是否正确

当现有用户登录时,他们输入用户名和密码。 服务器需要检查输入的密码是否与存储的密码匹配。 这是通过调用相同的 crypt() 具有以下参数的函数:

  1. 输入的密码

  2. 实际密码的存储散列(而不是生成的盐)

如果输入的密码与实际密码相同,则输入密码的哈希值与存储的(实际密码的)哈希值匹配。

一般语法是:

--pseudocode

crypt(entered_password, stored_hash_of_actual_password) ;

假设用户输入了正确的密码, supersecurepassword. 要对此进行测试,请调用 crypt() 功能如下:

SELECT crypt('supersecurepassword', 'generated_hash_value_from_actual_password') ;

上面的第二个参数是上一个命令生成的散列值——粘贴你之前复制的散列值。 此命令输出与实际密码生成的哈希值相同的哈希值。

现在,假设用户输入了错误的密码, wrong_password. 打电话给 crypt() 功能如下:

SELECT crypt('wrong_password', 'generated_hash_value_from_actual_password') ;

输出的哈希值与实际密码的哈希值不同。

换句话说,调用 crypt() 带有字符串的函数 (string1) 和散列 (hash1)那个字符串(string1) 生成相同的散列 (hash1).

实际使用

在实际使用中,哈希存储在表中并从表中查询。 本节中的示例展示了怎样做到这一点:

创建一个 user_account 包含三列的表 – 自动生成的 ID、用户名和用户密码的哈希值:

CREATE table user_account (user_id SERIAL, user_name VARCHAR(10), password_hash VARCHAR(100)) ;

在表中插入一行测试数据:

INSERT INTO user_account (user_name, password_hash) 

VALUES ('user1', crypt('user1_password', gen_salt('md5'))) ;

上面的命令插入用户名 user1,以及密码的 MD5 散列值, user1_password.

要更改用户密码的(哈希值),请使用 SQL UPDATE 命令:

UPDATE user_account 

SET password_hash = crypt('user1_new_password', gen_salt('md5')) 

WHERE user_name="user1" ;

要将输入的密码与正确的密码相匹配,请调用 crypt() 使用输入的密码和正确密码的哈希作为盐来运行。

如果用户尝试使用正确的密码登录, user1_new_password:

SELECT (password_hash = crypt('user1_new_password', password_hash)) 

    AS password_match 

FROM user_account 

WHERE user_name="user1" ;

这应该输出 t, 为了 true作为价值 password_match.

如果登录尝试使用了错误的密码, user1_wrong_password:

SELECT (password_hash = crypt('user1_wrong_password', password_hash)) 

    AS password_match 

FROM user_account 

WHERE user_name="user1" ;

这应该输出 f, 为了 false作为价值 password_match.

高级用法

当使用扩展 DES 或 Blowfish 算法计算散列时,可以自定义(调整)算法生成散列所经历的迭代次数。 在这种情况下, gen_salt() 函数可以接受一个额外的参数, iter_count, 为迭代次数。 语法是:

-- pseudocode

gen_salt(type, iter_count)

在上面的声明中, type 或者是 xdes 或者 bf.

  1. 扩展 DES 的迭代次数 (xdes) 算法可以是 1 到 16777215 之间的奇数。默认值为 725。

  2. Blowfish 的迭代次数 (bf) algorithm 可以是 4 到 31 之间的整数。默认值为 6。

如果哈希已经在 N 次迭代后生成,则任何暴力破解尝试也需要对密码猜测进行哈希 N 次。 N 的值越大,暴力破解密码哈希的难度就越大。 然而,使散列计算太慢对于常规使用是不切实际的。 作为演示,散列密码 supersecurepassword 使用具有默认迭代次数 6 的 Blowfish 算法:

SELECT crypt('supersecurepassword', gen_salt('bf', 6)) ;

注意大约需要多长时间(使用计算机或手机上的时钟)。 现在运行具有更高迭代次数的相同哈希函数:

SELECT crypt('supersecurepassword', gen_salt('bf', 30)) ;

这需要更长的时间。 如果花费的时间太长,请使用取消操作 CTRL + C 并以较少的迭代次数重试。

如果您输入无效的迭代次数,则会出现如下错误:

ERROR:  gen_salt: Incorrect number of rounds

调整哈希函数

选择迭代次数是可用性和安全性之间的平衡。 迭代次数越多,计算哈希所需的时间就越长。 这使得它不太用户友好,但也通过阻止暴力尝试来提高安全性。 一个常见的建议是选择迭代次数,使标准服务器硬件每秒可以计算 4 到 100 个哈希值。

结论

实际上,密码是次优的解决方案。 在服务器上存储密码(即使以散列形式)将全部责任推给应用程序开发人员和管理员。 身份验证是一个复杂的主题,建议使用专门从事它的专门服务提供商,例如 OpenID. 允许用户使用一组标准凭据(例如他们的 Google 帐户)登录对用户和应用程序开发人员都是有利的。

尽管如此,基于密码的身份验证在许多情况下都很方便并且被广泛使用。 因此,必须小心谨慎地安全存储用户密码,并保护用户的安全和应用程序的完整性。

注意事项

  1. 本文仅讨论怎样在数据库中安全地存储密码。 这也称为保护静态数据。 同样重要的是保护传输中的数据。 通过 Internet 传输密码时(例如 example,从 Web 前端)始终确保 1)使用 HTTPS 和 2)将登录凭据作为表单数据发送,而不是作为 URL 参数。

  2. 在密码被散列之前,它以明文形式提供。 这使得它容易受到有权访问数据库和/或网络服务器的不法员工的攻击。 因此,上述系统依赖于信任应用程序的开发人员和管理员。 如果无法建立这种信任,请考虑在客户端使用加密方法。 这样,明文密码永远不会出现在服务器上。

  3. 此外,散列密码仅对获得对数据库(或数据库转储)的只读访问权限的攻击者有效。 对数据库具有写访问权限的攻击者会发现简单地覆盖密码哈希会更容易。

最终,没有任何安全措施是真正万无一失的。 更强的安全措施意味着更复杂的系统设计或更不友好的系统。 您需要根据特定应用程序的安全需求取得平衡。

文章标题 名称(可选) 电子邮件(可选) 描述

发送建议

注:本教程在Vultr VPS上测试通过,如需部署请前往Vultr.com