在 Java 中,使用 char[] 而不是 String 来存储密码还有意义吗?

我在 char[]Usenet 天了解了使用 comp.lang.java.* 来存储密码。

搜索 Stack Overflow,您还可以轻松找到像这样被高度评价的问题: [Why is char[] preferred over String for passwords?](https://stackoverflow.com/questions/8881291/why-is-char-preferred-over-string-for-passwords) 这与我很久很久以前学到的一致。

我仍然编写我的 API 以使用 char[] 作为密码。但现在这只是空洞的理想吗?

例如,查看 Atlassian Jira 的 Java API: LoginManager.authenticate,它将您的密码作为字符串。

Thales ' Luna Java API: login() method in LunaSlotManager 。在所有人中,有一个 HSM 供应商使用 String 作为 HSM 插槽密码。

我想我还在某处读到 URLConnection(和许多其他类)的内部使用 String 在内部处理数据。因此,如果您曾经发送过密码(尽管密码通过网络通过 TLS 加密),它将在您的服务器内存中的字符串中。

访问服务器内存是一个难以实现的攻击因素,现在可以将密码存储为 String 吗?还是 Thales 这样做是因为由于其他人编写的课程,您的密码无论如何都会以 String 结尾?

stack overflow In Java, is there still a point in using char[] instead of String to store passwords?
原文答案

答案:

作者头像

首先,让我们回想一下 reason 建议使用 char[] 而不是 StringString 是不可变的,因此一旦创建字符串,对字符串内容的控制是有限的直到(可能之后)内存被垃圾收集。因此,可以转储进程内存的攻击者可能会读取密码数据。同时, char[] 对象的内容可以在创建后被覆盖。假设这已经完成,并且 GC 在此期间没有将对象移动到另一个物理内存位置,这意味着密码内容可以在使用后确定性地(在某种程度上)被销毁。在那之后读取进程内存的攻击者将无法获得密码。

所以使用 char[] 而不是 String 可以防止 very specific 攻击场景,其中攻击者可以完全访问进程内存,1 但只能在特定时间点而不是连续访问。即使在这种情况下,使用 char[] 并覆盖其内容不会 prevent 攻击,它只会降低其成功的机会(如果攻击者碰巧在创建和删除密码之间读取进程内存,他们可以阅读)。

我不知道 any 证据表明(a)这种情况有多频繁,也不知道(b)这种缓解在多大程度上降低了这种情况下的成功概率。据我所知,这纯属猜测。

~事实上,在大多数系统上,这种情况很可能是 does not exist at all: ,一个可以访问另一个进程内存的攻击者也可以获得 full tracing access 。例如,在 Linux 和 Windows 上,任何可以读取另一个进程内存的进程也可以向该进程注入任意逻辑(例如,通过 LD_PRELOAD 和类似机制2)。所以我想说这种缓解 at best 的好处有限,而且可能根本没有。~

…实际上我可以想到一个具体的反例:加载不受信任的插件库的应用程序。一旦通过传统方式(即在同一内存空间中)加载该库,它就可以访问父应用程序。在这种情况下,使用 char[] 而不是 String 可能有意义,并在完成后覆盖其内容,如果处理了密码 before 插件已加载。但更好的解决方案是不要将不受信任的插件加载到相同的内存空间中。一种常见的替代方法是在单独的进程中启动它并通过 IPC 进行通信。

(对于更脆弱的场景,请参阅 Gilles 的答案。我仍然认为好处是相对有限的,但显然不是零。)


1 如 Gilles 的回答所示,这是不正确的:无需 full 内存访问即可成功发起攻击。

2 尽管 LD_PRELOAD 明确要求攻击者不仅可以访问另一个进程,还可以访问 launch 该进程,或者访问其父进程。

作者头像

(注意:我是安全专家,但不是 Java 专家。)

是的,使用 char[] 而不是字符串作为密码有显着的安全优势。这在某种程度上也适用于其他高度机密的数据,尽管大多数高度机密的数据(例如加密密钥)往往是字节而不是字符。

使用 char[] 的旧且仍然有效的原因是在使用内存时立即清理内存,而使用 String 则无法做到这一点。这是一种非常牢固的安全实践。例如,在 (in) 著名的密码处理 FIPS 140 要求中,通常被认为是安全要求,实际上级别 1(最简单的级别)的安全要求非常少。实际上只有两个:一个是您只能使用经过批准的加密算法,另一个是密钥、密码和其他敏感数据必须在使用后擦除。

这种做法是密码原语的生产实现通常使用具有手动内存管理的语言(例如 C、C++ 或 Rust)实现的原因之一:密码学实施者希望保留对敏感数据去向的控制权,并确保擦除所有副本敏感材料。

作为可能出错的示例,请考虑(不)著名的 Heartbleed 错误。它允许 Internet 上连接到易受攻击的服务器的任何人转储服务器的一些内存,而不会被检测到。攻击者无法控制内存的哪一部分,但可以一次又一次地尝试。攻击者可以发出请求,导致可转储部分在堆中移动,因此可能会转储整个内存。

这种bug很常见吗?不,这个引起了很多关注,因为它在一个非常流行的软件中并且后果很糟糕。但是这样的错误确实存在,最好防止它们。

另外,从Java 8开始,还有一个原因,就是避免字符串去重String deduplication 表示如果两个 String 对象的内容相同,则可以合并。如果攻击者在尝试重复数据删除时可以挂载 side channel attack ,则字符串重复数据删除会出现问题。攻击确实 not 需要对密码进行重复数据删除(尽管在这种情况下更容易):一旦某些代码将密码与另一个字符串进行比较,就会出现问题。

比较字符串是否相等的常用方法是:

  • 如果长度不同,则返回 false。
  • 否则一一比较字符。只要一个位置有不同的字符,就返回false。
  • 如果到达字符串的末尾而没有遇到差异,则返回 true。

这有一个计时侧通道:中间步骤的时间取决于字符串开头的相同字符的数量。假设攻击者可以测量这个时间,并且可以上传一些字符串进行比较(例如,通过向服务器发出合法请求)。攻击者注意到与 sssssssss 比较比与 aaaaaaaaa 比较花费的时间稍长,因此密码必须以 s 开头。然后攻击者尝试改变第二个字符,并发现与 swwwwwwww 比较再次需要稍长的时间。因此,攻击者可以在相对较短的时间内逐个字符地重构密码。

在字符串重复数据删除的上下文中,攻击更加困难,因为(据我所知)重复数据删除代码首先对要比较的字符串进行哈希处理。这可能意味着攻击者必须首先猜测哈希值。但是给定哈希表中哈希值的总数(即哈希桶的数量,而不是 hash 方法的全部范围)足够小,可以枚举。

可以肯定的是,这不是一次简单的攻击。但我绝对不会排除它,尤其是对于本地攻击者,但即使对于远程攻击者也是如此。 Remote timing attacks are practical ( still )。

总之,是的,您不应该使用 String 作为密码。将它们读取为 char[] ,仔细跟踪任何副本,如果您正在验证它们,请尽快 hash 并擦除所有副本。

如果您需要为第三方服务存储密码,即使没有单独的加密密钥访问控制,最好以加密形式存储它。加密密码的副本比密码本身的副本更不容易通过侧通道泄漏,密码本身是一个低熵的可打印字符串。

我想我还在某处读到 URLConnection(和许多其他类)的内部使用 String 在内部处理数据。因此,如果您曾经发送过密码(尽管密码是通过 TLS 在线加密的),它将在您的服务器内存中的字符串中。

我不是 Java 专家,但这听起来不对:连接的明文(TLS 或其他)是字节流,而不是字符流。它应该是 8 位字节的数组,而不是 Unicode 代码点的数组。

或者,由于其他人编写的课程,您的密码无论如何都会以字符串结尾,这就是 Thales 这样做的原因ing它。

可能。或者可能是因为他们不是 Java 专家,或者因为编写高级层的人通常不是最重要的安全专家。

作者头像

答案中有很多细节,但这里是它的短处:是的,理论上,将密码放在一个数组中并擦除它可以提供安全优势。实际上,只有当您可以避免将密码存储在字符串中时,这才有帮助。也就是说,如果您将密码存储在 String 中并将 String 的内容放入 char[] 中,它不会神奇地使 String 从堆中消失。必要的要求是密码永远不会放在字符串中。我很想看到它在真正的 Java 应用程序中成功实现。

作者头像

几乎所有其他人的答案加上一点:交换存储介质上的空间。

如果 JVM 堆被分页到磁盘并且密码仍然作为字符串(不可变而不是 GC'd)在内存中,它将被写入交换文件。然后可以扫描此交换文件以查找密码值,因此,本质上是另一种基于时间的攻击向量,仍然很难利用,但显然不是那么困难,因为我们在这里:D。

擦除可变数组至少可以减少密码在内存中的时间。

我听到的故事是,如果攻击者可以攻击您的进程(如 DDOS)并触发进程换出,那么攻击交换空间比攻击内存要容易一些,并且交换空间在引导/崩溃/等过程中被保留。这允许另一个攻击向量,攻击者将交换驱动器拉出以扫描交换空间。

作者头像

这不是通过网络传输的时刻的想法。确实,使用 String 确实更好,因为它更方便用于通过网络发送,当然要确保它已正确加密。

由于堆栈转储和逆向工程,以及字符串不可变的问题,在应用程序中使用密码是不同的:如果输入了密码,即使对变量的引用更改为另一个字符串,也无法确定关于垃圾收集器何时真正从堆中删除字符串。因此,能够看到转储的黑客也将能够看到密码。使用 char 数组可以防止这种情况发生,因为您可以直接更改数组中的数据,而无需依赖垃圾收集器。

现在你可能会说:那么当它通过网络作为字符串发送时,它仍然是可见的,不是吗?是的,但这就是为什么在发送之前对其进行加密很重要的原因。尽可能不要通过网络发送纯文本密码。