This is an existing website with approx. 100K accounts, and passwords are hashed using bcrypt with a high number of rounds.

The current design that I'm questioning is that we're sending the username and password to the server, and doing the bcrypt on the server, instead of sending a SHA generated in the browser and bcrypting that, so that we never touch the user's password.

The original rationale was that if we're sending a SHA, then the SHA just becomes the password, and nothing is gained. But that doesn't seem true.

We see pretty routinely that people try to log in to our website using their Apple or Google username/password combo, because they don't fully understand the difference between our systems and Apple/Google's systems.

Now, their clear-text password does hit a highly isolated system behind the AWS load balancer, and this is even more isolated in the future. We touch the clear-text password for a microsecond and then forget.

But it still makes me queasy. So the question is, what's the remedy?

  • Would sending a SHA to the server be better, so that we at least never see the password server-side, even briefly?

  • What would a migration path look like, beyond changing this and resetting all passwords, requiring users to create new passwords whose bcrypt is now based on a SHA?

  • Any other ideas?

  • $\begingroup$ Seeing a hash of the password is an improvement on seeing the plaintext password, although it doesn't help with weak passwords. There are issues with pre-hashing and bcrypt though, which led to hmac-bcrypt. A much simpler, albeit slightly less secure alternative to OPAQUE is like how passkeys work, which involves a challenge-response protocol. It's explained here, and I'd recommend reading these slides as well. $\endgroup$ Dec 25, 2022 at 21:03
  • 2
    $\begingroup$ @samuel-lucas6: Convert your comment to an answer and will vote it up. $\endgroup$
    – mentallurg
    Dec 25, 2022 at 23:27

4 Answers 4


This is an excellent question, and one of the motivations for OPAQUE, as explained in the Introduction section. Unfortunately, PAKEs are still rarely used as far as I know, so this issue has sort of just been accepted.

Would sending a SHA to the server be better, so that we at least never see the password server-side, even briefly?

Yes, it's a slight improvement from that perspective. However, hashes for weak passwords will be well known and easy to find, so it doesn't solve the issue completely if such hashes are logged.

What would a migration path look like, beyond changing this and resetting all passwords, requiring users to create new passwords whose bcrypt is now based on a SHA?

The other dilemma is that pre-hashing is particularly problematic with bcrypt and thus often not recommended.

  • Unsalted/unpeppered hashes can allow shucking attacks. This is an argument against pre-hashing beyond just bcrypt.
  • Null bytes from pre-hashing can lead to colliding passwords.
  • Some implementations reportedly can't handle binary inputs properly.

Solutions include Base64 or hex encoding the hash to address points 2 and 3, not pre-hashing, or using hmac-bcrypt, which is designed to address all of these problems whilst performing pre-hashing. You could replicate the hmac-bcrypt pre-hashing approach.

Any other ideas?

You could use public-key cryptography without needing to rely on a PAKE. This is easier to implement but less secure because it doesn't prevent precomputation. The idea is explained nicely in a blog post by Frank Denis, author of the libsodium and LibHydrogen cryptographic libraries.

In simplified terms (please see the blog post for the full details), it goes like this:

  1. Do client-side password-based key derivation (not bcrypt, which isn't a KDF) to generate a deterministic seed. Use context string || username as the salt, or you could get the server to send a salt after the user provides their username.
  2. Generate a key pair on the client from the seed.
  3. The server sends a random 256-bit nonce to the client.
  4. The client computes a signature over context string || username || nonce and sends it to the server alongside their public key and username.
  5. The server verifies the signature using the received information.

Now passwords don't need to be sent to the server, and there's no server-side password hashing DoS risk. However, passkeys are hopefully the future.

  • 1
    $\begingroup$ Beautiful answer that teases out the exact issues I was looking for. Top notch! $\endgroup$
    – Per
    Dec 27, 2022 at 8:48
  • 1
    $\begingroup$ I had no idea about shucking, and it says it right there in the docs, and also that hmac would be the correct digest if one wished to pre-hash. $\endgroup$
    – Per
    Dec 27, 2022 at 8:53
  • 1
    $\begingroup$ For right now, I'll leave it alone. I'm not in favor of OPAQUE or PAKEs, because it adds a huge, complex component, setting off my alarms. Thanks for laying out the landscape. $\endgroup$
    – Per
    Dec 27, 2022 at 8:55
  • 1
    $\begingroup$ Thanks. That's fair enough. Maybe keep an eye on passkeys though, which are just starting to roll out. $\endgroup$ Dec 27, 2022 at 12:30
  • $\begingroup$ The solution based on asymmetric encryption that you described is definitely more secure compared to other methods mentioned in this thread. But there are some problems in the answer. $\endgroup$
    – mentallurg
    Dec 27, 2022 at 14:55

Would sending a SHA to the server be better


If the hashing is done server side & the server's password database leaks, it's useless because a client cannot use the hash for logging in because client has to send the password & not the hash for logging in.

If the hashing is done on the client side & the server's password database leaks, then a fake client can login by sending the hashes to the server.

So that way is less secure.

Any other ideas?

PAKE - Password Authenticated Key Exchange - https://eprint.iacr.org/2018/163.pdf

  • 2
    $\begingroup$ You may be misreading the question slightly. It's not either/or. The choice is between sending the clear-text password AND bcrypting on the server, versus sending a client-side SHA, and STILL bcrypting on the server. The difference then seems mainly that we lose the ability to see the password server-side, which is desired. $\endgroup$
    – Per
    Dec 25, 2022 at 13:38
  • 1
    $\begingroup$ @per The client side SHA is then like the password, right? So the advantage you get is only if this kind of protocol doesn't become popular & you are the only one using it. The moment this protocol becomes popular, then the SHA hash is again the password for a majority of the systems & the original problem you are trying to solve resurfaces. $\endgroup$
    – user93353
    Dec 25, 2022 at 13:44
  • 1
    $\begingroup$ Not really. I means that only the browser sees the actual password, so at least we don't have the responsibility of having Apple/Google logins thrown at us, because what passes through our servers can't be used to log in to Apple/Google. But certainly, it's only half of it. Our website could get hacked and some Javascript could exfiltrate passwords before the SHA. The point is to reduce the touch points, in a PCI spirit. Nothing beats not touching things. $\endgroup$
    – Per
    Dec 25, 2022 at 13:49
  • 1
    $\begingroup$ @per - But certainly, it's only half of it - Assuming both you & Apple/Google use your suggested protocol, then what else is the unknown half? What am I missing? $\endgroup$
    – user93353
    Dec 25, 2022 at 13:56
  • 1
    $\begingroup$ OK, let's say our SHA is a concatenation of "ilovemeatballs" + the user's password. That will never be Apple's protocol. The point is only to stop the clear-text password from hitting our servers. $\endgroup$
    – Per
    Dec 25, 2022 at 14:02


If you want to choose between two methods only, sending plain password or sending password hash, then sending hash is better.


If there is a mistake on the server side and the password was written to a log, this may give some insights about how particular user generates passwords: What characters are used, how long the passwords are, how much are the passwords indistinguishable from a random sequence.

Besides, as you mentioned, some users may reuse their password for multiple systems. If mistakenly written to the log, this may be a security problem.

If users send hashes, you will avoid these potential problems.

About brute-forcing: SHA is very fast by design. That's why trying some character sequence as a password directly, i.e. applying bcrypt directly, or first hashing with SHA and then applying bcrypt, will take similar amount of resources. In both cases the success of brute-forcing will primarily depend on the cost factor that you use in bcrypt.

But if you want that the client doesn't send even the hash, consider what the others suggested, PAKE / OPAQUE.

To avoid the risk related to password reuse and thus password shucking, you can add a salt during pre-hashing, e.g. user you application name or domain name as a salt. Then for the same password with the same hashing function you will get a different hash value.

  • $\begingroup$ Comments are not for extended discussion; this conversation has been moved to chat. $\endgroup$
    – Maarten Bodewes
    Dec 27, 2022 at 20:44

If you are designing from scratch then try this -

Server-Side/Back-End :

Store the passwords using password-based-encryption or HMAC-HASH (derive a key from the password + use a salt that is known publicly but specific to your organization).

This way even if the user has re-used passwords and the database is compromised (last-pass is the most recent one as of writing), the hacker cannot mount a dictionary attack.


The reason for the salt being public is simply because that's the only way we can repeat the process at the client end.

Hence, once the user enters the password, repeat the process (the client has the public salt and the password hashing/key-derivation algorithm).

This way the clear password never reaches the server (which I guess is what you are looking for).


This would be a prolonged one. You can auto-enroll all users at the point of successful login to the new system. But you'll need to retain old user's on the older setup/format. Also you'll need to handle both old and new format's at the client and server end (have some kind of a flag to determine whether the user is on the older setup or the new one). If two-steps are fine, you could first ask the user for the email and then figure out which system they are on (old or new) and subsequently prompt the user for the password accordingly !

Update 29-Dec-2022 :

To address the concern mentioned in the comments, where the database itself is compromised, we can introduce the following modifications to the scheme -

  1. Include the User-Id as part of the the public-salt. This way each password will effectively have its own salt ! So what is sent to the server is effectively - hash(hash({#clear-password}), {#user-id}, {#server-public-salt})

    Let's call this resulting value as client-user-credentials.

  2. The server while storing, will now apply a private hash on the above user-secret and then store. So the server effectively stores - hash({#client-user-credentials}, {#server-private-salt})

  3. Going forward, the client always calculates client-user-credentials and transmits to server. This does not remove the possibility of man-in-the-middle scenarios but does address the concern that even in the event of a database breach, it would not be possible for the hacker to mount an attack that would compromise the account.

There is still however a possibility that if at all the server is compromised, the hacker can linger for some time and capture the clear client-user-credentials but that would be difficult to solve and perhaps other schemes suggested else-where in the answers can be used.

  • 2
    $\begingroup$ If an attacker obtains database with hashes, it will be sufficient to send hashes, and the server will accept them. This will be a serious security problem. $\endgroup$
    – mentallurg
    Dec 25, 2022 at 23:11
  • $\begingroup$ Thanks. Had not considered that. Perhaps we can introduce a re-hashing step at the server. $\endgroup$ Dec 29, 2022 at 15:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.