2

经过一天漫长的编程,我想发布一些对其他人有用的东西。

几天前,我想知道如何使用正确的过程来处理JPasswordField's 方法getPassword()以将值传递给服务器并获得答案。

这就是问题:

如何JPasswordField以安全的方式正确地从 a 获取值并处理它以使用服务器创建登录过程?

4

1 回答 1

4

这是我达到的解决方案。

首先,我决定了对我的目的来说足够安全的登录过程,我不想向服务器发送纯密码,也不想在我的数据库中存储纯密码(显然)。

首先要说的是,保护密码的一个好方法是永远不会在网络上以简单易读的形式交换它,这显然是因为可能存在“中间人”,简而言之就是有人以他们的方式阅读您的邮件到服务器。

密码需要被散列,这意味着它被转换为相当长的十六进制字符序列。哈希的好处是(希望)是单向的。您无法对密码进行哈希处理。

有很多算法可以做到这一点,我选择 SHA256。

然后对密码进行哈希处理,但仅在我们之间,这还不够。如果黑客能够窃取散列,则有一些技术可以使他成功“翻译”散列。只是为了在他的等式中添加一个变量,让他的生活更艰难,我们可以在对密码进行哈希处理之前添加一个盐。盐是一段字符串,可以添加到我们想要的密码的任何位置。这避免了某种基于字典和最常用密码的攻击。

但是,如果一个比我受过更好训练的黑客无法读取密码,我该怎么办?

答案很简单,我不必。

但是要理解这一点,我们需要在“注册过程”中跳一下,即新用户添加到我的数据库中的那一刻。就是这个:

  1. 客户端要求服务器注册发送昵称。
  2. 服务器使用作为密码盐的令牌进行回答。
  3. 客户端对密码加盐,对其进行哈希处理并将其发送回服务器。
  4. 服务器现在收到一些不可读的内容,因此没有安全问题,并将其与昵称和盐一起存储。加盐散列密码是“公共秘密”。

所以登录过程是这样的:

  1. 客户端要求服务器登录
  2. 服务器用盐回答
  3. 客户端对密码进行加盐,然后对其进行哈希处理并将其发送回服务器。
  4. 服务器将共享密钥与接收到的字符串进行比较。如果它们相等,则允许用户登录。

这应该很好,但是在这种情况下,如果黑客知道共享密钥可以毫无问题地访问服务器,因为这样做我们只是更改了密码,不可读,但仍然可以直接使用。

为了避免这种行为,我们只需要在我们的链中添加一个段落:

  1. 客户端要求服务器登录
  2. 服务器用盐和随机会话盐回答
  3. 客户端对密码加盐,对其进行哈希处理。此时,它再次对哈希进行加盐并重新对其进行哈希处理。然后它将 hashed-salted-hash-of-salted-password 发送回服务器
  4. 服务器获取共享密钥,用随机会话 salt 对其进行加盐,然后对其进行哈希处理。如果两个字符串相等,则允许用户登录。

现在程序已经清楚了,我们有一个问题要解决。如果我处理任何类型的字符串,它可以在内存中保留很长时间,所以如果我将密码放在字符串中,它可以以普通形式长时间读取。这对我们来说不是那么好,但我们并不是第一个想到它的,java 确实创造了一种方法来避免这个密码持久化。解决方案是使用字符数组。这是因为即使数组持久化在内存中,其数据在内存中也是无序分布的,很难重新创建原始密码。

重新发明热水?是的,只需在 JPasswordField 中使用 getPassword() 方法。

但这对于新手来说是相当困难的。我们得到一个 char[] 数组,对于非专家来说这很奇怪。

我们想到的第一件事就是将该数组转换为纯字符串.......但这正是我想要避免的。所以我需要按原样处理数组。

然后我们需要一种方法对密码进行加盐和哈希处理,结果可能是这样的:

public static String digestSalted(String salt, char[] password) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA-256");

    ArrayList<Byte> list = new ArrayList<Byte>();
    for (int i = 0; i < password.length; i++) {
        //String ch = String.valueOf(password[i]);
        //byte[] b = ch.getBytes();
        //for (int j = 0; j < b.length; j++) {
        //  list.add(b[j]);
        //}
                    list.add((byte)password[i]);
    }
    byte[] saltInBytes = salt.getBytes();
    byte[] toBeHashed = new byte[(saltInBytes.length + list.size())];
    for (int i = 0; i < saltInBytes.length; i++) {
        toBeHashed[i] = saltInBytes[i];
    }
    for (int i = saltInBytes.length; i < list.size() + saltInBytes.length; i++) {
        toBeHashed[i] = list.get(i - saltInBytes.length);
    }

    md.update(toBeHashed);

    byte byteData[] = md.digest();

    StringBuffer hexString = new StringBuffer();
    for (int i = 0; i < byteData.length; i++) {
        String hex = Integer.toHexString(0xff & byteData[i]);
        if (hex.length() == 1) {
            hexString.append('0');
        }
        hexString.append(hex);
    }
    return hexString.toString();

}

此方法创建一个字节数组,通过许多小字符串然后附加盐。加盐后,它会使用 SHA256 对结果进行哈希处理。

现在返回可以是一个字符串,因为是散列的,不存在安全问题。

这为问题的第一部分提供了解决方案。

第二部分只是在服务器和客户端之间实现我们的协议。

我只会在客户端中显示足以理解该过程的代码。我正在使用一个阻塞队列,从套接字读取消息时会在其中放置消息。这是代码:

public void login(String nickname, char[] password) {
    if (cl == null) {
        throw new RuntimeException();
    }
    long s = Sys.getTime();
    cl.send("NICK " + nickname);
    IncomingMessage reply = null;
    try {
        reply = this.mh.getMessage(); //The response to NICK msg
        if (reply.getCommand().equalsIgnoreCase("LOGIN")) {
            ArrayList<String> params = reply.getParams();
            String accountSalt = params.get(0);
            String randomSalt = params.get(1);
            try {
                String sharedSecret = SHAHash.digestSalted(accountSalt, password);
                String saltedSharedSecret = SHAHash.digestSalted(randomSalt, sharedSecret);
                if (saltedSharedSecret != null) {
                    cl.send("PASS " + saltedSharedSecret);
                    reply = this.mh.getMessage();
                    if (reply.getCommand().equalsIgnoreCase("WELCOME") && reply.getParams().get(0).equals(nickname)) {
                        // ************ LOG ************ //
                        LOG.config("Logged in.");
                        // ***************************** //
                        this.running = true;
                        this.loggedIn = true;
                        mh.startExecutor();
                        LOG.config("Time passed: " + (Sys.getTime() - s));
                        mh.startGame();
                    } else {
                        // ************ LOG ************ //
                        LOG.warning("A problem has occured while trying to login to the server.");
                        // ***************************** //
                        JOptionPane.showMessageDialog(null, "Error while logging to the server, shutting down.\n- ERROR 006 -");
                        System.exit(0);
                    }
                }
            } catch (NoSuchAlgorithmException e) {
                // ************ LOG ************ //
                LOG.warning("Error while SHA hashing the password, shutting down.");
                // ***************************** //
                JOptionPane.showMessageDialog(null, "Error while SHA hashing the password, shutting down.\n- ERROR 005 -");
                System.exit(0);
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

代码,现在我们已经清楚了协议是如何工作的,很容易理解应该考虑的事情this.mh.getMessage()是阻塞方法,这意味着线程将等待队列中可用的东西,然后再尝试获取它。

这(几乎)我如何解决我的问题。让我知道答案是否有任何错误,或者您是否需要澄清。我希望这对某人有用。好好编程

于 2013-08-29T01:59:35.093 回答