2
$\begingroup$

I would like to know if deriving a key from a pseudo-random string with a single iteration is secure.

Concretely, I am designing a system where a secret key is derived in the client side, and then sent to the server for authentication. The steps are as follows:

  1. User enters email and password
  2. Derive k0 <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
  3. Derive k1 <- pbkdf2(password: k0, salt: password, alg: 'sha256', iteration: 1)
  4. Send to server: (email, k1)
  5. Server derives pbkdf2(password: k1, salt: somerandomstring, alg: 'sha256', iteration: 20000) and compares it with the hash in the database

k0 is the 256-bit secret key. The client uses it to encrypt things using a block cipher. The client needs to send something to the server for authentication purposes. But the server should not know the client's secret key. My idea is that step 3 prevents the server from knowing the client's secret key.

But I am wondering if 1 iteration of pbkdf2 is enough. I think it is sufficient because k0 is pseudo-random, and the attacker shouldn't be able to figure it out from k1. (Exhaustive search takes 2^256 steps). Is this correct?


EDIT

For clarification, here is my design so far:

## Register

Client
* User enters email and password
* Derive k0 <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
* Derive k1 <- pbkdf2(password: k0, salt: password, alg: 'sha256', iteration: 1)
* POST to '/register': (email, k1, kdf, kdf_iteration)

Server
* Generate salt <- 256bit random string
* Derive k2 <- pbkdf2(password: k1, salt: salt, alg: 'sha256', iteration: 20000)
* Create a user with (hashed_password, salt) = (k2, salt)
* Login the user and reply with a session key

## Login

Client
* User enters email and password
* GET '/prelogin': (kdf, kdf_iteration)
* Derive k0 <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
* Derive k1 <- pbkdf2(password: k0, salt: password, alg: 'sha256', iteration: 1)
* POST to '/login': (email, k1)

Server
* Look up user (hashed_password, salt)
* Derive k2 <- pbkdf2(password: k1, salt: salt, alg: 'sha256', iteration: 20000)
* Check k2 == hashed_password, and reply with a session token

EDIT2

Clarification on what this is for:

This is to add end-to-end encryption for an open source note taking app (https://github.com/dnote/cli).

Basically, the client can sync the data with the server. Before leaving the client, all data is encrypted using k0 which server does not know.

The server should have no idea about how to decrypt the data, so I decided to derive k1 from k0 to send to the server rather than sending k0 directly to the server.

$\endgroup$
  • 1
    $\begingroup$ I don't think k0 is really acting as a proper secret key then, is it? Because in that case, the hash of k0 is not really useful for any cryptographic purpose (e.g. you cannot execute any cryptographic primitive like a signature or encryption using the hash of the secret key instead of the secret key itself). Thus, what you probably mean is that k0 is acting as the user password towards the server, essentially, and in that case you don't really need to use the double hashing; simply follow standard guidelines for password hashing (e.g. crackstation.net/hashing-security.htm). $\endgroup$ – Daniel Jan 29 at 10:10
  • $\begingroup$ This is not double hashing, this is a double password-based key derivation. PBKDF functions are used to derive keys from passwords and they also aim to reduce the brute-force attacks as done on hashcat. You don't need this. $\endgroup$ – kelalaka Jan 29 at 10:15
  • $\begingroup$ Thanks for your comment. I forgot to mention that the server should not know the client's secret key. Therefore I think the step 3 is necessary. But I wonder if 1 iteration is enough in step 3. I have updated the question. $\endgroup$ – Sung Cho Jan 29 at 10:32
  • $\begingroup$ Could you explain what you are trying to achieve with this construction? Why are you using the same key for different purposes? This is against best-practices. $\endgroup$ – Elias Jan 30 at 7:34
  • $\begingroup$ @Elias Thanks, please see my edit for my intention with this design. Are you saying it might be better off to create two separate keys (one for encryption and another for authentication), based on this circumstance? $\endgroup$ – Sung Cho Jan 30 at 10:09
1
$\begingroup$

The problem with this scheme is that $k_1$ can be calculated from $k_0$. That may not be a huge issue, but it can easily be avoided.

You can make sure that $k_0$ and $k_1$ are derived from the original master $k$ which is derived from the password. So you would have k = PBKDF2(password, mail, iteration_count) and k0 = KDF(k, "Enc") and k0 = KDF(k, "Auth").

If you've just PBKDF2 with SHA-256 you could define the second KDF(k, label) as PBKDF2(password: k, salt: label, alg: 'sha256', iteration: 1).

Other options are given in my other answer on the followup question.

$\endgroup$
  • $\begingroup$ Thanks for making it easy to understand. Could you clarify why you think it's bad that k1 can be calculated from k0? In my view, that poses no problem because k0 is secret. Under what circumstance would it be a problem? $\endgroup$ – Sung Cho Jan 31 at 5:57
  • 1
    $\begingroup$ When it becomes non-secret. KDF's are used for two purposes: to derive keys for different purposes (so you cannot use one for the other, e.g. preventing playing back your own messages to yourself in a transport protocol) and to distance them in case one of them gets compromised. See it as in depth defense, if you like. In principle all the root CA keys in my browser should also be secret, but the one from DigiNotar could be used freely by hackers. Now that's an issue, right? That's what you get when you trust too many keys. $\endgroup$ – Maarten - reinstate Monica Jan 31 at 13:51
  • $\begingroup$ Okay, that makes sense. Just found a relevant argument from the PKCS5 RFC: tools.ietf.org/html/rfc8018#section-8. > "In general, different keys should be derived from a password for different uses to minimize the possibility of unintended interactions." $\endgroup$ – Sung Cho Jan 31 at 20:51
  • $\begingroup$ That may also point to a more theoretical reason; if you have a prove that something is secure given a specific key, but you can possibly recalculate the key using an earlier key, then you need to prove as well that the calculation of the key doesn't invalidate the security proof. This can be tricky, but if you're not using k0 for a hash in a very particular way then it is unlikely to be a practical problem. Key loss would be a more practical reason for trying to avoid "unintended interaction". $\endgroup$ – Maarten - reinstate Monica Jan 31 at 20:56
0
$\begingroup$
  1. I am not sure how much pbkdf2 protects the salt. The salt is usually stored alongside the hash to prevent precomputation of hashes and parallel bruteforcing. I have not seen any cases when it needed to be protected by the hash. So although this will usually be the case I don't think you should rely on it and use secret values as a salt.

  2. Since the server does not know k0 it cannot verify k1 so I don't think this adds any authentication.

$\endgroup$
  • 1
    $\begingroup$ About the second point, the server runs pbkdf2 further on k1 and stores the result in a database for later authentication. I clarified it in my recent edit. $\endgroup$ – Sung Cho Jan 29 at 19:52
  • $\begingroup$ As @Brolf has already pointed out there is a trivial replay attack against this authentication scheme. I agree that one iteration of PBKDF2 would be enough to protect k0 in your construction. $\endgroup$ – Anonymous20DB28 Jan 30 at 9:25
  • $\begingroup$ Oh, and please don't ignore my first point. ;) $\endgroup$ – Anonymous20DB28 Jan 30 at 9:25
  • $\begingroup$ In your first point, are you referring to the part where k1 is derived from k0 using password as a salt? Could you clarify a bit more? Thank you. $\endgroup$ – Sung Cho Jan 30 at 10:17
  • $\begingroup$ If the attacker knows the KDF output he usually knows the salt so in theory a KDF might not protect the inversion of the output with respect to the salt. $\endgroup$ – Anonymous20DB28 Jan 30 at 10:42
0
$\begingroup$

Anonymous20DB28 is right. In your system the server can not compute k1.

You probably want to create two separate keys.

  1. k0 = pbkdf2(password: password, salt: somerandom256bit, alg: 'sha256', iteration: 10000)
    This key you use to encrypt your data and store somerandom256bit next to your ciphertext.

  2. It is always a very bad idea to store a users password in cleartext or encrypted on the server!
    To authenticate your user it is a good practice only store a salted hash of the users password on the server.
    For instance store something like:
    | userID | email | k1 = pbkdf2(password: password, salt: anotherrandom256bit, alg: 'sha256', iteration: 10000) | anotherrandom256bit |
    Then when you want to authenticate your user send him anotherrandom256bit and the user computes k1 = pbkdf2(password: password, salt: anotherrandom256bit, alg: 'sha256', iteration: 10000) and sends k1 back to your server.

This method of authentication does not require you to store the users password but the user always sends the same value to your server what could lead to a replay attack. To prevent this you need one more step:

You store the same values as in 2 but to authenticate the user do the following:

  1. After the user wants to be authenticated generate again256randombit
  2. Send anotherrandom256bit and again256randombit back to the user
  3. On the server compute k2 = pbkdf2(password: k1, salt: again256randombit, alg: 'sha256', iteration: 10)
  4. The user computes first k1 = pbkdf2(password: password, salt: anotherrandom256bit, alg: 'sha256', iteration: 10000)
    Then he computes k2 = pbkdf2(password: k1, salt: again256randombit, alg: 'sha256', iteration: 10) and sends k2 back to the server.
  5. On the server compare the user generated k2 with the one computed in step 3

Here the transmitted authentication value changes each time the user want's to be authenticated and therefore a replay attack is impossible.
Another advantage of this approach is that the main computational cost is on the user's side. The server only calculates 10 iterations per login attempt (1 iteration would also be OK but it feels bad to hash just once).

$\endgroup$
  • $\begingroup$ Thanks. Even though server cannot compute k1, it can hash it using pbkdf2 and compare the result for authentication. What do you think about this approach compared to your suggestion of generating two keys? Please see my recent edit that added step 5. $\endgroup$ – Sung Cho Jan 29 at 20:12
  • $\begingroup$ I think that using two keys is disadvantageous because a brute-forcing attacker only needs to calculate one of them to verify her guess. In other words, the attacks are twice as easy as it would be if we used a single key with twice the iterations. i.e. our cost = 2a, attacker's cost = a $\endgroup$ – Sung Cho Jan 29 at 21:19
  • $\begingroup$ @SungWonCho No, the pbkdf is a One-Way-Funktion. The only possibility to break it is by calculating the pbkdf with all possible values for password. The weak spot is the usually small entropy of user passwords. It is impossible to reverse the sha256 hash which is the base of the pbkdf. If you still want higher security look at Argon2 as alternative to pbkdf $\endgroup$ – Brolf Jan 29 at 21:54
  • $\begingroup$ Thanks. Is that the answer to my first comment? To be clear, I did not mean that server should reverse k1 to obtain k0. Rather, I meant that server can derive another key from it to verify the user. I edited the question to include the design I came up with so far. Does it make sense? $\endgroup$ – Sung Cho Jan 29 at 22:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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