【问题标题】:Why does JPasswordField.getPassword() create a String with the password in it?为什么 JPasswordField.getPassword() 会创建一个包含密码的字符串?
【发布时间】:2010-11-02 07:23:44
【问题描述】:

Swing 的 JPasswordField 具有返回 char 数组的 getPassword() 方法。我对此的理解是,数组可以在使用后立即归零,这样你就不会在内存中长时间徘徊敏感的东西。找回密码的旧方法是使用getText(),它返回一个字符串对象,但它已被弃用。

所以,我的问题是为什么 Java 在使用 getPassword() 的检索过程中实际使用它?更清楚地说,我正在调试我的测试应用程序以获取其他内容**,我跟着调用并砰的一声...... JPasswordField 中的getText() 被调用了,当然,已经创建了一个带有我密码的漂亮字符串对象,并且现在是在记忆中徘徊。

自己试试吧:

public class PasswordTest() {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPasswordField passField = new JPasswordField();
        pass.addActionListener(new ActionListener() {
            public ActionPerformed(ActionEvent evt) {
                char[] p = passField.getPassword(); // put breakpoint
                // do something with the array
            }
        });
        frame.add(passField);
        frame.setVisible(true);
        frame.pack();
    }
}

跟进问题:getText() 的这种“隐藏”使用是否有任何危险?当然,如果专门的攻击者破坏了系统,它会获取您的密码,我说的是不那么专门的攻击者;)

**我在寻找一种在 Swing 组件上实际显示一些敏感数据而不使用 String 对象的方法时遇到了这个问题。除非我愿意重写 Swing API 的一部分(全部?),否则显然没有办法做到这一点。不会发生。

【问题讨论】:

  • 我质疑您需要重写 Swing API 以显示敏感数据的说法。你能为此制作一个自定义组件,它扩展 JComponent 并覆盖paintComponent吗?然后由您决定如何处理文本。

标签: java security swing passwords


【解决方案1】:

其实这是getPassword()的Sun实现:

public char[] getPassword() {
    Document doc = getDocument();
    Segment txt = new Segment();
    try {
        doc.getText(0, doc.getLength(), txt); // use the non-String API
    } catch (BadLocationException e) {
        return null;
    }
    char[] retValue = new char[txt.count];
    System.arraycopy(txt.array, txt.offset, retValue, 0, txt.count);
    return retValue;
}

其中唯一的getText 是对getText(int offset, int length, Segment txt) 的调用,它调用getChars(int where, int len, Segment txt),进而将字符直接复制到Segment 的缓冲区中。那里没有创建Strings

然后,Segment 的缓冲区被复制到返回值中,并在方法返回之前清零。

换句话说:任何地方都没有额外的密码副本。只要您按照指示使用它是绝对安全的。

【讨论】:

  • 我很惊讶;我花了大约 15 分钟的时间来研究 Swing 代码,并向自己保证没有发生任何棘手的事情。
  • 根据我的观察,Segment 获取原始数组:当您覆盖 Segment 的数组字段(使用零,使用反射)时,下一个 getText(...)-call 返回一个空数组。所以这将是一种破坏 JPasswordField 中密码的可读版本的方法。
【解决方案2】:

Swing 实现过于复杂,无法手动检查。你想要测试。

public class Pwd {
    public static void main(String[] args) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new javax.swing.JFrame("Pwd") {{
                    add(new javax.swing.JPasswordField() {
                        @Override public String getText() {
                            System.err.println("Awoooga!!");
                            return super.getText();
                        }
                        {
                            addActionListener(
                                new java.awt.event.ActionListener() {
                                    public void actionPerformed(
                                        java.awt.event.ActionEvent event
                                    ) {
                                        // Nice.
                                    }
                                }
                            );
                        }
                    });
                    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
                    pack();
                    setVisible(true);
                }};
            }
        });
    }
}

对我来说,看起来像是(毫无意义的)动作事件的命令字符串。也会有其他的方式来产生效果。

一个模糊的现代 VM 无论如何都会在内存中移动对象,因此清除 char[] 不一定有效。

【讨论】:

  • “现代虚拟机无论如何都会在内存中移动对象,因此清除 char[] 不一定有效。”这是一个很好的观点。在阅读其余内容之前+1。
  • 为什么移动 GC 会使清除 char[] 无效?你是说在 char[] 被写入密码之后但在它被清除之前,GC 运行并将数组复制到其他地方,留下密码的原始副本作为“垃圾”被覆盖通过以后的分配?
  • @Jason 是的。尽管我相信典型的实现会阻塞零内存(至少在类似“伊甸园”的空间中),而不是在分配时这样做。也更有可能代码乱码会导致实际问题——动作源,你记得清空文档吗,getText可能会创建一个额外的char[](但你不知道所以不能清空),文档实现可能会重新分配缓冲区等。
  • “现代虚拟机无论如何都会在内存中移动对象,因此清除 char[] 不一定有效。”晚会有点晚了,但到了。如果一个虚拟机移动地址空间,它必须是清理它来自的空间的那个。但是,如果它做到了,那么 JNI 怎么能工作呢?在 JNI 中,您可以访问物理地址空间,并且在该空间中也分配了数组。我去过那里并做到了。没有我希望的那么有趣。
  • @A.Grandt 您应该在直接在 JNI 中访问对象之前“固定”对象。我知道这不是一个很好的 API,并且会干扰 GC。
【解决方案3】:

好吧,我的错...当我看到对 getText() 的调用时,所有的铃声都开始响起,而没有注意到它实际上是由我通过 Action 监听器引入的,这是一个堆栈跟踪

PasswordTest$1.getText() line: 14   
PasswordTest$1(JTextField).fireActionPerformed() line: not available    
PasswordTest$1(JTextField).postActionEvent() line: not available    
JTextField$NotifyAction.actionPerformed(ActionEvent) line: not available    
SwingUtilities.notifyAction(Action, KeyStroke, KeyEvent, Object, int) line: not available

这里是使用的代码:

 import java.awt.event.*;

 import javax.swing.*;

 public class PasswordTest {
        public static void main(String[] args) {
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            final JPasswordField passField = new JPasswordField() {
                @Override
                public String getText() {
                    System.err.println("Awhooa: " + super.getText()); //breakpoint
                    return null;
                }
            };
            passField.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    char[] p = passField.getPassword();
                    System.out.println(p);
                }
            });
            frame.add(passField);
            frame.setVisible(true);
            frame.pack();
        }
    }

这是控制台输出:

Awhooa: secret
secret

对于 getPassword() 的实际调用,也许我遗漏了一些东西,但是 Segment 的缓冲区在哪里归零?我看到一个数组副本,但没有归零。返回的数组会自己清零,但是Segment的数组还在……

import java.util.Arrays;

public class ZeroingTest {
    public static void main(String[] args) {
        char[] a = {'a','b','c'};
        char[] b = new char[a.length];
        System.arraycopy(a, 0, b, 0, b.length);
        System.out.println("Before zeroing: " + Arrays.toString(a) + " " + Arrays.toString(b));
        Arrays.fill(a, '\0');
        System.out.println("After zeroing: " + Arrays.toString(a) + " " + Arrays.toString(b));
    }
}

还有输出:

Before zeroing: [a, b, c] [a, b, c]
After zeroing: [?, ?, ?] [a, b, c]

(我把问号放在那里是因为我不能跳过不可打印的字符)

-M

【讨论】:

  • 你是对的。我以为我看到 Segment 的缓冲区被清零了,但事实并非如此。也许你应该向 Sun 提交一个错误。
  • Java 8 6 年后还是一样。段没有被清除,只是留给 GC 处理。
【解决方案4】:

**我在寻找一种在 Swing 组件上实际显示一些敏感数据而不使用 String 对象的方法时遇到了这个问题。除非我愿意重写 Swing API 的一部分(全部?),否则显然没有办法做到这一点。不会发生。

您可以通过调用field.setEchoChar('\0') 告诉JPasswordField 显示字符。这保留了JPasswordField 提供的其余保护(没有Strings,没有剪切/复制)。

【讨论】:

    【解决方案5】:

    这对我有用,可以帮助你建立一个字符串化密码:

    String passText = new String(passField.getPassword());
    

    【讨论】:

    • 因此您完全违背了将密码保存在 char 数组中的全部目的。
    • @A.Grandt 虽然您的评论在技术上非常正确,但它也完全没用。密码验证通常针对数据库进行,并且几乎总是以字符串形式发送。例如,您可以改为解释如何在 JPA createQuery setParameter(string name, string value) 上下文中使用 char 数组,尽可能不使用字符串。
    • @Tuncay 我真诚地希望你不是建议任何头脑正常的人在他们的数据库中以明文形式存储密码?这样做是疯狂的,无法理解。密码将使用安全的彻底散列,而不是自制代码,至少使用 PBKDF2WithHmacSHA1 或 PBKDF2WithHmacSHA256,进行大量迭代,并为每个密码单独随机生成一个盐值。所有这些都以字节或字符数组的形式发生,因为您可以在使用后安全地清除它们的内容。只有散列密码可以或应该以字符串形式呈现。
    • @A.Grandt 此行用于用户提交表单后获取密码,因此在将ti持久化到数据库之前必须对其进行加密。所以我完全同意你的看法。
    • @A.Grant 我当然不是。我只是指出,在沿线的某一点,您几乎可以肯定需要一根绳子。例如对其进行 sha256 处理,或对其进行加密,或对其进行 base64 处理...
    【解决方案6】:
    import javax.swing.*;
    
    public class login extends javax.swing.JFrame {
    
    
    MainProg main = new MainProg();
    
        public login() {
            initComponents();
        }
    
        /**
         * This method is called from within the constructor to initialize the form.
         * WARNING: Do NOT modify this code. The content of this method is always
         * regenerated by the Form Editor.
         */
        @SuppressWarnings("unchecked")
        // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
        private void initComponents() {
    
            jLabel1 = new javax.swing.JLabel();
            jLabel2 = new javax.swing.JLabel();
            txtUser = new javax.swing.JTextField();
            txtPassword = new javax.swing.JTextField();
            jButton1 = new javax.swing.JButton();
    
            setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
            setTitle("Log In");
            setBackground(new java.awt.Color(255, 204, 204));
            setResizable(false);
    
            jLabel1.setText("Username:");
    
            jLabel2.setText("Password:");
    
            jButton1.setBackground(new java.awt.Color(204, 204, 204));
            jButton1.setText("Enter");
            jButton1.setOpaque(false);
            jButton1.addActionListener(new java.awt.event.ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent evt) {
                    jButton1ActionPerformed(evt);
                }
            });
    
            javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
            getContentPane().setLayout(layout);
            layout.setHorizontalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(layout.createSequentialGroup()
                    .addContainerGap()
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                        .addComponent(jButton1)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(jLabel1)
                                .addGap(18, 18, 18)
                                .addComponent(txtUser, javax.swing.GroupLayout.PREFERRED_SIZE, 210, javax.swing.GroupLayout.PREFERRED_SIZE))
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(jLabel2)
                                .addGap(20, 20, 20)
                                .addComponent(txtPassword))))
                    .addContainerGap(62, Short.MAX_VALUE))
            );
            layout.setVerticalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(layout.createSequentialGroup()
                    .addContainerGap()
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                        .addComponent(jLabel1)
                        .addComponent(txtUser, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addGap(18, 18, 18)
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                        .addComponent(jLabel2)
                        .addComponent(txtPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                    .addComponent(jButton1)
                    .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            );
    
            pack();
        }// </editor-fold>                        
    
        private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         
            String U = new String(this.txtUser.getText());
            String P = new String(this.txtPass.gettext());
    
    
            if(U.equals("Admin") && P.equals(Password))
            {
                JOptionPane.showMessageDialog(null,"Login successful!","Message",JOptionPane.INFORMATION_MESSAGE); 
                this.hide();
                calculator.show();
            }
            else 
            {
               JOptionPane.showMessageDialog(null,"Invalid username and password","Message",JOptionPane.ERROR_MESSAGE); 
               this.txtUser.setText("");
               this.txtPassword.setText("");                       
            }   
    
        }                                        
    
        /**
         * @param args the command line arguments
         */
        public static void main(String args[]) {
            /*
             * Set the Nimbus look and feel
             */
            //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
            /*
             * If Nimbus (introduced in Java SE 6) is not available, stay with the
             * default look and feel. For details see
             * http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
             */
            try {
                for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                    if ("Nimbus".equals(info.getName())) {
                        javax.swing.UIManager.setLookAndFeel(info.getClassName());
                        break;
                    }
                }
            } catch (ClassNotFoundException ex) {
                java.util.logging.Logger.getLogger(login.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (InstantiationException ex) {
                java.util.logging.Logger.getLogger(login.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                java.util.logging.Logger.getLogger(login.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            } catch (javax.swing.UnsupportedLookAndFeelException ex) {
                java.util.logging.Logger.getLogger(login.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
            }
            //</editor-fold>
    
            /*
             * Create and display the form
             */
            java.awt.EventQueue.invokeLater(new Runnable() {
    
                public void run() {
                    new login().setVisible(true);
                }
            });
        }
        // Variables declaration - do not modify                     
        private javax.swing.JButton jButton1;
        private javax.swing.JLabel jLabel1;
        private javax.swing.JLabel jLabel2;
        private javax.swing.JTextField txtPassword;
        private javax.swing.JTextField txtUser;
        // End of variables declaration                   
    }
    

    【讨论】:

    • 感谢您提供帮助 :-) 不幸的是,您的代码没有解决 OP 提出的基本问题(即调用 getPassword 是否会打开安全漏洞)。顺便说一句:请学习 java 命名约定并遵守它们
    【解决方案7】:

    这对我有用。

    String.valueOf(txtPass.getPassword())
    

    【讨论】:

    • 处理密码的关键是将它们保存在一个 char/byte 数组中,当您将其转换为字符串时,您将失去对它占用的地址空间的控制,因为 Java 字符串是不可变的(您无法更改其内容,您对字符串所做的每一次更改都会在内存中创建一个新副本)。您可以通过迭代来清除数组,或者使用 Arrays.fill(array, value) 它将覆盖内存中的实际数据,使其在使用后无法恢复。
    猜你喜欢
    • 2011-02-17
    • 1970-01-01
    • 1970-01-01
    • 2016-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-24
    相关资源
    最近更新 更多