使用System.console()读取密码

时间:2014-12-26 21:02:30

标签: java

java.io.Console类的javadoc中有一个安全提示:

  

安全说明:如果应用程序需要读取密码或其他安全数据,则应使用readPassword()或readPassword(String,Object ...)并在处理后手动将返回的字符数组归零,以最大限度地缩短敏感的生命周期内存中的数据。

 Console cons;
 char[] passwd;
 if ((cons = System.console()) != null &&
     (passwd = cons.readPassword("[%s]", "Password:")) != null) {
     ...
     java.util.Arrays.fill(passwd, ' ');
 }

我不明白为什么你需要这么激烈的措施? 当读取密码的方法弹出堆栈时, passwd 本地变量引用的数组对象将有资格进行垃圾回收。假设数组没有逃避方法范围,没有人(甚至是攻击者)可以获得对该数组的引用。

那么为什么你需要修改数组(删除密码),当你知道一旦方法弹出堆栈就有资格获得GC? 他们说:

  

最小化内存中敏感数据的生命周期

但对我而言,这种编程风格似乎相当......绝望。

4 个答案:

答案 0 :(得分:11)

仅仅因为对象符合垃圾收集条件并不意味着它会立即被垃圾收集。在垃圾收集实际执行之前的那段时间内,攻击者可能会获得堆内存转储,例如,他们可以从中检索密码。

通过将机会窗口归零来最小化。

修改:实践实验:

创建以下Java程序:

public class Main {
    private static void readPassword() {
        char[] password = System.console().readPassword();
    }

    public static void main(String[] args) throws Exception {
        readPassword();
        Thread.sleep(1000 * 3600);
    }
}
  1. javac Main.java
  2. java Main
  3. 输入密码(例如topsecret),按Enter键
  4. 接下来打开另一个终端,找出进程的PID(假设它是1000)并使用jmap创建堆转储:

    jmap -dump:format=b,file=dump.bin 1000
    

    安装并打开VisualVM探查器,转到文件/加载并选择刚刚创建的堆转储。

    接下来转到OQL控制台并运行以下查询:

    select a from char[] a where a.length == 9 && a[0] == 't'
    

    正如您在附带的屏幕截图中看到的那样,找到了包含“topsecret”的数组,即使在进行堆转储时,也没有对该数组的可访问引用。这样就可以证明,即使它们被本地引用,对象仍然会堆在堆上,直到收集垃圾为止。

    VisualVM

    现在,如果我要将数组清空并再次尝试整个过程,则找不到包含密码的数组。

答案 1 :(得分:8)

如果我是攻击者并且在该阵列被垃圾收集之前可能导致堆转储,我可以检查其内容。无法保证GC会“很快”(或者根本不会)运行,如果你有一些非常敏感的东西,你可能不希望它出现在堆转储中的磁盘上,无论赔率多么微小。

答案 2 :(得分:2)

这个问题让我想起了来自answer

的热门Why is char[] preferred over String for passwords?中的讨论
  

如评论中所述,垃圾收集器移动的数组可能会将数据的杂散副本留在内存中。我相信这是特定于实现的 - GC可以清除所有内存,以避免这种情况。即使它确实存在,仍然有时间char []包含实际字符作为攻击窗口。

答案 3 :(得分:2)

  

当读取密码的方法从堆栈中弹出时,passwd局部变量引用的数组对象将有资格进行垃圾回收。假设数组没有逃避方法范围,没有人(甚至是攻击者)可以获得对该数组的引用。

唉,这并不是那么简单:Java语言不能保证垃圾收集会在多长时间内发生,并且攻击者有可能在同一时间内读取内存。

  

最小化内存中敏感数据的生命周期

这个建议显然针对威胁模型,攻击者对堆内存具有间歇性读取访问权限,我们仍然必须尽最大努力使攻击者更难获取(但不一定是不可能)获取敏感数据。

你是否拥有这种威胁模型只是你可以评估的东西,但它并不像初看起来那样荒谬,因为所涉及的各种系统并不一定能保证主存的隐私,例如:

  • JVM

    • 堆转储还包含无法访问的对象(某些工具在打开转储文件时会过滤这些对象,但它们存在于文件中并且可以恢复)
    • 大多数垃圾收集器将活动对象从旧存储空间复制到新存储空间。这些副本可能比对象本身更长,并显示在进程'内存的转储中。
  • 操作系统

    • 操作系统的虚拟内存管理器可以选择将内存交换到磁盘,在关闭后可以保留,并恢复(例如通过引导到另一个操作系统)
    • 操作系统的休眠功能还将主内存写入磁盘
    • 某些操作系统的虚拟内存管理器在将其重新分配给另一个进程时不会清除物理内存。然后,其他进程可以读取前一进程留下的数据。
    • 操作系统可能允许进程调试另一个进程,使一个进程可以访问另一个进程的内存。
  • 管理程序

    • 当虚拟机管理程序获取VM的快照时,它会将虚拟主内存写入磁盘。如果攻击者可以访问该快照......
  • 硬件

    • 系统的主存储器即使在断电后也可以保持状态,从而能够从另一个操作系统或计算机检查数据。也许令人惊讶的是,大多数现代DRAM芯片属于这一类(source
    • 当然,硬件可以包含后门,如reported for Cisco's routers

尽管如此,大多数这些攻击都假定一定程度的访问可以实现更严重的攻击,因此无论如何都应该加以防范,在这种情况下,减少内存中敏感数据的生命周期是相当多余的。就个人而言,我不会保护数据,而是将其存储在内存中尽可能短的时间,但是通过使用可信硬件来保护内存的隐私,在物理上防止篡改,由专业人员配置的安全操作系统,以及操作系统级访问限制为实用的,以及任何类型的备份和诊断转储的安全存储。

因此,我认为使用char[]密码很少有用。顺便提一下,这个观点似乎是由JDBC API的设计者共享的,requires connection passwords to be passed as String阻止了它们在建立连接后被清除。

尽管如此,可能会出现无法保证主存储器隐私的情况,并且通过尽可能减少漏洞窗口来减轻损害是合适的。您的威胁模型应该回答是否是这种情况。