提问者:小点点

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


我学会了使用char[]comp.lang.java中存储Usenet时代的密码。*

搜索Stack Overflow,您还可以轻松找到像这样的高度支持的问题:为什么char[]比String更受欢迎?这与我很久以前学到的一致。

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

例如,看看艾特莱森Jira的JavaAPI:LoginManager.验证,它将您的密码作为字符串。

或者Thales在LunaSlotManager中的LunaJava: login()方法。在所有人中,HSM供应商使用String作为HSM插槽密码。

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

访问服务器内存是一个如此难以实现的攻击因素,以至于现在可以将密码存储为String吗?还是Thales这样做是因为由于其他人编写的类,您的密码最终会出现在String中?


共3个答案

匿名用户

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

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

我不知道有任何证据显示(a)这种情况有多频繁,也不知道(b)这种缓解在该情况下降低了多少成功的可能性。据我所知,这纯粹是猜测。

事实上,在大多数系统上,这种情况可能根本不存在:可以访问另一个进程内存的攻击者也可以获得完全跟踪访问权限。例如,在Linux和Windows上,任何可以读取另一个进程内存的进程也可以向该进程注入任意逻辑(例如,通过LD_PRELOAD和类似的机制2)。所以我想说这种缓解充其量只有有限的好处,而且可能根本没有好处。

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

(更易受攻击的场景见Gilles的回答,我还是觉得收益比较有限,但明明不是nil。)

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

2尽管LD_PRELOAD特别要求攻击者不仅可以访问另一个进程,还可以启动该进程或访问其父进程。

匿名用户

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

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

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

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

举一个可能出错的例子,考虑一下著名的Heartbleedbug。它允许互联网上任何连接到易受攻击服务器的人转储服务器的部分内存,而不会被检测到。攻击者无法控制内存的哪一部分,但可以一次又一次地尝试。攻击者可以发出请求,导致可转储部分在堆中移动,从而可能转储整个内存。

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

此外,从Java8开始,还有另一个原因,这就是避免字符串消重。字符串消重意味着如果两个String对象具有相同的内容,它们可能会被合并。如果攻击者可以在尝试重消时发起侧通道攻击,则字符串消重是有问题的。该攻击不需要密码消重(尽管在这种情况下更容易):一旦某些代码将密码与另一个字符串进行比较,就会出现问题。

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

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

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

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

可以肯定的是,这不是一个容易的攻击。但我绝对不排除它,尤其是对于本地攻击者,但即使是远程攻击者。远程定时攻击是实用的(仍然)。

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

如果您需要为第三方服务存储密码,即使没有对加密密钥进行单独的权限改造,也最好将其以加密形式存储。与密码本身的副本相比,加密密码的副本更不容易通过侧通道泄露,密码本身是一个低熵的可打印字符串。

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

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

或者由于其他人编写的类,您的密码最终会出现在字符串中,这就是Thales这样做的原因。

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

匿名用户

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