UnifiedPush / wishlist

New distributor, new lib, more OS, app support etc.
4 stars 0 forks source link

WebPush over UnifiedPush #15

Closed karmanyaahm closed 5 months ago

karmanyaahm commented 2 years ago

A compatibility layer. Needs implementations in connectors and a rewrite proxy, afaict. It would be very useful to be able to rely on all the existing crypto for an e2ee UnifiedPush experience.

karmanyaahm commented 2 years ago

Links, might be useful to others or myself in the future:

https://github.com/mozilla/rust-ece/ What is used for webpush crypto in firefox. https://github.com/web-push-libs/encrypted-content-encoding python library https://github.com/web-push-libs/ecec C library

https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/chrome/browser/push_messaging/push_messaging_service_impl.cc#L333 Chromium implementation https://github.com/chromium/chromium/tree/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/modules/push_messaging

p1gp1g commented 2 years ago

It looks like we don't need a rewrite-proxy (https://www.rfc-editor.org/rfc/rfc8291.html#section-5)

Also, we wrote the registration for android for fedilab a few months ago, it would not require a long time to use it for a library

strider72 commented 2 years ago

What is an example use case of this? What problem does it fix? Does web push on Android normally go through Google FCM?

strider72 commented 2 years ago

I guess my answer is this (from Element chat):

(Without unifiedpush) : There are 2 ways to use webpush :

  • with a lib and every app using it have a connection wayting for incoming requests
  • with a lib that does queries to a single app that listen for incoming requests for all the other apps

Unifiedpush only specify how internal app sends data and how server send to distributor. Actually, it is possible to have a webpush distributor

So UP is that single app that listens for requests

p1gp1g commented 2 years ago

IIRC, browsers use FCM for webpush. Also it is usefull for app implementing webpush (e.g. mastodon)

karmanyaahm commented 2 years ago

What is an example use case of this? What problem does it fix? Does web push on Android normally go through Google FCM?

This advantage is not necessarily for web browsers, but rather being able to use UP with application servers that already support webpush, and also benefitting from the encryption at the same time.

emersion commented 2 years ago

There's no need for a rewrite proxy.

p1gp1g commented 2 years ago

Indeed. For webpush, salt, content-size and public key are sended following RFC8188 in the post data. source: RFC8291

   +-----------+--------+-----------+---------------+
   | salt (16) | rs (4) | idlen (1) | keyid (idlen) |
   +-----------+--------+-----------+---------------+

Example:


   POST /push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV HTTP/1.1
   Host: push.example.net
   TTL: 10
   Content-Length: 145
   Content-Encoding: aes128gcm

   DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml
   mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT
   pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN

Salt: DGv6ra1nlYgDCS1FRnbzlw Application server public key: BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8


Without a rewrite proxy, we only miss :

karmanyaahm commented 2 years ago

So I checked the following libraries, and they accept all 2XX status codes, don't check response headers, and don't support any advanced features (delivery receipts) of webpush based on my quick survey:

The only limitation with pywebpush is >202 response code is marked as failing.

So from my rough survey, not adhering to rfc8030 super tightly (with response headers and extra but mandatory features such as receipt notifications) should be fine for most applications. i.e. another need for a reverse proxy - push response - is alleviated

karmanyaahm commented 2 years ago

So, in terms of library API, we have two main options (if there are more good options, you can comment them below). This broadly applies to all libraries Android, Flutter, Linux

Integrated

The old API:

func Register(instance string, distributor string)
func onNewEndpoint(endpoint string)
func Unregister(instance string)

the new API

func Register(instance string, distributor string)
func RegisterWithWebpush(instance string, distributor string)
func onNewEndpoint(endpoint string, p256dh string, auth string) // if using webpush, p256dh = auth = ""  and can be ignored
func Unregister(instance string) 

Here, onMessage will automatically provide the decrypted payload if registration is done with WebPush

Separate library

In an onNewEndpoint call, devs will call

func (webpush) generateKeys(instance string) (p256dh string, auth string) //this instance doesn't have to be the same as the UP library, but probably will be for simplicity 
// it should save the private key automatically

Then in onMessage, devs will call

func (webpush) decrypt(instance string, ciphertext []byte)  (plaintext []byte)
//uses the stored keys for that instance and decrypts the text

I think integrated will be simpler for devs (fewer imports, automatic decryption), but might result in more code to maintain for the core UP libraries - vice-versa for a separate library

Also, if we want to encourage WebPush/encryption adoption for all UP apps, I think integrated is the way to go (since then there's one less step to take for encryption by app devs)

Comments are welcome from everyone :), which do you prefer?

p1gp1g commented 2 years ago

The old API should be :

func Register(instance string, distributor string)
func onNewEndpoint(instance string, endpoint string)
func Unregister(instance string)

On my side, I see a 3rd option : a "unifiedpush_webpush" library that depends on the unifiedpush library (connector) :

func Register(instance string, distributor string)
func onNewEndpoint(instance string, endpoint string, p256dh string, auth string) // if using webpush, p256dh = auth = ""  and can be ignored
func Unregister(instance string)
karmanyaahm commented 1 year ago

Just doing some reading.

This was the change in (the future) RFC8188 that resulted in moving keys from headers to the body: https://github.com/httpwg/http-extensions/pull/252

This is where it started affecting WebPush: https://mailarchive.ietf.org/arch/msg/webpush/aLfZBx6wZRKMZ7X2AG_t1X2TlIE/ https://github.com/webpush-wg/webpush-encryption/pull/9

https://mailarchive.ietf.org/arch/msg/webpush/19pz0qIZBNN0GheAmKu72cYLr9k/

it may be layered over Webcoket, newly proposed WiSH, etc - and in many cases the implementation will be greatly simplified if the binary blob can be sent as-is (after any protocol-specific authentication ).

totally agree with Costin

karmanyaahm commented 1 year ago

ok so, gateway idea:

1. Discovery

Turn {"unifiedpush":{"version":1}} into {"unifiedpush":{"version":1,"cryptokey":1}} if this gateway is supported.

2. Gateway operation

If Content-Encoding == "aesgcm", convert the data in "crypto-key" and "encryption" to the following body header used in aes128gcm WebPush.

   +-----------+--------+-----------+---------------+
   | salt (16) | rs (4) | idlen (1) | keyid (idlen) |
   +-----------+--------+-----------+---------------+

OR Alternative Gateway operation

If "crypto-key" and "encryption" headers exist, simply append them to the message body. This trades server complexity for client library complexity, which then has to parse and decrypt both formats.


If the "cryptokey" gateway is not supported by the endpoint, fall back to https://cryptokey.gateway.unifiedpush.org/?url=https://oldversion.ntfy.sh/1234 or something. If every push server natively supports this, the backup gateway should be rarely used, avoiding centralization risks. In a few months (to allow for everyone to update), Tusky and Fedilab can avoid wake+poll.

Like Matrix, this is probably simple and non-intrusive enough to be included natively in push servers. I will experiment with this stuff as soon as I have some time.

karmanyaahm commented 1 year ago

also, a ton of application servers still use the old protocol (aesgcm) (image from March 2021, but I doubt it's changed that much)

image https://github.com/mozilla/rust-ece/issues/53#issuecomment-802371008

karmanyaahm commented 1 year ago
  1. Gateway operation

So, aesgcm and aes128gcm use different nonces and context strings and stuff, so it is not possible to transparently convert between the two formats on the server...

I spent way too long figuring this out :frowning_face:.

So to appending headers it is.

karmanyaahm commented 1 year ago

Essentially this,

https://github.com/karmanyaahm/common-proxies/blob/1567aa9ea7fc6aef80162180f4e54208cd8784f9/gateway/webpush.go#L33-L41

p1gp1g commented 1 year ago

UP-Example now supports WebPush: https://github.com/UnifiedPush/android-example/tree/1.5.2

I don't think it is useful to have yet another lib for WebPush over UnifiedPush.

What do you think about leaving flutter-connector-webpush too ? We probably can just write an example.

karmanyaahm commented 1 year ago

Yeah, it's probably fine if we make a docs page explaining how to use the Store and WebPush classes. Maybe the actual decryption could also be a function in the library, just to create a strong boundary between the 'scary cryptography' parts and app logic?

karmanyaahm commented 1 year ago

image mozilla/rust-ece#53 (comment)

Hi @jrconlin 👋

UnifiedPush is an open source protocol for Android (and Linux) push notifications. As we're working on increased WebPush compatibility, I was curious as to what that graph looks like nowadays? Is aesgcm still dominant?

Thanks for all your open source WebPush tooling.

jrconlin commented 1 year ago

Is aesgcm still dominant?

Sigh. If anything, it's gotten worse. image

Apparently, if you provide libraries for an early version of a protocol, expect that version to live forever.

Just to recap, aesgcm is based on a draft of the webpush encryption RFC, where the Diffie Hellman public key is URL safe, unpadded, base64 encoded into the Crypto-Key: dh= header parameter, and the Salt is encoded into the Encryption: salt= header parameter. For that content type, the recipient decryption system expects those values to be included as part of the delivered message.

For aes128gcm encoding, those values are prefixed to the encrypted body. Granted, there are additional complications if you're dealing with a multi-part push message, but those are fairly rare because they're very complicated to implement. (Basically, at some point, it becomes TCP over WebPush.)

Theoretically, you might be able to convert between aesgcm and aes128gcm, since the values produced are the same, but I've never tried doing it, and I long since learned not to invoke the wrath of the cryptography gods.

karmanyaahm commented 1 year ago

Apparently, if you provide libraries for an early version of a protocol, expect that version to live forever.

🙁. Yeah, part of the issue we've noticed is on the app side too. For example, Mastodon has various WebPush -> FCM/APNS proxies that use aesgcm and so is unable to exclusively switch to aes128gcm. Worse, the ruby webpush library v1.0 only supports aes128gcm (not both), so it would be an ambitious endeavor to even get it to support both.

I kinda hoped Apple, having the market power, would only support aes128gcm when they started supporting WebPush, but that did not happen.

For that content type, the recipient decryption system expects those values to be included as part of the delivered message. For aes128gcm encoding, those values are prefixed to the encrypted body.

Yes, handling headers is the main complication in adding aesgcm to UP. But I guess if it's that popular we have to do it anyway.

Theoretically, you might be able to convert between aesgcm and aes128gcm, since the values produced are the same, but I've never tried doing it, and I long since learned not to invoke the wrath of the cryptography gods.

I tried doing that, but realized that the nonces for key derivation are different on the two standards. So, unfortunately it won't be possible to adapt on the push server (without private keys).

Thanks for the information though!

jrconlin commented 1 year ago

Ah, crap, you're right. I forgot about the nonces.

The silly thing is that I reached out to a bunch of the library authors a couple of years ago and encouraged them to switch to aes128gcm, for a LOT of reasons. A number of them did, indeed, make the default aes128gcm. Sadly, the folk actually implementing things, apparently, either never update their libraries or actively set the encryption to the older standard. It's a point of confusion and frustration for me.

(Extra fun fact: supporting both on the message server is also a serious pain. Technically, there were three standards for a while but we dropped the oldest. I would love to be able to work with the other folk at Google, Apple and Microsoft to declare aesgcm unsupported as of some future date. I'm going to guess that they'd probably be on board with that idea since it would greatly simplify things on their end, but the problem is that it's surprisingly hard to get in touch with those teams.)

p1gp1g commented 1 year ago

To summarize:

IMO, it is better to stay with the webpush specification, sync on push is sufficient for applications that only support the draft specification. What do you think ?


Concerning Mastodon, I think the best thing is to publish a draft-webpush gem, based on webpush gem so they can support both

jrconlin commented 1 year ago

FWIW, I'm absolutely fine with only working against the RFC spec. Feel free to note that the RFC has been out for 7 years and that most major WebPush libraries support it. Make using aesgcm a pain point for folk. (Granted, email with "+" in the local part has been a thing even longer than that and still there are tons of sites that think foo+sample@example.com is invalid, but that's a different problem.)

Needless to say, I'll be cheering for y'all so that folk finally stop using the old specification.

MzHub commented 6 months ago

IMO, it is better to stay with the webpush specification, sync on push is sufficient for applications that only support the draft specification. What do you think?

Considering that currently there is no WebPush support, I strongly agree with progress towards at least standards compatible WebPush support.

The draft version support will hopefully become less relevant over time, and if it doesn't it can be reconsidered to be solved later?

The standard support hopefully becomes more relevant over time.

p1gp1g commented 6 months ago

UnifiedPush is compatible with WebPush. Its support is limited (e.g. urgency is ignored) but since an accepted push message returns a 201 with TTL: 0, application servers shouldn't try to use WebPush's unimplemented features (for instance, the app server won't try to delete a pending message).

p1gp1g commented 5 months ago

What do you think about closing this issue ? UP supports (in a limited way, with TTL: 0) WebPush. This opened issue seems to add some confusion

If someone feels the need to, maybe we can open an issue to Extend WebPush support to add TTL>0, the ability to delete messages, even urgency etc.

p1gp1g commented 5 months ago

I'm closing it then.

If someone feels the need to, maybe we can open an issue to Extend WebPush support to add TTL>0, the ability to delete messages, even urgency etc.