3

I'd like to build a simple privnote-type clone for fun. The idea is this:

  1. User A writes a note in their browser, browser encrypts it client-side
  2. Server saves the pre-encrypted note without knowing the decryption key
  3. User A then sends a link like abc.hidden/mynoteid#mydecryptionkey to user B
  4. User B decrypts the message on a local browser

The question I'm struggling with is this - should the server allow anyone to fetch abc.hidden/mynoteid? Server being able to decrypt messages (I'd like this to be entirely immune to logging of any sort and all encryption/decryption happening clientside) defeats the entire purpose.

Because the notes are one-time-use-only, a fetching of the note must destroy it. But how can I know that a correct decryption key was supplied without decrypting the message server-side exposing it to logging?

Lastly, would a React app and a generic REST server with Redis to store messages suffice for this task? (Supposing that messages have a TTL, Redis seems an ideal choice) What happens if a malicious actor gains access somehow (without knowing the decryption keys which should be generated on the spot and only just once)

What encryption algorithm is best suited for this task? I don't think we need 10 seconds of bcrypt "work".

I understand that sending sensitive info over the internet is yucky but it happens a lot and if it does happen in a proverbial "marketing department", having a tool like that could ease some worries about PII.

Plus, I think it's a fun project either way.

1
  • Could you focus on a single question? For your question about the one-time-use-only scenario, perhaps you could have a public key that is known to A, B and the server, that way the note will not be exposed or deleted if a valid noteid is supplied without the corresponding public key.
    – Rik D
    Jan 6 at 7:47
3

should the server allow anyone to fetch abc.hidden/mynoteid?

This is up to the business to decide. Either everything is public and predictable (for instance the note identifier is an auto-incremented key), or the identifier is using a pseudo-random cryptographically secure number generator, or prior to accessing the note, the user should authenticate first.

But how can I know that a correct decryption key was supplied without decrypting the message server-side exposing it to logging?

The client app may do an HTTP POST telling that the message was decrypted (or that a wrong password was submitted). Beware of network issues, that is, the client may receive and decrypt the message, but be unable to do the actual HTTP POST because it disconnected during the decryption step.

I'm not entirely sure how to do that and then CONFIRM that it was received at a minimum before purging it from redis.

Either you trust the client, or you don't.

  • If you trust the client, the workflow can look like this. First, the client downloads the encrypted piece of text, and decrypts it, but doesn't show it yet to the user. Then, it HTTP POSTs, to indicate that the message was decrypted successfully. This is when you remove the message from Redis. Only after it receives the response from the server it can proceed with the display of the message to the user.

    This ensures that you will necessarily delete the message from Redis once the user was given a chance to read the message. This, however, doesn't mean the user have actually read it. Maybe while receiving the response to the HTTP POST, user's PC crashed.

    The workflow could be made more complex by adding a second HTTP POST. The first one would notify the server that the message was decrypted. The second one—that the message was displayed to the user. Again, the very fact of displaying a message doesn't mean the user could read it. Maybe he switched to another browser tab, and when switching back to yours a few seconds ago, his browser crashed. There could obviously be an edge case where the server would receive the first POST, but never get the second one.

  • If you don't trust the client, there is nothing you can do. A malicious client can claim that the message wasn't decrypted, or claim that it was. One possible scenario is a malicious user who is repeatedly requesting encrypted messages in order to bruteforce them, and in order to stay under the radar, HTTP POSTs that the decryption wasn't successful (or doesn't POST anything at all!), in order to prevent the message from being removed (otherwise, the legitimate recipients of those messages would sooner or later alert you that the messages are always deleted).

    Naturally, a failed decryption could raise red flags, as well as a lack of HTTP POST within seconds after the server delivered the encrypted message. Multiple failed attempts to decrypt the same message may indicate a legitimate user (i.e. the person tried the same link again and again, hoping that the error message will go away), whereas the repeated requests to read many messages may indicate a problem.

Note that the workflow can be made even more complex by adding a verification step. Instead of just loading the secret from the server, the client may first validate the decryption key against a (publicly available) PBKDF2 of the password. If and only if the client claims that the check succeeded, the server can deliver the actual encrypted message. If after that, the client claims that it cannot decrypt it, well, that's very suspicious, because the client confirmed having the correct key.

What happens if a malicious actor gains access somehow?

If a hacker gains access to your server, you're screwed. He can modify the JavaScript code to, for instance, send the plain messages to his own server. Or to send the plain messages to his own server and display something else to the actual users, such as a request to send money to a specific account.

Note that security wise, you're screwed even if the hacker doesn't gain access to the actual servers, but to the clients' machines, for instance through a browser extension. With the exact same effect, by the way, i.e. an extension can grab the decryption key, or the plain message, or tamper with the message being displayed.

I don't think we need 10 seconds of bcrypt "work".

By answering why ten seconds of bcrypt doesn't work, you would also figure out what would be the best suited algorithm for that. In other words, here again it's up to the business to determine the actual requirements.

there is zero need for this to be integrated anywhere so the user authentication is entirely unnecessary.

Authentication allows you to use PGP, which would be a much better alternative to what you try to do now. This, however, changes the paradigm. Instead of symmetrical encryption, you're now encrypting the messages asymmetrically, for a specific user (who owns a specific private key).

3
  • Thank you. Some follow-ups: there is zero need for this to be integrated anywhere so the user authentication is entirely unnecessary. Therefore, the server just needs to be able to send you an encrypted payload but I'm not entirely sure how to do that and then CONFIRM that it was received at a minimum before purging it from redis.
    – McDerp
    Jan 6 at 10:42
  • @McDerp: I edited my question to address a few points from your comment. Jan 6 at 11:06
  • Thank you for your input. Last question: If this was the project that was done well that you saw in an interview setting, what'd be your thoughts? Note: this is not an interview project, just curious.
    – McDerp
    Jan 6 at 12:12

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.