This story got me thinking: https://www.404media.co/fbi-extracts-suspects-deleted-signal-messages-saved-in-iphone-notification-database-2/
The FBI recovered Signal messages from a suspect’s phone, Signal’s encryption was cracked, nothing is safe, etc. But that’s not it. Signal’s encryption was not broken. What the FBI actually did was pull decrypted message content from Apple’s internal notification storage on the suspect’s iPhone, which had nothing to do with Signal’s cryptography and everything to do with how phones handles push notifications.
Even though this case happened on an iPhone, the problem is not iOS-exclusive. By default, both Android and iOS write decrypted notification messages to system databases that persist through app deletion and disappearing message settings. That data is accessible to anyone with physical access to your device.

What Actually Happened
In April 2026, 404 Media reported on an FBI agent testimony during a federal trial involving defendants charged with activities at an ICE detention facility in Texas. One defendant had Signal installed on her iPhone at the time of the events. By the time investigators examined the phone, Signal had been deleted.
They recovered Signal messages anyway, specifically incoming ones, through Apple’s internal notification storage. The app was gone but the messages were not.
How Signal Works on iOS
The FBI case involved an iPhone, so here’s how iOS handles push notification storage and Signal specifically.
APNs: Apple’s Centralized Push Infrastructure
Apple Push Notification Service (APNs) is the only way to deliver background notifications to iPhones. Every remote notification for every app must go through Apple’s servers before it reaches the device.
When an app registers for push notifications, the device gets an APNs device token, a unique identifier Apple uses to route notifications to that specific device. Signal’s servers hold this token and use it to request notification delivery from Apple. From that point on, Apple is a mandatory intermediary for every notification Signal sends.
NSE: The Notification Service Extension
Signal on iOS uses a UNNotificationServiceExtension (NSE), a sandboxed process that iOS fires between receiving an APNs notification and displaying it to the user. This is like a middleware that intercepts the notification before it ever shows up on your screen.
Here is what the flow looks:
- Signal’s server sends an APNs payload to Apple containing 2 things: a
mutable-content: 1flag, which tells iOS to run the NSE before displaying anything, and an encrypted ciphertext blob containing the actual message content. Apple routes this payload to the target device. The message content (even if encrypted), travels through Apple’s infrastructure. - Once the payload arrives on device, iOS fires Signal’s NSE in a separate sandboxed process to decrypt the ciphertext locally using Signal Protocol keys and calls
contentHandler()with the plain-text readable sender name and message text. - iOS takes that decrypted output, stores it in the phone’s notification database at
/var/mobile/Library/UserNotifications/, and displays the notification.
That local database is what the FBI extracted. It persists through app deletion, message expiry, and every disappearing message timer you have ever set.
How Signal Works on Android
Android handles this entirely differently:
What Google Actually Sees via FCM
Android does not allow apps to maintain persistent background connections to their own servers. Battery optimizations, App Standby, and background process limits all prevent it. Instead, Google centralizes all push delivery through a single persistent connection managed by Google Mobile Services (GMS).
GMS keeps an always-on TCP socket to Firebase Cloud Messaging (FCM) servers that survives across all power states short of the device being off. Every push notification for every app goes through this channel.
FCM supports 2 message types:
- Notification messages carry a title and body directly in the FCM payload. Android can render the notification from that payload without ever waking the app. The content travels through Google’s FCM infrastructure. Google’s servers carry the notification text.
- Data messages are what Signal uses. They carry only a custom key-value payload and require the receiving app to handle it in code. Signal’s data message payload is essentially empty, a high-priority wake-up ping with no message content whatsoever. The actual text of your message is never in the FCM payload at any point in transit.
All Google’s infrastructure ever sees is a target device FCM registration token, Signal’s app identifier (org.thoughtcrime.securesms), a priority flag, and a timestamp. Nothing else.
This is the contrast to iOS. On iOS, Apple’s APNs infrastructure carries an encrypted blob. On Android, Google’s FCM carries nothing related to your message content at any point in transit, not even ciphertext.
What Google does have is delivery metadata: your device’s FCM registration token, which maps to your Google account, and a timestamped record of when a Signal notification was delivered to your device (which is useful to track you so…).
The Double Ratchet: Where Decryption Happens
When GMS receives the FCM data message and wakes Signal’s FirebaseMessagingService, Signal gets roughly a 20-second execution window. It uses that window to open a TLS connection to Signal’s own servers and fetch the actual encrypted message payload.
The encryption is the Signal Protocol, which combines 2 mechanisms:
- X3DH (Extended Triple Diffie-Hellman) handles the initial key agreement when 2 parties establish a conversation for the first time.
- The Double Ratchet algorithm derives a new encryption key for every individual message using both a symmetric-key ratchet and a Diffie-Hellman ratchet. This means past messages remain encrypted even if a current key is compromised, because each ratchet step generates a new key and discards the old one. Future messages also become secure again once the ratchet advances after a compromise.
Signal’s servers store only encrypted ciphertext and cannot decrypt any of it. Signal cannot read your messages. Decryption happens entirely on your device, with private keys living in Android’s hardware-backed Keystore on devices with a secure element.
The Handoff: Where the OS Takes Over
Once Signal decrypts the message and needs to show it to you, it calls Android’s NotificationManager. Stripped down, that call looks like this:
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(senderName)
.setContentText(messageBody)
.setSmallIcon(R.drawable.ic_notification)
.build()
NotificationManagerCompat.from(context).notify(notificationId, notification)
The moment Signal calls .notify(), the senderName and messageBody leave Signal’s sandboxed process and are handed to a system service. The Android OS takes ownership of it for rendering: the lock screen, the notification drawer, connected Wear OS devices, and system-level logging.
If you configure Signal’s notification content setting to “No Name or Message”, what gets passed to .setContentTitle() and .setContentText() is a generic placeholder. The OS takes ownership of that placeholder instead, and nothing sensitive ends up in any database.
Where Android Stores Your Notifications
The System Notification Log
When Signal calls NotificationManager.notify(), Android writes the notification event to a SQLite database at /data/system/notification_log.db. This database is owned by the OS, not by Signal. Signal has no visibility into it and no way to delete entries from it. From Signal’s perspective, it does not know the database exists.
The schema stores: pkg (the posting app’s package name, e.g. org.thoughtcrime.securesms), uid, when (Unix timestamp in milliseconds), tag, key, and the content fields containing whatever strings Signal passed to .setContentTitle() and .setContentText() when it called notify(). If those strings were a sender name and message body, that is exactly what the database holds.
This database is invisible to users. There is no Settings menu that exposes it and no way to manually clear it through the Android interface. Accessing it requires physical device extraction via forensic tools.
Deleted Notifications Are Not Actually Gone
SQLite’s deletion behavior is the reason this database functions as a forensic artifact.
When SQLite deletes a row, it does not overwrite the bytes on disk. It marks the pages holding that row as free and available for reuse. The data remains there until SQLite decides to allocate those pages for new writes.
Forensic tools parse the database at the page level, reading “deallocated” pages to recover deleted row data. This content typically remains recoverable for weeks or more, until enough new writes physically displace it.
The FCM Queued Messages Store
Separately, undelivered FCM messages are queued in a LevelDB database at /data/data/com.google.android.gms/databases/fcm_queued_messages.ldb/, used by Play Services to buffer FCM payloads that arrived while the app was not running.
For most apps using notification-type FCM messages, this store can contain actual notification content. For Signal, it is less interesting forensically. Because Signal uses data-only FCM messages with empty payloads, the queued message store contains nothing but wake-up pings and delivery timestamps. Useful metadata (which to be fair, people have been killed from just metadata), but not message content.

What You Should Do
What you should do depends on your paranoia and how hard you want your life to be. The below focuses on Android because I’m more familiar with it.

1. Change Signal’s Notification Settings
- Open Signal -> Settings -> Notifications -> Show, and set it to “No Name or Message”
- This controls what strings Signal passes to
NotificationManager, which determines what ends up innotification_log.db. With this change, message content is never written to the notification database in the first place.
2. Remove Signal’s Notification Permissions Entirely
- Open Android Settings -> Apps -> Signal -> Notifications -> Off.
- This removes Signal’s notification permissions at the OS level, which causes GMS to deregister Signal’s FCM token and remove it from Google’s records.
- Signal falls back to WebSocket polling instead. Messages will arrive within seconds to a minute rather than instantly, but this also eliminates the FCM exposure vector entirely.
3. Use a VPN
- Always route your traffic through a trusted VPN.
- Any server your device connects to (including Google’s FCM servers) will see the VPN endpoint IP rather than your real one.
- Removing Signal’s notification permissions as above also eliminates this exposure vector, but a VPN adds a layer of protection across everything else running on your device.
4. Use GrapheneOS
- Use GrapheneOS to go a step further. GrapheneOS is a hardened Android fork built for Google Pixel hardware that runs without any Google tokens or infrastructure. No GMS, no FCM, no pinging Google’s servers.
- Signal on GrapheneOS can use its own WebSocket-based push delivery without any Google involvement whatsoever, which removes this concern altogether.
Note that everything changes constantly but this post should give you an idea on what to do no only on Signal, but any other app. As for the topic at hand, Signal’s encryption is not the weak link here. The weak link is the infrastructure your OS relies on to show you a notification and what it does with that content afterward.