3

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.

mc9
  • 177
  • 6

3 Answers3

1

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.

Maarten Bodewes
  • 96,351
  • 14
  • 169
  • 323
0
  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.

Anonymous20DB28
  • 261
  • 1
  • 8
0

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).

Brolf
  • 23
  • 3