Closed flamechair closed 1 month ago
Hey @iamtpage what do you exactly mean with end2end encryption, with SSL the communication should already be secure. Do you mean, that the messages shouldn't be stored in plain text in the database on server side?
Yes, that is was e2e usually refers to. In addition only the user should be able to decrypt the message.
I'd like to see D@RE on server and client, so if server/client is compromised (phone is lost/stolen, malware on server, etc) then the contents of the notifications are not.
Implementation wise this would be a PSK that encrypts data on server, transmits message to client(s), clients decrypt message locally via the PSK
So the client still sends plain-text notifications but the server encrypts the messages before storing them. Yeah that could work.
Technically to be true E2E the client would have to encrypt messages locally before sending them to the server. I'd settle for encrypted at rest messages on the server, otherwise I'd have to build encryption into the notification scripts I build and that can get hairy. I'm open to other possibilities if anyone has a better idea.
Okay, implementing this feature may require some effort and changes how clients view messages. So let's wait till more users want this feature to decide whether to implement it or not.
End to End encryption generally requires some degree of sender client. Not the worst idea but its definitely something that would be required unless you just wanted to tell people to use GPG or something weird.
Suggest looking into tweetNACL NATS nkeys uses it Secret is stored on each device. Provisioning key has to happen out of bound. Typically a QR code displayed and user scans it with camera. Like whats app a bit.
I'd use this feature.
+1 for @gedw99's suggestion.
Android's notification log seems to be problematic for end to end encryption. I know that Signal's Android application offers a few different options to limit what information gets logged. The only way to ensure that no information is logged is to completely disable notifications. This might be acceptable for a secure messaging application where notifications could be seen as an optional feature.
Personally, I view gotify as a notification application so completely disabling notifications is useless. Limiting the information in a notification and have a user check an application and/or website seems like a reasonable compromise for some.
I am curious on what others think about end to end encryption being an option or should it enforced if we go that route. Also curious if end to end encryption should be used within a browser.
Note, I do not know all that much about the inner workings of Android's notification log, so please feel free to educate me. Now that I think about it, I am not sure how all the different browsers log notifications.
@geeseven The E2E would be more to ensure the security of the message/notification in transit from server to client, not for the security of the message once it's delivered. Once it's delivered the responsibility would be on the user to have only trusted apps installed.
I think when we are talking about E2E we are talking about:
These are not related to E2E:
An implementation of E2E in gotify with forward secrecy might look like this(with Ed25519 for identity key, ECDHE for key exchange, AES in GCM cipher for symmetric encryption):
And when sending messages:
On the sending routine:
{
"encrypted_message":"<base64_encoded_message_ciphertext>",
"sender_ephemeral":{
"key":"<base64_encoded_sender_ephemeral_key>",
"signaure":"<base64_encoded_signature_of_the_sender_ephemeral_key>"
},
"message_key":[
{
"receiver_ephemeral":"<base64_encoded_receiver_ephemeral_key>",
"key":"<base64_encoded_message_key_encrypted_with_ecdhe_secret>"
},
{
"receiver_ephemeral":"<base64_encoded_receiver_ephemeral_key>",
"key":"<base64_encoded_message_key_encrypted_with_ecdhe_secret>"
},
{
"receiver_ephemeral":"<base64_encoded_receiver_ephemeral_key>",
"key":"<base64_encoded_message_key_encrypted_with_ecdhe_secret>"
}
]
}
On the receiving routine:
Since I think we need to keep the body size sent to the client as small as possible, gotify server looks up its database on which client owns the corresponding ephemeral key, and sends just enough information for it to decrypt and verify the message, eg the message sent to the client over WebSocket might look like this:
{
"encrypted_message":"<base64_encoded_message_ciphertext>",
"sender_ephemeral":{
"key":"<base64_encoded_sender_ephemeral_key>",
"signature":"<base64_encoded_signature_of_the_sender_ephemeral_key>"
},
"receiver_ephemeral":"<base64_encoded_receiver_ephemeral_key>",
"message_key":"<message_key_encrypted_with_ecdhe_secret>"
}
The client does the following:
This routine would be smooth in a normal operating condition. However, we need to deal with special ones:
To guarantee message delivery when the client is offline, the client should always pre-key a certain amount of ephemeral keys on the gotify server. Moreover, To guarantee forward secrecy, the ephemeral key should only be used once(eg. Clients might not accept more than one messages encrypted with the same ephemeral key; However in this case if the ephemeral key pool is empty, there will be no way for applications to send more messages with forward secrecy. If we want to balance deliverability and security, we might allow gotify server to send used ephemeral keys if the ephemeral key pool is starving. However, the client would issue a warning to these messages which used obsolete ephemeral keys, which does not provide forward secrecy, and also might be the result of a replay attack)
The application need to keep a list of known trusted clients by syncing a list of known public identity keys of clients with the gotify server. Maybe we can take the implementation by Matrix as a reference
The trust of additional clients could be obtained by either of:
In short, the gotify server would need to implement:
The application/client would need to implement:
As for whether to enforce E2E by @geeseven my opinion is no. E2E is mainly designed to keep your message secure from an untrusted central server(e.g. a gotify server hosted on a VPS, etc.) Maintaining trust of device keys and application keys is really exhausting, plus newly added device would be unable to decrypt previous messages due to forward secrecy. Moreover, in some cases where the client is less safe than the gotify server itself, E2E can't provide you much security ( I think a gotify server running on a raspberry Pi that is safely locked in my home is much more secure than my phone which is carried to all kinds of places and running a lot of unknown softwares). TLS is enough in this case. Of course, you could choose to let clients only accept E2E encrypted messages, but I don't think it should be enforced by the gotify communication protocol.
As for encrypted database storage, I cannot think of something the messages could be encrypted against. As long as you could obtain a copy of the configuration and the database, there is nothing to prevent an adversary from running a copy of the same gotify instance, authenticate itself and inspect on the message in plain text. So I don't think this would provide extra security.
The identity key storage API might look like this:
Add a new key or update existing key
POST /identities/:userName/:clientOrAppID?token=<access_token_corresponding_to_this_user>
{"key": "<base64_encoded_public_identity_key>"}
returns:
{"holder": "<client_or_app_ID>", "key": "<base64_encoded_public_identity_key>", "certifications":[]}
Delete keys
DELETE /identities/:userName/:clientOrAppID?token=<access_token_corresponding_to_this_user>
returns:
{"success": true}
Make a certification. The alg field denotes how the key can be verified. It can be verified by either with a shared secret as a root of trust, or signed by another already trusted ed25519 key. For example you can use a trusted device to scan a QR denoting the new identity on a new device. Then the trusted device could POST to this endpoint to add its signature on the new key.
POST /identities/:userName/:clientOrAppID/certify?token=<any_valid_access_token>
{
"alg":"hmac_sha256_with_preshared_secret",
"sign":"<hmac_sha256_digest>"
}
returns:
{
"holder":"<client_or_app_ID>",
"key":"<base64_encoded_public_identity_key>",
"certifications":[
{
"alg":"hmac_sha256_with_preshared_secret",
"sign":"<hmac_sha256_digest>"
}
]
}
Query keys for a user, build trust chain by starting from root trust and verifying along the signature chain
GET /identities/:userName?token=<any_valid_access_token>&skipNoCertification=true
returns:
[
{
"holder":"app0",
"key":"<base64_encoded_public_identity_key>",
"certifications":[
{
"alg":"hmac_sha256_with_preshared_secret",
"sign":"<hmac_sha256_digest>"
}
]
},
{
"holder":"client0",
"key":"<base64_encoded_public_identity_key>",
"certifications":[
{
"alg":"hmac_sha256_with_preshared_secret",
"sign":"<hmac_sha256_digest>"
}
]
},
{
"holder":"client1",
"key":"<base64_encoded_public_identity_key>",
"certifications":[
{
"alg":"ed25519_signature",
"sign":"<signer_public_identity_key_from_another_client_or_app>:<key_signature>"
}
]
}
]
And for the ephmeral key:
pre-keying:
POST /ephmeral/:clientID?token=<client_access_token>
[
{
"key":"<base64_encoded_ephmeral_key>",
"sign":"signature_of_ephmeral_key"
},
{
"key":"<base64_encoded_ephmeral_key>",
"sign":"<signature_of_ephmeral_key>"
}
]
returns:
{"success": true, "pool": <remaining_epheral_keys>}
clearing(when the client lost its local ephmeral key storage):
DELETE /ephmeral/:clientID/*ephmeralKeyHash?token=<client_access_token>
returns:
{"success": true, "pool": 0}
claiming:
GET /ephmeral/:clientID?token=<any_app_access_token>
return:
{
"identity_key":"<base64_encoded_identity_key>",
"key":"<base64_encoded_ephmeral_key>",
"sign":"<signature_of_ephmeral_key>"
}
@eternal-flame-AD Thanks for that detailed proposal.
I intended Gotify to be as simple as possible, with e2e encryption like this, sending a messages without any cli would be nearly impossible. Sure e2e could be optional it would still greatly increase the code complexity of both gotify/server and all clients.
Here is a different implementation using PGP, remember: I'm an encryption noob.
Gotify could derive two distinct passwords from the original user password. One for authentication to Gotify and one for the private PGP key.
For easy usage gotify/server would distribute both public and private key. Clients/applications should request a confirmation from the user that the key is correct and then cache the key.
Clients use the public PGP key to encrypt the message and then send it to gotify/server. When clients send a plaintext message, gotify/server will encrypt the message.
New api's:
GET /current/user/pgp/public
returns the public pgp keyGET /current/user/pgp/private
returns the private pgp key (requires client authentication)POST /message/encrypted
sends a encrypted message to gotify/serverWhen an evil hacker has access to the server hosting gotify/server, he has access to the private key but not the passphrase and therefore cannot read the messages. (he can read not encrypted messages)
It would be possible to show a fake web-ui and obtain the original user password. I don't see any way to protect against that as gotify/server provides the web-ui.
I understand that this approach wouldn't be as security as @eternal-flame-AD solution but it would be a lot simpler to implement and use.
First I need to clarify some words I used in the later text to prevent misunderstandings:
First I might need to make an important point: What is the root difference between the current authentication scheme and an additional layer of E2E authentication? Sorry I am not very good at explaining things to others, if my words sound ambiguous maybe this Wikipedia article would be helpful. In short, messages are encrypted to so-called "end-points"(in the gotify context it means a particular application and a particular client, not a generalized one like user)
In the current scheme, both applications and clients possess an access token, with the access token obtained by authenticating with user passwd and being protected by transport layer security, denotes that those possesses access tokens are authentic endpoints from an user. In short, messages are protected against those who can not be authenticated as the user
, if the user authentication method (currently user password) is compromised, the whole security collapses.
In a E2E scheme, in addition to the user authentication mentioned above, each application and client possesses their own signing key, which certificates that this message is sent by App0
, who trusts Client0
and Client1
, but not other clients even if they possesses access tokens. In short, messages are protected against those who can not be authenticated as a verified recipient(client in the gotify context)
, even if the user authentication method is compromised, the application would refuse to send messages to the newly added clients as long as they are not verified.
As for the PGP one you proposed I have to point out some potential risks:
For easy usage gotify/server would distribute both public and private key. Clients/applications should request a confirmation from the user that the key is correct and then cache the key.
When we are talking about E2E we are generally talking about the central server is NEVER trusted. Also keep in mind that confirming the key signature only indicates that both the client and application possesses this key. It does not prevent the server from keeping a copy of the plain private key which can be used to eavesdrop communication. In your context, try replacing "public and private key" with "access token", both generated issued by the server, both with a trust anchor on user authentication. Then you would find out that it is only as secure as the current implementation(with an encryption on database storage of course).
Gotify could derive two distinct passwords from the original user password ... one for the private PGP key. ...
GET /current/user/pgp/private
returns the private pgp key (requires client authentication)
I guess you meant client/server authentication token here? User passwords are shared among clients.
This would definitely render the whole E2E encryption useless. In other words, who possesses the authentication token would be able to obtain the private key. If:
the authentication token is stored with bcrypt, etc. on the gotify server, the adversary would need to listen for an authenticated request from the server, obtain the token and decrypt the PGP private key.
In both occasions, for an active attacker all he needes to do is obtain the access token and all "end-to-end encrypted" messages would be able to be read, he could also fake new requests.
It would be possible to show a fake web-ui and obtain the original user password. I don't see any way to protect against that as gotify/server provides the web-ui.
This is indeed a problem, similar to the android notification thing, which is about how to prevent the client from telling secret to others. I think they could be talked about later after the main scheme on the E2E thing is settled. We are actually talking about a scnario similar to a compromised client here. However, with forward secrecy, as long as the ephemeral keys are safely destroyed after use, nobody would be able to decrypt past sessions again.
There are also some additional setbacks on the PGP one:
App0
from sending a message as App1
, as long as it can pass the user authentication.I agree that the proposal is a big task and it would take quite an amount of work to complete, but I think the task could be split into rather simpler tasks clearly(see What needs implementing
) and implemented one by one. I wrote the whole process and wished to start from scratch because current E2E implementations are generally designed for two-way multi-peer communications(such as webchat), which are really reavy-duty and unncessasary to be brought in whole onto gotify. I know it sounded really complex but I think it does not require much extra user interaction as you thought it would be. I think the mandatory extra user interactions are:
If are interested in making it happen, I could provide a minimal example that demostrates how this works maybe next week.
Actually I also partially agree with you that this is not a very important feature, but it seemed that this issue received a lot of support. If this is really wanted by a lot of users than we might add this feature. Generally speaking, we are choosing between the two:
Also keep in mind that confirming the key signature only indicates that both the client and application possesses this key. It does not prevent the server from keeping a copy of the plain private key which can be used to eavesdrop communication.
The server should never obtain the plain private key. It only has access to the private key protected by the passphrase.
I think I got what you meant here. The required user interaction would be:
And this would implement
This would NOT implement
In general if we settled on not implementing E2E then this is a good point where we could start from on server storage encryption.
This is my evaluation on the three implementations we have talked about by now. I prefer the second or the third one.
Implementation | Privacy | Authentication | Integrity | Forward Secrecy | User-Interaction | Code Complexity | Extra API |
---|---|---|---|---|---|---|---|
Client-Only PGP | User-Level | No | Yes | No | + | + | + |
PGP on both sides | User-Level | User-Level | Yes | No | ++ | ++ | ++ |
Full E2E | Device-Level | Device-Level | Yes | Yes | ++ | +++ | +++ |
User-Level denotes that the key is shared between applications AND/OR clients. Thus messages could be only verified as sent by a certain user and encrypted to a certain user. Device-Level denotes that the key is not shared between different applications and different clients. Thus, messages could be signed as a certain application and encrypted to certain verified clients.
Could you clarify what you mean with client-only PGP and PGP on both sides?
In the client only mode, there is only one PGP key in the system(actually we can use symmetric cipher instead in this case), the public key is open and the private key is shared among clients. Application sends encrypted but not signed messages to clients. Thus, each time a new application is added, user need to make sure that it received the correct public key. On the both sides mode, in addition to the key on the client side(referred to as client key), application also derives user password in another way and share their own pgp key(referred to as application key), applications send encrypted and signed message to clients. In this case, verification would be needed whenever a client or application is added.
New thoughts: actually in your case I think a simpler solution is just to use the derived user password which is kept secret from the server as a symmetric key and I think that provides just as much security as your implementation.
Also I thought it would be complex to keep the key in consistent among endpoints by switching the encryption key smoothly while not losing old messages during a password change. I think this might add extra complexity to your solution. New clients would be unable to be added until old clients in possession of the private key upload the private key encrypted with the new password on the server. More dangerously, in a worst case scenario, due to user(either human or coding) error, the user password is changed but nobody in possession of the private key is notified of the password change(they have their own access tokens so they might not be aware of this, maybe they are all offline so server can't tell them that someone changed the password). If all devices in possession of the private key logged out before at least one device notices the password change and sends the newly encrypted private key to the server(server is not aware of this as it cannot be sure if the encrypted version of the pgp key is encrypted with the old key or the new key), next time they login they would have no way to use a single password to both authenticate with the server and decrypt the message key. This is not a non-recoverable error but is indeed annoying.
Alright, thanks for the clarification. I would say, we wait some time till we have feedback for the given proposals.
I'm voting for implementing E2E. If a user will not host Gotify server himself ( for example his friend will host it for him, or he will run it on VPS) - E2E is the must. We don't want our precious data to by going unencrypted through some server we don't have control of.
I'm going to put in a contrary opinion... I don't think E2E is within the scope of a simple application like this. I already have an XMPP client with E2E, you mentioned Matrix too. Just send your notifications over these instead if your data is sensitive and you are so worried about interception by the server. Otherwise gotify would be essentially just recreating them.
Web Push seems like a technology that solves a very similar to problem to what Gotify is trying to do and has E2E encryption built in. Specifically I’m talking about RFC8291 and RFC8188. They describe the encryption process (ECDH, HKDF and aes128gcm) and how to transmit the encrypted payload via HTTP.
With slight adjustments this scheme could be mapped to Gotify’s needs while reusing a great deal of the underlying technology. There are quite few libraries out there already which handle encryption/decryption.
Bonus: One might even think about supporting Web Push altogether so that existing applications can use Gotify without additional middleware. For example Mastodon allows to set custom Web Push endpoints. Right now I had to write a small script which decrypts the messages before relaying them to my Gotify server in plaintext.
@buckket Thanks for the info, but we would need to make some fundamental changes to support the key exchange problem, and it is in some ways not conforming with the design goal of gotify: just a simple push messaging system. This feature is still in consideration.
@eternal-flame-AD Why not use the approach that irssinotifier uses, you (the sender) pre-encrypt all data (Title, Channel, Text) with a static key in openssl:
my $pid = open2 my $out, my $in, qw(openssl enc -aes-128-cbc -salt -base64 -md md5 -A -pass stdin);
then you send that encrypted string to the server (which in case of irssinotifier runs on google-appengine). On the android-client, you then have to manually enter the same key. You dont need a complicated software and keymanagement to send notifications, just openssl...
But considering, that gotify is intended to be self-hosted, i would rather suggest to add an option to do some sort of certificate- or CA-pinning in the android-client, an evil ssl-breaker with a govt-owned-ca-certificate would be the only real practical way to decrypt our very important messages ;)
Edit: Duh, now i feel stupid, CA-pinning is already supported ;)
So what is happening with this subject?
Currently nothing. I'd say that the development costs for this feature outrange the current need for it.
Realistically at this point, if you don't trust your own TLS certificates and your own push notify hosts you should really rethink your security strategy. TLS already implements forward secrecy and GoLang is a really efficient language that cross compiles relatively easily so if you want you could run a Gotify server on a Raspberry PI you plug in at home if you don't trust anyone else with your server.
Most of the tools are available having End to End encryption would be nice but at that point, you might as well bake your own Signal Protocol equipped messaging app and just remove anything that allows the individuals to send messages in and just have one sender being the server itself or even have sender specific clients that can't receive.
Don't get me wrong I'm very much interested in this feature but as @jmattheis mentioned in their latest post its probably just too time consuming to add to this and might make sense for an entirely different app to be born.
+1 for this feature
I am not against this, but devil's advocate. Would it even make sense to protect secrets from your servers? This is a self-hostable service. You don't have to trust others with the server, so isn't TLS already forward secret already, at least making any of this stuff kind of extra? It's a fantastic idea, but implementation isn't trivial, and poorly implemented crypto is typically worse than no crypto at all. If you imply there's E2E on something and need help understanding how it's working, you might make bad decisions and create new security holes. I argue that E2E granted it be something I would always prefer to have personally as I don't think that central servers ever really need the ability to be transparent to any onlookers is necessary.
I think this can be closed for now. I no longer think any avenues discussed (which I feel are exhaustive) are worth implementing and this has been cold for a long time.
@eternal-flame-AD Just curious, what has made you change your mind exactly?
Hi,
To summarize, there are a couple points:
(1) We never go to clearly define what is the threat we want to defend, a man-in-the-middle, a backdoored or subpoenaed service provider, or a data breach (like the drive of the database being physically or logically stolen)?
(2) It is important to consider the added value and burden of this feature. On the value side, we can ensure a. a database dump does not leak message content b. if we enforce strict device key signing (like Matrix), we can ensure a compromised account identity does not lead to secret disclosure. On the burden side, I have explained that participating in E2E requires the collaboration of apps, server and clients, it is very difficult to push this change around retrospectively.
(3) When it comes to implementing this feature, it is very important to consider we are essentially reinventing a one-way version of Matrix, when you could have just use a script to PGP your message and have a modified client to decrypt and verify the signature for you. While I do have professional cybersecurity background and know how to design basic crypto system I believe a system I designed by myself is likely weaker than existing implementations in terms of considerations in security and other edge cases.
(4) E2E also does not prevent side-channel leakage, like the size, timing and other metadata of the message. It might be less of a concern when a user is sending large amount of requests to a E2E chat server but can be significant in cases like ours. Let's say a user is using a public service and do not want to be traced even if the server owner want to: there is no way to prevent the server owner from tracing messages from which IP address correlate to clients of these IP addresses, which is likely enough in many subpoena scenarios, still going back to (3) if this is not acceptable to you I think it is unlikely you will trust a crypto system designed by only me and not officially vetted.
(5) It is very hard to justify inclusion of this feature given the amount of additional code required (as I have laid out before, even not counting server-side logic, a one-line sending of messages can now require hundreds of line to take care of E2E conditions like pre-keying or even likely require a daemon to reliably be delivered to all clients, remember unlike E2E chats, we cannot allow other "apps" to be able to see other apps messages, that means whenever a new encryption is required (new device or an exhausted key pool), the app must be available with the message still ready to deliver it)
While most threats can be mitigated by self hosting your instances (likely costing <$5/mo) and doing due diligence on your security (disk encryption, database security, account credentials etc). If there is something you do not want to be seen, overwrite it, delete then vacuum (I think this can be a feature if enough people think a secure erase is useful).
Some comments about special implementations discussed:
Encrypting messages with user password provide limited protection especially on the integrity side, and couples your account credentials to every app you want to be able to send messages, while it can be made that the app do not really know what your password is, requiring a full key rotation just to change your account password is not ideal.
Thanks for the explanation. Indeed, it does all depend on the threat assessment. For me personally, I would just prefer that the data "at rest" wouldn't be in plain text (and thus be backed up in a readable form). This wouldn't require E2EE and could even be handled by server-side encryption only.
Off-topic: in my opinion, Matrix is a protocol entirely unfit for its own application (E2EE chats, whereas it is in fact a massive and meticulous logbook of every single activity done by every single user, which is being pushed around in its entirety to every "federated" server involved, effectively making it a centralized service), so I wouldn't even consider trying to use at all, let alone for pushing notifications around.
Whether matrix is good or not I'm not the right person nor is this the right place to discuss, but from a pure E2E implementation perspective and whether the safety triad of messages themselves are held, I believe they are one of the (still I won't say good but at least better option than asking me to design a new one just for this) . If you wrote a system for gotify and want me to give my informal opinions, feel free to send in a question ticket or email and I will be happy to look through it.
If you just want your data to be safe for rest definitely you should self host and do disk encryption and maybe even encrypting your DB. and as I said unless you encrypted your message end to end on both sides (and be mindful of who supplied the keys) no public instance is secure from an internal prying eye.
I think it was purely a would-be-nice feature. If anything, TLS already handles a lot of the problems on its own, and what are you doing using Gotify on some public instance where you can't trust the server anyway?
This would be a great feature to go along with the self-hosted aspect of this. By the way, love this tool!