31

When storing user's passwords that you need to verify against (but not use as plaintext) the current state of the art is:

  1. Hash the password
  2. Use a salt
  3. Use a slow hash function - bcrypt, scrypt, etc.

This provides the best possible protection against an attacker who has stolen the password database. It does not solve other issues like phishing, malware, password re-use, etc.

But there is one remaining problem: the slow hash function can allow a denial of service attack. A single request burns a lot of CPU, which makes a DOS possible with a relatively small number of concurrent requests, making it difficult to use defences like IP throttling.

Given the improving performance of JavaScript in browsers, it may now make sense to do this in the browser. I'm assuming here that the site is using SSL, so the JavaScript is delivered securely. If you're not using SSL, then using a slow hash function is unlikely to be a priority for you. There is at least one JavaScript implementation of bcrypt. But using this in a simple way would introduce two problems:

  1. The client needs to fetch the salt from the server. This introduces latency on login and, unless care is taken, can reveal whether a particular user account exists.
  2. If hashing is done purely on the client then the benefits of storing hashes are lost. At attacker who has stolen the password hashes can simply login using the hashes.

However, I think there are acceptable solutions to both of those problems:

  1. The salt can be generated as hash(server_salt + user_name) - where server_salt is a random number that is unique to the server, public, and the same for all users. The resulting hash appears to have the required properties of a salt.
  2. The server should do a single, fast, hash operation on the hash it receives. As an example: the server stores SHA-256(bcrypt(salt, password)). The client sends bcrypt(salt, password) then the server applies SHA-256 and checks the hash. This does NOT allow an attacker to conduct a fast offline brute force attack. They can do a fast brute force of SHA-256(password) because password has a limited amount of entropy - 2^50 or 2^60 or so. But a 128-bit bcrypt(salt, password) has entropy or 2^128, so they cannot readily brute force it.

So, is this a reasonable and secure approach?

I am aware of the general advice to "don't roll your own crypto". However, in this case, I am attempting to solve a problem that is not solved by off-the-shelf crypto. For some basic credibility, this has been looked at by John Steven (a recognised expert in the field) with positive outcome from a "brief" analysis.

paj28
  • 32,736
  • 8
  • 92
  • 130
  • You mean the client sends `bcrypt(salt, password)` not `bcrypt(password)` right? (in your solution paragraph no. 2) Feel I'd better ask even if I feel fairly certain – KajMagnus Jun 01 '15 at 17:24
  • @kajmagnus - yeah. I'm on mobile now; will edit question when on a computer. – paj28 Jun 02 '15 at 11:07
  • "This does NOT allow an attacker to conduct a fast offline brute force attack" It's true they can't do a fast attack to get the password, but they can do a fast attack to get a thing (`bcrypt(salt, password)`) that they can send to the server to authenticate. To a large extent this amounts to the same thing. – rlms Apr 03 '19 at 14:09
  • 1
    @rlms - They can't do a fast attack on that because it is 128-bit and effectively random – paj28 Apr 03 '19 at 15:44
  • You're right, I was misreading and being silly. – rlms Apr 04 '19 at 08:46
  • 2
    Libsodium suggests you do exactly this and provides the means to do so: https://libsodium.gitbook.io/doc/password_hashing – Erwan Legrand Sep 12 '19 at 09:35
  • @ErwanLegrand - Thanks, I will check that out. I pretty sub LibSodium didn't do that when I asked the question, so it's good to see things moving forward. – paj28 Sep 12 '19 at 09:38
  • I came here looking for a solution similar to yours. Looking to move my login system to one of the many serverless solutions and discovered that running bcrypt on the server hits computation limits. – Kernel James Mar 02 '22 at 02:48

2 Answers2

25

Using servername+username as salt (or a hash thereof) is not ideal, in that it leads to salt reuse when you change your password (since you keep your name and still talk to the same server). Another method is to obtain the salt from the server as a preparatory step; this implies an extra client-server roundtrip, and also means that the server would find it more difficult to hide whether a given user name exists or not (but it does not matter much in practice).

What you describe is a well-known idea, usually called server relief, which can be applied to just any password hashing function in just the way you describe:

  • Client obtains (or computes) the salt.
  • Client computes the slow salted hash value V and sends it as password.
  • Server stores h(V) for some fast hash function h, and uses it to verify the V value from the client.

This is safe. The main drawback is that the slow hashing will go at the speed of the client, so the number of iterations may have to be lowered, possibly considerably so if the client is computationally feeble -- as is the case for anything involving Javascript. If the system must work with a browser on a not-so-recent smartphone, then the iteration count will have to be 10 to 100 times smaller than what could be done on the server, implying a corresponding reduction in resistance to brute force (in case the attacker steals the hash values stored on the server).

So that's a trade-off: since the busy server offloads work on clients, it can use a higher iteration count without itself drowning under the load; but since the clients are weak, the iteration count must be lowered, usually much more than it was increased thanks to the offloading; this means that the construction is not worth the effort. Usually.


Of course, if the clients are computationally strong, e.g. this is a native-code application on a gaming machine, then server relief is a good idea and will look like the way you describe it.

Another possibility is offloading the work to another, third-party system, e.g. other clients (already connected): this can be done securely if the hash function includes the necessary mathematical structures. Such offloading is know as delegation. To my knowledge, delegation is offered by only a single password hashing function, called Makwa (see the specification), one of the candidates of the current Password Hashing Competition.

Tom Leek
  • 168,808
  • 28
  • 337
  • 475
  • 1
    Thanks...I had no idea this was called "server relief" and that it's an established idea. All the online references about it seem to be related to this PHC; was the idea around beforehand? (perhaps it had a different name?) Anywya, the PHC is very interesting! – paj28 May 25 '14 at 20:40
  • 2
    Additionally, you need to ge careful that you are not just replacing the password authentication with hash-of-the-password authentication (the hash just becomes the new password). In such a case, you lose the protection that the hash gives you against a lost password database. – atk May 26 '14 at 01:18
  • 1
    The hashing performance of javascript can be pretty good these days, well within 2x of native code when using AsmJS or Chrome. A hash tuned for 32-bit is a must. It depends on which clients must be supported. – user239558 May 26 '14 at 08:04
  • Only twice slower Javascript than native code for a hash function ? I'd love to see such benchmarks (this would even be faster than Java or C#). Do you have any reference ? – Tom Leek May 26 '14 at 20:51
  • This is my private implementation and not open source yet, sorry. – user239558 May 27 '14 at 09:12
  • My reference is a private implementation of blake2s vs the sha256sum linux utility. Blake2s hashes at around 70MB/s in javascript while sha256sum does 80MB/s on the same hardware. The blake2s optimized C implementation does 125MB/s. Blake2s SSE does 200 MB/s. – user239558 May 27 '14 at 09:19
  • @TomLeek You'd be surprised how fast JavaScript is these days. Here's some benchmarks comparing performance of ASM.js apps vs Native C++ compiled with Clang: http://arewefastyet.com/#machine=28&view=breakdown&suite=asmjs-ubench – Ajedi32 Jun 02 '15 at 13:49
  • Regarding "the server would find it more difficult to hide whether a given user name exists or not", this is fairly straightforward. When asked for the salt for a username, the server computes a fake salt HMAC(server-secret,lowecase(username)), then does a database lookup for the salt for that username. If the username is found, return the real salt to the client. If the username is not found, return the fake salt. This is constant time, fast, and returns deterministic salts for each user name query so an attacker can't distinguish real usernames. – rmalayter Feb 10 '17 at 14:13
1

I think the biggest concern about this approach is that it moves a security procedure you've determined is important off of your servers and on to the web browsers of users. While normally everything should work as expected you have no real way to verify that the password was actually hashed with bcrypt before it is sent to you. So if the client-side JavaScript is changed you may still end up with only a SHA256 hash of "123456" on your server.

Sure, you could check the format of data sent from the client to make sure it looks like it was hashed with bcrypt, but you don't know for sure that your desired operation completed.

That's not an ideal security model, but despite that I think the threats to this system are still fairly manageable. And you would be ahead of those sites who either don't hash or hash using plain MD5.

PwdRsch
  • 8,341
  • 1
  • 28
  • 35
  • If the client doesn't calculate the hash, then the result will be wrong. The server still needs to calculate the hash when changing the password, but not to verify it. – Gilles 'SO- stop being evil' May 26 '14 at 07:26
  • If the server does generate the bcrypt hash directly upon password change then you're correct. I didn't assume that since the question focuses on the client doing the bcrypt work. Without the server stepping in to do that a non-compliant client could submit a password change value without running it through bcrypt and subsequent authentications won't require it either. – PwdRsch May 26 '14 at 16:59
  • Just let the server compute the correct hash once, upon password change. This way, the client can never be stupid about it. – Domi May 08 '15 at 09:17
  • @Gilles Is this really necessary? Usually, the client is a web page or an application provided by the same company as the server, so it's usually fine. +++ If the user fools around with their client, then it's their problem. They could let the client store the hashed password instead. This also saves the client's time, it's equally insecure as what you're trying to prevent and can't be detected by the server. +++ If a user is foolish enough to use a third party client, then anything can happen and using a weaker hash is the smallest problem. +++ Any case I'm missing? – maaartinus Aug 12 '17 at 23:14