题 为什么char []比字符串更适合密码?


在Swing中,密码字段有一个 getPassword() (返回 char[])方法而不是通常的方法 getText() (返回 String) 方法。同样,我遇到了一个不使用的建议 String 处理密码。

为什么 String 在密码问题上对安全构成威胁? 使用感觉不方便 char[]


2901
2018-01-16 14:20


起源


你能引用这个建议的来源吗?我无法想到一个理由 char[] 除了可能是最业余的威胁之外更安全。 - Viruzzo
@Viruzzo看看javadocs。该 getText() 的方法 JPasswordField 不赞成使用 getPassword() “出于安全原因”。 - dogbane
的char []?为什么不用byte []?在任何线程转储/堆分析器中,您都可以看到字符串,如果包含密码,则很容易注意到。对于char []来说,它更难,但是,如果你的堆分析器将逐个显示字符,它将与String相同。对于byte [],它不太可能。 - mvmn
只是为了提供一个有效的方案,这可能很重要,在法医学中通常的做法是确保在内存被丢弃之前没有关闭需要调查的计算机,以防它们使用软件加密。即使是最好的加密系统也需要维持一个能够解密的状态。 - Zenexer
@scottlafoy两个 char[] 和 String 对象将通过引用传递;个人 char 价值不会。这是对象传递的时间 值而不是参考,创建了多个副本。通常,这不是问题,因为这些对象通常设计得很小。 - Zenexer


答案:


字符串是不可变的。这意味着一旦你创造了 String,如果另一个进程可以转储内存,那就没办法了(除了 反射你以前可以摆脱数据 垃圾收集 踢进去。

使用数组,您可以在完成数据后显式擦除数据。您可以使用您喜欢的任何内容覆盖数组,即使在垃圾回收之前,密码也不会出现在系统的任何位置。

是的,这个  安全问题 - 但即使使用 char[] 只会减少攻击者的机会窗口,而且只针对这种特定类型的攻击。

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


3720
2018-01-16 14:26



我的理解是,通过运行时可以完成的内存重组量,甚至是一个副本 char[] 可能会留在内存中,永远不会被清除,直到该内存被重用。我不知道有哪个来源证实了这一点。无论哪种方式,我都怀疑String会在GC时间被清除,而是在Object重新分配到该内存时。 - Mark Peters
@Mark Peters:如果GC启动,在清空char数组之前,你有另一个副本。但是这个副本至少在内存的一个繁重的使用区域,所以很快被覆盖的机会很高。 - Mnementh
@ Xeon06:在.NET中有 SecureString 这是更好的 - 但使用相对痛苦。 - Jon Skeet
我们是否掩盖了这样一个事实:可以读取此数据的恶意实体已经读取了对您系统的内存访问权限?看起来更令人担忧的是,做这个char []而不是String只是看起来像在海洋里撒尿。 - corsiKa
@corsika:正如我在答案中提到的,它正在减少一个特定的攻击向量,就是这样。这会让攻击者更加困难。 - Jon Skeet


虽然这里的其他建议似乎有效,但还有另外一个好理由。平淡无奇 String 你有更高的机会 不小心将密码打印到日志,监视器或其他一些不安全的地方。 char[] 不那么脆弱了。

考虑一下:

public static void main(String[] args) {
    Object pw = "Password";
    System.out.println("String: " + pw);

    pw = "Password".toCharArray();
    System.out.println("Array: " + pw);
}

打印:

String: Password
Array: [C@5829428e

1080
2018-01-16 19:37



@voo,但我怀疑你是通过直接写入流和连接来记录的。日志框架会将char []转换为良好的输出 - bestsss
@ Thr4wn默认执行 toString 是 classname@hashcode。 [C 代表 char[],其余是十六进制哈希码。 - Konrad Garus
有趣的想法。我想指出,这不会转换为Scala,它对数组有一个有意义的toString。 - mauhiz
我写了一个 Password 类的类型。它不那么模糊,更难以意外地传递到某个地方。 - rightfold
为什么有人会假设char数组将被转换为Object?我不确定为什么每个人都喜欢这个答案。假设您这样做:System.out.println(“Password”.toCharArray()); - GC_


引用官方文件, Java密码体系结构指南 这说的 char[] 与 String 密码(关于基于密码的加密,但当然更常见的是关于密码):

在对象中收集和存储密码似乎是合乎逻辑的   类型 java.lang.String。但是,这里有一点需要注意: Object的   类型 String 是不可变的,即没有定义的方法   允许您更改(覆盖)或清除a的内容 String   使用后。这个功能使 String 对象不适合   存储安全敏感信息,如用户密码。您   应始终收集并存储安全敏感信息    char 数组代替。

Java编程语言4.0版安全编码指南的准则2-2 也说类似的东西(虽然它最初是在日志记录的背景下):

准则2-2:不要记录高度敏感的信息

一些信息,例如社会安全号码(SSN)和   密码,非常敏感。不应保留此信息   超过必要的时间,甚至可以看到的地方   管理员。例如,它不应该发送到日志文件和   它的存在不应该通过搜索检测到。一些短暂的   数据可以保存在可变数据结构中,例如char数组,和   使用后立即清除。清算数据结构已减少   移动对象时,典型Java运行时系统的有效性   内存对程序员透明。

该指南也对实施和使用有影响   没有数据语义知识的低级库   他们正在处理。例如,一个低级字符串解析   库可能会记录它所使用的文本。应用程序可以解析SSN   与图书馆。这会产生SSN的情况   可供具有日志文件访问权限的管理员使用。


610
2018-01-17 03:20



这正是我在Jon的答案下面谈论的有缺陷/伪造的参考,这是一个众所周知的来源,有很多批评。 - bestsss
@bestass你还能引用一个参考吗? - user961954
@bestass抱歉,但是 String 很容易理解它在JVM中的表现......有充分的理由可以使用 char[] 代替 String 以安全的方式处理密码时。 - SnakeDoc
再一次虽然密码作为字符串从浏览器传递给请求,因为'string'不是char?所以无论你做什么,它都是一个字符串,此时它应该被作用并丢弃,从不存储在内存中? - Dawesi


字符数组(char[])可以在使用后通过将每个字符设置为零而不将字符串设置为清除。如果有人能够以某种方式查看内存映像,如果使用了字符串,他们可以使用纯文本查看密码,但是如果 char[] 使用0清除数据后,密码是安全的。


305
2018-01-16 14:27



默认情况下不安全。如果我们正在讨论Web应用程序,大多数Web容器都会将密码传递给 HttpServletRequest 明文中的对象。如果JVM版本是1.6或更低,它将在permgen空间。如果它在1.7中,它将在收集之前仍然可读。 (无论何时。) - avgvstvs
@avgvstvs:字符串不会自动移动到permgen空间,只适用于intern'ed字符串。除此之外,permgen空间也受到垃圾收集,只是速度较低。 permgen空间的真正问题在于它的固定大小,这正是没有人应该盲目调用的原因 intern() 在任意字符串上。但你是对的 String 实例存在于第一位(直到收集)并将其转换为 char[] 之后的数组不会改变它。 - Holger
@Holger看 docs.oracle.com/javase/specs/jvms/se6/html/...  “否则,将创建一个类String的新实例,其中包含CONSTANT_String_info结构给出的Unicode字符序列;该类实例是字符串文字派生的结果。最后,调用新String实例的intern方法。”在1.6中,JVM会在检测到相同序列时为您调用实习生。 - avgvstvs
@Holger,你是对的我把常量池和字符串池混为一谈,但permgen空间也是假的 只要 适用于实习字符串。在1.7之前,constant_pool和string_pool都位于permgen空间中。这意味着分配给堆的唯一一类字符串就像你说的那样, new String() 要么 StringBuilder.toString()  我管理了大量字符串常量的应用程序,结果我们有很多permgen creep。直到1.7。 - avgvstvs
@avgvstvs:好吧,字符串常量,正如JLS强制要求一样,总是被实现,因此实现字符串的语句最终在permgen空间中,隐式应用于字符串常量。唯一的区别是字符串常量首先在permgen空间中创建,而调用则是 intern() 在任意字符串上可能导致在permgen空间中分配等效字符串。后者可以获得GC,如果没有相同内容的文字字符串共享该对象... - Holger


有些人认为,一旦不再需要密码,就必须覆盖用于存储密码的内存。这减少了攻击者从系统中读取密码的时间窗口,并完全忽略了攻击者已经需要足够的访问权来劫持JVM内存来执行此操作的事实。具有这么多访问权限的攻击者可以捕获您的关键事件,使其完全无用(AFAIK,所以如果我错了请纠正我)。

更新

感谢我的评论,我必须更新我的答案。显然有两种情况可以增加(非常)次要的安全性改进,因为它减少了密码落在硬盘上的时间。我认为对于大多数用例来说,这样做太过分了。

  • 您的目标系统可能配置不当,或者您必须假设它并且您必须对核心转储感到偏执(如果系统不由管理员管理,则可能有效)。
  • 您的软件必须过于偏执,以防止数据泄露,攻击者可以访问硬件 - 使用类似的东西 TrueCrypt的 (停产), VeraCrypt, 要么 CipherShed

如果可能,禁用核心转储和交换文件将解决这两个问题。但是,它们需要管理员权限并且可能会降低功能(使用的内存更少),并且从正在运行的系统中提取RAM仍然是一个有效的问题。


183
2018-01-16 14:32



我将“完全没用”替换为“只是一个小小的安全改进”。例如,如果您碰巧具有对tmp目录的读访问权,配置错误的计算机以及应用程序崩溃,则可以访问内存转储。在这种情况下,您将无法安装键盘记录器,但是您 可以 分析核心转储。 - Joachim Sauer
一旦你完成它就从内存中擦除未加密的数据被认为是最好的做法,不是因为它是万无一失的(它不是);但因为它降低了你的威胁暴露水平。这样做不会阻止实时攻击;但是因为它通过显着减少内存快照上的追溯性攻击中暴露的数据量(例如,写入交换文件的应用程序内存副本,或者内存中已读取的数据)来提供损害缓解工具从正在运行的服务器并在其状态失败之前移动到另一个服务器)。 - Dan Neely
我倾向于同意这种回应的态度。我冒昧地提出大多数安全漏洞都发生在比内存中的位高得多的抽象层次。当然,在超安全防御系统中可能存在这样的情况,这可能是相当令人担忧的,但是在这个级别上认真思考对于99%的利用.NET或Java的应用程序来说是过度的(因为它与垃圾收集有关)。 - kingdango
在Heartbleed渗透服务器内存,泄露密码之后,我会将字符串“只是一个小的安全性改进”替换为“绝对必须不使用字符串作为密码,而是使用char []代替。” - Peter vdL
@PetervdL heartbleed只允许读取特定的重用缓冲区集合(用于安全关键数据和网络I / O而不清除它们之间 - 出于性能原因),您不能将它与Java字符串结合使用,因为它们在设计上是不可重用的。你也不能用Java来读取随机内存来获取String的内容。使用Java Strings无法实现导致出现问题的语言和设计问题。 - josefx


  1. 字符串是不可变的 在Java中,如果您将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它,并且因为字符串池中使用字符串以便重新使用,所以很有可能它会在内存中保留很长时间,构成安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码
  2. Java推荐 运用 getPassword() JPasswordField的方法,它返回一个char []并且不推荐使用 getText() 以明文形式返回密码的方法,说明安全原因。
  3. 的toString() 在日志文件或控制台中始终存在打印纯文本的风险,但如果使用Array,则不会打印数组的内容而是打印其内存位置。

    String strPwd = "passwd";
    char[] charPwd = new char[]{'p','a','s','s','w','d'};
    System.out.println("String password: " + strPwd );
    System.out.println("Character password: " + charPwd );
    

    字符串密码:passwd

    字符密码:[C @ 110b2345

最后的想法: 虽然使用char []还不够,但您需要擦除内容才能更安全。我还建议使用散列或加密密码而不是纯文本,并在验证完成后立即从内存中清除它。


117
2017-12-27 20:22



“由于任何有权访问内存转储的人都可以以明文形式找到密码”如何获取内存转储的访问权限并在那里找到密码? - Mohit Kanwar
@MohitKanwar通过jmap或jvisualvm触发内存转储。如果设置了heapdumponoutofmemoryerror,则可能会通过导致内存不足的情况触发远程堆转储。获得转储后,您可以使用jvisualvm或eclipse内存分析器工具或命令行工具来检查它,并检查所有对象及其值。 - Ryan


我不认为这是一个有效的建议,但是,我至少可以猜测原因。

我认为动机是要确保您可以在使用后及时清除内存中的所有密码。有了 char[] 你可以使用空格或某些东西来覆盖数组的每个元素。您无法编辑a的内部值 String 那样。

但仅此一点并不是一个好的答案;为什么不只是确保参考 char[] 要么 String 不逃避?然后没有安全问题。但事情就是这样 String 对象可以 intern()在理论上并在恒定的池中保持活力。我想用 char[] 禁止这种可能性。


72
2018-01-16 14:27



我不会说问题是你的引用将会或不会“逃避”。只是字符串将在内存中保持一段时间不被修改,而 char[] 可以修改,然后无论是否收集都无关紧要。因为字符串实习需要明确地为非文字进行,所以就像告诉a一样 char[] 可以由静态字段引用。 - Groo
内存中的密码不是表单中的字符串吗? - Dawesi


已经给出了答案,但我想分享一下我最近在Java标准库中发现的问题。虽然他们现在非常小心地用更换密码字符串 char[] 在任何地方(当然这都是好事),其他安全关键数据在从内存中清除时似乎被忽视了。

我在考虑例如该 专用密钥 类。考虑一种情况,您可以从PKCS#12文件加载私有RSA密钥,使用它来执行某些操作。现在,在这种情况下,只要对密钥文件的物理访问受到适当限制,单独嗅探密码对您没有多大帮助。作为攻击者,如果您直接获得密钥而不是密码,那将会好得多。所需的信息可以泄漏,核心转储,调试器会话或交换文件只是一些例子。

事实证明,没有任何东西可以让你清除a的私人信息 PrivateKey 从内存中,因为没有API可以让你擦除形成相应信息的字节。

这是一个糟糕的情况  描述了这种情况如何被潜在地利用。

例如,OpenSSL库在释放私钥之前覆盖关键内存部分。由于Java是垃圾收集的,我们需要显式方法来擦除和使Java密钥的私有信息无效,这些密钥将在使用密钥后立即应用。


58
2018-01-19 19:39



解决这个问题的一种方法是使用。的实现 PrivateKey 实际上并没有在内存中加载其私有内容:例如,通过PKCS#11硬件令牌。也许PKCS#11的软件实现可以负责手动清理内存。也许使用像NSS商店这样的东西(它的大部分实现与 PKCS11 Java中的商店类型更好。该 KeychainStore (OSX密钥库)将私钥的完整内容加载到其中 PrivateKey 实例,但它不应该。 (不知道是什么 WINDOWS-MY KeyStore在Windows上运行。) - Bruno
@Bruno当然,基于硬件的令牌不会受此影响,但是你或多或少被迫使用软件密钥的情况呢?并非每个部署都有预算来支付HSM。软件密钥库在某些时候必须将密钥加载到内存中,因此我们至少应该给IMO选择再次清除内存。 - emboss
当然,我只是想知道一些等同于HSM的软件实现在清理内存方面是否表现得更好。例如,当使用带有Safari / OSX的client-auth时,Safari进程实际上从未看到私钥,操作系统提供的底层SSL库直接与安全守护进程对话,后者提示用户使用密钥链中的密钥。虽然这一切都是在软件中完成的,但是如果将签名委托给可以更好地卸载或清除内存的不同实体(甚至是基于软件的),这样的类似分离可能会有所帮助。 - Bruno
@Bruno:有趣的想法是,一个负责清理内存的额外间接层确实可以透明地解决这个问题。为软件密钥库编写PKCS#11包装器已经可以解决问题了吗? - emboss


正如Jon Skeet所说,除了使用反射之外别无他法。

但是,如果您可以选择反射,则可以执行此操作。

public static void main(String[] args) {
    System.out.println("please enter a password");
    // don't actually do this, this is an example only.
    Scanner in = new Scanner(System.in);
    String password = in.nextLine();
    usePassword(password);

    clearString(password);

    System.out.println("password: '" + password + "'");
}

private static void usePassword(String password) {

}

private static void clearString(String password) {
    try {
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);
        char[] chars = (char[]) value.get(password);
        Arrays.fill(chars, '*');
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

什么时候跑

please enter a password
hello world
password: '***********'

注意:如果String的char []已被复制为GC循环的一部分,则前一个副本可能位于内存中的某个位置。

此旧副本不会出现在堆转储中,但如果您可以直接访问该进程的原始内存,则可以看到它。一般来说,你应该避免任何人有这种访问。


41
2017-07-08 09:26



最好还做一些事情来防止打印密码的长度 '***********'。 - chux
@chux你可以使用零宽度字符,虽然这可能比有用更令人困惑。如果不使用Unsafe,就无法更改char数组的长度。 ;) - Peter Lawrey
由于Java 8的字符串重复数据删除,我认为做这样的事情可能会非常具有破坏性......你可能最终清除程序中的其他字符串,这些字符串偶然具有相同的密码字符串值。不太可能,但可能...... - jamp
@jamp Java 9应该具有此功能以及紧凑字符串(即使用byte []])。我不认为这发生在Java 8中。 - Peter Lawrey
@PeterLawrey必须使用JVM参数启用它,但它就在那里。可以在这里阅读: blog.codecentric.de/en/2014/08/... - jamp


编辑: 在经过一年的安全研究之后回到这个答案,我意识到它实际上比较明确地比较了明文密码。请不要。 使用带有salt和合理迭代次数的安全单向散列。考虑使用库:这个东西很难做对!

原始答案: String.equals()使用的事实如何? 短路评估,因此容易受到定时攻击?这可能不太可能,但你可以 理论上 时间密码比较,以确定正确的字符序列。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // Quits here if Strings are different lengths.
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // Quits here at first different character.
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

有关定时攻击的更多资源:


32
2018-04-08 00:04



但是那也可以在char []比较中出现,在某些地方我们也会在密码验证中做同样的事情。那么char []如何比字符串更好? - Mohit Kanwar
你是绝对正确的,错误可以两种方式。知道问题是最重要的,考虑到Java中没有基于String或基于char []的密码的明确密码比较方法。我会说使用compare()对字符串的诱惑是使用char []的一个很好的理由。这样你至少可以控制比较的方式(没有扩展String,这是一个痛苦的imo)。 - Graph Theory
除了比较明文密码不是正确的事情,使用的诱惑 Arrays.equals 对于 char[] 是高达 String.equals。如果有人关心,有一个专用的密钥类封装了实际的密码并处理问题 - 哦等等,真正的安全包 有 专门的关键课程,这个Q&A只是一个 习惯 在他们之外,比方说,例如 JPasswordField,使用 char[] 代替 String (实际算法使用的地方 byte[] 无论如何)。 - Holger
安全相关软件应该做类似的事情 sleep(secureRandom.nextInt()) 无论如何,在拒绝登录尝试之前,这不仅可以消除定时攻击的可能性,还可以抵抗暴力尝试。 - Holger


这些都是人们应该选择的原因 炭[] 数组而不是  用于密码。

1。  因为如果你将密码作为纯文本存储在字符串中,字符串是不可变的,它将在内存中可用,直到垃圾收集器清除它,并且因为String在字符串池中用于可重用性,所以很有可能它会长时间保留在内存中,这构成了安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码,这是另一个原因,您应该始终使用加密密码而不是纯文本。由于字符串是不可变的,所以不能更改字符串的内容,因为任何更改都会产生新的字符串,而如果你使用char [],你仍然可以将所有元素设置为空白或零。因此,在字符数组中存储密码可以明显降低窃取密码的安全风险。

2。  Java本身建议使用JPasswordField的getPassword()方法,它返回一个char []和不推荐使用的getText()方法,它以明文形式返回密码,说明安全原因。很好地遵循Java团队的建议并坚持标准而不是反对它。

3。  使用String时,始终存在在日志文件或控制台中打印纯文本的风险,但如果使用Array,则不会打印数组的内容,而是打印其内存位置。虽然不是一个真正的原因,但仍然有意义。

    String strPassword="Unknown";
    char[] charPassword= new char[]{'U','n','k','w','o','n'};
    System.out.println("String password: " + strPassword);
    System.out.println("Character password: " + charPassword);

    String password: Unknown
    Character password: [C@110b053

参考来自: http://javarevisited.blogspot.com/2012/03/why-character-array-is-better-than.html 希望这可以帮助。


32
2018-05-08 10:17



这是多余的。这个答案是@SrujanKumarGulla写的答案的精确版本 stackoverflow.com/a/14060804/1793718。请不要复制粘贴或复制相同的答案两次。 - Lucky
1.)System.out.println(“字符密码:”+ charPassword)之间的区别是什么?和2.)System.out.println(charPassword);因为它给出与输出相同的“未知”。 - Vaibhav_Sharma