1

I’m a beginner in cryptography and for my first project I use the client’s password to encrypt some data. More specifically, I use the password as passphrase in RSA private key generation). However, I do not want to trust the server when sending the login and password for registration/login. If the server saves the password secretly, it can decrypt the data too (since the encrypted data is also stored on the server, together with the enxrypted private key).

I thought about a fix: never send the real password to the server, but always the hash of the password (the real password is only saved in the client code as a global variable when a login is succesful).

Is this a good solution? What would be the problems here? Are there any (easy) alternatives?

Thanks in advance.

2
  • Isn't this the same question you already asked in your previous topic?
    – Ja1024
    Commented May 10 at 9:27
  • @Ja1024 It is a little bit, but I wanted to go deeper into the topic, and I don't know how but I missed your last comment there.
    – yolooow
    Commented May 10 at 15:00

1 Answer 1

1

Obligatory reminder: while it's fun and valuable to experiment with cryptography, don't use any cryptographic tool you haven't had reviewed by an expert (ideally multiple experts) for anything you care about. If you need to ask a question like this, you're almost certainly making (or will make) a lot of other mistakes. Even once you're not making a beginner's first project, don't trust your own skill. That especially applies the more of the cryptosystem you design yourself; you can get a lot further using an existing high-level library than trying to design something yourself, even using library implementations of the primitives. Secure cryptography is really hard!


What you're designing is a familiar concept: "zero-knowledge" end-to-end encryption. There are already a number of cryptosystems built around this purpose, with perhaps the most obvious consumer-facing one being password managers. The best password managers (that support sync at all) all operate on this model; you encrypt a "vault" of passwords, and authenticate to the password manager service, with the same password... but the service is completely unable to decrypt your vault. There's a few ways to do it, but they all involve client-side hashing of the password before transmission. A few important considerations:

  1. Password hashing is, by default, very weak, for two reason. One is password quality: most people use weak passwords, reuse at least some passwords, and have an incorrect model (that is still widely pushed by industry) as to what constitutes a strong password. This means most passwords have low inherent entropy; it doesn't take that many tries (as computers measure) to brute-force one. On the other side, hashing functions - even "secure" ones like the SHA[123] families - are extremely fast and require minimal resources. In many uses, those are necessary properties, but in password hashing they are very bad indeed; a high-end GPU can brute-force check billions, even tens of billions, of candidate passwords per second (exact number depends on things like which hash function is used). Even a "one in a billion" password is thus the work of a fraction of a second to crack.

To mitigate the first problem, passwords are typically "salted" before hashing. This means they are combined with a value that is at least moderately long, and unique to each password (the "salt"). Salts are typically generated randomly, but there are other options. Salting gives two advantages: you can't pre-compute password hashes into a giant lookup table (called a "rainbow table") that allows looking up a hash to find the password that (probably) generated it, and even if multiple people have the same password, their hashes will be unique. Salts can be stored in plain text (usually in the DB along with the password hash), though ideally they aren't known to the public (else an attacker can precompute a rainbow table at least for one user at a time). A random "pepper", also called "secret salt" and stored only in the server (not in the DB), is sometimes used in combination with salt to provide extra protection against a database compromise.

To address the fast hashing problem, you want to use a purpose-build "password hashing function" (or "key derivation function"; the meanings are not identical but in practice most things are either neither or both). Such functions have a few properties:

  • They are designed to be slow, with a tunable "cost" or "work factor" so you can slow them down even more as hardware gets faster.
  • They require a salt, as described above.
  • They (at least, the good ones) require a meaningful amount of memory (in a way that's hard to trade off against compute time, especially pre-compute time, and can't be shared between parallel attempts), limiting the number of guesses that can be made in parallel. The reason GPUs are so fast at hash cracking is that they are highly parallel and can try tens of thousands of candidates at the same time, but they only have a few GB (at most, a few tens of GB) of RAM and tens or hundreds of MB of cache, so if you can make each password hashing process take at least a few KB you can slow the parallel attacks by a lot by forcing them out of cache, and if you can make them take a few MB you can slow it more by making the GPU incapable of highly parallel attacks at all. The best functions thus require a tunable amount of RAM.

Right now, there are four widely known password hashing functions. They are:

  • PBKDF2, a simple construction of iterated hashes with repeated salting. Widely supported but not at all "memory-hard", so it's easy to parallelize and requires very high iteration counts.
  • bcrypt, a construction based on a component of the Blowfish symmetric block cipher. Nearly as widely supported, and very widely used, it has some limitations (maximum input length and fixed output length, fixed memory cost of only a few KB) but is considered solid even today despite being quite old.
  • scrypt, a function built with support for tunable memory hardness as well as compute cost. Never as widely adopted as the ones above, it has mostly been superseded.
  • argon2, a highly-tunable family of functions (most commonly used is probably argon2id) that won a multi-year competition to select a new password hashing function. Although originally treated with some suspicion (as new things in cryptography rightly are), it's increasingly widely adopted, with high-quality implementations available for most languages.

To avoid the risk of a malicious (compromised, court-ordered, or otherwise adversarial) server brute-forcing the password hash sent by the client, you want to perform the client-side hashing with one of those functions. Unfortunately, there's nowhere to store the salt - at least, nowhere that an attacker can't retrieve it pre-authentication (since legitimate users also need it pre-authentication) - so you generally use a deterministic salt (still unique per user, but based on the user's email address or similar) and can't use pepper at all.

The server then takes the client-provided hash and runs it through additional hashing before comparing it to the authentication database. You don't want to use only client-side hashing, or else the hash in the database becomes "password-equivalent" for authentication. The combination of client-side and server-side hashing (with the server-side hashing using server-only salts) provides the best security.

Meanwhile, the client can't use the hash that it just sent to the server as the encryption key for the secret data. Nor can it use anything derived from that hash to produce the key. Otherwise, a malicious server could just perform those same steps (on the transmitted hash) to re-derive the user's key. Instead, the most common approach is to derive the key from the password client-side, and then hash the key further to generate the authentication hash that the server gets. Alternatively, the key derivation and authentication hash can use different salts or even different algorithms, but that adds complexity and cost for basically no benefit.


Some final notes:

  • This is not a complete design; there's a lot more to building a really secure end-to-end encryption system.
  • Systems like these make password reset a destructive operation. If a user ever resets - not to be confused with "changes" or "rotates" - their password, it will be impossible to recover the old derived key or anything encrypted only with it.
  • You probably don't want to save the password itself anywhere, even client-side. Better to save the derived key.
  • One alternative to all of this is to use a key that is unrelated to the authentication password. The key could be a different password, or some other kind of secret (e.g. a random key file stored on a flashdrive). Or the server could avoid using passwords for authentication, relying instead on FIDO2 authenticators (passkeys) or similar, and then the client never sends any form of the password to the server at all.
  • If you're creating this as a web app, there's no way to achieve reliable zero-knowledge, because you always have to trust the server somewhat (it sends you the JS that your client then executes). A malicious server could change the JS it sends to include a keylogger, transmit the password and/or derived key along with the usual data, or otherwise compromise the security of your design.
  • If you want people to trust this system (or rather, the well-reviewed successor system once you've thrown the first few away and learned a lot in the process), you should open-source at least the client component (if the client does its job well, the server could remain proprietary - after all, the client doesn't trust the server at all anyhow - but usually you'd open source it too).
11
  • This answer is AWESOME, thank you very much! I just have 2 more questions:
    – yolooow
    Commented May 10 at 14:59
  • 1) " so you generally use a deterministic salt (still unique per user, but based on the user's email address or similar)", are there certain simple techniques you could advise me to use for the salt generation?
    – yolooow
    Commented May 10 at 15:00
  • 2) in the case of a Google login, a user doesn't have any password. Can I use the same technique as in my first question to generate a "password" from its username? Thank you in advance.
    – yolooow
    Commented May 10 at 15:00
  • 1) A simple option is just take the SHA2-256 (or similar hash) of the username (or email address, or similar). An attacker can usually predict it, but at least it's unique per user. 2) Bad idea; both the server and an attacker can be assumed to know the username (they aren't secret), so you can't use it (without some actual secret) to derive a key. There's no way to do SSO with no app-specific password (or keyfile, or similar) and get end-to-end encryption; the user has to supply some secret. However, you can use SSO for authentication and a client-only password for key derivation
    – CBHacking
    Commented May 11 at 8:33
  • Thank you very much! For 2. you mention "client-only password", you mean that I still ask the client to supply some sort of password? And store it on his device/encrypted on the server?
    – yolooow
    Commented May 11 at 9:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .