Search code examples
javasecurityspring-security

Comparing hashed passwords


Best practices recommend that I store hashed and salted passwords. I already know it. My question is: how to provide clear and secure mechanism to check password (its hash)?

We have a CLI client where user enter his plain password. CLI client is separate app on remote machine. I can hash password and send to backend and backend should check credentials (there are also user permissions). But: a) Spring security encoders (PBKDF2, Argon2, Bcrypt, Scrypt etc.) gaves me different hash each time which is obvious because salt is also random. "Static" salt could solve the problem: once user has been created the salt stored in DB and i could hash password with it. b) Man in the middle can send same proper hash which will be compared to stored in database c) Providing additional DB interractions to CLI (or "static" user salt) client ruins up its purpose and architecture. The CLI knows nothing about backend except its address and port

Is there a way to do it? Or how it should be done referring to best security practicies? Thanks in advance.


Solution

  • When you hash passwords, you do not send the hashed value to the thing that verifies identity (let's call that 'the authenticator'), i.e. if your authenticator is implemented as (pseudo-code):

    String dbHash = db.select(
      "SELECT passHash FROM users WHERE username = ?", req.getParam("username"));
    String inHash = req.getParam("passHash");
    if (!dbHash.equals(inHash)) throw new NotAuthorizedException();
    return; // user is authenticated
    

    You send the actual password. And then you let the authenticator do the job of hashing it - which will work because this is, roughly, how passhash verification algorithms work:

    String dbHash = db.select("SELECT passHash FROM users WHERE username = ?", username);
    String[] parts = dbHash.split("::");
    String salt = parts[0];
    String storedHash = parts[1];
    
    String userSuppliedHash = applyBCrypt(salt, userSuppliedPassword);
    if (!userSuppliedHash.equals(storedHash)) throw new NotAuthorizedException();
    return; // user is authenticated
    

    And you should duplicate that functionality. That's the 'standard'.

    Now, the 'standard' has issues. It does not protect the user against a compromised server - a compromised server gets the actual password. The user can protect against this somewhat - don't re-use passwords. If the server is compromised, whatever said user was going to do on that server is therefore also compromised. It also means the compromiser has to continuously compromise that server (in contrast to storing raw passwords directly, in which case a single DB dump is all the hacker needs).

    There are solutions to this problem. They do not involve shifting the responsibility of finding out the salt and applying the hash. Generally, if you want to take authentication further than the obvious scheme (which is: To just send those passwords plaintext - see below), you forego passwords entirely and work with keyfiles (and the client is free to encrypt its private key using any entirely client side password based scheme if you want). And/or turn authentication into a challenge/response scheme. For example:

    • User requests a login from server, just sends username.
    • Server rolls up a large random number. It sends this number to the client. Both client and server perform: HASH(CONCAT(randomNumber, password)). The client sends this. The server verifies this. This way a compromised server can man-in-the-middle but cannot just recover the password.

    There are many such schemes available but they are rarely used for form-based web logins. Look into OAuth and friends. They thought this stuff through.

    plaintext???

    Yes, of course, that's a huge issue. But you don't solve that issue by letting the client perform the hashing (which would involve the client asking the server for the salt, or using a static salt, which has big, big issues all on its own, don't do that1), because now the hash is effectively the password and you're... still sending that plaintext.

    The solution is to use a secure comms channel. Not secure as in 'we know the user is authenticated' - secure as in 'the channel is encrypted'. For example, https is not (unless you use client side certs; I recommend against this) an authentication mechanism. But it does provide a secure channel.


    [1] If a site stores all passwords hashed, but uses a static hash, I simply scan for the most common hash value. That'll be iloveyou, abc123, or one of the other 'top 5 used passwords'. I now know the password of each and every user with a common password immediately. That's not good. With proper implementations (where each password hash has its own salt), there's nothing for it, but for a hacker to just try iloveyou as password for every account and pray its right. This still gets them a boatload of accounts, but at least they have to ping everything instead of knowing beforehand which accounts have that password.