Exporting messages from Signal for iOS: a journey

Exporting messages from Signal for iOS: a journey

I started using Signal 5 years ago as I became increasingly conscious of my data footprint. Around the same time, I closed my Facebook and Instagram accounts, and later, my Google Account.

The problem with Signal

Signal makes it very difficult, and in most cases, impossible, for one to back up, export, or migrate message data. The Signal team insists these restrictions are meant to protect users’ privacy. However, backup and migration policies differ for each official Signal client and sometimes contradict each other.

For example, while iOS offers no official backups process, Android backups are built into the app. At the same time, iOS users can migrate their data to a new device (as long as their phone number hasn’t changed), but this is impossible on Android. Meanwhile, decrypting messages from the desktop client is trivial, but linking one’s phone and desktop client only syncs messages forward in time.

Why export Signal's message history

Because it’s my fucking data.

The road to exporting my data was long, frustrating and filled with dead ends – it took over a year to get here. What's paradoxical is that someone else's phone turned out to be the key to my data.

In recent years, I moved most of my conversations to Signal, including those with family and close friends. Shortly after we started dating, I asked my partner to switch to Signal, and we’ve used it exclusively ever since. At some point, I thought it would be nice to export our conversation history – a sort of time capsule of our relationship. “Easy,” I thought. I was wrong.

Since we started dating, I had switched from a OnePlus 2 to an iPhone XS Max. And since migrating from Android to iOS isn’t possible, part of our conversation history was locked on my old phone. Jailbreaking the iPhone was out of the question since my version of iOS had not been jailbreaken. The Signal Desktop client could have offered an easy avenue to decrypt our chat history, but there was a period of several months during which I had not linked my iPhone to the Signal desktop client.

I opened a thread about this in the Signal Community Forum in June of 2019. It quickly became apparent that backing up my Signal data would be an uphill battle.

Luckily, my partner had an iPhone 7, which she recently replaced with a company phone. Her old iPhone contained our entire message history in one place and was easy to jailbreak. Jackpot.

One important lesson I learned during this process: to ensure access to your conversation history, install the Desktop client and link it to your mobile device as soon as you start using Signal. The Signal database and encryption key are both accessible on your computer, allowing for easy decryption. Linking the desktop client later will only sync messages forward in time from the moment the devices are linked.

Tips to increase your chances of exporting your Signal database

If you haven’t linked the desktop client and extracting the iOS database is your only option, this guide is for you. If you've decided to embark upon this journey, here's some advice.

Stop updating iOS

Your best chance of jailbreaking iOS is on older firmware versions. Turn off automatic updates and decline prompts to update iOS.

Backup SHSH blobs with every new iOS update

SHSH blobs are the digital signatures that Apple generates and uses to personalize iOS firmware files for each iOS device. Apple only signs firmware updates for a limited time after release. Having these signatures handy allows one to install versions of iOS after Apple stops signing them – like when a jailbreak becomes available for that version. In some cases, you may be able to downgrade iOS to a previous version.

Backup SHSH blobs every time Apple releases a new version of iOS (e.g., 14.0, 14.1, 14.1.1). I personally use the blobsaver app, but TSS Saver is a popular alternative.

Learn about jailbreaking and stay up to date on developments

Developers and hackers are constantly working to break the security of iOS, and new methods to jailbreak iPhones are frequently made public. Stay informed on jailbreak releases by following the /r/jailbreak subreddit.

This exhaustive list of jailbreak compatibility by device and iOS version is also a great resource.

It's also useful to understand the difference between untethered, semi-untethered, semi-tethered, and tethered jailbreaks.

Recognize that this may not work

Depending on your iPhone, you might never be able to jailbreak it or extract Signals decryption key from the iOS Keychain. Apple is making it increasingly difficult to jailbreak iOS devices – improvements to hardware and software are leaving fewer cracks for hackers to exploit.

Ok, let's get our hands dirty.

How to (maybe) backup Signal for iPhone

This guide explains how I was able to backup and extract data from Signal's encrypted SQLite database on an iPhone 7 with iOS 13.6.1.

Prerequisites

  • Physical access to the iPhone with Signal still installed
  • An original iPhone cable
  • A Mac or Linux computer
  • Patience and a bit of luck

Shell environments will be differentiated as such.

// Host machine
$ <command>

// iPhone 
root# <command>

Step 1: Jailbreak iOS

For iOS 13.6.1 on an iPhone 7, I used checkra1n which offers a straight forward semi-tethered jailbreak. Depending on the iPhone device and version of iOS, a jailbreak may or may not be available.

See this updated list of iOS jailbreaks for device compatibility and instructions.

Step 2: SSH into the iPhone

Cydia usually comes with OpenSSH installed and enabled, allowing shell access over IP or USB. If SSH access isn't activated, launch Cydia and install OpenSSH.

As always, the root password on iOS is alpine.

If the iPhone and the host machine are on the same network, SSH into the phone using its IP address. It may be found in the phone's WiFi settings.

$ ssh root@[iphone ip] -p 22

If SSH over the air isn't possible, USB may be an alternative. However, this involves enabling a proxy service on the host machine.

First, install libimobiledevice on the host machine.

$ brew install libimobiledevice

Then, edit com.usbmux.iproxy.plist and append the following XML to the file.

$ nano ~/Library/LaunchAgents/com.usbmux.iproxy.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.usbmux.iproxy</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/iproxy</string>
<string>2222</string>
<string>22</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>

And finally, launch the proxy on the host machine.

$ launchctl load ~/Library/LaunchAgents/com.usbmux.iproxy.plist

It should now be possible to SSH into the iPhone over USB on port 2222.

$ ssh root@localhost -p 2222

Step 3: Install required packages

Once logged into to the iPhone's shell, install the following packages as they will come in handy.

root# apt install zip unzip nano wget

Step 4: Backup the Signal data directory

Locate Signal's data directory

Much like macOS, iOS stores application data in a Library-like directory. In iOS 13, its located here /private/var/mobile/Containers/Shared/AppGroup/.

Signal's data directory contains the encrypted SQLite database file in ./grdb/signal.sqilte and attachments, such as images and videos in ./Attachments.

The data directory may be found by searching the filesystem for Signal's encrypted database file, signal.sqlite.

root# find / -type f -iname "signal.sqlite"

That should return something which looks like this:
/private/var/mobile/Containers/Shared/AppGroup/01484069-3446-4CC0-8BE7-7464E7D08FDF/grdb/signal.sqlite

The Signal directory is: /private/var/mobile/Containers/Shared/AppGroup/01484069-3446-4CC0-8BE7-7464E7D08FDF/

Zip the Signal directory

It is recommended to zip the entire directory, not only the database file, as it also contains images and other message attachments. This archive can reach several gigabytes.

root# cd /private/var/mobile/Containers/Shared/AppGroup/
root# zip -r signal-backup.zip <Signal directory>

# ex: zip -r signal-backup.zip /private/var/mobile/Containers/Shared/AppGroup/01484069-3446-4CC0-8BE7-7464E7D08FDF/

Retrieve the backup on the host machine

In a new terminal session on the host machine, use scp to copy the backup.

$ scp -P 22 root@localhost:/private/var/mobile/Containers/Shared/AppGroup/signal-backup.zip ~/

Once complete, verify that the backup has fully transferred by unpacking it.

Step 5: Extract the database encryption key

This is the most complex part of this operation and it helps to have a basic understanding of core iOS development concepts.

Understanding iOS Entitlements

From the Apple Developer Documentation:

An entitlement is a right or privilege that grants an executable particular capabilities. For example, an app needs the HomeKit Entitlement — along with explicit user consent — to access a user’s home automation network. An app stores its entitlements as key-value pairs embedded in the code signature of its binary executable.

Enter Keychain Dumper

We will use Keychain-Dumper to attempt extracting the Signal encryption key. It requires entitlements to access Keychain data for any particular app.

Reading through Keychain-Dumper's GitHub issues, I learned that entitlements changed in iOS 13.5 – prior to this version, an application could be given wildcard entitlements. Changes in 13.5 made it such that apps need specific entitlements. Thankfully, it's possible to update an executable's entitlements, even as a binary.

The keychain_dumper binary included in its GitHub repo has wildcard entitlements. We'll need to update them in order to give it permission to decrypt the Signal key.

Install Keychain Dumper on the iPhone

Back on the iPhone, download and extract the keychain_dumper binary, and move it to /usr/bin.

root# wget https://github.com/ptoomey3/Keychain-Dumper/releases/download/1.0.0/keychain_dumper-1.0.0.zip
root# unzip keychain_dumper-1.0.0.zip
root# mv keychain_dumper /usr/bin

Update keychain_dumper's entitlements

It's necessary to create a bash script that will look through the apps installed on your phone and give keychain_dumper entitlements to access them. At the time of writing, the updateEntitlements.sh script included in the repo doesn't take into account the changes in iOS 13.5.

root# nano updateEntitlements.sh

Add this script to the file.

#!/bin/bash

KEYCHAIN_DUMPER_FOLDER=/usr/bin
ENTITLEMENT_PATH=$KEYCHAIN_DUMPER_FOLDER/ent.xml
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" > $ENTITLEMENT_PATH
echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $ENTITLEMENT_PATH
echo "<plist version=\"1.0\">" >> ENTITLEMENT_PATH
echo "  <dict>" >> $ENTITLEMENT_PATH
echo "    <key>keychain-access-groups</key>" >> $ENTITLEMENT_PATH
echo "    <array>" >> $ENTITLEMENT_PATH

for d in /var/mobile/Containers/Shared/AppGroup/* ; do  
	cd $d ; 
	echo "        <string>$(plutil -MCMMetadataIdentifier .com.apple.mobile_container_manager.metadata.plist)</string>" >> $ENTITLEMENT_PATH ; 
	cd .. ; 
done

# amend app specific application-identifier
for d in /private/var/containers/Bundle/Application/* ; do
	cd $d/*.app/ ;
	executableName=`plutil -CFBundleExecutable Info.plist` ;
	checkingPath=`pwd` ;
	echo "Checking... $checkingPath" ;
	# extract current MachO entitlement to file
	ldid -e "${executableName}" >> ent.xml ;
	applicationIdentifier=$(plutil -application-identifier ent.xml) ;

	echo "        <string>$applicationIdentifier</string>" >> $ENTITLEMENT_PATH ; 

	# clean up
	rm ent.xml ;
	cd ../../ ;
done

echo "    </array>">> $ENTITLEMENT_PATH
echo "    <key>platform-application</key> <true/>">> $ENTITLEMENT_PATH
echo "    <key>com.apple.private.security.no-container</key>  <true/>">> $ENTITLEMENT_PATH
echo "  </dict>">> $ENTITLEMENT_PATH
echo "</plist>">> $ENTITLEMENT_PATH

cd $KEYCHAIN_DUMPER_FOLDER
ldid -Sent.xml keychain_dumper

Set execution permissions for the script.

root# chmod u+x updateEntitlements.sh

The updateEntitlements.sh script makes use of a tool called plutil which isn't installed in iOS by default. Luckily, an iOS compatible version is available in Cydia in the apt.binger.com repository.

Install the plutil package from Cydia and test that it's working on the iPhone.

root# plutil
Usage: plutil options file...
-help                   Print this message
-full                   Print an exhaustive list of options
-verbose                Show verbose output
-show                   Show property list data
-keys                   List top level dictionary keys
-create                 Create a new empty property list

-key keyname            Recover value for key. Multiple uses builds keypath
-value value            Set value for keypath
-remove                 Remove value at keypath
-type typeid            Type to use while setting key. Valid types are int,
                        float, bool, json, and string (default). Use json to
                        define arrays and dictionaries
-convert format         Convert each property list file to selected format.
                        Formats are xml1 and binary1 and json. Note that json
                        files are saved to filename.json

Next, run the custom updateEntitlements.sh script.

root# sh updateEntitlements.sh

The list of apps installed on the phone appear.

Checking... /private/var/containers/Bundle/Application/00D4C0A7-CDE4-4379-9ED9-81C63332230C/Files.app
Checking... /private/var/containers/Bundle/Application/3190DC2F-8DB6-43C3-BB6B-9827AFA1FD53/Signal.app
Checking... /private/var/containers/Bundle/Application/60EFCFA7-BA20-4630-A2E3-26245EE47A47/Home.app
Checking... /private/var/containers/Bundle/Application/77414874-3716-4C4D-819F-9B8E76AE1CA7/News.app
Checking... /private/var/containers/Bundle/Application/87AF76AA-C5F0-433A-9AF1-7C685D467C17/MobileStore.app

Use the ldid utility to check entitlements were properly set.

root# ldid -e /usr/bin/keychain_dumper | grep signal

This should produce the following output.

<string>group.org.whispersystems.signal.group</string>
<string>group.org.whispersystems.signal.group.staging</string>
<string>U68MSDN6DR.org.whispersystems.signal</string>

For keychain_dumper to work, it needs all three of these entitlements. It will not work if group entitlements are missing.

Run Keychain Dumper and cross your fingers.

root# keychain_dumper -a

If all went well, the decrypted Signal private key should appear in hex format.

Generic Password
----------------
Service: GRDBKeyChainService
Account: GRDBDatabaseCipherKeySpec
Entitlement Group: U68MSDN6DR.org.whispersystems.signal
Label: (null)
Accessible Attribute: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, protection level 4
Description: (null)
Comment: (null)
Synchronizable: 0
Generic Field: (null)
Keychain Data (Hex): 0xabbe15b2bc59d74a7fbb21ee74a13a6e2f097d38fc61e3b9b879b82325ae4542d46c339ae3c092e3e00aa2a74ec5c8be

Your private key, Keychain Data (Hex), should be a 99 byte string starting with 0x. If the keychain data looks like this +62BnrvZ18x\/uyHudKE61i8JfTj8auO5uHm4IyWu1ULMbDOb484S4+AKoq3Oxc3L, that probably means the key was not properly decrypted.

Step 5: Decrypt the Signal database

The worst is over. With both the encrypted database file and encryption key in hand, the data may be decrypted. For this, sqlcipher, an SQLite extension that provides encryption for database files, is required.

Back on the host machine, install sqlcipher.

$ brew install sqlcipher

Next, open the database file.

$ sqlcipher <signal backup>/grdb/signal.sqlite
SQLite version 3.33.0 2020-08-14 13:23:32 (SQLCipher 4.4.2 community)
Enter ".help" for usage hints.
sqlite>

Enter the following commands in the sqlite> prompt, updating with your private key. It's important to remove the characters 0x from the beginning of the private key – this simply serves as an indicator that the key is in hexadecimal format.

sqlite> PRAGMA key="x'abbe15b2bc59d74a7fbb21ee74a13a6e2f097d38fc61e3b9b879b82325ae4542d46c339ae3c092e3e00aa2a74ec5c8be'"; 
sqlite> PRAGMA cipher_plaintext_header_size = 32; 
sqlite> .tables 

If a list of tables appears, congratulations, you've decrypted your Signal database 🎉

grdb_migrations
indexable_text
indexable_text_fts
indexable_text_fts_config
indexable_text_fts_data
indexable_text_fts_docsize
indexable_text_fts_idx
keyvalue
...

Next, let's export a decrypted database to a file.

sqlite> ATTACH DATABASE 'signal_decrypted.sqlite' AS signal_decrypted KEY ''; 
sqlite> SELECT sqlcipher_export('signal_decrypted'); 
sqlite> DETACH DATABASE signal_decrypted;

The decrypted database is now found under signal_decrypted.sqlite.

Step 6: Locate your messages

Messages are stored in the model_TSInteraction table.

In a follow up post, I will explain Signal's database structure and how to export the message history to a usable format.

Resources

The following resources helped me through this process, and to write this guide.

seb3point0

seb3point0

Host of The Interop and Epicenter. Founder of Interop Ventures.