5 years of UnifiedPush

It has already been 5 years since UnifiedPush started! It also means I don't have any Play Services, the official or microG reimplementation, for 5 years now. It is a good moment to do a recap, and think about what can be UnifiedPush in 5 years.

It turns out I don't remember in details how all started, I need to read some historical pull requests and chats.

Why do I need push notifications?

I think I've installed my first alternative ROM, LineageOS, around 2013, and never went back to stock ROMs since then. At this time, I didn't really care about the apps I was installing, it was mainly to take control of my devices and get rid of the bloatwares.

I understood that I needed the Play Services, or a reimplementation, for some applications to properly work, and I was vaguely knowing why. So, every time I updated my phone, I had to boot into the custom recovery (TWRP), to flash a zip, to get microG. It was, well .. not the best user experience.

Then, I tried to stay without the Play Services, it was even worse, messages weren't reliable, the battery drained and there were many foreground notifications, which I understood were required to keep a service running.

So I decided to go with a fork of LineageOS that includes microG by default, and distributed by microG team: LineageOS for microG.

Even after using this new system, the experience was nearly the same. Why? Because most of my apps were from F-Droid. Push notifications with Google (via microG) require the use of a proprietary library *, which comes with telemetry, unless explicitly configured to exclude them. F-Droid deny this library, which is fair given that their purpose is to promote free software.

* That's actually possible to use FCM (Google notifs) without Google lib, but I didn't know that at this moment. Cf. UnifiedPush blog post about push notifications for decentralized applications, or Molly issue regarding FOSS FCM implementation.

Gotify (2020)

So, we're in 2020, and I finally want to look why I can't use microG with Fedilab and Element from F-Droid, and if we can replace microG with another notification app.

It turns out among others notification applications, F-Droid distributes Gotify. It isn't able to forward notifications to other apps, but there is an issue opened for that feature, and jmattheis, the developer seems open to the idea.

I didn't touch any Android dev at this moment, but I tried to hack something. Fortunately, jmattheis review helped a lot to make things less hacky. So here came gotify-connector.

It looks like from the pull request history that "connector" comes from jmattheis, for which I added "distributor" later.

At this moment, the feature has picked the interest of some persons, including sorunome, karmanyaahm and sparchatus. Sorunome, contributor to FluffyChat, told me that the feature may interest people in OpenPush matrix room.

First UnifiedPush version (2020)

Late 2020, looking at some p2p projects, I thought it would be cool having a p2p based solution too. So came the questions about ecosystem lock-in of a gotify only solution, adoption, and fragmentation. If we have multiple applications able to provide push notifications, we should have a library that is compatible with all of them. When a new application providing push notifications is published, then all existing applications supporting the thing would be directly compatible. Going that way, we needed to specify how it should work first.

I shared the idea in OpenPush room, and it picked the interest of someone in particular, sparchatus, who helped me to write the specifications. We discussed many edge cases to see how things could be.

I published a first version of the specifications, a library, and a fork of gotify until the support was merged *.

Sorunome was interested in implementing the support in Fluffychat. It required a flutter lib, karmanyaahm wrote a lib porting the already published library to the framework. We also needed something to translate matrix push protocol, and make gotify server compatible: karmanyaahm wrote common-proxies for this.

* Which actually never happened 🤷

FluffyChat, Fedilab, and more (2021)

Early 2021, FluffyChat was supporting UnifiedPush. And soon came Fedilab too, as the dev, Thomas, was directly interested.

Starting with these 2 applications was a chance for the project: we had support for matrix, and many other chats using matrix bridges, and for the fediverse. This covered enough applications for some FOSS enthusiasts. Retrospectively, UnifiedPush may never have started without these 2 applications.

After that, some applications started to implement the feature, such as a Tox application, or FMD, a FOSS solution to find your device.

Mid 2021, I implemented UnifiedPush support for Element, which was soon merged by SchildiChat, a fork. I think the experience from SchildiChat helped for it being merged into Element mid 2022.

UnifiedPush for Linux (mid 2021)

At this moment, vurpo came to UnifiedPush matrix room to talk about push notifications for Linux devices. So we had UnifiedPush for Linux by mirroring the specifications for Android to D-Bus IPC.

ntfy, NextPush (2021)

During 2021, a new project appeared on the Internet: ntfy. A project like Gotify, that can work without any account, with a public server. The app is extremely easy to use, as you have nothing to set up. And the developer, binwiederhier, was directly interested in supporting UnifiedPush, to make ntfy a distributor.

Merged early 2022, it was an important step for UnifiedPush: we have a distributor to recommend by default.

I have also implemented NextPush at the same period, giving an easy opportunity to self-host a push server, if you already host a Nextcloud server

In the same time, Gotify developer informed us that they finally prefer not to merge the support, as they don't use it and prefer to avoid adding maintenance to their project, which is perfectly understandable. With this new position, the official support of UnifiedPush by ntfy, and the new NextPush app, I preferred to discontinued Gotify forks as well.

KUnifiedPush (mid 2022)

Mid 2022, the KDE team, and particularly vkrause, published KUnifiedPush: a distributor for Linux, compatible with different push server, like ntfy or NextPush. Until then, we only had POC implementations of distributors for Linux. KUnifiedPush also provide libraries for KDE applications.

This allowed Linux applications to finally support the protocol.

Full-time on UnifiedPush (2024 - 2025)

At the end of 2023, we have more than 20 applications supporting UnifiedPush, and another distributor: Conversations. Element being probably the one with the larger user base at this moment. Someone advised me to apply for a grant with NLnet, as it would boost development of the project.

During the application process with NLnet, COVESA reached me because they wanted to support the project, but needed a few features that weren't present, to get a more robust authorization mechanism and avoid registration spamming.

UnifiedPush has always been compatible with web push (RFC8030 and RFC8291 but RFC8292, aka VAPID, wasn't). Embracing the standard to require web push was a potential step to take. The specifications needed to be updated in that direction, to require encryption (RFC8291) and to handle authorizations with VAPID (RFC8292). Relying on standard will hopefully help for the adoption, as the server side implementation may be used for web applications in the same time.

At the end of 2024, I've started working full-time on UnifiedPush.

Working with COVESA also allowed to get Sunup, a distributor using Mozilla's push server, autopush, and to add a self-hostable backend for autopush. This feature is currently being merged.

NLnet gave the opportunity to polish many things that were pending, to add a migration feature to the protocol, which can be used to get a fallback service when your self-hosted server is down, to implement the actual web push specifications on Mastodon, and to add web push/UnifiedPush to some applications. It includes Fennec/IronFox, forks of Firefox, so we can now get push notifications with web applications. It also includes SimpleX (being merged), Nextcloud (being merged), DeltaChat (TODO), and flatline (TODO), a self-hostable version of Signal server, hopefully upstreamed to Signal servers.

The idea is to increase the network effect: the more applications support UnifiedPush, the more UnifiedPush can be relevant for users, and the more users will use UnifiedPush. If the number of UnifiedPush users increases, it pushes applications' developers to support the protocol. At the end, we can use our phone with the push service we want, to get an expected user experience even without the Play Services.

Retrospective

It was by chance that I started UnifiedPush and the project would never have existed without other projects like F-Droid, gotify, matrix, Fluffychat or Fedilab, and many more, without the help of many people.

I think it shows how the FOSS ecosystem can be beneficial for everyone. I develop Sunup, but often contribute to ntfy. The projects could be seen as "concurrent", but aren't: the applications answer different needs. We don't have anything to win or lose if a user chose one app over the other. But we all win if a user chose to use one, no matter which, as it increases the network effect.

If UnifiedPush wasn't started 5 years ago, I'm sure an equivalent project would have started since then. This is something that was awaited in the mobile FOSS community, and there were already some research work on the subject.

I wasn't aware how many things were implied with push notifications. It is understandable that giving a single entity the capacity to provide such an important feature give them incredible power. This is concerning when their solution doesn't follow least-privilege policies, come with system rights, has access to the full system, and with "features" we don't want, such as advertisement and telemetry.

I now understand why push servers may be a tool for mass surveillance and how an open solution is important for resilience. Some networks exist outside the Internet, some regions in the world suffer from services block, some users may be banned from these services. When a service is controlled by a single entity, nothing can be done when they consider your device too old to be supported. Offering an open alternative is a response to all these problems.

The idea is not to move everyone to an open solution, but to give the freedom to. Supporting these alternatives also reduces risks of power abuse from Google. If you develop an application, ask yourself how fast could you recover from being banned by Google?

Working full-time on UnifiedPush is incredible. I'm extremely happy a foundation like NLnet exists. I hope my work is beneficial for the project and for most of the users. When it all started, I didn't imagine a second I could work on this, I just wanted my matrix and mastodon notifications without the Play Services.

I would love to continue working daily on UnifiedPush, and there are probably tons of things to do, specially for Linux devices, and many apps to port the feature to. But NLnet funds aren’t unlimited, our main goals are reached - improving the protocol, improving the existing code and documentation, boosting the network effect on Android -, and I don’t want to take the potential place of another project.

Among other things, we still need to improve libraries for UnifiedPush on Linux, and it’d be great to have a UI for KUnifiedPush to publish it on Flatpak. There are some important applications, such as Mozilla sync service, that use an allow-list of authorized push servers, defeating the purpose of self-hosting: it would be great implementing a better anti-SSRF mechanism. We will probably have to build these blocks and others together. If you want to contribute, do not hesitate to PM on Mastodon or join UnifiedPush matrix room.

UnifiedPush in 5 years

The best thing that could happen to UnifiedPush on Android in 5 years would be for it to no longer exist.

If Android gives us a system API to let the user define their push service we wouldn't need UnifedPush anymore. Passkeys (API to login without passwords), used to be provided by the Play Services only. Today, probably to increase the adoption, Android has migrated to a system API (Credential Provider), to allow any password manager to provide the service. With a Push Service API, UnifiedPush would have kind of been integrated into the OS. The applications would receive push endpoints like we do, and they would send web push requests, following standards, like web applications does, like UnifiedPush does. Migration from UnifiedPush would be minimal.

If we manage to have such a Push Service API, we can expect many more apps supporting the feature. And we will finally be able to choose the services we want to trust.

Hopefully, working on UnifiedPush can push in that direction by increasing the demand, and highlighting the need.

On Linux, I think the adoption depends a lot on how the mobile Linux ecosystem evolves. I personally think and wishes that it goes in the right direction. And I think a lot of things can happen in 5 years on the matter.

About Signal PIN

Edit 2025-12-26

Correction of the risk analysis: compromission of the PIN infrastructure in combinaison of a weak PIN can lead to metadata leak, including the social graph. I had wrongly assumed that the IKs were restored with the masterkey, but they are restored only with the backups or with a device transfert. This resulted in a higher risk in case of compromission.

Signal threat model is well defined: your communications are protected even from a rogue server. It follows the Kerckhoff's principle, a crypto system should be secure if everything, except the keys, is public knowledge.

This is why jurisdiction is nearly irrelevant to the security of encrypted messaging apps

The only security property the server should be able to impact is availability, which is why the jurisdiction actually matters a bit, specially when the said jurisdiction imposes embargoes to your region. A good encrypted messaging app is one you can use.

With such a threat model, there is no need for a Trusted Execution Environment (TEE) like SGX, which is supposed to ensure the storage is accessible to attested programs only. So, why Signal does need a TEE?

TL;DR

Signal uses your PIN to encrypt and upload your keys; set a high-entropy PIN (+20 random alphanums) to protect your metadata including your social graph, and stay resistant to brute force and dictionary attacks if the PIN infrastructure is ever compromised.

Account and identities

When you register on Signal, you get 2 different identities: one for your phone number, the PNI, and another for you account, the ACI.

Details about the PNI and ACI

The PNI is your phone number and the ACI a random id. When you change your phone number, you get a new PNI. And your ACI never change for your account.

When you configure the phone number visibility , who can see my phone number and who can find me from my phone, you change the possibility from someone to find the PNI from the ACI and vice versa.

If someone register with your old phone number, they get your old PNI.

The contact discovery is a registry that link the username and account URL to user account. From a username you can get the account id, but not the other way around.

The application generates 2 identity keys (IK) during the registration, one for each identity. And these keys are the root of all the session keys encrypting and protecting your communications.

The safety number you can use to verify a contact is generated from your and your contact IK.

After generating the identity keys, the mobile application generates prekeys that are signed by the IK and stored on signal server. These prekeys are used by other users for a first contact, to calculate a shared secret. These prekeys are what allows you to be contacted when you're not online. When the session keys with a contact are desynchronized, your contact picks new prekeys and calculate a new shared secret.

Keeping these IK secret is mandatory to avoid Adversary-In-The-Middle. If the server has access to the IK, it can send your contact prekeys it has generated. Doing it allows the server to decrypt incoming messages, and forward them re-encrypted with a legit prekey you have generated.

Registration lock

The registration lock is a feature that prevent someone to re-register to Signal with your phone number.

If someone re-register with your phone number, and you didn't enable registration lock, a new account is created for them, with your old PNI.

They will use a new ACI and new IKs. So your contacts who haven't updated your phone number will see that the account with your (old) phone number has a new identity.

Your safety number with S1m has changed.

This warning is a solution good enough to warn about something suspicious. Or it may be expected when your contact has yet again lose their phone (👀).

But most people don't really look at the safety number: taking over a phone number may be enough to impersonate the person.

This is concerning because SMS aren't secure:

  1. they are vulnerable to SCAM like SIM swap
  2. they aren't encrypted and can be read using an IMSI catcher, or by your ISP (Don't use SMS as a 2FA).

To protect against SMS attacks, Signal gives the registration lock: you set a PIN to protect your account, and Signal will require this PIN to use the registered phone number.

As people may change their phone number, the registration lock expires if someone re-register with the phone number and the account has been inactive for 7 days.

Account recovery

So, what happen when you are restoring your account on a new device? You enter your phone number, receive an SMS, resolve a couple of captchas and? You verify your Signal PIN.

For this, the application will interact with the Secure Value Recovery (SVR), part of the Signal server. To prove the knowledge of the PIN, the application hashes the PIN with the argon2i algorithm (salted with the ACI, and the enclave id), and split the result in 2 different keys: the SVR access key, and the SVR encryption key.

The access key is used to request the SVR to download the encrypted masterkey. Which is the masterkey, encrypted with the SVR encryption key.

Details about the masterkey

The masterkey (derived from the Account Entropy Pool) is one of the main key from which other secrets are derived, like the recovery password and the registration lock token.

The masterkey is decrypted, and the recovery password is calculated from it. If registration lock is enabled, the registration lock token is calculated too. Then the application request the Signal server to finish the registration using the recovery password and the registration lock token.

The application download the encrypted account data stored on the storage server. The data includes the encrypted profile key (used for the profile name and picture), and your social graph. Then the app decrypts them with a key derived from the masterkey.

You can now talk to your peers, and, if you haven't restored your data from a backup, your safety numbers has changed.

Note: This previously contained an error: without a backup, or a device transfert, all the safety numbers change.

Trusted Execution Environment ?

The SVR is one of the components using the SGX (Intel Trusted Execution Environment). Why? Because as we've seen above, the masterkey is stored there encrypted using a potentially very weak secret: the Signal PIN.

Using a TEE is an answer to this use of the PIN. It allows the client to verify that the code running on the enclave is the one expected, and the TEE should prevent any code outside the enclave to access its data. The certified code limits how many times you can request the service, so it should be impossible to guess the access token (and the PIN).

But, this is a shift in the threat model. Remember? Your data must be protected even from a rogue server. It should be secure if everything, except the keys, is public knowledge. Now, if you use a weak PIN, your metadata including your social graph relies on the security of the SGX.

If an attacker can bypass SGX security: they can download your encrypted masterkey, and guess your PIN: Argon2id is pretty slow, but the PIN requirement is only 4 digits, making brute force attacks possible. Then it is possible to download and recover the account data, including the social graph.

Note: it is impossible to get the identity keys from the masterkey alone, they are stored in the backups only. If your backup file is compromised, and a rogue server get access to your IKs, it could upload its own prekeys and start an Adversary-In-The-Middle attack.

And of course, SGX isn't immune to vulnerabilities (just random links):

Weak PIN requirement

The registration lock is older than the account recovery. (cf. the migration to PIN v2)

When the PIN was only about the registration lock, it was OK to use a 4 digit PIN, as its purpose was to protect against an external attacker trying to take over a phone number identity (but not over the ACI and the IK). At this time, the PIN was sent unencrypted, and was probably stored hashed on the server, as we usually do with passwords.

When the PINv2 and the account recovery were introduced, the requirements were changed: new PIN needed to be 6 digits and more.

The old PIN (which were sent unencrypted to the server) were directly used for the new recovery system, they were used to encrypt and send the master key to the SVR.

But then, the PIN code was cleaned up, and the new requirement was dropped and stayed 4 digits until then.

Recommendations

I am sure some users have a weak PIN thinking it is only about SMS attacks.

I think users are used to set "long" alphanum passwords everywhere, and it wouldn't be a friction to users if Signal were increasing their requirements, especially when the PIN isn't required.

With the introduction of the new cloud backup, Signal asks the user to write down the root of other secrets (the AEP) which is probably going to replace any use of the PIN. So, it looks like Signal is already working to totally get rid of this.

As you likely have a password safe, generate a high-entropy PIN (+20 random alphanums) and you're good.