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])

	os.mkdir("%s/%s" % (OUTPUT_DIR, name))
    except FileExistsError:

    for attachment in message_parsed["attachments"]:
	    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,
	src_path = "%s/%s" % (ATTACHMENTS_PATH, attachment["path"])
	shutil.copy(src_path, dest_path)