使用反射技术在垃圾回收之前清除内容,安全地使用字符串作为密码。

16

使用反射来擦除字符串(String)是否能让使用字符串与使用char[]一样安全?

从安全角度考虑,通常最好使用char[]存储/传递密码,因为可以在代码中尽早将其内容清零,这可能会比垃圾回收清理并重复使用内存(擦除所有痕迹)的时间要短得多,从而缩短了进行内存攻击的窗口。

但是,char[]不如String方便,因此如果需要的话,将String“擦洗”一下可能很方便,从而使Stringchar[]一样安全。

下面是一种使用反射来清空String字段的方法。

这个方法“可以”,并且是否实现了使Stringchar[]一样安全的目标?

public static void scrub(String str) throws NoSuchFieldException, IllegalAccessException {
    Field valueField = String.class.getDeclaredField("value");
    Field offsetField = String.class.getDeclaredField("offset");
    Field countField = String.class.getDeclaredField("count");
    Field hashField = String.class.getDeclaredField("hash");
    valueField.setAccessible(true);
    offsetField.setAccessible(true);
    countField.setAccessible(true);
    hashField.setAccessible(true);
    char[] value = (char[]) valueField.get(str);
    // overwrite the relevant array contents with null chars
    Arrays.fill(value, offsetField.getInt(str), countField.getInt(str), '\0');
    countField.set(str, 0); // scrub password length too
    hashField.set(str, 0); // the hash could be used to crack a password
    valueField.setAccessible(false);
    offsetField.setAccessible(false);
    countField.setAccessible(false);
    hashField.setAccessible(false);
}

这里有一个简单的测试:

String str = "password";
scrub(str);
System.out.println('"' + str + '"');

输出:

""

注意: 您可以假设密码不是String常量,因此调用此方法不会对内部化字符串产生不利影响。

另外,出于简单起见,我将该方法保留在相当“原始”的状态。如果我要使用它,我将不会声明抛出的异常(try/catch/忽略它们)并重构重复的代码。


嗯,它似乎将字符归零,但这不是非常依赖于JVM的实现吗? - fge
内存窥探?那只是一种幻想攻击。为什么不担心更现实的事情,比如NSA窥探您的网络流量。 - ZhongYu
对我尖叫“未定义行为”。祝你好运,面对龙、迅猛龙和黑洞 :) - CodesInChaos
3
你的代码依赖于String类的实现。而恰巧从7u6版本开始,Oracle JDK/openJDK采用了不同的实现方式,因此你的“scrub”方法会抛出异常。 - assylias
一个好的String实现不会有偏移量。你应该更加健壮(这在大多数反射代码中并不常见)。“内存窥探”并不是完全不切实际的攻击,特别是当你考虑到交换/休眠可能会将数据放置在物理介质上很长一段时间时。 - Tom Hawtin - tackline
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
7

存在两个潜在的安全问题:

  1. 字符串可能与其他字符串共享其后备数组;例如,如果通过在较大的 String 上调用 substring 来创建 String,那么当您将整个 value 数组设为零时,您有可能会覆盖其他不包含密码的字符串的状态。

    解决方法是只清除密码字符串使用的后备数组部分。

  2. Java 语言规范(JLS)(17.5.3) 警告使用反射更改 final 变量的影响是未定义的。

    然而,这个背景是 Java 内存模型和编译器允许积极缓存 final 变量。在这种情况下:

    • 您预计该字符串将成为线程限定的,并且

    • 您不应再使用任何这些变量。

我不认为这些都是真正的问题...修复过于激进的 value 清零后,便没有了问题。


但真正的问题是迅猛龙。 :-)


我感到困惑的是您为什么要打这样的密码。如果您考虑一下,您正在防止的是某人可以读取进程内存...或者核心转储或交换文件...以检索密码。但是,如果有人可以做到这一点,则您的系统安全必须已经受到了损害...因为那些东西很可能需要 root 访问权限(或等效权限)。而且,如果他们有 root 访问权限,他们可以“调试”您的程序并在应用程序清除密码之前捕获密码。


也许代码过于粗暴/简单。如果只清除所指向的数组部分呢?并且仅将计数器设置为零(以消除密码长度的痕迹)呢? - Bohemian
主要关注点——安全性,不受此问题影响。在重置后,字符串可能会被视为损坏。 - Marko Topolnik
好的 - 我已经编辑了代码,只清理实际使用的数组部分。 - Bohemian
@MarkoTopolnik - 这要看情况。如果你认为安全比所有其他问题(包括应用程序的正确功能)都更重要,那么我猜密码删除器损坏其他字符串也没关系。否则...你至少应该考虑一下这个问题。 - Stephen C
我明白你的意思 - 我的评论并不是关于结构共享的问题。但是另一个相关的危险正在潜伏:使用反射会使你承诺使用特定的字符串实现。 - Marko Topolnik
1
@MarkoTopolnik - 是的,如果String使用的(私有)表示发生了变化,最可能的结果是zapper中出现反射异常。 - Stephen C

1

我对String的反对意见之一是,不小心复制字符串太容易了。理论上可以安全地使用字符串,但整个库生态系统都基于这样的假设:复制字符串完全没有问题。最终考虑到所有限制,字符串可能并不像它们通常的用例那样方便。


但是一个人也可以复制char[]。尽管你的观点是正确的,但密码周围的编码纪律应该受到严密的审查,以确保不管类型如何都不会进行复制。 - Bohemian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,