9

I’m developing an Android app with a MySQL database for storing user login credentials. I’m using jBCrypt for hashing passwords before storing them in the database. On registration, the password is hashed on client-side as follows:

String salt = BCrypt.gensalt();
String hash = BCrypt.hashpw("password", salt).split("\\$")[3];
salt = salt.split("\\$")[3];
hash = hash.substring(salt.length(), hash.length());

In this case, BCrypt.hashpw() will give me the hash

$2a$10$rrll.6qqZFLPe8.usJj.je0MayttjWiUuw/x3ubsHCivFsPIKsPgq

I then remove the params ($2a$10$) and store the first 22 characters as salt and the last 31 as hash in the database:

------------------------------------------------------------------------
|  uid  |          salt            |               hash                |
------------------------------------------------------------------------
|   1   |  rrll.6qqZFLPe8.usJj.je  |  0MayttjWiUuw/x3ubsHCivFsPIKsPgq  |
------------------------------------------------------------------------

Now, whenever a client wants to log in, they enter their username and password and only the salt is returned from the database. The client calculates their hash by calling BCrypt.hashpw() with their salt:

String salt = "$2a$10$" + returnedSalt;
String hash = BCrypt.hashpw(“password”, salt).split("\\$")[3];
hash = hash.substring(salt.length(), hash.length());

giving me:

hash = "0MayttjWiUuw/x3ubsHCivFsPIKsPgq"

which is equal to the hash stored in the database. The client then sends the username and the calculated hash back to the server. If they match, the user gets logged in.

I know that I can simplify this process by fetching the entire BCrypt hash directly and compare it with the given password with

if (BCrypt.checkpw(“password”, bCryptHash))
  // match

but it feels wrong to send the entire hashed password to the user to perform the check.

I understand that it is preferable to hash the passwords server-side, but is there something wrong with this solution? Am I missing something?

Say that I have an unencrypted HTTP connection between the phone and the server. Would this be a secure solution?

h4ckNinja
  • 3,006
  • 15
  • 24
yberg
  • 93
  • 1
  • 6
  • 1
    If i'm reading this right, If you send the hash to the client, an evil client can make a password attempt for an account they don't own, listen to what you send for the comparison and then crack the hash at their leisure to gain access. You just spent all that time trying to make a secure algorithm, don't blow it on the last step! – Ohnana Mar 08 '16 at 18:11
  • 2
    Similar question with answers [here](https://security.stackexchange.com/questions/23006/client-side-password-hashing). Short summary is that without HTTPS a captured hash from a valid user is as good as the original password to an attacker. So not very secure. – PwdRsch Mar 08 '16 at 18:59
  • 1
    @Ohnana even worse than that, an attacker can just wait for a legit client to successfully calculate their own hash and send it back, just sniff the username and hash being sent back and then send it again to authenticate themselves under the legit users account. – Jason Mar 08 '16 at 23:04

3 Answers3

35

The client is the attacker. Walk around your office while chanting that sentence 144 times; be sure to punctuate your diction with a small drum. That way, you will remember it.

In your server, you are sending Java code to run on the client. The honest client will run your code. Nobody forces the attacker to do so as well; and you use client authentication precisely because you fear that the client maybe be somebody else, who tries to impersonate the normal user. From your server point of view, you only see the bytes that you send to the client, and the bytes that come back. You cannot make sure that these bytes were computed with your code. The attacker, being an evil scoundrel, is perfectly able to envision not running your code and instead send the answer that your server expects.

Consider now a simple password authentication scheme where you simply store the users' passwords in your database. To authenticate, ask the user to send the password, and then compare it with what you stored. This is very simple. This is also very bad: it is called plaintext passwords. The problem is that any simple read-only glimpse at the database (be it a SQL injection attack, a stolen backup tape, a retrieved old harddisk from a dumpster...) will give the attacker all the passwords. To state things plainly, the mistake here is in storing in the database exactly the values that, when sent from the client, grant access.

And your proposed scheme ? Exact same thing. You store in the database the hash value "as is". And when the client sends that exact value, access is granted. Of course, the honest client will send the value by hashing a password. But, let's face it: many attackers are not honest people.


Now there is value in doing part of the hashing on the client side. Indeed, good password hashing is an arms race, in which the hashing is done expensive on purpose. Offloading some of the work on clients can be a nice thing. It does not work as well when clients are feeble, e.g. smartphones with Java, or, even worse, Javascript (which is a completely different thing, despite the name similarity).

In that case, you would need to run bcrypt on the client, and store on the server not the bcrypt output, but the hash of the bcrypt output with some reasonable hash function (a fast one like SHA-256 would be fine). The processing of a password P would then be a bcrypt on the client, then a SHA-256 of the result, computed on the server. This will push most of the CPU expense on the client, and will be as secure as a raw bcrypt for what it is meant to do (see below).


I understand that you want to "encrypt" passwords (hashing is not encryption !) because you want to use plain HTTP. You do not like HTTPS for the same reason as everybody else, which is the dreaded SSL certificate. Paying 20 bucks a year for a SSL certificate would be akin to having your skin removed with a potato-peeler sprinkled with lemon juice.

Unfortunately, there is no escaping the peeler. As others have remarked, even if you have a rock solid authentication mechanism, raw HTTP is still unprotected and an active attacker can simply wait for a user to authenticate, and hijack the connection from that point. A very common example of "active attacker" are people who simply run a fake WiFi access point -- that is, a completely real WiFi access point, but they also keep the option to hijack connections at any point. This is a kind of attack model that cannot be countered without a comprehensive cryptographic protocol that extends over all the data, not just an initial authentication method. About the simplest kind of such protocol is SSL/TLS. Any protocol that provides the same guarantees, that you absolutely need, will also be as complex as SSL/TLS, and much harder to implement because, contrary to SSL/TLS, it is not already implemented in the client software.

SSL is there, just use it. As for the lemon, suck it up.

(If the financial cost is a barrier, there are free alternatives, such as Let's Encrypt and pki.io. Whether they fit your bill remains to be seen, but they are worth considering if you are really short on cash.)

Tom Leek
  • 168,808
  • 28
  • 337
  • 475
  • 5
    Doesn't client hashing of _anything_ just make their hash the "password," even if you trust them? – Michael Mar 08 '16 at 21:27
  • 3
    Regarding the cost of SSL: Take a look at https://letsencrypt.org/getting-started if you're on a Linux server. Pretty cool implementation, free and mostly very easy to set up. There isn't much of an excuse these days – Sam Mar 08 '16 at 21:34
  • I was going to mention letsencrypt and pki.io as free alternatives. There are limitations, such as letsencrypt requires renewal every 60 days, but that's easy to automate. – h4ckNinja Mar 08 '16 at 21:50
  • TLS cert: $20. Building your own TLS replacement: priceless (and not in a good way). – jpmc26 Mar 09 '16 at 05:23
  • 1
    I wish people would quit thinking that SSL certs cost money!! Let's Encrypt and StartSSL both work well, and are COMPLETELY FREE. Remember, everyone, COMPLETELY FREE. No money required, at all. – BenjiWiebe Mar 09 '16 at 05:50
  • 3
    @Michael, yes and no. The server-side SHA-256 means an attacker can't read passwords out of the database, while the client-side bcrypt means that guessing passwords out of a dictionary is an expensive process. This leaves sniffing the halfway-hashed password off the wire as the only practical attack. – Mark Mar 09 '16 at 09:38
  • @BenjiWiebe It's a little hard to believe there's no catch. (The frequent renewal requirement being one example.) But here's why they get ignored: even if a someone is unwilling to use the free versions for any reason, buying a cert at typical market prices and installing it is still *much, much, much* cheaper than trying to replace TLS yourself. Bottom line: it *doesn't matter* if there are free options; the alternative to buying a cert is still prohibitively expensive. So free certs aren't necessary to shut down the argument. – jpmc26 Mar 10 '16 at 07:28
  • @jpmc26 Sometimes someone is not willing to pay for an SSL cert because they aren't entirely sure it is needed. What if the project flops, after you've shelled out the cash? Also, there isn't a catch with StartSSL, at least I've never found one. Also, their certs do not need renewing for one year... – BenjiWiebe Mar 10 '16 at 19:20
  • @BenjiWiebe If you can't afford shell out $50 for your project... well... you probably shouldn't have a project. You're gonna spend *way* more than that just on man hours alone, even without TLS. As for whether it's needed, that's an easy question: do you have *any* info that you don't want an attacker to have? If yes, you need TLS. (Or if that's too hard: is your site a static HTML site? If no, you very likely need TLS.) – jpmc26 Mar 10 '16 at 19:38
  • Are you sure that making a SHA256 out of a BCrypt Hash won't result in less entropy on the final hash? Sometimes when we apply some of these crypto magical algorithms one on top of the others they remove the entropy of the result making it weaker, and to be honest I don't have the mathematical background to be sure this wont happen. – mFeinstein Mar 26 '17 at 06:47
8

If you are not using a secured connection (https) then your schema is flawed in several ways:

  • The hashed password is all that is needed to log in: you are vulnerable to a replay attack;
  • Since the connection is not encrypted, the javascript you send to the client can be manipulated by the attacker (the attacker could just let the client send the password back to them).

If you want secure login, you need to be able to guarantee the client side code is correct and that there is nothing of value to be sniffed on the wire.

In almost all circumstances, the best solution for this is to use a SSL/TLS secured connection. You should send the non-hashed password over this secure connection and do the hashing on the server side.

Also, by removing the $2a$10$ part, you prevent yourself from increasing the iteration count anywhere in the future: because you do not store the iteration count, you have no method of identifying old hashes from new ones after you decide to increase the iteration count.

(If you can guarantee the client code, you could theoretically use SRP but getting it right is a complex affair and you are likely to fail).

Jacco
  • 7,402
  • 4
  • 32
  • 53
2

If you are sending anything over an unencrypted HTTP connection, assume it is being watched and can be compromised with MITM.

If someone were to MITM you, they could supply their own salt and respond with their own hash. Therefore any attacker could completely spoof your authentication process.

You should be hashing the password server side and using a secure connection.

d1str0
  • 2,348
  • 14
  • 24
  • I wonder if you could hash it on client side, then hash the hash on the server side... – SomeoneSomewhereSupportsMonica Mar 08 '16 at 19:55
  • @SomeoneSomewhere the problem is any attacker could pretend to be the server. – d1str0 Mar 08 '16 at 20:12
  • Sorry, I should have been more clear. MITM is definitely a problem that can't be sorted with this, and TLS is essential. However, hashing it client-side does at least stop a captured password being used elsewhere (superfish-style attacks come to mind). And hashing it server-side prevents the hash/password being stolen from the server then being re-used on that server. – SomeoneSomewhereSupportsMonica Mar 08 '16 at 20:20