Using the hash of the user’s password for encrypting/decrypting an E2E encryption/decryption key: is this good practice? [migrated]

I am developing a zero-knowledge app, meaning the data is encrypted in the client before it’s transmitted (over SSL) and decrypted after the data is received. If the database is ever compromised, without the user’s decryption keys the attacker knows nothing.

Of course, when the app is hosted on a web server, an attacker could still inject malicious scripts, but that’s always a risk. The idea is that the user data is encrypted by default. As long as no malicious code was added to the client code, the server should not be able to obtain the user data.

The title summarises how I intended to do this, but actually it’s a bit more convoluted:

  • On account registration, a secure random string is generated as (AES) encryption key (could also be private/public key generation here I guess). Let’s call this key K1.
    • All data will be encrypted/decrypted (e.g. using AES) with this key.
  • The plain text password is hashed to create another key. Let’s call this K2 = hash(plain password) (for example using SHA256)
    • K2 is used to encrypt K1 for secure storage of the key in the remote database in the user profile.
    • If the user changes his password, all that needs to be done is re-encrypting K1 with K2 = hash(new password), so not all the data has to be decrypted and re-encrypted.
    • K2 is stored in localStorage as long as the user is authenticated: this is used to decrypt K1 at bootstrap.
  • K2 is hashed again to generate the password that is sent to the API: P = hash(K2) (also using SHA256 for example)
    • This is to prevent that the decryption key K2 (and therefore, K1) can be deduced from the password that the API/database receives.
  • In the API, the password P that is received is hashed again before it is compared/stored in the database (this time with a stronger function such as bcrypt).

My question is: does this mechanism make sense or are there any gaping security holes that I missed?

The only downsides that I see are inherent to zero-knowledge, E2E encrypted apps:

  • Forgotten password = all data is lost (cannot be decrypted). This is why the user is recommended to write down the encryption key K1 after creating the account: then the data can always be recovered.
  • Searching, indexing, manipulating, analysing the data is limited because everything has to be done client-side.