Deriving multiple hashes from a single password for different use cases
I'm designing a service to store secrets without relying on traditional mail-password system.
I will describe this service to give a bit more context for my questions, at the end.
secret
The payload we want to store (encrypted) for the user (size limited)identifier
A random value stored by the user (e.g., in a local file), required to retrieve the secret.password
A user-chosen password (may be weak).pass_hash1
A deterministic hash derived from password used server-side to compute storage key.pass_hash2
A deterministic hash derived from password used client-side to encrypt/decrypt the secret.secret_id
=hash(identifier + pass_hash1)
Unique record key in the server’s database.secret_encrypted
=encryption(private_key: pass_hash2, payload: secret)
The ciphertext of the secret using pass_hash2.
Store
On the client side, we generate a random secure
identifier
, probably store in a file, and, the user define apassword
that may be weak.From the
password
we generate two hashes:
pass_hash1
for authenticationpass_hash2
to encrypt the secret
- The client encrypts his
secret
usingpass_hash2
and make astore secret request
to the server containing:
identifier
pass_hash1
secret_encrypted
- The server receive
store secret
request and generate asecret_id
=hash(identifier + pass_hash1)
. Then, the server create a new database entry:
- id:
secret_id
- created_at:
DateTime.now()
- value:
secret_encrypted
Fetch
The client, own a file that contains his
identifier
and know hispassword
From the
password
we generate two hashes:
pass_hash1
for authenticationpass_hash2
to encrypt the secret
- The client make a
fetch secret
request to the server containing:
identifier
pass_hash1
- The server receive the
fetch secret
request an perform:
- Checks an in-memory
Map<identifier, DateTime?>
to check if the secret for thisidentifier
has already been requested recently. If not enough time elapsed theidentifier
remains rate-limited against targeted brute-force. - Compute
secret_id
=hash(identifier + pass_hash1)
and lookup in the database for an entry, if something is found it returns thesecret_encrypted
else it add theidentifier
andDatetime.now()
to the map to limit further attempts.
- The user can recover his
secret_encrypted
by deciphering it usingpass_hash2
as encryption key
Privacy and security goals
A user can store multiple secrets and we shouldn't be able to link any secret to a specific user. Each secret has his own identifier
.
This microservice shouldn't be able to answer the request give me the entry associated to this identifier
. The identifier
does not persist, it is temporary available in-memory map Map<identifier, DateTime?>
in case of failed attempts on fetch secret
. The map is cleared each time the server reboots. The identifier
alone without pass_hash1
is not enough to compute the secret_id
.
This microservice shouldn't be able to tell if many entries belongs to one or multiple users, we do not persist pass_hash1
it's processed at REST of the store secret
request, then forgotten.
In case of database leak, attackers shouldn't be able to read the stored secrets. The secrets are encrypted using pass_hash2
, the resistance to local brute-force depends on the quality of the user password
.
The microservice shouldn't be able to read the secrets as well because they are encrypted.
Questions
- Is there any existing standards close to what I'm trying to achieve (ProtonMail Key management server?) ?
- Is it safe to derivate two hashes from the same password to be used in two different ways: authentication and encryption ?
- I wanted to use deterministic hashing function (SHA-256, BLAKE2 …) over password hashing (bcrypt …) to avoid to deal with multiple output especially when I need to compute
secret_id
. Since hashes are never exposed, is it ok ? - Any other advice ?