Back up all of signal-desktop's media files
Table of Contents
1. Introduction
I don't own a smartphone, so I need to use Signal's desktop client (registering my phone number with signal-cli) for my encrypted instant messaging needs. This situation kind of sucks, as Signal's desktop client is more of an afterthought and is missing many crucial features. One annoyance with it is that you can only view the recent media of a conversation in the desktop client. Pictures and videos that you received or sent some time ago are not visible. I worked around this problem by writing a Python script that directly accesses Signal's database and data store to dump all of the images and videos in a directory, grouping them by conversation.
2. Prerequisites
You need to install Python 3 and the module sqlcipher3
. It can be installed with:
$ pip3 install --user sqlcipher3-binary
Then, you need to find your Signal config directory. On Linux systems, it is
typically under ~/.config/Signal/
, so I will use this path from here on. If
your's is at a different path, you need to substitute all occurences of
~/.config/Signal/
down below with the proper path.
You also need to obtain the database key from
~/.config/Signal/config.json
. The signal devs have decided to encrypt their
sqlite database… and store the key in plaintext! What a senseless thing to do.
3. The script
Here's the script. You need to add your key at the top and modify the paths if
necessary. Afterwards, you should be able to just run the script and find all of
your exported media in OUTPUT_DIR
.
import json import os import shutil import sqlcipher3 from mimetypes import guess_extension DB_PATH = "/home/user/.config/Signal/sql/db.sqlite" ATTACHMENTS_PATH = "/home/user/.config/Signal/attachments.noindex" DB_KEY = "paste_your_db_key_here" OUTPUT_DIR = "/path/to/output/dir" c = sqlcipher3.connect(DB_PATH) c.execute("PRAGMA key = \"x\'%s\'\";" % DB_KEY) resp = c.execute("SELECT json FROM messages " "WHERE hasVisualMediaAttachments = '1';") messages = resp.fetchall() for message in messages: message_json = message[0] message_parsed = json.loads(message_json) conversationid = message_parsed["conversationId"] resp = c.execute("SELECT profileFullName FROM conversations " "WHERE id = '%s';" % conversationid) name = resp.fetchone()[0] if not name: resp = c.execute("SELECT name FROM conversations WHERE id = '%s';" % conversationid) name = resp.fetchone()[0] name = "".join([c if c.isalnum() else "_" for c in name]) try: os.mkdir("%s/%s" % (OUTPUT_DIR, name)) except FileExistsError: pass for attachment in message_parsed["attachments"]: try: dest_path = "%s/%s/%s" % (OUTPUT_DIR, name, attachment["fileName"]) except KeyError: extension = guess_extension(attachment["contentType"]) dest_path = "%s/%s/signal-%s%s" % (OUTPUT_DIR, name, attachment["uploadTimestamp"], extension) src_path = "%s/%s" % (ATTACHMENTS_PATH, attachment["path"]) shutil.copy(src_path, dest_path)