0

I've read a lot of documentations about encryption in the last weeks and I think I now have a good idea of what system I will use for my project (standard hybrid cryptosystem using RSA/AES). However, there is still an unanswered question and I really need advice on this one.

The project have to manage sensitive health data which need to be properly secured from ReactJs/Flutter clients through a REST API to a MongoDB database. Each individual account element can be shared to other account(s).

REST API use HTTPS

ReactJs client use HTTPS

Flutter mobile application client currently use nothing, what should I do to have same security level as web client for data on transit?

1- Is my current view for encrypting this data securely correct?

Register :

  • Client (ReactJs/Flutter) side, send email and password in plain text (SSL) to REST API.

  • Server (API) side, generate RSA 4096 bits (or is 2048 enough?) key pair. Store in db public key in plain text in user model.

  • Generate 256 bits user salt using CSPRNG. Store in db salt in plain text in user model.

  • Derive password with PBKDF2 with 100000 iterations, user salt, 32 bytes length, sha256 (are these parameters + salt parameters enough?).

  • Generate 256 bits IV using CSPRNG (is this enough?). Store in db IV in plain text in user model.

  • Encrypt private key using AES with derived password key and IV. Store in db encrypted private key in user model.

Login :

  • Client (ReactJs/Flutter) side, send email and password in plain text (SSL) to REST API.

  • Server (API) side, derive password with PBKDF2 and same parameters as before. Store derived password key in server-side user session.

Element creation :

  • Client (ReactJs/Flutter) side, send sensitive data in plain text (SSL) to REST API.

  • Server (API) side, generate 256 bits data encryption key for the element using CSPRNG (is this enough?).

  • Encrypt generated data encryption key of the element with all public keys of users who have access to the resource (including the resource author). Store encrypted data encryption keys in element attached with uids of users who have access to the said resource.

  • Generate 256 bits IV using CSPRNG (is this enough?). Store in db IV in plain text in element model.

  • Encrypt sensitive data with generated data encryption key and IV using AES. Store in db encrypted sensitive data.

Element get :

  • Client (ReactJs/Flutter) side, request resource ids to REST API.

  • Server (API) side, decrypt user's private key with derived password key stored in server-side user session and user's IV.

  • Decrypt the encrypted data encryption key attached to the uid in the element with decrypted user's private key.

  • Decrypt sensitive encrypted data of element with decrypted data encryption key and element's IV.

  • Send to client plain text decrypted data.

2- This model currently use server-side encryption with encrypted keys stored server-side...

  • I read that server-side encryption is not enough because the server is using the decrypted data and it can be read server-side if it's compromised. I agree with that.

  • I want to adapt this model for client-side encryption but I also read that client-side encryption and particularly Javascript client-side encryption is not safe. So I'm lost...

  • My idea was to get encrypted element, salts, IVs and encrypted data encryption key of element from the REST API for each request in order to decrypt and encrypt my data directly on the client + store derived password key in client because we don't want to fill password for each action... (but where can I store it securely in the client? Is secured/httpOnly cookie enough?). I don't see a clear way of doing that in a secured manner (storing derived password key client-side and doing the encryption + decryption client-side).

  • So what shoud I do for my REST API to manage only encrypted data (implement client-side encryption with this model?) OR Is the current model with server-side encryption/decryption is finally enough for that kind of sensitive data and we don't need to change it?

3- Can you help me resolve those identified flaws?

  • Anyone with compromised access to the db can generate its own data encryption key to encrypt new unwanted data on the element and then encrypt this new data encryption key with users' public keys. This new unwanted data could be then read in users' clients and this a flaw. My idea was to encrypt with a master key (stored in env var) the result of data encryption key encryption with public key and then decrypt it before doing the decryption with user's private key server-side in order to avoid this situation. Is this OK?

  • Anyone with compromised access to the db can view all derived password keys of current active sessions and then decrypt sensitive data of currently logged-in users. My idea was also to encrypt those derived password keys with the same master key and decrypt it server-side when we use it. Again, is this OK?

Sorry for my long post and the big number of questions but I think they are very important for me in order to understand the whole system, I tried to explain the best way possible. Thanks for ur help implementing a secured encryption system for my project!

10
  • 1
    Something about "I think I have a good idea about what I will do" and "sensitive health data" just rubs me the wrong way. – MechMK1 Feb 14 at 16:25
  • Actually hybrid cryptosystem is quite standard. This is what I want to implement after the researches I did for that. I don't understand where is the problem? Did you read my post at least? – Mister_Ideas Feb 14 at 16:55
  • Yes, I did read it. My point was rather that design of sensitive data storage and access should be done by someone with more experience in the field. – MechMK1 Feb 14 at 17:09
  • This is why I'm asking for help here to check if my implementation is good enough. You could try answering that question, it will help me. We are talking about an end-of-studies project so we don't have that kind of resources. – Mister_Ideas Feb 14 at 17:13
  • 1
    I admit that all the zigging and zagging kicked into TL;DR. A quick scan of the rest lead me to this possibly incorrect synopsis. You're putting a bunch of data in a database (Mongo NoSQL) . One presumes that you want to use the primary function of a database, which is to search. This precludes encrypting the data in the database. That leaves securing access and transit. Standard HTTPS and Login does this. It looks like you also want additional client validation too. Client side certificates or 2FA devices can do this. It seems like you are trying to invent the MonkeyHammerWrenchDrill . – user10216038 Feb 14 at 19:11
1

First, I see no issues in using the db just as an object store of encrypted blobs. It has the obvious limitations, but that's all.

Second, it is possible to do client-side encryption in javascript. There are good libraries you can use, don't try to roll your own. I would recommend using openpgp.js so you would just be working with openpgp keys and encrypted files.

Third, if you want to do the encryption client-side I would suggest:

Login:

  • The user provides their username (I think you are using an email for that) and a password.
  • Their browser calculates HMAC("login-password", password) and HMAC("private-key", password) locally
  • The browser sends username and the result of HMAC("login-password", password) to the server
  • The server validates the username and the 'password' it has been given. All the usual rules apply (logs of login attempts, blocking/throttling, detecting logins from unusual locations, etc.)
  • On success the server sends to the client their encrypted private key
  • Client decrypts their private key with HMAC("private-key", password)

Registration

  • Client creates their key locally. Provides to the server their username, HMAC("login-password", password), publicKey, Encryption(privateKey, HMAC("private-key", password))
  • For a new system I would recommend using argon rather than PBKDF2, although it isn't that relevant

Element get:

  • Client requests element from server
  • Server validates the user is allowed to fetch the element and sends the encrypted file to the client
  • Client decrypts with their private key (obtained in the last step of login)

Element create:

  • User creates the content to upload
  • User queries the server for the public keys of the people it should be encrypted (depending on the specifics of your system, the user may be choosing specific people or have only a generic knowledge of the kind of people that would need access)
  • The user encrypts the file locally with the public keys that it was given and sends to server
  • The encrypted file is stored in the server along the list of people allowed to view it

Some notes:

All encryption and decryption happen client side. Your server never sees any plaintext content or private keys. This is the best standard one can achieve.

Even if the full database was stolen, an attacker wouldn't be able obtain the stored contents assuming the user passwords were good enough. Also note you could use separate databases / servers for storing the user accounts and the actual contents.

Weak points:

The server is providing the people and keys the data must be encrypted to. Thus, an attacker with write access, could add their own key to the list of keys that should be given access to certain content. You can alleviate this somewhat by listing all the people to which it will be encrypted prior to doing it. If the user knows to whom it should be encrypted (e.g. this is a system in which the users explicitly choose with whom to share the data), that would help. However, the attacker could replace the key of one of the recipients with their own key and, while you could show the key fingerprint in the interface (so that it can be detected), it would be very rare for people to actually verify them.

Related to this, you should think how should the data be shared. Suppose we would have patients sharing data with their doctors. Should they only encrypt the data to their doctor? What should happen if he needs to be treated by a different doctor? Should they be encrypted to a whole department? How do you plan to handle people joining and leaving?

Since this is a web application, the client side protection is good as long as the server keeps being honest. If an attacker replaced the client application code, for anyone which accesses the system after that, it could silently add himself as recipient, decrypt and leak the data or, simply, send the attacker a copy of the username and password to log in.

Metadata:

So far, I only covered the data itself. Beware of the metadata. Someone sharing a lot of files with an oncologist is likely to have cancer (now or in the past, which could be guessed from their timestamps). A folder full of tests named as a disease, is likely to be suffered by the patient, and so on.


UPDATE: to address the questions in the comments:

not all the user's data will be encrypted, only the health related data

No problem. You just define your application so that certain fields are encrypted while others aren't. Depending on how you define them you might have an additional concern of an attacker perhaps being able to reconfigure your application to convert encrypted fields into plaintext ones. Additionally, there's a documentation / UI question on how to design them so that users are not confused on what is stored securely and what isn't.

I read that we can't trust Javascript to do crypto operations

This is false. See the Web Cryptography API. This was true some decades ago, but no longer true. You should be careful with this, though. One thing you particularly don't want to do is to try to generate your own random values from weak-entropy sources (such as time values). Use getRandomValues()

and we can't trust the client at all to do it because all the encryptions operations would be visible in source code.

Viewing the operations is not insecure at all. Per Kerckhoffs's principle the security of a cryptosystem should be secure even if everything about the system, except the key, is public knowledge. If you use AES properly, there is no concern in an attacker knowing that you are using, say AES-GCM. Embedding the key in the source code would be a terrible idea, but it's not what we are proposing here.

So why I should doing that?

You came here asking for ways to improve the system you thought in.

I want to have the benefits of AES for encrypting and the agility of RSA for the permissions so I think I will stick with that.

I don't see where you wanted RSA for permissions. Anyway, what I described above works with AES+RSA (as well as any other hybrid system, the actual algorithms aren't relevant, as long as they are secure)

how can I store the encryption/private key securely in browser? I don't know what to choose between a secure/httpOnly cookie with that key in plain text or localStorage or indexDB.

A httpOnly cookie would be sent to your application. 20 years ago, that was all you had available, but nowadays you would use a different method. Still, you are risking to permanently lose access to the files if (when) the user changes computers/browsers or simply clears their stored data. That's why I stored it encrypted on the server with a user-chosen password (and then derived two secrets from the original password, to keep that transparent).

Finally, do you have an idea for the flaw where anyone could generate a new key for a resource, encrypt new unwanted data with it, and encrypt it with all public keys of currently authorized users to see the resource, replacing the original content. Is new layer of encryption needed?

No. You don't need encryption, you need permissions. Even with the encryption, your backend should check authorization, and not let users read nor write resources they should not have access to. If you are concerned about someone authorized overwriting the contents, you can store a history of the changes, so that it's possible to browse previous versions (although per GDPR you will need to support permanently deleting records).

You could also include cryptographic signatures on the fields to verify locally who wrote the record. Similar issues of trusting the matching between public keys and people apply, though.


New update:

I still don't understand where to store the decrypted private key using password for all the duration of the session. When we log-in, we have to store it in the client in order to not re-enter the password at each encryption or after page reload for example. So what should I use instead of a cookie in the browser and for the Flutter mobile app to send the decrypted private key each time after only one login in a secured way?

I'm not familiar with Flutter environment. Ideally, I would keep it in a variable and use a dynamic page with no real browsing to other pages. If that's not available for you, I would probably use something like sessionStorage

I have also a problem with HMAC("login-password", password). When we register, what should be stored? HMAC("login-password", password) or bcrypt(HMAC("login-password", password))?

From the point of view of your app, the user password is HMAC("login-password", password) so you store a salted hash of that. Thus the bcrypt(HMAC("login-password", password)) option (although not necessarily with bcrypt, see e.g. my argon suggestion above)

"Depending on how you define them you might have an additional concern of an attacker perhaps being able to reconfigure your application to convert encrypted fields into plaintext ones." But even if I choose to encrypt all of the user's data, an attacker accessing the application could just remove the function call doing the encryption, what is the difference?

There is not a big difference. It depends on the layer where it is done. If you had a flag in the database saying which fields to encrypt, changing it may be simpler than modifying your the files with the code.

I was currently using npmjs.com/package/csprng for my tests, is this one also ok for random or should I change?

That package seems fine, according to its documentation.

And my main concern: with this system using only client-side encryption, as the API server, how can we check that: 1- the data that we want to be encrypted is actually and correctly encrypted since we don't do any encryption operation in the API side anymore?

You don't verify the encryption server-side. If the server could decrypt it in order to verify the contents, that would defeat the point of only the client being able to decrypt it. What you should do instead is to properly test that the client correctly encrypts the parameters that it should. It can also reject fields that should be encrypted but actually aren't correctly encrypted.

Note this doesn't preclude you from using a convention of e.g. placing a prefix on encrypted blobs. Your server could verify that fields that it is present for fields that are expected to be encrypted, in order to prevent unintentionally storing a non-encrypted version there.

2- the call to our API side is done from a trusted and validated client version that we developed and not another app using our register/login endpoints (I want to black that also but I don't know how to do it exactly since those endpoints are public by definition).

This is a completely different problem. And one that cannot be solved. You can have the validated client voluntarily provide their version to the server (so that it can e.g. warn about an update being available), up to using an embedded client certificate. Yet, no matter what you do, it can be reversed to use different client.

The good part is that it doesn't really matter which client is used. It doesn't affect the security of your system. A "bad" client could use weak random numbers, or even break the protocol by storing something else (and how is that different than a client that deleted the contents?), but that doesn't affect the security of the data stored using a proper client. If the security of the system relies in the (by definition) untrusted client being honest, then you're doing it wrong.

15
  • Thanks for the detailed answer. First of all, not all the user's data will be encrypted, only the health related data (medicines, treatments but not their color or favorite status for example). The problem with client-side encryption is I read that we can't trust Javascript to do crypto operations and we can't trust the client at all to do it because all the encryptions operations would be visible in source code. So why I should doing that? I want to have the benefits of AES for encrypting and the agility of RSA for the permissions so I think I will stick with that. What library should I use? – Mister_Ideas Feb 15 at 10:03
  • An important question also is how can I store the encryption/private key securely in browser? I don't know what to choose between a secure/httpOnly cookie with that key in plain text or localStorage or indexDB. What is the standard for storing that kind of key in the browser and a Flutter mobile application? Finally, do you have an idea for the flaw where anyone could generate a new key for a resource, encrypt new unwanted data with it, and encrypt it with all public keys of currently authorized users to see the resource, replacing the original content. Is new layer of encryption needed? – Mister_Ideas Feb 15 at 10:08
  • @Mister_Ideas see my update – Ángel Feb 15 at 21:39
  • I have a better understanding every day thanks to you! I have still some questions (last ones I hope for you, thanks for taking the time!). I still don't understand where to store the decrypted private key using password for all the duration of the session. When we log-in, we have to store it in the client in order to not re-enter the password at each encryption or after page reload for example. So what should I use instead of a cookie in the browser and for the Flutter mobile app to send the decrypted private key each time after only one login in a secured way? – Mister_Ideas Feb 16 at 11:00
  • I have also a problem with HMAC("login-password", password). When we register, what should be stored? HMAC("login-password", password) or bcrypt(HMAC("login-password", password))? If we only store HMAC("login-password", password) then the hash becomes the password and it is the same as storing plain-text passwords, isn't it? How do you check the validity of the password at login step with your model precisely? – Mister_Ideas Feb 16 at 11:03

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.