加密存储速查表¶
简介¶
本文提供了一个简单的模型,用于实现保护静态数据的解决方案。
密码不应使用可逆加密存储——应改用安全的密码哈希算法。密码存储速查表包含有关存储密码的更多指导。
架构设计¶
设计任何应用程序的第一步是考虑系统的整体架构,因为这将对技术实现产生巨大影响。
此过程应从考虑应用程序的威胁模型(即,您试图保护数据免受何种威胁)开始。
使用专用密钥或秘密管理系统可以提供额外的安全保护层,并使秘密管理变得显著更容易——但这会增加复杂性和管理开销——因此可能不适用于所有应用程序。请注意,许多云环境提供这些服务,因此应尽可能利用它们。秘密管理速查表包含有关此主题的更多指导。
在哪里执行加密¶
加密可以在应用程序堆栈的多个级别执行,例如:
- 在应用程序级别。
- 在数据库级别(例如,SQL Server TDE)
- 在文件系统级别(例如,BitLocker 或 LUKS)
- 在硬件级别(例如,加密的 RAID 卡或 SSD)
最合适的层级将取决于威胁模型。例如,硬件级加密在防御服务器物理盗窃方面是有效的,但如果攻击者能够远程入侵服务器,则无法提供保护。
最小化敏感信息存储¶
保护敏感信息的最佳方法是根本不存储它。尽管这适用于所有类型的信息,但它最常适用于信用卡详细信息,因为它们对攻击者来说极具吸引力,并且 PCI DSS 对其存储方式有严格的要求。在可能的情况下,应避免存储敏感信息。
算法¶
对于对称加密,应首选使用密钥至少为 128 位(理想情况下为 256 位)且具有安全模式的 AES 算法。
对于非对称加密,首选使用椭圆曲线密码学(ECC)和安全曲线(如 Curve25519)作为算法。如果 ECC 不可用且必须使用 RSA,请确保密钥至少为 2048 位。
还有许多其他对称和非对称算法,它们各有优缺点,在特定用例中可能优于或劣于 AES 或 Curve25519。在考虑这些算法时,应考虑许多因素,包括:
- 密钥大小。
- 算法的已知攻击和弱点。
- 算法的成熟度。
- 第三方(如 NIST 的算法验证计划)的批准。
- 性能(加密和解密)。
- 可用库的质量。
- 算法的可移植性(即,其支持范围有多广)。
在某些情况下,可能存在限制可使用算法的监管要求,例如 FIPS 140-2 或 PCI DSS。
自定义算法¶
不要这样做。
加密模式¶
有各种模式可用于允许分组密码(如 AES)加密任意数量的数据,就像流密码一样。这些模式具有不同的安全性和性能特性,全面讨论它们超出了本速查表的范围。某些模式要求生成安全的初始化向量(IV)和其他属性,但这些应由库自动处理。
在可用时,应始终使用认证模式。这些模式提供数据完整性、真实性以及机密性的保证。最常用的认证模式是 GCM 和 CCM,应优先使用它们。
如果 GCM 或 CCM 不可用,则应使用 CTR 模式或 CBC 模式。由于这些模式不提供数据真实性的任何保证,因此应实现单独的认证,例如使用 先加密后MAC 技术。使用此方法处理可变长度消息时需要谨慎。
ECB 不应在非常特殊的情况下使用。
随机填充¶
对于 RSA,启用随机填充至关重要。随机填充也称为 OAEP 或最佳非对称加密填充。此类防御通过在负载开始时添加随机性来防止已知明文攻击。
在这种情况下,通常使用 PKCS#1 的填充方案。
安全随机数生成¶
加密密钥、IV、会话 ID、CSRF 令牌或密码重置令牌等各种安全关键功能都需要随机数(或字符串)。因此,安全生成这些数字至关重要,并且攻击者不可能猜测和预测它们。
计算机通常无法生成真正的随机数(不带特殊硬件),因此大多数系统和语言提供两种不同类型的随机性。
伪随机数生成器 (PRNG) 提供低质量的随机性,速度快得多,可用于非安全相关功能(如页面上的排序结果或随机化 UI 元素)。但是,它们绝不能用于任何安全关键功能,因为攻击者通常可以猜测或预测其输出。
加密安全伪随机数生成器 (CSPRNG) 旨在产生更高质量的随机性(更严格地说,更大的熵量),使其可以安全地用于安全敏感功能。但是,它们速度较慢且 CPU 密集,在请求大量随机数据时,在某些情况下可能会阻塞。因此,如果需要大量非安全相关的随机性,它们可能不适合。
下表显示了每种语言的推荐算法,以及不应使用的不安全函数。
语言 | 不安全函数 | 加密安全函数 |
---|---|---|
C | random() , rand() |
getrandom(2) |
Java | Math.random() , StrictMath.random() , java.util.Random , java.util.SplittableRandom , java.util.concurrent.ThreadLocalRandom |
java.security.SecureRandom, java.util.UUID.randomUUID() |
PHP | array_rand() , lcg_value() , mt_rand() , rand() , uniqid() |
random_bytes(), PHP 8 中的 Random\Engine\Secure, PHP 7 中的 random_int(), PHP 5 中的 openssl_random_pseudo_bytes() |
.NET/C# | Random() |
RandomNumberGenerator |
Objective-C | arc4random() /arc4random_uniform() (使用 RC4 密码), GKRandomSource 的子类, rand(), random() |
SecRandomCopyBytes |
Python | random() |
secrets() |
Ruby | rand() , Random |
SecureRandom |
Go | 使用 math/rand 包的 rand |
crypto.rand 包 |
Rust | rand::prng::XorShiftRng |
rand::prng::chacha::ChaChaRng 和 Rust 库的其余 CSPRNGs。 |
Node.js | Math.random() |
crypto.randomBytes(), crypto.randomInt(), crypto.randomUUID() |
UUID 和 GUID¶
通用唯一标识符(UUID 或 GUID)有时被用作快速生成随机字符串的方法。尽管它们可以提供合理的随机性来源,但这将取决于所创建 UUID 的类型或版本。
具体来说,版本 1 UUID 由高精度时间戳和生成它们的系统的 MAC 地址组成,因此不是随机的(尽管考虑到时间戳精确到 100ns,它们可能很难猜测)。类型 4 UUID 是随机生成的,但其是否使用 CSPRNG 完成将取决于实现。除非已知在特定语言或框架中是安全的,否则不应依赖 UUID 的随机性。
纵深防御¶
应用程序应设计为即使加密控制失效也能保持安全。任何以加密形式存储的信息也应受到额外安全层的保护。应用程序也不应依赖加密 URL 参数的安全性,并应强制执行强访问控制以防止未经授权的信息访问。
密钥管理¶
流程¶
应实施(并测试)正式流程,涵盖密钥管理的所有方面,包括:
- 生成和存储新密钥。
- 将密钥分发给所需方。
- 将密钥部署到应用服务器。
- 轮换和废弃旧密钥
密钥生成¶
密钥应使用加密安全函数随机生成,例如安全随机数生成部分讨论的那些函数。密钥不应基于常用词语或短语,或基于通过乱敲键盘生成的“随机”字符。
当使用多个密钥时(例如数据加密密钥和密钥加密密钥分开),它们应完全相互独立。
密钥生命周期与轮换¶
加密密钥应根据以下几个不同标准进行更改(或轮换):
- 如果已知(或怀疑)之前的密钥已被泄露。
- 这也可能是由访问密钥的人员离开组织引起的。
- 在经过指定时间段后(称为密码周期)。
- 有许多因素可能会影响合适的密码周期,包括密钥的大小、数据的敏感性以及系统的威胁模型。有关更多指导,请参阅 NIST SP 800-57 第 5.3 节。
- 在密钥已用于加密特定数量的数据后。
- 对于 64 位密钥,通常为
2^35
字节(约 34GB),对于 128 位块大小,通常为2^68
字节(约 295 EB)。
- 对于 64 位密钥,通常为
- 如果算法提供的安全性发生重大变化(例如宣布了新的攻击)。
一旦满足这些条件之一,应生成新密钥并用于加密任何新数据。处理用旧密钥加密的现有数据主要有两种方法:
- 解密并用新密钥重新加密。
- 将每个项目标记为用于加密它的密钥 ID,并存储多个密钥以允许解密旧数据。
通常应首选第一种选项,因为它极大地简化了应用程序代码和密钥管理流程;然而,它可能并非总是可行。请注意,旧密钥通常应在退役后存储一段时间,以防需要解密旧数据备份或副本。
重要的是,在需要之前就准备好轮换密钥所需的代码和流程,以便在发生泄露时可以快速轮换密钥。此外,还应实施流程以允许更改加密算法或库,以防在算法或实现中发现新的漏洞。
密钥存储¶
安全地存储加密密钥是最难解决的问题之一,因为应用程序始终需要某种程度地访问密钥才能解密数据。虽然可能无法完全保护密钥免受已完全入侵应用程序的攻击者,但可以采取一些步骤,使他们更难获取密钥。
在可用时,应使用操作系统、框架或云服务提供商提供的安全存储机制。这些包括:
- 物理硬件安全模块 (HSM)。
- 虚拟 HSM。
- 密钥库,例如 Amazon KMS 或 Azure Key Vault。
- 外部秘密管理服务,例如 Conjur 或 HashiCorp Vault。
- .NET 框架中 ProtectedData 类提供的安全存储 API。
与简单地将密钥放入配置文件相比,使用这些类型的安全存储有许多优点。这些具体的优点将根据所使用的解决方案而异,但它们包括:
- 密钥的集中管理,尤其是在容器化环境中。
- 轻松的密钥轮换和替换。
- 安全密钥生成。
- 简化对 FIPS 140 或 PCI DSS 等法规标准的合规性。
- 使攻击者更难导出或窃取密钥。
在某些情况下,这些都不可用,例如在共享主机环境中,这意味着无法为任何加密密钥获得高度保护。但是,仍可遵循以下基本规则:
- 不要将密钥硬编码到应用程序源代码中。
- 不要将密钥提交到版本控制系统。
- 使用严格的权限保护包含密钥的配置文件。
- 避免将密钥存储在环境变量中,因为这些密钥可能通过
phpinfo()
或/proc/self/environ
文件等函数意外暴露。
秘密管理速查表提供了有关安全存储秘密的更多详细信息。
密钥与数据的分离¶
在可能的情况下,加密密钥应与加密数据存储在不同的位置。例如,如果数据存储在数据库中,则密钥应存储在文件系统中。这意味着如果攻击者只能访问其中之一(例如通过目录遍历或 SQL 注入),他们将无法同时访问密钥和数据。
根据环境的架构,可能可以将密钥和数据存储在不同的系统上,这将提供更大程度的隔离。
加密存储的密钥¶
在可能的情况下,加密密钥本身应以加密形式存储。为此至少需要两个独立的密钥:
- 数据加密密钥 (DEK) 用于加密数据。
- 密钥加密密钥 (KEK) 用于加密 DEK。
为了使其有效,KEK 必须与 DEK 分开存储。加密的 DEK 可以与数据一起存储,但只有当攻击者也能获取存储在另一个系统上的 KEK 时,它才能被使用。
KEK 的强度也应至少与 DEK 相同。Google 的信封加密指南包含有关如何管理 DEK 和 KEK 的更多详细信息。
在更简单的应用架构中(例如共享托管环境),如果 KEK 和 DEK 无法分开存储,则这种方法的价值有限,因为攻击者很可能能够同时获取两个密钥。但是,它仍然可以为不熟练的攻击者提供额外的屏障。
密钥派生函数(KDF)可用于从用户提供的输入(如密码短语)生成 KEK,然后用于加密随机生成的 DEK。这允许在不重新加密数据的情况下轻松更改 KEK(当用户更改其密码短语时)(因为 DEK 保持不变)。