首页 文章

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

提问于
浏览
3021

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

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

18 回答

  • 36

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

  • 32

    java中的字符串是不可变的 . 因此,每当创建一个字符串时,它将保留在内存中,直到它被垃圾收集 . 所以任何有权访问内存的人都可以读取字符串的值 .
    如果修改了字符串的值,那么它将最终创建一个新字符串 . 因此,原始值和修改后的值都会保留在内存中,直到它被垃圾回收 .

    使用字符数组,一旦提供密码的目的,就可以修改或删除数组的内容 . 修改后,即使在垃圾收集开始之前,也不会在内存中找到数组的原始内容 .

    出于安全考虑,最好将密码存储为字符数组 .

  • 1

    将密码用于String或Char []总是有争议的,因为这两种方法都有自己的实用和缺点 . 它取决于用户期望满足的需求 . 下面的行可能有助于更好地理解何时使用哪个容器:由于java中的String是不可变的,因此每当有人试图操纵你的字符串时,它就会创建一个新的Object,而现有的仍然是未经过回放的 . 这可以被视为将密码存储为String的优点,但另一方面 . 即使在使用后,String对象仍保留在内存中 . 因此,如果有人获得内存位置,可以轻松跟踪存储在该位置的密码 . 虽然来到Char []这是可变的,但它有利于在使用后,程序员可以明确地清理数组或覆盖值 . 因此,在没有使用之后,它被清理,没有人能够知道您存储的信息 .

    基于上述情况,可以获得Idea是否与String或Char []一起使用 .

    谢谢 .

  • 28

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

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

  • 1114

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

    Update

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

    • 您的目标系统可能配置不当,或者您必须假设它是,并且您必须对核心转储感到偏执(如果系统不由管理员管理,则可能有效) .

    • 您的软件必须过于偏执,以防止数据泄露,攻击者可以访问硬件 - 使用TrueCrypt(已停用),VeraCryptCipherShed等内容 .

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

  • 193

    字符串是不可变的,一旦创建就无法更改 . 以字符串形式创建密码在堆或String池上留下对密码的迷你引用 . 现在,如果有人采用Java进程的堆转储并仔细扫描,他可能能够猜出密码 . 当然,这些未使用的字符串将被垃圾收集,但这取决于GC何时启动 .

    另一方面,一旦认证完成,char []就是可变的,你可以用任何字符覆盖它们,如所有的M或反斜杠 . 现在,即使有人进行堆转储,他也可能无法获取当前未使用的密码 . 这样可以为您提供更多控制,例如自己清除对象内容与等待GC执行此操作 .

  • 16

    字符串是不可变的,它将转到字符串池 . 一旦编写,就无法覆盖 .

    char[] 是一个在使用密码后应该覆盖的数组,这是应该如何完成的:

    char[] passw = request.getPassword().toCharArray()
    if (comparePasswords(dbPassword, passw) {
     allowUser = true;
     cleanPassword(passw);
     cleanPassword(dbPassword);
     passw=null;
    }
    
    private static void cleanPassword (char[] pass) {
     for (char ch: pass) {
      ch = '0';
     }
    }
    

    攻击者可以使用它的一种情况是崩溃转储 - 当JVM崩溃并生成内存转储时 - 您将能够看到密码 .

    这不一定是恶意的外部攻击者 . 这可以是支持用户,可以访问服务器以进行监视 . 他可以查看故障转储并找到密码 .

  • 15

    除非你在使用后手动清理它,否则char数组没有给你vs String,我没有看到任何人真正这样做 . 所以对我来说char [] vs String的偏好有点夸张 .

    看一下广泛使用的Spring Security库here并问问自己 - 即使你使用复杂的方法来隐藏它们,Spring Security的人员也不称职或char []密码只是让所有密码都得到 .

    但是,Java一直在变化,而像String Deduplication feature of Java 8这样的一些可怕功能可能会在您不知情的情况下实际执行String对象 . 但这是不同的对话 .

  • 125

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

    我认为动机是要确保您可以在使用后立即清除内存中的所有密码 . 使用 char[] ,您可以使用空白或某些内容覆盖数组的每个元素 . 您无法以这种方式编辑 String 的内部值 .

    但仅此一点并不是一个好的答案;为什么不确保对 char[]String 没有安全问题的引用 . 但问题是 String 对象在理论上可以被 intern() 编辑并且在常量池中保持活跃 . 我想使用 char[] 禁止这种可能性 .

  • 633

    正如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循环的一部分,则前一个副本可能位于内存中的某个位置 .

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

  • 3860

    引用官方文档时,Java Cryptography Architecture guide说这是关于 char[]String 密码(关于基于密码的加密,但当然更常见的是关于密码):

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

    Guideline 2-2 of the Secure Coding Guidelines for the Java Programming Language, Version 4.0也说了类似的东西(虽然它最初是在记录的上下文中):

    准则2-2:不记录高度敏感的信息某些信息(如社会安全号码(SSN)和密码)非常敏感 . 即使是管理员,也不应将此信息保留的时间超过必要的时间,也不应保存在何处 . 例如,它不应该发送到日志文件,并且不应通过搜索检测到它的存在 . 一些瞬态数据可以保存在可变数据结构中,例如char数组,并在使用后立即清除 . 清除数据结构降低了典型Java运行时系统的有效性,因为对象在内存中透明地移动到程序员 . 本指南还对低级库的实现和使用有影响,这些低级库没有他们正在处理的数据的语义知识 . 例如,低级字符串解析库可以记录它所处理的文本 . 应用程序可以使用库解析SSN . 这会产生一种情况,即管理员可以使用SSN访问日志文件 .

  • 43

    简短而直截了当的答案是因为 char[] 是可变的而 String 对象不是 .

    Java中的 Strings 不可变对象 . 这就是为什么它们一旦创建就无法修改的原因,因此从内存中删除内容的唯一方法就是让它们被垃圾收集 . 只有这样,当对象释放的内存才能被覆盖时,数据才会消失 .

    现在Java中的垃圾收集不会在任何保证的时间间隔内发生 . 因此, String 可以在内存中持续很长时间,如果进程在此期间崩溃,则字符串的内容可能会在内存转储或某些日志中结束 .

    使用字符数组,您可以读取密码,尽快完成密码,然后立即更改内容 .

  • 319

    Strings are immutable . 这意味着一旦你创建了 String ,如果另一个进程可以转储内存,那么除了reflection之外你没办法在garbage collection开始之前摆脱数据 .

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

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

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

  • 75

    这些都是原因,应该选择 char[] 数组而不是 String 作为密码 .

    1. 由于字符串在Java中是不可变的,如果将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它,并且因为String在字符串池中用于可重用性,所以它很可能会保留在记忆中持续很长时间,这构成了安全威胁 .

    由于任何有权访问内存转储的人都可以以明文形式找到密码,因此您应始终使用加密密码而不是纯文本的另一个原因 . 由于字符串是不可变的,因此无法更改字符串的内容,因为任何更改都会产生新的字符串,而如果使用char [],您仍然可以将所有元素设置为空或零 . 因此,在字符数组中存储密码可以明显降低窃取密码的安全风险 .

    2. Java本身建议使用JPasswordField的getPassword()方法,该方法返回char [],而不是弃用的getText()方法,该方法以明文形式返回密码,说明安全原因 . 遵循Java团队的建议并遵守标准而不是反对它们是很好的 .

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

    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
    

    参考自this blog . 我希望这有帮助 .

  • 11

    虽然这里的其他建议似乎有效,但还有另外一个好理由 . 平原 String 你有更高的机会 accidentally printing the password to logs ,监视器或其他一些不安全的地方 . 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
    
  • 60

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

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

    事实证明,没有什么可以让你从内存中清除 PrivateKey 的私人信息,因为没有API可以让你擦除形成相应信息的字节 .

    这是一个糟糕的情况,因为paper描述了这种情况如何可能被利用 .

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

  • 14

    在Java中

    • Strings are immutable 如果将密码存储为纯文本,它将在内存中可用直到垃圾收集器清除它并且因为字符串池中的字符串用于可重用性,它很可能会长时间保留在内存中,这会带来安全威胁 . 由于任何有权访问内存转储的人都可以以明文形式找到密码

    • Java recommendation 使用JPasswordField的 getPassword() 方法返回char []并弃用 getText() 方法,该方法以明文形式返回密码,说明安全原因 .

    • toString() 总是存在在日志文件或控制台中打印纯文本的风险,但如果使用数组,则不会打印数组的内容而是打印其内存位置 .

    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

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

  • 35

    Edit: 经过一年的安全研究后回到这个答案,我意识到这实际上比较明显地比较了明文密码 . 请不要 . Use a secure one-way hash with a salt and a reasonable number of iterations . 考虑使用库:这个东西很难做对!

    Original answer: String.equals()使用short-circuit evaluation,因此容易受到定时攻击的影响?它可能不太可能,但理论上你可以对密码进行比较,以确定正确的字符序列 .

    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;
    }
    

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

相关问题