├── .gitignore ├── Makefile ├── database.proto ├── sigbak.h ├── sbk-recipient.c ├── backup.proto ├── sbk-quote.c ├── mime.c ├── sbk-attachment-tree.c ├── cmd-check-backup.c ├── cmd-export-database.c ├── sbk-thread.c ├── sbk-read.c ├── sbk-file.c ├── README.md ├── sbk-edit.c ├── sbk-database.c ├── LICENSE.md ├── sigbak.c ├── sbk-sqlite.c ├── sbk-open.c ├── sbk-frame.c ├── cmd-export-avatars.c ├── sbk-reaction.c ├── sigbak.1 ├── sbk-internal.h ├── sbk-mention.c ├── sbk.h ├── cmd-dump-backup.c ├── cmd-export-attachments.c ├── sbk-recipient-tree.c ├── sbk-attachment.c ├── cmd-export-messages.c └── sbk-message.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.[do] 2 | /*.pb-c.[ch] 3 | /obj/ 4 | /sigbak 5 | /tags 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROG= sigbak 2 | SRCS= cmd-check-backup.c cmd-dump-backup.c cmd-export-attachments.c \ 3 | cmd-export-avatars.c cmd-export-database.c \ 4 | cmd-export-messages.c mime.c sbk-attachment-tree.c \ 5 | sbk-attachment.c sbk-database.c sbk-edit.c sbk-file.c \ 6 | sbk-frame.c sbk-mention.c sbk-message.c sbk-open.c \ 7 | sbk-quote.c sbk-reaction.c sbk-read.c sbk-recipient-tree.c \ 8 | sbk-recipient.c sbk-sqlite.c sbk-thread.c sigbak.c 9 | PROTOS= backup.proto database.proto 10 | 11 | SRCS+= ${PROTOS:.proto=.pb-c.c} 12 | BUILDFIRST= ${PROTOS:.proto=.pb-c.h} 13 | CLEANFILES= ${PROTOS:.proto=.pb-c.c} ${PROTOS:.proto=.pb-c.h} 14 | 15 | CFLAGS+= -I. 16 | LDADD+= -lcrypto 17 | 18 | .if !(make(clean) || make(cleandir) || make(obj)) 19 | CFLAGS+!= pkg-config --cflags libprotobuf-c sqlite3 20 | LDADD+!= pkg-config --libs libprotobuf-c sqlite3 21 | .endif 22 | 23 | .SUFFIXES: .pb-c.c .pb-c.h .proto 24 | 25 | .proto.pb-c.c .proto.pb-c.h: 26 | protoc --c_out=. --proto_path=${.CURDIR} $< 27 | 28 | .include 29 | -------------------------------------------------------------------------------- /database.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014-2016 Open Whisper Systems 3 | * 4 | * Licensed according to the LICENSE file in this repository. 5 | */ 6 | 7 | syntax = "proto3"; 8 | 9 | package signal; 10 | 11 | option java_package = "org.thoughtcrime.securesms.database.model.databaseprotos"; 12 | option java_multiple_files = true; 13 | 14 | // DEPRECATED -- only here for database migrations 15 | message ReactionList { 16 | option deprecated = true; 17 | 18 | message Reaction { 19 | string emoji = 1; 20 | uint64 author = 2; 21 | uint64 sentTime = 3; 22 | uint64 receivedTime = 4; 23 | } 24 | 25 | repeated Reaction reactions = 1; 26 | } 27 | 28 | message BodyRangeList { 29 | message BodyRange { 30 | enum Style { 31 | BOLD = 0; 32 | ITALIC = 1; 33 | SPOILER = 2; 34 | STRIKETHROUGH = 3; 35 | MONOSPACE = 4; 36 | } 37 | 38 | message Button { 39 | string label = 1; 40 | string action = 2; 41 | } 42 | 43 | int32 start = 1; 44 | int32 length = 2; 45 | 46 | oneof associatedValue { 47 | string mentionUuid = 3; 48 | Style style = 4; 49 | string link = 5; 50 | Button button = 6; 51 | } 52 | } 53 | 54 | repeated BodyRange ranges = 1; 55 | } 56 | -------------------------------------------------------------------------------- /sigbak.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #ifndef SIGBAK_H 18 | #define SIGBAK_H 19 | 20 | #include "sbk.h" 21 | 22 | #ifndef nitems 23 | #define nitems(a) (sizeof (a) / sizeof (a)[0]) 24 | #endif 25 | 26 | enum cmd_status { 27 | CMD_OK, 28 | CMD_ERROR, 29 | CMD_USAGE 30 | }; 31 | 32 | struct cmd_entry { 33 | const char *name; 34 | const char *alias; 35 | const char *usage; 36 | enum cmd_status (*exec)(int, char **); 37 | }; 38 | 39 | const char *mime_get_extension(const char *); 40 | 41 | int get_passphrase(const char *, char *, size_t); 42 | int unveil_dirname(const char *, const char *); 43 | void sanitise_filename(char *); 44 | char *get_recipient_filename(struct sbk_recipient *, const char *); 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /sbk-recipient.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include "sbk-internal.h" 18 | 19 | #define ISEMPTY(s) ((s) == NULL || *(s) == '\0') 20 | 21 | const char * 22 | sbk_get_recipient_display_name(const struct sbk_recipient *rcp) 23 | { 24 | if (rcp != NULL) 25 | switch (rcp->type) { 26 | case SBK_CONTACT: 27 | if (!ISEMPTY(rcp->contact->nickname_joined_name)) 28 | return rcp->contact->nickname_joined_name; 29 | if (!ISEMPTY(rcp->contact->system_joined_name)) 30 | return rcp->contact->system_joined_name; 31 | if (!ISEMPTY(rcp->contact->profile_joined_name)) 32 | return rcp->contact->profile_joined_name; 33 | if (!ISEMPTY(rcp->contact->profile_given_name)) 34 | return rcp->contact->profile_given_name; 35 | if (!ISEMPTY(rcp->contact->phone)) 36 | return rcp->contact->phone; 37 | if (!ISEMPTY(rcp->contact->email)) 38 | return rcp->contact->email; 39 | break; 40 | case SBK_GROUP: 41 | if (!ISEMPTY(rcp->group->name)) 42 | return rcp->group->name; 43 | break; 44 | } 45 | 46 | return "Unknown"; 47 | } 48 | -------------------------------------------------------------------------------- /backup.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2018 Open Whisper Systems 3 | * 4 | * Licensed according to the LICENSE file in this repository. 5 | */ 6 | 7 | syntax = "proto2"; 8 | 9 | package signal; 10 | 11 | option java_package = "org.thoughtcrime.securesms.backup.proto"; 12 | 13 | message SqlStatement { 14 | message SqlParameter { 15 | optional string stringParamter = 1; 16 | optional uint64 integerParameter = 2; 17 | optional double doubleParameter = 3; 18 | optional bytes blobParameter = 4; 19 | optional bool nullparameter = 5; 20 | } 21 | 22 | optional string statement = 1; 23 | repeated SqlParameter parameters = 2; 24 | } 25 | 26 | message SharedPreference { 27 | optional string file = 1; 28 | optional string key = 2; 29 | optional string value = 3; 30 | optional bool booleanValue = 4; 31 | repeated string stringSetValue = 5; 32 | optional bool isStringSetValue = 6; 33 | } 34 | 35 | message Attachment { 36 | optional uint64 rowId = 1; 37 | optional uint64 attachmentId = 2; 38 | optional uint32 length = 3; 39 | } 40 | 41 | message Sticker { 42 | optional uint64 rowId = 1; 43 | optional uint32 length = 2; 44 | } 45 | 46 | message Avatar { 47 | optional string name = 1; 48 | optional string recipientId = 3; 49 | optional uint32 length = 2; 50 | } 51 | 52 | message DatabaseVersion { 53 | optional uint32 version = 1; 54 | } 55 | 56 | message Header { 57 | optional bytes iv = 1; 58 | optional bytes salt = 2; 59 | optional uint32 version = 3; 60 | } 61 | 62 | message KeyValue { 63 | optional string key = 1; 64 | optional bytes blobValue = 2; 65 | optional bool booleanValue = 3; 66 | optional float floatValue = 4; 67 | optional int32 integerValue = 5; 68 | optional int64 longValue = 6; 69 | optional string stringValue = 7; 70 | } 71 | 72 | message BackupFrame { 73 | optional Header header = 1; 74 | optional SqlStatement statement = 2; 75 | optional SharedPreference preference = 3; 76 | optional Attachment attachment = 4; 77 | optional DatabaseVersion version = 5; 78 | optional bool end = 6; 79 | optional Avatar avatar = 7; 80 | optional Sticker sticker = 8; 81 | optional KeyValue keyValue = 9; 82 | } -------------------------------------------------------------------------------- /sbk-quote.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include "sbk-internal.h" 21 | 22 | void 23 | sbk_free_quote(struct sbk_quote *qte) 24 | { 25 | if (qte != NULL) { 26 | free(qte->text); 27 | sbk_free_attachment_list(qte->attachments); 28 | sbk_free_mention_list(qte->mentions); 29 | free(qte); 30 | } 31 | } 32 | 33 | int 34 | sbk_get_quote(struct sbk_ctx *ctx, struct sbk_quote **qtep, sqlite3_stmt *stm, 35 | int id_idx, int author_idx, int body_idx, int mentions_idx, 36 | struct sbk_message_id *mid) 37 | { 38 | struct sbk_quote *qte; 39 | 40 | if (sqlite3_column_int64(stm, id_idx) == 0 && 41 | sqlite3_column_int64(stm, author_idx) == 0) { 42 | /* No quote */ 43 | return 0; 44 | } 45 | 46 | if ((qte = calloc(1, sizeof *qte)) == NULL) { 47 | warn(NULL); 48 | return -1; 49 | } 50 | 51 | qte->id = sqlite3_column_int64(stm, id_idx); 52 | 53 | qte->recipient = sbk_get_recipient_from_id_from_column(ctx, stm, 54 | author_idx); 55 | if (qte->recipient == NULL) 56 | goto error; 57 | 58 | if (sbk_sqlite_column_text_copy(ctx, &qte->text, stm, body_idx) == -1) 59 | goto error; 60 | 61 | if (sbk_get_attachments_for_quote(ctx, qte, mid) == -1) 62 | goto error; 63 | 64 | if (sbk_get_long_message(ctx, &qte->text, &qte->attachments) == -1) 65 | goto error; 66 | 67 | if (sbk_get_mentions_for_quote(ctx, &qte->mentions, stm, mentions_idx) 68 | == -1) { 69 | warnx("Cannot get mentions for quote"); 70 | goto error; 71 | } 72 | 73 | if (sbk_insert_mentions(&qte->text, qte->mentions) == -1) { 74 | warnx("Cannot insert mentions in quote"); 75 | goto error; 76 | } 77 | 78 | *qtep = qte; 79 | return 0; 80 | 81 | error: 82 | sbk_free_quote(qte); 83 | return -1; 84 | } 85 | -------------------------------------------------------------------------------- /mime.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | 19 | #include "sigbak.h" 20 | 21 | static struct { 22 | const char *type; 23 | const char *extension; 24 | } mime_extensions[] = { 25 | { "application/gzip", "gz" }, 26 | { "application/msword", "doc" }, 27 | { "application/pdf", "pdf" }, 28 | { "application/rtf", "rtf" }, 29 | { "application/vnd.oasis.opendocument.presentation", "odp" }, 30 | { "application/vnd.oasis.opendocument.spreadsheet", "ods" }, 31 | { "application/vnd.oasis.opendocument.text", "odt" }, 32 | { "application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx" }, 33 | { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx" }, 34 | { "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx" }, 35 | { "application/vnd.rar", "rar" }, 36 | { "application/x-7z-compressed", "7z" }, 37 | { "application/x-bzip2", "bz2" }, 38 | { "application/x-tar", "tar" }, 39 | { "application/zip", "zip" }, 40 | 41 | { "audio/aac", "aac" }, 42 | { "audio/flac", "flac" }, 43 | { "audio/ogg", "ogg" }, 44 | { "audio/mp4", "mp4" }, 45 | { "audio/mpeg", "mp3" }, 46 | 47 | { "image/gif", "gif" }, 48 | { "image/jpeg", "jpg" }, 49 | { "image/png", "png" }, 50 | { "image/svg+xml", "svg" }, 51 | { "image/tiff", "tiff" }, 52 | { "image/webp", "webp" }, 53 | 54 | { "text/html", "html" }, 55 | { "text/plain", "txt" }, 56 | { "text/x-signal-plain", "txt" }, 57 | 58 | { "video/mp4", "mp4" }, 59 | { "video/mpeg", "mpg" }, 60 | }; 61 | 62 | const char * 63 | mime_get_extension(const char *type) 64 | { 65 | size_t i, len; 66 | 67 | /* We're not interested in content-type parameters */ 68 | len = strcspn(type, ";"); 69 | 70 | for (i = 0; i < nitems(mime_extensions); i++) 71 | if (strncmp(mime_extensions[i].type, type, len) == 0) 72 | return mime_extensions[i].extension; 73 | 74 | return NULL; 75 | } 76 | -------------------------------------------------------------------------------- /sbk-attachment-tree.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include "sbk-internal.h" 21 | 22 | static int sbk_cmp_attachment_entries(struct sbk_attachment_entry *, 23 | struct sbk_attachment_entry *); 24 | 25 | RB_GENERATE_STATIC(sbk_attachment_tree, sbk_attachment_entry, entries, 26 | sbk_cmp_attachment_entries) 27 | 28 | void 29 | sbk_free_attachment_tree(struct sbk_ctx *ctx) 30 | { 31 | struct sbk_attachment_entry *entry; 32 | 33 | while ((entry = RB_ROOT(&ctx->attachments)) != NULL) { 34 | RB_REMOVE(sbk_attachment_tree, &ctx->attachments, entry); 35 | sbk_free_file(entry->file); 36 | free(entry); 37 | } 38 | } 39 | 40 | static int 41 | sbk_cmp_attachment_entries(struct sbk_attachment_entry *a, 42 | struct sbk_attachment_entry *b) 43 | { 44 | if (a->id.row_id < b->id.row_id) 45 | return -1; 46 | 47 | if (a->id.row_id > b->id.row_id) 48 | return 1; 49 | 50 | return (a->id.unique_id < b->id.unique_id) ? -1 : 51 | (a->id.unique_id > b->id.unique_id); 52 | } 53 | 54 | int 55 | sbk_insert_attachment_entry(struct sbk_ctx *ctx, Signal__BackupFrame *frm, 56 | struct sbk_file *file) 57 | { 58 | struct sbk_attachment_entry *entry; 59 | 60 | if (!frm->attachment->has_rowid) 61 | goto invalid; 62 | 63 | if (!frm->attachment->has_attachmentid) { 64 | if (ctx->db_version < 65 | SBK_DB_VERSION_REMOVE_ATTACHMENT_UNIQUE_ID) 66 | goto invalid; 67 | frm->attachment->attachmentid = 0; 68 | } 69 | 70 | if ((entry = malloc(sizeof *entry)) == NULL) { 71 | warn(NULL); 72 | sbk_free_file(file); 73 | return -1; 74 | } 75 | 76 | entry->id.row_id = frm->attachment->rowid; 77 | entry->id.unique_id = frm->attachment->attachmentid; 78 | entry->file = file; 79 | RB_INSERT(sbk_attachment_tree, &ctx->attachments, entry); 80 | return 0; 81 | 82 | invalid: 83 | warnx("Invalid attachment frame"); 84 | sbk_free_file(file); 85 | return -1; 86 | } 87 | 88 | struct sbk_file * 89 | sbk_get_attachment_file(struct sbk_ctx *ctx, 90 | const struct sbk_attachment_id *id) 91 | { 92 | struct sbk_attachment_entry find, *result; 93 | 94 | find.id = *id; 95 | result = RB_FIND(sbk_attachment_tree, &ctx->attachments, &find); 96 | return (result != NULL) ? result->file : NULL; 97 | } 98 | -------------------------------------------------------------------------------- /cmd-check-backup.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sigbak.h" 22 | 23 | static enum cmd_status cmd_check_backup(int, char **); 24 | 25 | const struct cmd_entry cmd_check_backup_entry = { 26 | .name = "check-backup", 27 | .alias = "check", 28 | .usage = "[-p passfile] backup", 29 | .exec = cmd_check_backup 30 | }; 31 | 32 | static enum cmd_status 33 | cmd_check_backup(int argc, char **argv) 34 | { 35 | struct sbk_ctx *ctx; 36 | struct sbk_file *file; 37 | Signal__BackupFrame *frm; 38 | char *backup, *passfile, passphr[128]; 39 | unsigned long long n; 40 | int c, ret; 41 | 42 | passfile = NULL; 43 | 44 | while ((c = getopt(argc, argv, "p:")) != -1) 45 | switch (c) { 46 | case 'p': 47 | passfile = optarg; 48 | break; 49 | default: 50 | return CMD_USAGE; 51 | } 52 | 53 | argc -= optind; 54 | argv += optind; 55 | 56 | if (argc != 1) 57 | return CMD_USAGE; 58 | 59 | backup = argv[0]; 60 | 61 | if (unveil(backup, "r") == -1) 62 | err(1, "unveil: %s", backup); 63 | 64 | if (passfile == NULL) { 65 | if (pledge("stdio rpath tty", NULL) == -1) 66 | err(1, "pledge"); 67 | } else { 68 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 69 | err(1, "unveil: %s", passfile); 70 | 71 | if (pledge("stdio rpath", NULL) == -1) 72 | err(1, "pledge"); 73 | } 74 | 75 | if ((ctx = sbk_ctx_new()) == NULL) 76 | return CMD_ERROR; 77 | 78 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 79 | sbk_ctx_free(ctx); 80 | return CMD_ERROR; 81 | } 82 | 83 | if (sbk_open(ctx, backup, passphr) == -1) { 84 | explicit_bzero(passphr, sizeof passphr); 85 | sbk_ctx_free(ctx); 86 | return CMD_ERROR; 87 | } 88 | 89 | explicit_bzero(passphr, sizeof passphr); 90 | 91 | if (pledge("stdio", NULL) == -1) 92 | err(1, "pledge"); 93 | 94 | ret = 0; 95 | n = 1; 96 | 97 | while ((frm = sbk_get_frame(ctx, &file)) != NULL) { 98 | sbk_free_frame(frm); 99 | if (file != NULL) { 100 | ret = sbk_write_file(ctx, file, NULL); 101 | sbk_free_file(file); 102 | if (ret == -1) 103 | break; 104 | } 105 | n++; 106 | } 107 | 108 | if (!sbk_eof(ctx) || ret == -1) { 109 | warnx("Error in frame %llu", n); 110 | ret = -1; 111 | } 112 | 113 | sbk_close(ctx); 114 | sbk_ctx_free(ctx); 115 | return (ret == -1) ? CMD_ERROR : CMD_OK; 116 | } 117 | -------------------------------------------------------------------------------- /cmd-export-database.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sigbak.h" 23 | 24 | static enum cmd_status cmd_export_database(int, char **); 25 | 26 | const struct cmd_entry cmd_export_database_entry = { 27 | .name = "export-database", 28 | .alias = "db", 29 | .usage = "[-p passfile] backup database", 30 | .exec = cmd_export_database 31 | }; 32 | 33 | static enum cmd_status 34 | cmd_export_database(int argc, char **argv) 35 | { 36 | struct sbk_ctx *ctx; 37 | char *backup, *db, *passfile, passphr[128]; 38 | int c, fd, ret; 39 | 40 | passfile = NULL; 41 | 42 | while ((c = getopt(argc, argv, "p:")) != -1) 43 | switch (c) { 44 | case 'p': 45 | passfile = optarg; 46 | break; 47 | default: 48 | return CMD_USAGE; 49 | } 50 | 51 | argc -= optind; 52 | argv += optind; 53 | 54 | if (argc != 2) 55 | return CMD_USAGE; 56 | 57 | backup = argv[0]; 58 | db = argv[1]; 59 | 60 | if (unveil(backup, "r") == -1) 61 | err(1, "unveil: %s", backup); 62 | 63 | /* For the export database and its temporary files */ 64 | if (unveil_dirname(db, "rwc") == -1) 65 | return CMD_ERROR; 66 | 67 | /* For SQLite */ 68 | if (unveil("/dev/urandom", "r") == -1) 69 | err(1, "unveil: /dev/urandom"); 70 | 71 | /* For SQLite */ 72 | if (unveil("/tmp", "rwc") == -1) 73 | err(1, "unveil: /tmp"); 74 | 75 | if (passfile == NULL) { 76 | if (pledge("stdio rpath wpath cpath flock tty", NULL) == -1) 77 | err(1, "pledge"); 78 | } else { 79 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 80 | err(1, "unveil: %s", passfile); 81 | 82 | if (pledge("stdio rpath wpath cpath flock", NULL) == -1) 83 | err(1, "pledge"); 84 | } 85 | 86 | /* Prevent SQLite from writing to an existing file */ 87 | if ((fd = open(db, O_RDONLY | O_CREAT | O_EXCL, 0666)) == -1) { 88 | warn("%s", db); 89 | return CMD_ERROR; 90 | } 91 | 92 | close(fd); 93 | 94 | if ((ctx = sbk_ctx_new()) == NULL) 95 | return CMD_ERROR; 96 | 97 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 98 | sbk_ctx_free(ctx); 99 | return CMD_ERROR; 100 | } 101 | 102 | if (sbk_open(ctx, backup, passphr) == -1) { 103 | explicit_bzero(passphr, sizeof passphr); 104 | sbk_ctx_free(ctx); 105 | return CMD_ERROR; 106 | } 107 | 108 | explicit_bzero(passphr, sizeof passphr); 109 | 110 | if (passfile == NULL && 111 | pledge("stdio rpath wpath cpath flock", NULL) == -1) 112 | err(1, "pledge"); 113 | 114 | ret = sbk_write_database(ctx, db); 115 | sbk_close(ctx); 116 | sbk_ctx_free(ctx); 117 | return (ret == -1) ? CMD_ERROR : CMD_OK; 118 | } 119 | -------------------------------------------------------------------------------- /sbk-thread.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include "sbk-internal.h" 21 | 22 | /* For database versions < THREAD_AUTOINCREMENT */ 23 | #define SBK_QUERY_1 \ 24 | "SELECT " \ 25 | "_id, " \ 26 | "date, " \ 27 | "message_count, " /* meaningful_messages */ \ 28 | "recipient_ids " /* recipient_id */ \ 29 | "FROM thread " \ 30 | "ORDER BY _id" 31 | 32 | /* 33 | * For database versions 34 | * [THREAD_AUTOINCREMENT, THREAD_AND_MESSAGE_FOREIGN_KEYS) 35 | */ 36 | #define SBK_QUERY_2 \ 37 | "SELECT " \ 38 | "_id, " \ 39 | "date, " \ 40 | "message_count, " /* meaningful_messages */ \ 41 | "thread_recipient_id " /* recipient_id */ \ 42 | "FROM thread " \ 43 | "ORDER BY _id" 44 | 45 | /* For database versions >= THREAD_AND_MESSAGE_FOREIGN_KEYS */ 46 | #define SBK_QUERY_3 \ 47 | "SELECT " \ 48 | "_id, " \ 49 | "date, " \ 50 | "meaningful_messages, " \ 51 | "recipient_id " \ 52 | "FROM thread " \ 53 | "ORDER BY _id" 54 | 55 | #define SBK_COLUMN__ID 0 56 | #define SBK_COLUMN_DATE 1 57 | #define SBK_COLUMN_MEANINGFUL_MESSAGES 2 58 | #define SBK_COLUMN_RECIPIENT_ID 3 59 | 60 | void 61 | sbk_free_thread_list(struct sbk_thread_list *lst) 62 | { 63 | struct sbk_thread *thd; 64 | 65 | if (lst != NULL) { 66 | while ((thd = SIMPLEQ_FIRST(lst)) != NULL) { 67 | SIMPLEQ_REMOVE_HEAD(lst, entries); 68 | free(thd); 69 | } 70 | free(lst); 71 | } 72 | } 73 | 74 | struct sbk_thread_list * 75 | sbk_get_threads(struct sbk_ctx *ctx) 76 | { 77 | struct sbk_thread_list *lst; 78 | struct sbk_thread *thd; 79 | sqlite3_stmt *stm; 80 | const char *query; 81 | int ret; 82 | 83 | if (sbk_create_database(ctx) == -1) 84 | return NULL; 85 | 86 | if ((lst = malloc(sizeof *lst)) == NULL) { 87 | warn(NULL); 88 | return NULL; 89 | } 90 | 91 | SIMPLEQ_INIT(lst); 92 | 93 | if (ctx->db_version >= 94 | SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS) 95 | query = SBK_QUERY_3; 96 | else if (ctx->db_version >= SBK_DB_VERSION_THREAD_AUTOINCREMENT) 97 | query = SBK_QUERY_2; 98 | else 99 | query = SBK_QUERY_1; 100 | 101 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 102 | goto error; 103 | 104 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 105 | if ((thd = malloc(sizeof *thd)) == NULL) { 106 | warn(NULL); 107 | goto error; 108 | } 109 | 110 | thd->recipient = sbk_get_recipient_from_id_from_column(ctx, 111 | stm, SBK_COLUMN_RECIPIENT_ID); 112 | if (thd->recipient == NULL) { 113 | free(thd); 114 | goto error; 115 | } 116 | 117 | thd->id = sqlite3_column_int64(stm, SBK_COLUMN__ID); 118 | thd->date = sqlite3_column_int64(stm, SBK_COLUMN_DATE); 119 | thd->nmessages = sqlite3_column_int64(stm, 120 | SBK_COLUMN_MEANINGFUL_MESSAGES); 121 | SIMPLEQ_INSERT_TAIL(lst, thd, entries); 122 | } 123 | 124 | if (ret != SQLITE_DONE) 125 | goto error; 126 | 127 | sqlite3_finalize(stm); 128 | return lst; 129 | 130 | error: 131 | sbk_free_thread_list(lst); 132 | sqlite3_finalize(stm); 133 | return NULL; 134 | } 135 | -------------------------------------------------------------------------------- /sbk-read.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | static void 24 | sbk_warnx_hint(const char *msg) 25 | { 26 | warnx("%s (invalid passphrase or corrupted backup?)", msg); 27 | } 28 | 29 | int 30 | sbk_grow_buffers(struct sbk_ctx *ctx, size_t size) 31 | { 32 | unsigned char *buf; 33 | 34 | if (size <= ctx->ibufsize) 35 | return 0; 36 | 37 | if (ctx->ibufsize <= (SIZE_MAX - EVP_MAX_BLOCK_LENGTH) / 2 && 38 | size <= ctx->ibufsize * 2) 39 | size = ctx->ibufsize * 2; 40 | else if (size > SIZE_MAX - EVP_MAX_BLOCK_LENGTH) { 41 | warnx("Buffer size too large"); 42 | return -1; 43 | } 44 | 45 | if ((buf = realloc(ctx->ibuf, size)) == NULL) { 46 | warn(NULL); 47 | return -1; 48 | } 49 | ctx->ibuf = buf; 50 | ctx->ibufsize = size; 51 | 52 | size += EVP_MAX_BLOCK_LENGTH; 53 | 54 | if ((buf = realloc(ctx->obuf, size)) == NULL) { 55 | warn(NULL); 56 | return -1; 57 | } 58 | ctx->obuf = buf; 59 | ctx->obufsize = size; 60 | 61 | return 0; 62 | } 63 | 64 | int 65 | sbk_decrypt_init(struct sbk_ctx *ctx, uint32_t counter) 66 | { 67 | if (!HMAC_Init_ex(ctx->hmac_ctx, NULL, 0, NULL, NULL)) { 68 | warnx("Cannot initialise MAC context"); 69 | return -1; 70 | } 71 | 72 | ctx->iv[0] = counter >> 24; 73 | ctx->iv[1] = counter >> 16; 74 | ctx->iv[2] = counter >> 8; 75 | ctx->iv[3] = counter; 76 | 77 | if (!EVP_DecryptInit_ex(ctx->cipher_ctx, NULL, NULL, ctx->cipher_key, 78 | ctx->iv)) { 79 | warnx("Cannot initialise cipher context"); 80 | return -1; 81 | } 82 | 83 | return 0; 84 | } 85 | 86 | int 87 | sbk_decrypt_update(struct sbk_ctx *ctx, size_t ibuflen, size_t *obuflen) 88 | { 89 | int len; 90 | 91 | if (!HMAC_Update(ctx->hmac_ctx, ctx->ibuf, ibuflen)) { 92 | warnx("Cannot compute MAC"); 93 | return -1; 94 | } 95 | 96 | if (ibuflen > (unsigned int)(INT_MAX - EVP_MAX_BLOCK_LENGTH)) { 97 | sbk_warnx_hint("Ciphertext too long"); 98 | return -1; 99 | } 100 | 101 | if (!EVP_DecryptUpdate(ctx->cipher_ctx, ctx->obuf, &len, ctx->ibuf, 102 | ibuflen)) { 103 | warnx("Cannot decrypt data"); 104 | return -1; 105 | } 106 | 107 | *obuflen = len; 108 | return 0; 109 | } 110 | 111 | int 112 | sbk_decrypt_final(struct sbk_ctx *ctx, size_t *obuflen, 113 | const unsigned char *theirmac) 114 | { 115 | unsigned char ourmac[EVP_MAX_MD_SIZE]; 116 | unsigned int ourmaclen; 117 | int len; 118 | 119 | if (!HMAC_Final(ctx->hmac_ctx, ourmac, &ourmaclen)) { 120 | warnx("Cannot compute MAC"); 121 | return -1; 122 | } 123 | 124 | if (memcmp(ourmac, theirmac, SBK_MAC_LEN) != 0) { 125 | warnx("MAC mismatch"); 126 | return -1; 127 | } 128 | 129 | if (!EVP_DecryptFinal_ex(ctx->cipher_ctx, ctx->obuf + *obuflen, 130 | &len)) { 131 | warnx("Cannot decrypt data"); 132 | return -1; 133 | } 134 | 135 | *obuflen += len; 136 | return 0; 137 | } 138 | 139 | int 140 | sbk_read(struct sbk_ctx *ctx, void *ptr, size_t size) 141 | { 142 | if (fread(ptr, size, 1, ctx->fp) != 1) { 143 | if (ferror(ctx->fp)) 144 | warn(NULL); 145 | else 146 | sbk_warnx_hint("Unexpected end of file"); 147 | return -1; 148 | } 149 | 150 | return 0; 151 | } 152 | -------------------------------------------------------------------------------- /sbk-file.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | void 24 | sbk_free_file(struct sbk_file *file) 25 | { 26 | free(file); 27 | } 28 | 29 | int 30 | sbk_write_file(struct sbk_ctx *ctx, struct sbk_file *file, FILE *fp) 31 | { 32 | size_t ibuflen, len, obuflen; 33 | unsigned char mac[SBK_MAC_LEN]; 34 | 35 | if (sbk_grow_buffers(ctx, BUFSIZ) == -1) 36 | return -1; 37 | 38 | if (fseeko(ctx->fp, file->pos, SEEK_SET) == -1) { 39 | warn("Cannot seek"); 40 | return -1; 41 | } 42 | 43 | if (sbk_decrypt_init(ctx, file->counter) == -1) 44 | return -1; 45 | 46 | if (!HMAC_Update(ctx->hmac_ctx, ctx->iv, SBK_IV_LEN)) { 47 | warnx("Cannot compute MAC"); 48 | return -1; 49 | } 50 | 51 | for (len = file->len; len > 0; len -= ibuflen) { 52 | ibuflen = (len < BUFSIZ) ? len : BUFSIZ; 53 | 54 | if (sbk_read(ctx, ctx->ibuf, ibuflen) == -1) 55 | return -1; 56 | 57 | if (sbk_decrypt_update(ctx, ibuflen, &obuflen) == -1) 58 | return -1; 59 | 60 | if (fp != NULL && fwrite(ctx->obuf, obuflen, 1, fp) != 1) { 61 | warn("Cannot write file"); 62 | return -1; 63 | } 64 | } 65 | 66 | if (sbk_read(ctx, mac, sizeof mac) == -1) 67 | return -1; 68 | 69 | obuflen = 0; 70 | 71 | if (sbk_decrypt_final(ctx, &obuflen, mac) == -1) 72 | return -1; 73 | 74 | if (obuflen > 0 && fp != NULL && fwrite(ctx->obuf, obuflen, 1, fp) != 75 | 1) { 76 | warn("Cannot write file"); 77 | return -1; 78 | } 79 | 80 | return 0; 81 | } 82 | 83 | static char * 84 | sbk_decrypt_file_data(struct sbk_ctx *ctx, struct sbk_file *file, 85 | size_t *buflen, int terminate) 86 | { 87 | size_t ibuflen, len, obuflen, obufsize; 88 | unsigned char mac[SBK_MAC_LEN]; 89 | char *obuf, *ptr; 90 | 91 | if (buflen != NULL) 92 | *buflen = 0; 93 | 94 | if (sbk_grow_buffers(ctx, BUFSIZ) == -1) 95 | return NULL; 96 | 97 | if (fseeko(ctx->fp, file->pos, SEEK_SET) == -1) { 98 | warn("Cannot seek"); 99 | return NULL; 100 | } 101 | 102 | if (terminate) 103 | terminate = 1; 104 | 105 | if ((size_t)file->len > SIZE_MAX - EVP_MAX_BLOCK_LENGTH - terminate) { 106 | warnx("File too large"); 107 | return NULL; 108 | } 109 | 110 | obufsize = file->len + EVP_MAX_BLOCK_LENGTH + terminate; 111 | 112 | if ((obuf = malloc(obufsize)) == NULL) { 113 | warn(NULL); 114 | return NULL; 115 | } 116 | 117 | if (sbk_decrypt_init(ctx, file->counter) == -1) 118 | goto error; 119 | 120 | if (!HMAC_Update(ctx->hmac_ctx, ctx->iv, SBK_IV_LEN)) { 121 | warnx("Cannot compute MAC"); 122 | goto error; 123 | } 124 | 125 | ptr = obuf; 126 | 127 | for (len = file->len; len > 0; len -= ibuflen) { 128 | ibuflen = (len < BUFSIZ) ? len : BUFSIZ; 129 | 130 | if (sbk_read(ctx, ctx->ibuf, ibuflen) == -1) 131 | goto error; 132 | 133 | if (sbk_decrypt_update(ctx, ibuflen, &obuflen) == -1) 134 | goto error; 135 | 136 | memcpy(ptr, ctx->obuf, obuflen); 137 | ptr += obuflen; 138 | } 139 | 140 | if (sbk_read(ctx, mac, sizeof mac) == -1) 141 | goto error; 142 | 143 | obuflen = 0; 144 | 145 | if (sbk_decrypt_final(ctx, &obuflen, mac) == -1) 146 | goto error; 147 | 148 | if (obuflen > 0) { 149 | memcpy(ptr, ctx->obuf, obuflen); 150 | ptr += obuflen; 151 | } 152 | 153 | if (terminate) 154 | *ptr = '\0'; 155 | 156 | if (buflen != NULL) 157 | *buflen = ptr - obuf; 158 | 159 | return obuf; 160 | 161 | error: 162 | free(obuf); 163 | return NULL; 164 | } 165 | 166 | char * 167 | sbk_get_file_data(struct sbk_ctx *ctx, struct sbk_file *file, size_t *len) 168 | { 169 | return sbk_decrypt_file_data(ctx, file, len, 0); 170 | } 171 | 172 | char * 173 | sbk_get_file_data_as_string(struct sbk_ctx *ctx, struct sbk_file *file) 174 | { 175 | return sbk_decrypt_file_data(ctx, file, NULL, 1); 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sigbak 2 | ====== 3 | 4 | [sigbak][1] is a utility to read the backups created by the [Signal Android 5 | app][2]. It can be used to export messages, attachments and other data. 6 | 7 | For example, the following two commands will export all messages and 8 | attachments from the backup `signal-2022-01-23-12-34-56.backup`. Messages will 9 | be exported to the `messages` directory and attachments to the `attachments` 10 | directory: 11 | 12 | sigbak export-messages signal-2022-01-23-12-34-56.backup messages 13 | sigbak export-attachments signal-2022-01-23-12-34-56.backup attachments 14 | 15 | The complete documentation is available in the `sigbak.1` manual page. You can 16 | also [read it online][3]. 17 | 18 | Dependencies 19 | ------------ 20 | 21 | sigbak depends on libcrypto (from either [LibreSSL][4] or [OpenSSL][5]), 22 | [protobuf-c][6] and [SQLite][7]. You will also need a C compiler, `make` and 23 | `pkg-config`. 24 | 25 | Building 26 | -------- 27 | 28 | sigbak should build on most modern Unix-like systems. This section contains 29 | generic build instructions. See the sections below for build instructions for 30 | specific systems. 31 | 32 | First install all required packages (see the "Dependencies" section above). For 33 | example, on Debian or Ubuntu, run: 34 | 35 | sudo apt-get install build-essential git libprotobuf-c-dev libsqlite3-dev libssl-dev pkg-config protobuf-c-compiler 36 | 37 | After you have installed the required packages, run: 38 | 39 | git clone https://github.com/tbvdm/sigbak.git 40 | cd sigbak 41 | git checkout portable 42 | make 43 | 44 | Building on OpenBSD 45 | ------------------- 46 | 47 | To build sigbak on OpenBSD, run: 48 | 49 | doas pkg_add git protobuf-c sqlite3 50 | git clone https://github.com/tbvdm/sigbak.git 51 | cd sigbak 52 | make 53 | 54 | Building on macOS 55 | ----------------- 56 | 57 | On macOS, first install [Homebrew][8]. Then install the sigbak formula from [my 58 | Homebrew tap][9]: 59 | 60 | brew install --HEAD tbvdm/tap/sigbak 61 | 62 | To update the sigbak formula, run: 63 | 64 | brew upgrade --fetch-HEAD sigbak 65 | 66 | If you prefer to build sigbak manually, run: 67 | 68 | brew install libressl make pkgconf protobuf-c 69 | git clone https://github.com/tbvdm/sigbak.git 70 | cd sigbak 71 | git checkout portable 72 | PKG_CONFIG_PATH=$(brew --prefix libressl)/lib/pkgconfig gmake 73 | 74 | Building on Windows 75 | ------------------- 76 | 77 | On Windows, first install [Cygwin][10]. During the installation, you will be 78 | given the opportunity to install additional packages. Ensure the `curl`, 79 | `gcc-core`, `gcc-g++`, `git`, `libprotobuf-devel`, `libsqlite3-devel`, 80 | `libssl-devel`, `make` and `pkg-config` packages are installed. 81 | 82 | The [Cygwin User's Guide][11] might be useful if you need help with the 83 | installation. 84 | 85 | After the installation has completed, start the Cygwin terminal. 86 | 87 | Unfortunately, Cygwin does not provide a package for `protobuf-c`, so you will 88 | have to build it from source. In the Cygwin terminal, run: 89 | 90 | curl -LO https://github.com/protobuf-c/protobuf-c/releases/download/v1.4.1/protobuf-c-1.4.1.tar.gz 91 | tar fxz protobuf-c-1.4.1.tar.gz 92 | cd protobuf-c-1.4.1 93 | ./configure 94 | make install 95 | cd .. 96 | rm -r protobuf-c-1.4.1 97 | 98 | Now you can build sigbak. In the Cygwin terminal, run: 99 | 100 | git clone https://github.com/tbvdm/sigbak.git 101 | cd sigbak 102 | git checkout portable 103 | PKG_CONFIG_PATH=/usr/local/lib/pkgconfig make install 104 | 105 | If you prefer, you can use [this PowerShell script][12] to install Cygwin and 106 | sigbak automatically. Press Windows+R to open the Run window, paste the 107 | following command and press Enter: 108 | 109 | powershell -nop -c "iex (iwr https://github.com/tbvdm/cygwin-install-scripts/raw/master/install-cygwin-sigbak.ps1)" 110 | 111 | In the Cygwin terminal, you can access your Windows drives through the 112 | `/cygdrive` directory. For example: 113 | 114 | cd /cygdrive/c/Users/Alice/Documents 115 | sigbak export-messages signal.backup messages 116 | 117 | Reporting problems 118 | ------------------ 119 | 120 | Please report bugs and other problems with sigbak. If sigbak shows errors or 121 | warnings unexpectedly, please report them as well. You can [open an issue on 122 | GitHub][13] or [send an email][14]. 123 | 124 | [1]: https://github.com/tbvdm/sigbak 125 | [2]: https://www.signal.org/ 126 | [3]: https://www.kariliq.nl/man/sigbak.1.html 127 | [4]: https://www.libressl.org/ 128 | [5]: https://www.openssl.org/ 129 | [6]: https://github.com/protobuf-c/protobuf-c 130 | [7]: https://www.sqlite.org/ 131 | [8]: https://brew.sh/ 132 | [9]: https://github.com/tbvdm/homebrew-tap 133 | [10]: https://cygwin.com/ 134 | [11]: https://cygwin.com/cygwin-ug-net/setup-net.html#internet-setup 135 | [12]: https://github.com/tbvdm/cygwin-install-scripts/raw/master/install-cygwin-sigbak.ps1 136 | [13]: https://github.com/tbvdm/sigbak/issues 137 | [14]: https://www.kariliq.nl/contact.html 138 | -------------------------------------------------------------------------------- /sbk-edit.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include "sbk-internal.h" 21 | 22 | #define SBK_QUERY \ 23 | "SELECT " \ 24 | "_id, " \ 25 | "date_sent, " \ 26 | "date_received, " \ 27 | "body, " \ 28 | "quote_id, " \ 29 | "quote_author, " \ 30 | "quote_body, " \ 31 | "quote_mentions, " \ 32 | "revision_number " \ 33 | "FROM message " \ 34 | "WHERE _id = ?1 OR latest_revision_id = ?1 " \ 35 | "ORDER BY _id" 36 | 37 | #define SBK_COLUMN__ID 0 38 | #define SBK_COLUMN_DATE_SENT 1 39 | #define SBK_COLUMN_DATE_RECEIVED 2 40 | #define SBK_COLUMN_BODY 3 41 | #define SBK_COLUMN_QUOTE_ID 4 42 | #define SBK_COLUMN_QUOTE_AUTHOR 5 43 | #define SBK_COLUMN_QUOTE_BODY 6 44 | #define SBK_COLUMN_QUOTE_MENTIONS 7 45 | #define SBK_COLUMN_REVISION_NUMBER 8 46 | 47 | static void 48 | sbk_free_edit(struct sbk_edit *edit) 49 | { 50 | if (edit != NULL) { 51 | free(edit->text); 52 | sbk_free_attachment_list(edit->attachments); 53 | sbk_free_mention_list(edit->mentions); 54 | sbk_free_quote(edit->quote); 55 | free(edit); 56 | } 57 | } 58 | 59 | void 60 | sbk_free_edit_list(struct sbk_edit_list *lst) 61 | { 62 | struct sbk_edit *edit; 63 | 64 | if (lst != NULL) { 65 | while ((edit = TAILQ_FIRST(lst)) != NULL) { 66 | TAILQ_REMOVE(lst, edit, entries); 67 | sbk_free_edit(edit); 68 | } 69 | free(lst); 70 | } 71 | } 72 | 73 | static int 74 | sbk_get_quote_for_edit(struct sbk_ctx *ctx, struct sbk_edit *edit, 75 | sqlite3_stmt *stm) 76 | { 77 | return sbk_get_quote(ctx, &edit->quote, stm, SBK_COLUMN_QUOTE_ID, 78 | SBK_COLUMN_QUOTE_AUTHOR, SBK_COLUMN_QUOTE_BODY, 79 | SBK_COLUMN_QUOTE_MENTIONS, &edit->id); 80 | } 81 | 82 | static struct sbk_edit * 83 | sbk_get_edit(struct sbk_ctx *ctx, sqlite3_stmt *stm) 84 | { 85 | struct sbk_edit *edit; 86 | 87 | if ((edit = calloc(1, sizeof *edit)) == NULL) { 88 | warn(NULL); 89 | return NULL; 90 | } 91 | 92 | if (sbk_sqlite_column_text_copy(ctx, &edit->text, stm, SBK_COLUMN_BODY) 93 | == -1) 94 | goto error; 95 | 96 | edit->id.table = SBK_SINGLE_TABLE; 97 | edit->id.row_id = sqlite3_column_int(stm, SBK_COLUMN__ID); 98 | edit->revision = sqlite3_column_int(stm, SBK_COLUMN_REVISION_NUMBER); 99 | edit->time_sent = sqlite3_column_int64(stm, SBK_COLUMN_DATE_SENT); 100 | edit->time_recv = sqlite3_column_int64(stm, SBK_COLUMN_DATE_RECEIVED); 101 | 102 | if (sbk_get_attachments_for_edit(ctx, edit) == -1) 103 | goto error; 104 | 105 | if (sbk_get_long_message(ctx, &edit->text, &edit->attachments) == -1) 106 | goto error; 107 | 108 | if (sbk_get_mentions_for_edit(ctx, edit) == -1) 109 | goto error; 110 | 111 | if (sbk_insert_mentions(&edit->text, edit->mentions) == -1) { 112 | warnx("Cannot insert mentions in edit"); 113 | goto error; 114 | } 115 | 116 | if (sbk_get_quote_for_edit(ctx, edit, stm) == -1) { 117 | warnx("Cannot get quote for edit"); 118 | goto error; 119 | } 120 | 121 | return edit; 122 | 123 | error: 124 | sbk_free_edit(edit); 125 | return NULL; 126 | } 127 | 128 | int 129 | sbk_get_edits(struct sbk_ctx *ctx, struct sbk_message *msg) 130 | { 131 | struct sbk_edit *edit; 132 | sqlite3_stmt *stm; 133 | int ret; 134 | 135 | if (sbk_sqlite_prepare(ctx, &stm, SBK_QUERY) == -1) 136 | return -1; 137 | 138 | if (sbk_sqlite_bind_int(ctx, stm, 1, msg->id.row_id) == -1) 139 | goto error; 140 | 141 | if ((msg->edits = malloc(sizeof *msg->edits)) == NULL) { 142 | warn(NULL); 143 | goto error; 144 | } 145 | 146 | TAILQ_INIT(msg->edits); 147 | 148 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 149 | if ((edit = sbk_get_edit(ctx, stm)) == NULL) 150 | goto error; 151 | TAILQ_INSERT_TAIL(msg->edits, edit, entries); 152 | msg->nedits++; 153 | } 154 | 155 | if (ret != SQLITE_DONE) 156 | goto error; 157 | 158 | /* Set times from the original message */ 159 | if ((edit = TAILQ_FIRST(msg->edits)) != NULL) { 160 | msg->time_sent = edit->time_sent; 161 | msg->time_recv = edit->time_recv; 162 | } 163 | 164 | sqlite3_finalize(stm); 165 | return 0; 166 | 167 | error: 168 | sbk_free_edit_list(msg->edits); 169 | msg->edits = NULL; 170 | sqlite3_finalize(stm); 171 | return -1; 172 | } 173 | -------------------------------------------------------------------------------- /sbk-database.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sbk-internal.h" 23 | 24 | static int 25 | sbk_bind_param(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, 26 | Signal__SqlStatement__SqlParameter *par) 27 | { 28 | if (par->stringparamter != NULL) 29 | return sbk_sqlite_bind_text(ctx, stm, idx, 30 | par->stringparamter); 31 | 32 | if (par->has_integerparameter) 33 | return sbk_sqlite_bind_int64(ctx, stm, idx, 34 | par->integerparameter); 35 | 36 | if (par->has_doubleparameter) 37 | return sbk_sqlite_bind_double(ctx, stm, idx, 38 | par->doubleparameter); 39 | 40 | if (par->has_blobparameter) 41 | return sbk_sqlite_bind_blob(ctx, stm, idx, 42 | par->blobparameter.data, par->blobparameter.len); 43 | 44 | if (par->has_nullparameter) 45 | return sbk_sqlite_bind_null(ctx, stm, idx); 46 | 47 | warnx("Unknown SQL parameter type"); 48 | return -1; 49 | } 50 | 51 | static int 52 | sbk_exec_statement(struct sbk_ctx *ctx, Signal__SqlStatement *sql) 53 | { 54 | sqlite3_stmt *stm; 55 | size_t i; 56 | 57 | if (sql->statement == NULL) { 58 | warnx("Invalid SQL frame"); 59 | return -1; 60 | } 61 | 62 | /* Don't try to create tables with reserved names */ 63 | if (strncasecmp(sql->statement, "create table sqlite_", 20) == 0) 64 | return 0; 65 | 66 | if (sbk_sqlite_prepare(ctx, &stm, sql->statement) == -1) 67 | return -1; 68 | 69 | for (i = 0; i < sql->n_parameters; i++) 70 | if (sbk_bind_param(ctx, stm, i + 1, sql->parameters[i]) == -1) 71 | goto error; 72 | 73 | if (sbk_sqlite_step(ctx, stm) != SQLITE_DONE) 74 | goto error; 75 | 76 | sqlite3_finalize(stm); 77 | return 0; 78 | 79 | error: 80 | sqlite3_finalize(stm); 81 | return -1; 82 | } 83 | 84 | static int 85 | sbk_set_database_version(struct sbk_ctx *ctx, Signal__DatabaseVersion *ver) 86 | { 87 | char *sql; 88 | int ret; 89 | 90 | if (!ver->has_version) { 91 | warnx("Invalid version frame"); 92 | return -1; 93 | } 94 | 95 | ctx->db_version = ver->version; 96 | 97 | if (asprintf(&sql, "PRAGMA user_version = %" PRIu32, ver->version) == 98 | -1) { 99 | warnx("asprintf() failed"); 100 | return -1; 101 | } 102 | 103 | ret = sbk_sqlite_exec(ctx, sql); 104 | free(sql); 105 | return ret; 106 | } 107 | 108 | int 109 | sbk_create_database(struct sbk_ctx *ctx) 110 | { 111 | Signal__BackupFrame *frm; 112 | struct sbk_file *file; 113 | int ret; 114 | 115 | if (ctx->db != NULL) 116 | return 0; 117 | 118 | if (sbk_sqlite_open(&ctx->db, ":memory:") == -1) 119 | goto error; 120 | 121 | if (sbk_rewind(ctx) == -1) 122 | goto error; 123 | 124 | if (sbk_sqlite_exec(ctx, "BEGIN TRANSACTION") == -1) 125 | goto error; 126 | 127 | ret = 0; 128 | 129 | while ((frm = sbk_get_frame(ctx, &file)) != NULL) { 130 | if (frm->version != NULL) 131 | ret = sbk_set_database_version(ctx, frm->version); 132 | else if (frm->statement != NULL) 133 | ret = sbk_exec_statement(ctx, frm->statement); 134 | else if (frm->attachment != NULL) 135 | ret = sbk_insert_attachment_entry(ctx, frm, file); 136 | else 137 | sbk_free_file(file); 138 | 139 | sbk_free_frame(frm); 140 | 141 | if (ret == -1) 142 | goto error; 143 | } 144 | 145 | if (sbk_sqlite_exec(ctx, "END TRANSACTION") == -1) 146 | goto error; 147 | 148 | if (ctx->state != SBK_LAST_FRAME) 149 | goto error; 150 | 151 | return 0; 152 | 153 | error: 154 | sbk_free_attachment_tree(ctx); 155 | sqlite3_close(ctx->db); 156 | ctx->db = NULL; 157 | return -1; 158 | } 159 | 160 | int 161 | sbk_write_database(struct sbk_ctx *ctx, const char *path) 162 | { 163 | sqlite3 *db; 164 | sqlite3_backup *bak; 165 | int ret; 166 | 167 | if (sbk_create_database(ctx) == -1) 168 | return -1; 169 | 170 | if (sbk_sqlite_open(&db, path) == -1) 171 | goto error; 172 | 173 | if ((bak = sqlite3_backup_init(db, "main", ctx->db, "main")) == NULL) { 174 | sbk_sqlite_warnd(db, "Cannot write database"); 175 | goto error; 176 | } 177 | 178 | if ((ret = sqlite3_backup_step(bak, -1)) != SQLITE_DONE) { 179 | warnx("Cannot write database: %s", sqlite3_errstr(ret)); 180 | sqlite3_backup_finish(bak); 181 | goto error; 182 | } 183 | 184 | sqlite3_backup_finish(bak); 185 | 186 | if (sqlite3_close(db) != SQLITE_OK) { 187 | sbk_sqlite_warnd(db, "Cannot close database"); 188 | return -1; 189 | } 190 | 191 | return 0; 192 | 193 | error: 194 | sqlite3_close(db); 195 | return -1; 196 | } 197 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | Every file in the sigbak repository is either 4 | 5 | - licensed under the ISC license, 6 | - licensed under a BSD license, or 7 | - licensed under the AGPLv3 license. 8 | 9 | # Details 10 | 11 | Except for the files specified below, every file in the sigbak repository is 12 | licensed under the ISC license, as follows: 13 | 14 | ``` 15 | Copyright (c) 2018 Tim van der Molen 16 | 17 | Permission to use, copy, modify, and distribute this software for any 18 | purpose with or without fee is hereby granted, provided that the above 19 | copyright notice and this permission notice appear in all copies. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 22 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 23 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 24 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 25 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 26 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 27 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 28 | ``` 29 | 30 | The files `compat/bs_cbb.c`, `compat/bytestring.h`, `compat/hkdf.c` and 31 | `compat/hkdf.h` are licensed under the ISC license as well, but with the 32 | following copyright notice: 33 | 34 | ``` 35 | Copyright (c) 2014, Google Inc. 36 | ``` 37 | 38 | The file `compat/readpassphrase.c` is licensed under the ISC license as well, 39 | but with the following copyright notice: 40 | 41 | ``` 42 | Copyright (c) 2000-2002, 2007, 2010 43 | Todd C. Miller 44 | ``` 45 | 46 | The file `compat/readpassphrase.h` is licensed under the ISC license as well, 47 | but with the following copyright notice: 48 | 49 | ``` 50 | Copyright (c) 2000, 2002 Todd C. Miller 51 | ``` 52 | 53 | The file `compat/reallocarray.c` is licensed under the ISC license as well, but 54 | with the following copyright notice: 55 | 56 | ``` 57 | Copyright (c) 2008 Otto Moerbeek 58 | ``` 59 | 60 | The file `compat/tree.h` is licensed under the 2-clause BSD license, as 61 | follows: 62 | 63 | ``` 64 | Copyright 2002 Niels Provos 65 | All rights reserved. 66 | 67 | Redistribution and use in source and binary forms, with or without 68 | modification, are permitted provided that the following conditions 69 | are met: 70 | 1. Redistributions of source code must retain the above copyright 71 | notice, this list of conditions and the following disclaimer. 72 | 2. Redistributions in binary form must reproduce the above copyright 73 | notice, this list of conditions and the following disclaimer in the 74 | documentation and/or other materials provided with the distribution. 75 | 76 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 77 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 78 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 79 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 80 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 81 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 82 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 83 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 84 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 85 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 86 | ``` 87 | 88 | The file `compat/queue.h` is licensed under the 3-clause BSD license, as 89 | follows: 90 | 91 | ``` 92 | Copyright (c) 1991, 1993 93 | The Regents of the University of California. All rights reserved. 94 | 95 | Redistribution and use in source and binary forms, with or without 96 | modification, are permitted provided that the following conditions 97 | are met: 98 | 1. Redistributions of source code must retain the above copyright 99 | notice, this list of conditions and the following disclaimer. 100 | 2. Redistributions in binary form must reproduce the above copyright 101 | notice, this list of conditions and the following disclaimer in the 102 | documentation and/or other materials provided with the distribution. 103 | 3. Neither the name of the University nor the names of its contributors 104 | may be used to endorse or promote products derived from this software 105 | without specific prior written permission. 106 | 107 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 108 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 109 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 110 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 111 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 112 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 113 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 114 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 115 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 116 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 117 | SUCH DAMAGE. 118 | ``` 119 | 120 | The files `backup.proto` and `database.proto` are licensed under the 121 | [AGPLv3](https://github.com/signalapp/Signal-Android/blob/ace47c61b1aa4c32b8468343615e3e7288915dea/LICENSE). 122 | -------------------------------------------------------------------------------- /sigbak.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "sigbak.h" 28 | 29 | extern const struct cmd_entry cmd_check_backup_entry; 30 | extern const struct cmd_entry cmd_dump_backup_entry; 31 | extern const struct cmd_entry cmd_export_attachments_entry; 32 | extern const struct cmd_entry cmd_export_avatars_entry; 33 | extern const struct cmd_entry cmd_export_database_entry; 34 | extern const struct cmd_entry cmd_export_messages_entry; 35 | extern const struct cmd_entry cmd_export_stickers_entry; 36 | 37 | static const struct cmd_entry *commands[] = { 38 | &cmd_check_backup_entry, 39 | &cmd_dump_backup_entry, 40 | &cmd_export_attachments_entry, 41 | &cmd_export_avatars_entry, 42 | &cmd_export_database_entry, 43 | &cmd_export_messages_entry, 44 | &cmd_export_stickers_entry 45 | }; 46 | 47 | __dead static void 48 | usage(const char *cmd, const char *args) 49 | { 50 | fprintf(stderr, "usage: %s %s %s\n", getprogname(), cmd, args); 51 | exit(1); 52 | } 53 | 54 | int 55 | get_passphrase(const char *passfile, char *buf, size_t bufsize) 56 | { 57 | int fd; 58 | ssize_t len; 59 | char *c, *d; 60 | 61 | if (bufsize == 0) 62 | return -1; 63 | 64 | if (passfile == NULL) { 65 | if (readpassphrase("Enter 30-digit passphrase (whitespace is " 66 | "ignored): ", buf, bufsize, 0) == NULL) { 67 | warnx("Cannot read passphrase"); 68 | explicit_bzero(buf, bufsize); 69 | return -1; 70 | } 71 | } else { 72 | if (strcmp(passfile, "-") == 0) 73 | fd = STDIN_FILENO; 74 | else if ((fd = open(passfile, O_RDONLY)) == -1) { 75 | warn("%s", passfile); 76 | return -1; 77 | } 78 | 79 | if ((len = read(fd, buf, bufsize - 1)) == -1) { 80 | warn("%s", passfile); 81 | explicit_bzero(buf, bufsize); 82 | close(fd); 83 | return -1; 84 | } 85 | 86 | if ((c = memchr(buf, '\n', len)) != NULL) 87 | len = c - buf; 88 | 89 | buf[len] = '\0'; 90 | 91 | if (fd != STDIN_FILENO) 92 | close(fd); 93 | } 94 | 95 | /* Remove whitespace */ 96 | for (c = d = buf; *c != '\0'; c++) 97 | if (!isspace((unsigned char)*c)) 98 | *d++ = *c; 99 | *d = '\0'; 100 | 101 | return 0; 102 | } 103 | 104 | int 105 | unveil_dirname(const char *path, const char *perms) 106 | { 107 | char *dir, *tmp; 108 | 109 | if ((tmp = strdup(path)) == NULL) { 110 | warn(NULL); 111 | return -1; 112 | } 113 | 114 | if ((dir = dirname(tmp)) == NULL) { 115 | warnx("dirname() failed"); 116 | free(tmp); 117 | return -1; 118 | } 119 | 120 | if (unveil(dir, perms) == -1) { 121 | warn("unveil: %s", dir); 122 | free(tmp); 123 | return -1; 124 | } 125 | 126 | free(tmp); 127 | return 0; 128 | } 129 | 130 | void 131 | sanitise_filename(char *name) 132 | { 133 | char *c; 134 | 135 | if (strcmp(name, ".") == 0) { 136 | name[0] = '_'; 137 | return; 138 | } 139 | 140 | if (strcmp(name, "..") == 0) { 141 | name[0] = name[1] = '_'; 142 | return; 143 | } 144 | 145 | for (c = name; *c != '\0'; c++) 146 | if (*c == '/' || iscntrl((unsigned char)*c)) 147 | *c = '_'; 148 | } 149 | 150 | char * 151 | get_recipient_filename(struct sbk_recipient *rcp, const char *ext) 152 | { 153 | char *fname; 154 | const char *detail, *name; 155 | int ret; 156 | 157 | name = sbk_get_recipient_display_name(rcp); 158 | 159 | if (rcp->type == SBK_GROUP) 160 | detail = "group"; 161 | else 162 | detail = rcp->contact->phone; 163 | 164 | if (ext == NULL) 165 | ext = ""; 166 | 167 | if (detail != NULL) 168 | ret = asprintf(&fname, "%s (%s)%s", name, detail, ext); 169 | else 170 | ret = asprintf(&fname, "%s%s", name, ext); 171 | 172 | if (ret == -1) { 173 | warnx("asprintf() failed"); 174 | return NULL; 175 | } 176 | 177 | sanitise_filename(fname); 178 | return fname; 179 | } 180 | 181 | int 182 | main(int argc, char **argv) 183 | { 184 | const struct cmd_entry *cmd; 185 | size_t i; 186 | 187 | if (argc < 2) 188 | usage("command", "[argument ...]"); 189 | 190 | argc--; 191 | argv++; 192 | cmd = NULL; 193 | 194 | for (i = 0; i < nitems(commands); i++) 195 | if (strcmp(argv[0], commands[i]->name) == 0 || 196 | strcmp(argv[0], commands[i]->alias) == 0) { 197 | cmd = commands[i]; 198 | break; 199 | } 200 | 201 | if (cmd == NULL) 202 | errx(1, "%s: Invalid command", argv[0]); 203 | 204 | switch (cmd->exec(argc, argv)) { 205 | case CMD_OK: 206 | return 0; 207 | case CMD_ERROR: 208 | return 1; 209 | case CMD_USAGE: 210 | usage(cmd->name, cmd->usage); 211 | return 1; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /sbk-sqlite.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sbk-internal.h" 23 | 24 | int 25 | sbk_sqlite_open(sqlite3 **db, const char *path) 26 | { 27 | if (sqlite3_open(path, db) != SQLITE_OK) { 28 | sbk_sqlite_warnd(*db, "Cannot open database"); 29 | return -1; 30 | } 31 | 32 | return 0; 33 | } 34 | 35 | int 36 | sbk_sqlite_exec(struct sbk_ctx *ctx, const char *sql) 37 | { 38 | char *errmsg; 39 | 40 | if (sqlite3_exec(ctx->db, sql, NULL, NULL, &errmsg) != SQLITE_OK) { 41 | warnx("Cannot execute SQL statement: %s", errmsg); 42 | sqlite3_free(errmsg); 43 | return -1; 44 | } 45 | 46 | return 0; 47 | } 48 | 49 | int 50 | sbk_sqlite_prepare(struct sbk_ctx *ctx, sqlite3_stmt **stm, const char *query) 51 | { 52 | if (sqlite3_prepare_v2(ctx->db, query, -1, stm, NULL) != SQLITE_OK) { 53 | sbk_sqlite_warn(ctx, "Cannot prepare SQL statement"); 54 | return -1; 55 | } 56 | 57 | return 0; 58 | } 59 | 60 | int 61 | sbk_sqlite_step(struct sbk_ctx *ctx, sqlite3_stmt *stm) 62 | { 63 | int ret; 64 | 65 | ret = sqlite3_step(stm); 66 | if (ret != SQLITE_ROW && ret != SQLITE_DONE) 67 | sbk_sqlite_warn(ctx, "Cannot execute SQL statement"); 68 | 69 | return ret; 70 | } 71 | 72 | int 73 | sbk_sqlite_bind_null(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx) 74 | { 75 | if (sqlite3_bind_null(stm, idx) != SQLITE_OK) { 76 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 77 | return -1; 78 | } 79 | 80 | return 0; 81 | } 82 | 83 | int 84 | sbk_sqlite_bind_int(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, int val) 85 | { 86 | if (sqlite3_bind_int(stm, idx, val) != SQLITE_OK) { 87 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 88 | return -1; 89 | } 90 | 91 | return 0; 92 | } 93 | 94 | int 95 | sbk_sqlite_bind_int64(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, 96 | sqlite3_int64 val) 97 | { 98 | if (sqlite3_bind_int64(stm, idx, val) != SQLITE_OK) { 99 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 100 | return -1; 101 | } 102 | 103 | return 0; 104 | } 105 | 106 | int 107 | sbk_sqlite_bind_double(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, 108 | double val) 109 | { 110 | if (sqlite3_bind_double(stm, idx, val) != SQLITE_OK) { 111 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 112 | return -1; 113 | } 114 | 115 | return 0; 116 | } 117 | 118 | int 119 | sbk_sqlite_bind_blob(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, 120 | const void *val, size_t len) 121 | { 122 | if (sqlite3_bind_blob(stm, idx, val, len, SQLITE_STATIC) != 123 | SQLITE_OK) { 124 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 125 | return -1; 126 | } 127 | 128 | return 0; 129 | } 130 | 131 | int 132 | sbk_sqlite_bind_text(struct sbk_ctx *ctx, sqlite3_stmt *stm, int idx, 133 | const char *val) 134 | { 135 | if (sqlite3_bind_text(stm, idx, val, -1, SQLITE_STATIC) != SQLITE_OK) { 136 | sbk_sqlite_warn(ctx, "Cannot bind SQL parameter"); 137 | return -1; 138 | } 139 | 140 | return 0; 141 | } 142 | 143 | int 144 | sbk_sqlite_column_text_copy(struct sbk_ctx *ctx, char **buf, sqlite3_stmt *stm, 145 | int idx) 146 | { 147 | #ifdef notyet 148 | const unsigned char *txt; 149 | int len; 150 | 151 | *buf = NULL; 152 | 153 | if (sqlite3_column_type(stm, idx) == SQLITE_NULL) 154 | return 0; 155 | 156 | if ((txt = sqlite3_column_text(stm, idx)) == NULL) { 157 | sbk_sqlite_warn(ctx, "Cannot get column text"); 158 | return -1; 159 | } 160 | 161 | if ((len = sqlite3_column_bytes(stm, idx)) < 0) { 162 | sbk_sqlite_warn(ctx, "Cannot get column size"); 163 | return -1; 164 | } 165 | 166 | if ((*buf = malloc((size_t)len + 1)) == NULL) { 167 | warn(NULL); 168 | return -1; 169 | } 170 | 171 | memcpy(*buf, txt, (size_t)len + 1); 172 | return len; 173 | #else 174 | const unsigned char *txt; 175 | 176 | *buf = NULL; 177 | 178 | if (sqlite3_column_type(stm, idx) == SQLITE_NULL) 179 | return 0; 180 | 181 | if ((txt = sqlite3_column_text(stm, idx)) == NULL) { 182 | sbk_sqlite_warn(ctx, "Cannot get column text"); 183 | return -1; 184 | } 185 | 186 | if ((*buf = strdup((const char *)txt)) == NULL) { 187 | warn(NULL); 188 | return -1; 189 | } 190 | 191 | return 0; 192 | #endif 193 | } 194 | 195 | static void 196 | sbk_sqlite_vwarnd(sqlite3 *db, const char *fmt, va_list ap) 197 | { 198 | char *msg; 199 | 200 | if (fmt == NULL) 201 | warnx("%s", sqlite3_errmsg(db)); 202 | else { 203 | if (vasprintf(&msg, fmt, ap) == -1) { 204 | warnx("vasprintf() failed"); 205 | return; 206 | } 207 | warnx("%s: %s", msg, sqlite3_errmsg(db)); 208 | free(msg); 209 | } 210 | } 211 | 212 | void 213 | sbk_sqlite_warnd(sqlite3 *db, const char *fmt, ...) 214 | { 215 | va_list ap; 216 | 217 | va_start(ap, fmt); 218 | sbk_sqlite_vwarnd(db, fmt, ap); 219 | va_end(ap); 220 | } 221 | 222 | void 223 | sbk_sqlite_warn(struct sbk_ctx *ctx, const char *fmt, ...) 224 | { 225 | va_list ap; 226 | 227 | va_start(ap, fmt); 228 | sbk_sqlite_vwarnd(ctx->db, fmt, ap); 229 | va_end(ap); 230 | } 231 | -------------------------------------------------------------------------------- /sbk-open.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | #include "sbk-internal.h" 25 | 26 | static int 27 | sbk_compute_keys(struct sbk_ctx *ctx, const char *passphr, 28 | const unsigned char *salt, size_t saltlen) 29 | { 30 | EVP_MD_CTX *md_ctx; 31 | unsigned char key[SHA512_DIGEST_LENGTH]; 32 | unsigned char deriv_key[SBK_DERIV_KEY_LEN]; 33 | size_t passphrlen; 34 | int i, ret; 35 | 36 | if ((md_ctx = EVP_MD_CTX_new()) == NULL) 37 | goto error; 38 | 39 | passphrlen = strlen(passphr); 40 | 41 | /* The first round */ 42 | if (!EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL)) 43 | goto error; 44 | if (salt != NULL) 45 | if (!EVP_DigestUpdate(md_ctx, salt, saltlen)) 46 | goto error; 47 | if (!EVP_DigestUpdate(md_ctx, passphr, passphrlen)) 48 | goto error; 49 | if (!EVP_DigestUpdate(md_ctx, passphr, passphrlen)) 50 | goto error; 51 | if (!EVP_DigestFinal_ex(md_ctx, key, NULL)) 52 | goto error; 53 | 54 | /* The remaining rounds */ 55 | for (i = 0; i < SBK_ROUNDS - 1; i++) { 56 | if (!EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL)) 57 | goto error; 58 | if (!EVP_DigestUpdate(md_ctx, key, sizeof key)) 59 | goto error; 60 | if (!EVP_DigestUpdate(md_ctx, passphr, passphrlen)) 61 | goto error; 62 | if (!EVP_DigestFinal(md_ctx, key, NULL)) 63 | goto error; 64 | } 65 | 66 | if (!HKDF(deriv_key, sizeof deriv_key, EVP_sha256(), key, SBK_KEY_LEN, 67 | (const unsigned char *)"", 0, (const unsigned char *)SBK_HKDF_INFO, 68 | strlen(SBK_HKDF_INFO))) 69 | goto error; 70 | 71 | memcpy(ctx->cipher_key, deriv_key, SBK_CIPHER_KEY_LEN); 72 | memcpy(ctx->mac_key, deriv_key + SBK_CIPHER_KEY_LEN, SBK_MAC_KEY_LEN); 73 | 74 | ret = 0; 75 | goto out; 76 | 77 | error: 78 | warnx("Cannot compute keys"); 79 | ret = -1; 80 | 81 | out: 82 | explicit_bzero(key, sizeof key); 83 | explicit_bzero(deriv_key, sizeof deriv_key); 84 | if (md_ctx != NULL) 85 | EVP_MD_CTX_free(md_ctx); 86 | return ret; 87 | } 88 | 89 | struct sbk_ctx * 90 | sbk_ctx_new(void) 91 | { 92 | struct sbk_ctx *ctx; 93 | 94 | if ((ctx = calloc(1, sizeof *ctx)) == NULL) { 95 | warn(NULL); 96 | return NULL; 97 | } 98 | 99 | if ((ctx->cipher_ctx = EVP_CIPHER_CTX_new()) == NULL) { 100 | warnx("Cannot create cipher context"); 101 | goto error; 102 | } 103 | 104 | if ((ctx->hmac_ctx = HMAC_CTX_new()) == NULL) { 105 | warnx("Cannot create MAC context"); 106 | goto error; 107 | } 108 | 109 | if (sbk_grow_buffers(ctx, 8192) == -1) 110 | goto error; 111 | 112 | return ctx; 113 | 114 | error: 115 | sbk_ctx_free(ctx); 116 | return NULL; 117 | } 118 | 119 | void 120 | sbk_ctx_free(struct sbk_ctx *ctx) 121 | { 122 | if (ctx != NULL) { 123 | EVP_CIPHER_CTX_free(ctx->cipher_ctx); 124 | HMAC_CTX_free(ctx->hmac_ctx); 125 | free(ctx->ibuf); 126 | free(ctx->obuf); 127 | free(ctx); 128 | } 129 | } 130 | 131 | int 132 | sbk_open(struct sbk_ctx *ctx, const char *path, const char *passphr) 133 | { 134 | Signal__BackupFrame *frm; 135 | uint8_t *salt; 136 | size_t saltlen; 137 | 138 | if ((ctx->fp = fopen(path, "rb")) == NULL) { 139 | warn("%s", path); 140 | return -1; 141 | } 142 | 143 | ctx->backup_version = 0; 144 | ctx->state = SBK_FIRST_FRAME; 145 | 146 | if ((frm = sbk_get_first_frame(ctx)) == NULL) 147 | goto error; 148 | 149 | if (frm->header == NULL) { 150 | warnx("Missing header frame"); 151 | goto error; 152 | } 153 | 154 | if (frm->header->has_version) { 155 | ctx->backup_version = frm->header->version; 156 | if (ctx->backup_version > 1) { 157 | warnx("Backup version %u not yet supported", 158 | ctx->backup_version); 159 | goto error; 160 | } 161 | } 162 | 163 | if (!frm->header->has_iv) { 164 | warnx("Missing IV"); 165 | goto error; 166 | } 167 | 168 | if (frm->header->iv.len != SBK_IV_LEN) { 169 | warnx("Invalid IV size"); 170 | goto error; 171 | } 172 | 173 | memcpy(ctx->iv, frm->header->iv.data, SBK_IV_LEN); 174 | ctx->counter = ctx->counter_start = 175 | ((uint32_t)ctx->iv[0] << 24) | ((uint32_t)ctx->iv[1] << 16) | 176 | ((uint32_t)ctx->iv[2] << 8) | ctx->iv[3]; 177 | 178 | if (frm->header->has_salt) { 179 | salt = frm->header->salt.data; 180 | saltlen = frm->header->salt.len; 181 | } else { 182 | salt = NULL; 183 | saltlen = 0; 184 | } 185 | 186 | if (sbk_compute_keys(ctx, passphr, salt, saltlen) == -1) 187 | goto error; 188 | 189 | if (!EVP_DecryptInit_ex(ctx->cipher_ctx, EVP_aes_256_ctr(), NULL, NULL, 190 | NULL)) { 191 | warnx("Cannot initialise cipher context"); 192 | goto error; 193 | } 194 | 195 | if (!HMAC_Init_ex(ctx->hmac_ctx, ctx->mac_key, SBK_MAC_KEY_LEN, 196 | EVP_sha256(), NULL)) { 197 | warnx("Cannot initialise MAC context"); 198 | goto error; 199 | } 200 | 201 | if (sbk_rewind(ctx) == -1) 202 | goto error; 203 | 204 | sbk_free_frame(frm); 205 | ctx->db = NULL; 206 | ctx->db_version = 0; 207 | RB_INIT(&ctx->attachments); 208 | RB_INIT(&ctx->recipients); 209 | return 0; 210 | 211 | error: 212 | explicit_bzero(ctx->cipher_key, SBK_CIPHER_KEY_LEN); 213 | explicit_bzero(ctx->mac_key, SBK_MAC_KEY_LEN); 214 | sbk_free_frame(frm); 215 | fclose(ctx->fp); 216 | return -1; 217 | } 218 | 219 | void 220 | sbk_close(struct sbk_ctx *ctx) 221 | { 222 | sbk_free_recipient_tree(ctx); 223 | sbk_free_attachment_tree(ctx); 224 | explicit_bzero(ctx->cipher_key, SBK_CIPHER_KEY_LEN); 225 | explicit_bzero(ctx->mac_key, SBK_MAC_KEY_LEN); 226 | sqlite3_close(ctx->db); 227 | fclose(ctx->fp); 228 | } 229 | -------------------------------------------------------------------------------- /sbk-frame.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sbk-internal.h" 23 | 24 | void 25 | sbk_free_frame(Signal__BackupFrame *frm) 26 | { 27 | if (frm != NULL) 28 | signal__backup_frame__free_unpacked(frm, NULL); 29 | } 30 | 31 | static int 32 | sbk_has_file_data(Signal__BackupFrame *frm) 33 | { 34 | return frm->attachment != NULL || frm->avatar != NULL || 35 | frm->sticker != NULL; 36 | } 37 | 38 | static int 39 | sbk_skip_file_data(struct sbk_ctx *ctx, Signal__BackupFrame *frm) 40 | { 41 | uint32_t len; 42 | 43 | if (frm->attachment != NULL && frm->attachment->has_length) 44 | len = frm->attachment->length; 45 | else if (frm->avatar != NULL && frm->avatar->has_length) 46 | len = frm->avatar->length; 47 | else if (frm->sticker != NULL && frm->sticker->has_length) 48 | len = frm->sticker->length; 49 | else { 50 | warnx("Invalid frame"); 51 | return -1; 52 | } 53 | 54 | if (fseeko(ctx->fp, len + SBK_MAC_LEN, SEEK_CUR) == -1) { 55 | warn("Cannot seek"); 56 | return -1; 57 | } 58 | 59 | ctx->counter++; 60 | return 0; 61 | } 62 | 63 | static struct sbk_file * 64 | sbk_get_file(struct sbk_ctx *ctx, Signal__BackupFrame *frm) 65 | { 66 | struct sbk_file *file; 67 | 68 | if ((file = malloc(sizeof *file)) == NULL) { 69 | warn(NULL); 70 | return NULL; 71 | } 72 | 73 | if ((file->pos = ftello(ctx->fp)) == -1) { 74 | warn("Cannot get file position"); 75 | goto error; 76 | } 77 | 78 | if (frm->attachment != NULL) { 79 | if (!frm->attachment->has_length) { 80 | warnx("Invalid attachment frame"); 81 | goto error; 82 | } 83 | file->len = frm->attachment->length; 84 | } else if (frm->avatar != NULL) { 85 | if (!frm->avatar->has_length) { 86 | warnx("Invalid avatar frame"); 87 | goto error; 88 | } 89 | file->len = frm->avatar->length; 90 | } else if (frm->sticker != NULL) { 91 | if (!frm->sticker->has_length) { 92 | warnx("Invalid sticker frame"); 93 | goto error; 94 | } 95 | file->len = frm->sticker->length; 96 | } 97 | 98 | file->counter = ctx->counter; 99 | return file; 100 | 101 | error: 102 | sbk_free_file(file); 103 | return NULL; 104 | } 105 | 106 | static Signal__BackupFrame * 107 | sbk_unpack_frame(unsigned char *buf, size_t len) 108 | { 109 | Signal__BackupFrame *frm; 110 | 111 | if ((frm = signal__backup_frame__unpack(NULL, len, buf)) == NULL) 112 | warnx("Cannot unpack frame"); 113 | 114 | return frm; 115 | } 116 | 117 | Signal__BackupFrame * 118 | sbk_get_first_frame(struct sbk_ctx *ctx) 119 | { 120 | Signal__BackupFrame *frm; 121 | uint32_t len; 122 | 123 | if (sbk_read(ctx, &len, sizeof len) == -1) 124 | return NULL; 125 | 126 | len = be32toh(len); 127 | 128 | if (len == 0) { 129 | warnx("Invalid frame length"); 130 | return NULL; 131 | } 132 | 133 | if (sbk_grow_buffers(ctx, len) == -1) 134 | return NULL; 135 | 136 | if (sbk_read(ctx, ctx->ibuf, len) == -1) 137 | return NULL; 138 | 139 | if ((frm = sbk_unpack_frame(ctx->ibuf, len)) == NULL) 140 | return NULL; 141 | 142 | return frm; 143 | } 144 | 145 | static Signal__BackupFrame * 146 | sbk_get_encrypted_frame(struct sbk_ctx *ctx, struct sbk_file **file) 147 | { 148 | Signal__BackupFrame *frm; 149 | unsigned char *mac; 150 | uint32_t elen; /* Length of encrypted frame */ 151 | size_t dlen; /* Length of decrypted data */ 152 | 153 | if (sbk_decrypt_init(ctx, ctx->counter++) == -1) 154 | return NULL; 155 | 156 | /* 157 | * Get the length of the encrypted frame. In newer backups, the frame 158 | * length itself is encrypted, too. 159 | */ 160 | if (ctx->backup_version == 0) { 161 | if (sbk_read(ctx, &elen, sizeof elen) == -1) 162 | return NULL; 163 | } else { 164 | if (sbk_read(ctx, ctx->ibuf, sizeof elen) == -1) 165 | return NULL; 166 | if (sbk_decrypt_update(ctx, sizeof elen, &dlen) == -1) 167 | return NULL; 168 | if (dlen != sizeof elen) { 169 | warnx("Cannot read frame length"); 170 | return NULL; 171 | } 172 | memcpy(&elen, ctx->obuf, sizeof elen); 173 | } 174 | 175 | elen = be32toh(elen); 176 | 177 | if (elen <= SBK_MAC_LEN) { 178 | warnx("Invalid frame length"); 179 | return NULL; 180 | } 181 | 182 | if (sbk_grow_buffers(ctx, elen) == -1) 183 | return NULL; 184 | 185 | if (sbk_read(ctx, ctx->ibuf, elen) == -1) 186 | return NULL; 187 | 188 | elen -= SBK_MAC_LEN; 189 | mac = ctx->ibuf + elen; 190 | 191 | if (sbk_decrypt_update(ctx, elen, &dlen) == -1) 192 | return NULL; 193 | 194 | if (sbk_decrypt_final(ctx, &dlen, mac) == -1) 195 | return NULL; 196 | 197 | if ((frm = sbk_unpack_frame(ctx->obuf, dlen)) == NULL) 198 | return NULL; 199 | 200 | if (sbk_has_file_data(frm)) { 201 | if (file == NULL) { 202 | if (sbk_skip_file_data(ctx, frm) == -1) { 203 | sbk_free_frame(frm); 204 | return NULL; 205 | } 206 | } else { 207 | if ((*file = sbk_get_file(ctx, frm)) == NULL) { 208 | sbk_free_frame(frm); 209 | return NULL; 210 | } 211 | if (sbk_skip_file_data(ctx, frm) == -1) { 212 | sbk_free_frame(frm); 213 | sbk_free_file(*file); 214 | return NULL; 215 | } 216 | } 217 | } 218 | 219 | return frm; 220 | } 221 | 222 | Signal__BackupFrame * 223 | sbk_get_frame(struct sbk_ctx *ctx, struct sbk_file **file) 224 | { 225 | Signal__BackupFrame *frm; 226 | 227 | if (file != NULL) 228 | *file = NULL; 229 | 230 | switch (ctx->state) { 231 | case SBK_FIRST_FRAME: 232 | frm = sbk_get_first_frame(ctx); 233 | ctx->state = SBK_OTHER_FRAME; 234 | return frm; 235 | case SBK_OTHER_FRAME: 236 | if ((frm = sbk_get_encrypted_frame(ctx, file)) == NULL) 237 | return NULL; 238 | if (frm->has_end) 239 | ctx->state = SBK_LAST_FRAME; 240 | return frm; 241 | case SBK_LAST_FRAME: 242 | default: 243 | return NULL; 244 | } 245 | } 246 | 247 | int 248 | sbk_rewind(struct sbk_ctx *ctx) 249 | { 250 | if (fseeko(ctx->fp, 0, SEEK_SET) == -1) { 251 | warn("Cannot seek"); 252 | return -1; 253 | } 254 | 255 | clearerr(ctx->fp); 256 | ctx->counter = ctx->counter_start; 257 | ctx->state = SBK_FIRST_FRAME; 258 | return 0; 259 | } 260 | 261 | int 262 | sbk_eof(struct sbk_ctx *ctx) 263 | { 264 | return ctx->state == SBK_LAST_FRAME; 265 | } 266 | -------------------------------------------------------------------------------- /cmd-export-avatars.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "sigbak.h" 28 | 29 | static enum cmd_status cmd_export_avatars(int, char **); 30 | static enum cmd_status cmd_export_stickers(int, char **); 31 | 32 | const struct cmd_entry cmd_export_avatars_entry = { 33 | .name = "export-avatars", 34 | .alias = "avt", 35 | .usage = "[-p passfile] backup [directory]", 36 | .exec = cmd_export_avatars 37 | }; 38 | 39 | const struct cmd_entry cmd_export_stickers_entry = { 40 | .name = "export-stickers", 41 | .alias = "stk", 42 | .usage = "[-p passfile] backup [directory]", 43 | .exec = cmd_export_stickers 44 | }; 45 | 46 | enum type { 47 | AVATAR, 48 | STICKER 49 | }; 50 | 51 | static const char * 52 | get_extension(const char *data, size_t datalen) 53 | { 54 | if (datalen >= 3 && memcmp(data, "\xff\xd8\xff", 3) == 0) 55 | return ".jpg"; 56 | 57 | if (datalen >= 8 && memcmp(data, "\x89PNG\r\n\x1a\n", 8) == 0) 58 | return ".png"; 59 | 60 | if (datalen >= 12 && memcmp(data, "RIFF", 4) == 0 && 61 | memcmp(data + 8, "WEBP", 4) == 0) 62 | return ".webp"; 63 | 64 | return ""; 65 | } 66 | 67 | static int 68 | write_file(const char *path, const char *data, size_t datalen) 69 | { 70 | FILE *fp; 71 | 72 | if ((fp = fopen(path, "wx")) == NULL) { 73 | warn("%s", path); 74 | return -1; 75 | } 76 | 77 | if (fwrite(data, datalen, 1, fp) != 1) { 78 | warn("%s", path); 79 | fclose(fp); 80 | return -1; 81 | } 82 | 83 | fclose(fp); 84 | return 0; 85 | } 86 | 87 | static int 88 | write_avatar(struct sbk_ctx *ctx, Signal__Avatar *avt, struct sbk_file *file) 89 | { 90 | char *base, *data, *name; 91 | const char *ext; 92 | size_t datalen; 93 | int ret; 94 | 95 | data = name = NULL; 96 | ret = -1; 97 | 98 | if (avt->recipientid != NULL) 99 | base = avt->recipientid; 100 | else if (avt->name != NULL) 101 | base = avt->name; 102 | else { 103 | warnx("Invalid avatar frame"); 104 | goto out; 105 | } 106 | 107 | if (strchr(base, '/') != NULL) { 108 | warnx("Invalid avatar filename"); 109 | goto out; 110 | } 111 | 112 | if ((data = sbk_get_file_data(ctx, file, &datalen)) == NULL) 113 | goto out; 114 | 115 | ext = get_extension(data, datalen); 116 | 117 | if (asprintf(&name, "%s%s", base, ext) == -1) { 118 | warnx("asprintf() failed"); 119 | name = NULL; 120 | goto out; 121 | } 122 | 123 | ret = write_file(name, data, datalen); 124 | 125 | out: 126 | free(name); 127 | free(data); 128 | return ret; 129 | } 130 | 131 | static int 132 | write_sticker(struct sbk_ctx *ctx, Signal__Sticker *stk, struct sbk_file *file) 133 | { 134 | char *data, *name; 135 | const char *ext; 136 | size_t datalen; 137 | int ret; 138 | 139 | data = name = NULL; 140 | ret = -1; 141 | 142 | if (!stk->has_rowid) { 143 | warnx("Invalid sticker frame"); 144 | goto out; 145 | } 146 | 147 | if ((data = sbk_get_file_data(ctx, file, &datalen)) == NULL) 148 | goto out; 149 | 150 | ext = get_extension(data, datalen); 151 | 152 | if (asprintf(&name, "%" PRIu64 "%s", stk->rowid, ext) == -1) { 153 | warnx("asprintf() failed"); 154 | name = NULL; 155 | goto out; 156 | } 157 | 158 | ret = write_file(name, data, datalen); 159 | 160 | out: 161 | free(name); 162 | free(data); 163 | return ret; 164 | } 165 | 166 | static int 167 | write_files(int argc, char **argv, enum type type) 168 | { 169 | struct sbk_ctx *ctx; 170 | struct sbk_file *file; 171 | Signal__BackupFrame *frm; 172 | char *backup, *passfile, passphr[128]; 173 | const char *outdir; 174 | int c, ret; 175 | 176 | passfile = NULL; 177 | 178 | while ((c = getopt(argc, argv, "p:")) != -1) 179 | switch (c) { 180 | case 'p': 181 | passfile = optarg; 182 | break; 183 | default: 184 | return CMD_USAGE; 185 | } 186 | 187 | argc -= optind; 188 | argv += optind; 189 | 190 | switch (argc) { 191 | case 1: 192 | backup = argv[0]; 193 | outdir = "."; 194 | break; 195 | case 2: 196 | backup = argv[0]; 197 | outdir = argv[1]; 198 | if (mkdir(outdir, 0777) == -1 && errno != EEXIST) 199 | err(1, "mkdir: %s", outdir); 200 | break; 201 | default: 202 | return CMD_USAGE; 203 | } 204 | 205 | if (unveil(backup, "r") == -1) 206 | err(1, "unveil: %s", backup); 207 | 208 | if (unveil(outdir, "rwc") == -1) 209 | err(1, "unveil: %s", outdir); 210 | 211 | if (passfile == NULL) { 212 | if (pledge("stdio rpath wpath cpath tty", NULL) == -1) 213 | err(1, "pledge"); 214 | } else { 215 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 216 | err(1, "unveil: %s", passfile); 217 | 218 | if (pledge("stdio rpath wpath cpath", NULL) == -1) 219 | err(1, "pledge"); 220 | } 221 | 222 | if ((ctx = sbk_ctx_new()) == NULL) 223 | return CMD_ERROR; 224 | 225 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 226 | sbk_ctx_free(ctx); 227 | return CMD_ERROR; 228 | } 229 | 230 | if (sbk_open(ctx, backup, passphr) == -1) { 231 | explicit_bzero(passphr, sizeof passphr); 232 | sbk_ctx_free(ctx); 233 | return CMD_ERROR; 234 | } 235 | 236 | explicit_bzero(passphr, sizeof passphr); 237 | 238 | if (chdir(outdir) == -1) { 239 | warn("chdir: %s", outdir); 240 | sbk_close(ctx); 241 | sbk_ctx_free(ctx); 242 | return CMD_ERROR; 243 | } 244 | 245 | if (pledge("stdio wpath cpath", NULL) == -1) 246 | err(1, "pledge"); 247 | 248 | ret = CMD_OK; 249 | 250 | while ((frm = sbk_get_frame(ctx, &file)) != NULL) { 251 | if (frm->avatar != NULL && type == AVATAR) { 252 | if (write_avatar(ctx, frm->avatar, file) == -1) 253 | ret = CMD_ERROR; 254 | } else if (frm->sticker != NULL && type == STICKER) { 255 | if (write_sticker(ctx, frm->sticker, file) == -1) 256 | ret = CMD_ERROR; 257 | } 258 | sbk_free_frame(frm); 259 | sbk_free_file(file); 260 | } 261 | 262 | if (!sbk_eof(ctx)) 263 | ret = CMD_ERROR; 264 | 265 | sbk_close(ctx); 266 | sbk_ctx_free(ctx); 267 | return ret; 268 | } 269 | 270 | static enum cmd_status 271 | cmd_export_avatars(int argc, char **argv) 272 | { 273 | return write_files(argc, argv, AVATAR); 274 | } 275 | 276 | static enum cmd_status 277 | cmd_export_stickers(int argc, char **argv) 278 | { 279 | return write_files(argc, argv, STICKER); 280 | } 281 | -------------------------------------------------------------------------------- /sbk-reaction.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | #define SBK_SELECT \ 24 | "SELECT " \ 25 | "author_id, " \ 26 | "emoji, " \ 27 | "date_sent, " \ 28 | "date_received " \ 29 | "FROM reaction " 30 | 31 | /* For database versions < SINGLE_MESSAGE_TABLE_MIGRATION */ 32 | #define SBK_WHERE_1 \ 33 | "WHERE message_id = ? AND is_mms = ? " 34 | 35 | /* For database versions >= SINGLE_MESSAGE_TABLE_MIGRATION */ 36 | #define SBK_WHERE_2 \ 37 | "WHERE message_id = ? " 38 | 39 | #define SBK_ORDER \ 40 | "ORDER BY date_sent" 41 | 42 | /* For database versions < SINGLE_MESSAGE_TABLE_MIGRATION */ 43 | #define SBK_QUERY_1 \ 44 | SBK_SELECT \ 45 | SBK_WHERE_1 \ 46 | SBK_ORDER 47 | 48 | /* For database versions >= SINGLE_MESSAGE_TABLE_MIGRATION */ 49 | #define SBK_QUERY_2 \ 50 | SBK_SELECT \ 51 | SBK_WHERE_2 \ 52 | SBK_ORDER 53 | 54 | #define SBK_COLUMN_AUTHOR_ID 0 55 | #define SBK_COLUMN_EMOJI 1 56 | #define SBK_COLUMN_DATE_SENT 2 57 | #define SBK_COLUMN_DATE_RECEIVED 3 58 | 59 | static void 60 | sbk_free_reaction(struct sbk_reaction *rct) 61 | { 62 | if (rct != NULL) { 63 | free(rct->emoji); 64 | free(rct); 65 | } 66 | } 67 | 68 | void 69 | sbk_free_reaction_list(struct sbk_reaction_list *lst) 70 | { 71 | struct sbk_reaction *rct; 72 | 73 | if (lst != NULL) { 74 | while ((rct = SIMPLEQ_FIRST(lst)) != NULL) { 75 | SIMPLEQ_REMOVE_HEAD(lst, entries); 76 | sbk_free_reaction(rct); 77 | } 78 | free(lst); 79 | } 80 | } 81 | 82 | /* 83 | * For database versions >= REACTION_REFACTOR 84 | */ 85 | 86 | static struct sbk_reaction * 87 | sbk_get_reaction(struct sbk_ctx *ctx, sqlite3_stmt *stm) 88 | { 89 | struct sbk_reaction *rct; 90 | 91 | if ((rct = calloc(1, sizeof *rct)) == NULL) { 92 | warn(NULL); 93 | return NULL; 94 | } 95 | 96 | rct->recipient = sbk_get_recipient_from_id_from_column(ctx, stm, 97 | SBK_COLUMN_AUTHOR_ID); 98 | if (rct->recipient == NULL) 99 | goto error; 100 | 101 | if (sbk_sqlite_column_text_copy(ctx, &rct->emoji, stm, 102 | SBK_COLUMN_EMOJI) == -1) 103 | goto error; 104 | 105 | rct->time_sent = sqlite3_column_int64(stm, SBK_COLUMN_DATE_SENT); 106 | rct->time_recv = sqlite3_column_int64(stm, SBK_COLUMN_DATE_RECEIVED); 107 | 108 | return rct; 109 | 110 | error: 111 | sbk_free_reaction(rct); 112 | return NULL; 113 | } 114 | 115 | static struct sbk_reaction_list * 116 | sbk_get_reactions(struct sbk_ctx *ctx, sqlite3_stmt *stm) 117 | { 118 | struct sbk_reaction_list *lst; 119 | struct sbk_reaction *rct; 120 | int ret; 121 | 122 | if ((lst = malloc(sizeof *lst)) == NULL) { 123 | warn(NULL); 124 | goto error; 125 | } 126 | 127 | SIMPLEQ_INIT(lst); 128 | 129 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 130 | if ((rct = sbk_get_reaction(ctx, stm)) == NULL) 131 | goto error; 132 | SIMPLEQ_INSERT_TAIL(lst, rct, entries); 133 | } 134 | 135 | if (ret != SQLITE_DONE) 136 | goto error; 137 | 138 | sqlite3_finalize(stm); 139 | return lst; 140 | 141 | error: 142 | sbk_free_reaction_list(lst); 143 | sqlite3_finalize(stm); 144 | return NULL; 145 | } 146 | 147 | int 148 | sbk_get_reactions_from_table(struct sbk_ctx *ctx, struct sbk_message *msg) 149 | { 150 | sqlite3_stmt *stm; 151 | const char *query; 152 | 153 | if (ctx->db_version >= SBK_DB_VERSION_SINGLE_MESSAGE_TABLE_MIGRATION) 154 | query = SBK_QUERY_2; 155 | else 156 | query = SBK_QUERY_1; 157 | 158 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 159 | return -1; 160 | 161 | if (sbk_sqlite_bind_int(ctx, stm, 1, msg->id.row_id) == -1) { 162 | sqlite3_finalize(stm); 163 | return -1; 164 | } 165 | 166 | if (ctx->db_version < SBK_DB_VERSION_SINGLE_MESSAGE_TABLE_MIGRATION) 167 | if (sbk_sqlite_bind_int(ctx, stm, 2, 168 | msg->id.table != SBK_SMS_TABLE) == -1) { 169 | sqlite3_finalize(stm); 170 | return -1; 171 | } 172 | 173 | if ((msg->reactions = sbk_get_reactions(ctx, stm)) == NULL) 174 | return -1; 175 | 176 | return 0; 177 | } 178 | 179 | /* 180 | * For database versions < REACTION_REFACTOR 181 | */ 182 | 183 | static Signal__ReactionList * 184 | sbk_unpack_reaction_list_message(const void *buf, size_t len) 185 | { 186 | Signal__ReactionList *msg; 187 | 188 | if ((msg = signal__reaction_list__unpack(NULL, len, buf)) == NULL) 189 | warnx("Cannot unpack reaction list"); 190 | 191 | return msg; 192 | } 193 | 194 | static void 195 | sbk_free_reaction_list_message(Signal__ReactionList *msg) 196 | { 197 | if (msg != NULL) 198 | signal__reaction_list__free_unpacked(msg, NULL); 199 | } 200 | 201 | int 202 | sbk_get_reactions_from_column(struct sbk_ctx *ctx, 203 | struct sbk_reaction_list **lst, sqlite3_stmt *stm, int idx) 204 | { 205 | struct sbk_reaction *rct; 206 | struct sbk_recipient_id id; 207 | Signal__ReactionList *msg; 208 | const void *blob; 209 | size_t i; 210 | int len; 211 | 212 | *lst = NULL; 213 | 214 | if (sqlite3_column_type(stm, idx) != SQLITE_BLOB) { 215 | /* No reactions */ 216 | return 0; 217 | } 218 | 219 | if ((blob = sqlite3_column_blob(stm, idx)) == NULL) { 220 | sbk_sqlite_warn(ctx, "Cannot get reactions column"); 221 | return -1; 222 | } 223 | 224 | if ((len = sqlite3_column_bytes(stm, idx)) < 0) { 225 | sbk_sqlite_warn(ctx, "Cannot get reactions size"); 226 | return -1; 227 | } 228 | 229 | if ((msg = sbk_unpack_reaction_list_message(blob, len)) == NULL) 230 | return -1; 231 | 232 | if ((*lst = malloc(sizeof **lst)) == NULL) { 233 | warn(NULL); 234 | goto error1; 235 | } 236 | 237 | SIMPLEQ_INIT(*lst); 238 | 239 | for (i = 0; i < msg->n_reactions; i++) { 240 | if ((rct = malloc(sizeof *rct)) == NULL) { 241 | warn(NULL); 242 | goto error1; 243 | } 244 | 245 | id.new = msg->reactions[i]->author; 246 | id.old = NULL; 247 | 248 | rct->recipient = sbk_get_recipient_from_id(ctx, &id); 249 | if (rct->recipient == NULL) 250 | goto error2; 251 | 252 | if ((rct->emoji = strdup(msg->reactions[i]->emoji)) == NULL) { 253 | warn(NULL); 254 | goto error2; 255 | } 256 | 257 | rct->time_sent = msg->reactions[i]->senttime; 258 | rct->time_recv = msg->reactions[i]->receivedtime; 259 | SIMPLEQ_INSERT_TAIL(*lst, rct, entries); 260 | } 261 | 262 | sbk_free_reaction_list_message(msg); 263 | return 0; 264 | 265 | error2: 266 | free(rct); 267 | 268 | error1: 269 | sbk_free_reaction_list(*lst); 270 | sbk_free_reaction_list_message(msg); 271 | *lst = NULL; 272 | return -1; 273 | } 274 | -------------------------------------------------------------------------------- /sigbak.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2019 Tim van der Molen 2 | .\" 3 | .\" Permission to use, copy, modify, and distribute this software for any 4 | .\" purpose with or without fee is hereby granted, provided that the above 5 | .\" copyright notice and this permission notice appear in all copies. 6 | .\" 7 | .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | .\" 15 | .Dd July 3, 2024 16 | .Dt SIGBAK 1 17 | .Os 18 | .Sh NAME 19 | .Nm sigbak 20 | .Nd export messages from Signal Android backups 21 | .Sh SYNOPSIS 22 | .Nm sigbak 23 | .Ar command 24 | .Op Ar argument ... 25 | .Sh DESCRIPTION 26 | .Nm 27 | is a utility to read the backups created by the Signal Android app. 28 | It can be used to export messages, attachments and other data. 29 | .Pp 30 | A Signal backup consists primarily of an SQLite database. 31 | This database contains the messages, amongst other things. 32 | Attachments, avatars and stickers, however, are stored as separate files in the 33 | backup. 34 | .Pp 35 | Every Signal backup is encrypted with a 30-digit passphrase. 36 | By default, 37 | .Nm 38 | prompts for the passphrase. 39 | Alternatively, the 40 | .Fl p 41 | option (see below) may be used to specify a file that contains the passphrase. 42 | If the specified file is 43 | .Sq - , 44 | the passphrase is read from standard input. 45 | Whitespace in the passphrase is ignored. 46 | .Pp 47 | The commands are as follows: 48 | .Bl -tag -width Ds 49 | .Tg check 50 | .It Ic check-backup Oo Fl p Ar passfile Oc Ar backup 51 | .D1 Pq Alias: Ic check 52 | .Pp 53 | Check that the file 54 | .Ar backup 55 | can be decrypted and parsed correctly. 56 | The check is aborted after the first encountered error. 57 | .Tg dump 58 | .It Ic dump-backup Oo Fl p Ar passfile Oc Ar backup 59 | .D1 Pq Alias: Ic dump 60 | .Pp 61 | Print the raw contents of the file 62 | .Ar backup 63 | in a readable format on standard output. 64 | .Tg att 65 | .It Xo 66 | .Ic export-attachments 67 | .Op Fl aIMm 68 | .Oo Fl p Ar passfile Oc 69 | .Ar backup Op Ar directory 70 | .Xc 71 | .D1 Pq Alias: Ic att 72 | .Pp 73 | Export the attachments from the file 74 | .Ar backup . 75 | The attachment files are created in separate directories, one for each 76 | conversation. 77 | These directories are created in 78 | .Ar directory , 79 | or in the current directory if 80 | .Ar directory 81 | is not specified. 82 | .Pp 83 | By default, long-message attachments are not exported. 84 | Long-message attachments contain the text of longer messages. 85 | The 86 | .Fl a 87 | option may be used to export these attachments, too. 88 | .Pp 89 | If the 90 | .Fl I 91 | option is specified, the attachment id is prepended to the filename of each 92 | exported attachment. 93 | .Pp 94 | If the 95 | .Fl M 96 | option is specified, the file modification time of each exported attachment is 97 | set to the time the attachment was sent. 98 | The 99 | .Fl m 100 | option is similar, but uses the time the attachment was received. 101 | .Tg avt 102 | .It Ic export-avatars Oo Fl p Ar passfile Oc Ar backup Op Ar directory 103 | .D1 Pq Alias: Ic avt 104 | .Pp 105 | Export all avatars from the file 106 | .Ar backup 107 | to 108 | .Ar directory , 109 | or to the current directory if 110 | .Ar directory 111 | is not specified. 112 | .Tg db 113 | .It Ic export-database Oo Fl p Ar passfile Oc Ar backup Ar database 114 | .D1 Pq Alias: Ic db 115 | .Pp 116 | Export the SQLite database from the file 117 | .Ar backup 118 | to the file 119 | .Ar database . 120 | .Tg msg 121 | .It Xo 122 | .Ic export-messages 123 | .Oo Fl f Ar format Oc 124 | .Oo Fl p Ar passfile Oc 125 | .Ar backup Op Ar directory 126 | .Xc 127 | .D1 Pq Alias: Ic msg 128 | .Pp 129 | Export the messages from the file 130 | .Ar backup . 131 | The messages are written to separate files, one for each conversation. 132 | These files are created in 133 | .Ar directory , 134 | or in the current directory if 135 | .Ar directory 136 | is not specified. 137 | .Pp 138 | The 139 | .Fl f 140 | option may be used to specify the output format. 141 | The following output formats are supported: 142 | .Bl -tag -width Ds 143 | .It Cm csv 144 | Messages are written in CSV (comma-separated values) format. 145 | See the 146 | .Sx CSV FORMAT 147 | section below for details. 148 | .It Cm text 149 | Messages are written as plain text. 150 | This is the default. 151 | .It Cm text-short 152 | Messages are written as plain text, in short form. 153 | Every message is written on a single line. 154 | .El 155 | .Tg stk 156 | .It Ic export-stickers Oo Fl p Ar passfile Oc Ar backup Op Ar directory 157 | .D1 Pq Alias: Ic stk 158 | .Pp 159 | Export all stickers from the file 160 | .Ar backup 161 | to 162 | .Ar directory , 163 | or to the current directory if 164 | .Ar directory 165 | is not specified. 166 | .El 167 | .Sh CSV FORMAT 168 | The 169 | .Ic export-messages 170 | command can export messages in CSV format. 171 | In this format, each record describes either a message or a message reaction, 172 | and consists of the following eight fields. 173 | .Pp 174 | The 175 | .Em first 176 | field is an integer specifying the time a message or reaction was sent. 177 | For incoming messages, the 178 | .Em second 179 | field is an integer specifying the time a message or reaction was received. 180 | The times in these two fields are specified in Unix time, with millisecond 181 | precision. 182 | .Pp 183 | The 184 | .Em third 185 | field is an integer specifying the thread id. 186 | .Pp 187 | The 188 | .Em fourth 189 | field is an integer specifying the message type. 190 | A value of 0 indicates an incoming message, 1 indicates an outgoing message, 191 | and 2 indicates a reaction. 192 | Reaction records always immediately follow after the record of the message they 193 | are reactions to. 194 | .Pp 195 | The 196 | .Em fifth 197 | field is an integer specifying the number of attachments a message has. 198 | For reactions, this field is 0. 199 | .Pp 200 | The 201 | .Em sixth 202 | field is a string containing the phone number of the sender or recipient. 203 | For outgoing messages sent to a group, this field is the string 204 | .Sq group . 205 | .Pp 206 | The 207 | .Em seventh 208 | field is a string containing the name of the sender, recipient or group. 209 | .Pp 210 | The 211 | .Em eighth 212 | field is a string containing the message text or reaction emoji. 213 | .Sh EXIT STATUS 214 | .Ex -std 215 | .Sh EXAMPLES 216 | Export all messages from the file 217 | .Pa signal.backup 218 | to the directory 219 | .Pa messages : 220 | .Pp 221 | .Dl $ sigbak export-messages signal.backup messages 222 | .Pp 223 | First create a passphrase file, then export all messages and attachments: 224 | .Bd -literal -offset indent 225 | $ echo 01234 56789 01234 56789 01234 56789 > passfile 226 | $ sigbak export-messages -p passfile signal.backup messages 227 | $ sigbak export-attachments -p passfile signal.backup attachments 228 | .Ed 229 | .Pp 230 | Export the SQLite database and use 231 | .Xr sqlite3 1 232 | to view the contents of the 233 | .Sq message 234 | table: 235 | .Bd -literal -offset indent 236 | $ sigbak export-database signal.backup signal.db 237 | $ sqlite3 signal.db 'select * from message' | less 238 | .Ed 239 | .Sh SEE ALSO 240 | .Lk https://github.com/tbvdm/sigbak 241 | .Sh AUTHORS 242 | The 243 | .Nm 244 | utility was written by 245 | .An Tim van der Molen Aq Mt tim@kariliq.nl . 246 | -------------------------------------------------------------------------------- /sbk-internal.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #ifndef SBK_INTERNAL_H 18 | #define SBK_INTERNAL_H 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #include 26 | 27 | #include "sbk.h" 28 | 29 | #define SBK_IV_LEN 16 30 | #define SBK_KEY_LEN 32 31 | #define SBK_CIPHER_KEY_LEN 32 32 | #define SBK_MAC_KEY_LEN 32 33 | #define SBK_DERIV_KEY_LEN (SBK_CIPHER_KEY_LEN + SBK_MAC_KEY_LEN) 34 | #define SBK_MAC_LEN 10 35 | #define SBK_ROUNDS 250000 36 | #define SBK_HKDF_INFO "Backup Export" 37 | 38 | #define SBK_MENTION_PLACEHOLDER "\357\277\274" /* U+FFFC */ 39 | #define SBK_MENTION_PREFIX "@" 40 | 41 | /* Based on SignalDatabaseMigrations.kt in the Signal-Android repository */ 42 | #define SBK_DB_VERSION_QUOTED_REPLIES 7 43 | #define SBK_DB_VERSION_RECIPIENT_IDS 24 44 | #define SBK_DB_VERSION_REACTIONS 37 45 | #define SBK_DB_VERSION_SPLIT_PROFILE_NAMES 43 46 | #define SBK_DB_VERSION_MENTIONS 68 47 | #define SBK_DB_VERSION_THREAD_AUTOINCREMENT 108 48 | #define SBK_DB_VERSION_REACTION_REFACTOR 121 49 | #define SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS 166 50 | #define SBK_DB_VERSION_SINGLE_MESSAGE_TABLE_MIGRATION 168 51 | #define SBK_DB_VERSION_REACTION_FOREIGN_KEY_MIGRATION 174 52 | #define SBK_DB_VERSION_MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION 185 53 | #define SBK_DB_VERSION_RESET_PNI_COLUMN 200 54 | #define SBK_DB_VERSION_RECIPIENT_TABLE_VALIDATIONS 201 55 | #define SBK_DB_VERSION_REMOVE_ATTACHMENT_UNIQUE_ID 215 56 | #define SBK_DB_VERSION_ADD_NICKNAME_AND_NOTE_FIELDS_TO_RECIPIENT_TABLE 223 57 | 58 | #define XSTRINGIFY(x) #x 59 | #define STRINGIFY(x) XSTRINGIFY(x) 60 | 61 | enum sbk_frame_state { 62 | SBK_FIRST_FRAME, /* We're about to read the first frame */ 63 | SBK_LAST_FRAME, /* We've read the last frame */ 64 | SBK_OTHER_FRAME /* We're somewhere in between */ 65 | }; 66 | 67 | struct sbk_file { 68 | off_t pos; 69 | uint32_t len; 70 | uint32_t counter; 71 | }; 72 | 73 | struct sbk_attachment_entry { 74 | struct sbk_attachment_id id; 75 | struct sbk_file *file; 76 | RB_ENTRY(sbk_attachment_entry) entries; 77 | }; 78 | 79 | RB_HEAD(sbk_attachment_tree, sbk_attachment_entry); 80 | 81 | struct sbk_recipient_id { 82 | char *old; /* For older databases */ 83 | int new; /* For newer databases */ 84 | }; 85 | 86 | struct sbk_recipient_entry { 87 | struct sbk_recipient_id id; 88 | struct sbk_recipient recipient; 89 | RB_ENTRY(sbk_recipient_entry) entries; 90 | }; 91 | 92 | RB_HEAD(sbk_recipient_tree, sbk_recipient_entry); 93 | 94 | struct sbk_ctx { 95 | FILE *fp; 96 | sqlite3 *db; 97 | unsigned int backup_version; 98 | unsigned int db_version; 99 | struct sbk_attachment_tree attachments; 100 | struct sbk_recipient_tree recipients; 101 | EVP_CIPHER_CTX *cipher_ctx; 102 | HMAC_CTX *hmac_ctx; 103 | unsigned char cipher_key[SBK_CIPHER_KEY_LEN]; 104 | unsigned char mac_key[SBK_MAC_KEY_LEN]; 105 | unsigned char iv[SBK_IV_LEN]; 106 | uint32_t counter; 107 | uint32_t counter_start; 108 | enum sbk_frame_state state; 109 | unsigned char *ibuf; 110 | size_t ibufsize; 111 | unsigned char *obuf; 112 | size_t obufsize; 113 | }; 114 | 115 | /* sbk-attachment-tree.c */ 116 | void sbk_free_attachment_tree(struct sbk_ctx *); 117 | struct sbk_file *sbk_get_attachment_file(struct sbk_ctx *, 118 | const struct sbk_attachment_id *); 119 | int sbk_insert_attachment_entry(struct sbk_ctx *, Signal__BackupFrame *, 120 | struct sbk_file *); 121 | 122 | /* sbk-attachment.c */ 123 | void sbk_free_attachment(struct sbk_attachment *); 124 | int sbk_get_attachments_for_edit(struct sbk_ctx *, struct sbk_edit *); 125 | int sbk_get_attachments_for_message(struct sbk_ctx *, 126 | struct sbk_message *); 127 | int sbk_get_attachments_for_quote(struct sbk_ctx *, struct sbk_quote *, 128 | struct sbk_message_id *); 129 | 130 | /* sbk-database.c */ 131 | int sbk_create_database(struct sbk_ctx *); 132 | 133 | /* sbk-edit.c */ 134 | void sbk_free_edit_list(struct sbk_edit_list *); 135 | int sbk_get_edits(struct sbk_ctx *, struct sbk_message *); 136 | 137 | /* sbk-file.c */ 138 | char *sbk_get_file_data_as_string(struct sbk_ctx *, struct sbk_file *); 139 | 140 | /* sbk-frame.c */ 141 | Signal__BackupFrame *sbk_get_first_frame(struct sbk_ctx *); 142 | 143 | /* sbk-mention.c */ 144 | void sbk_free_mention_list(struct sbk_mention_list *); 145 | int sbk_get_mentions_for_edit(struct sbk_ctx *, struct sbk_edit *); 146 | int sbk_get_mentions_for_message(struct sbk_ctx *, struct sbk_message *); 147 | int sbk_get_mentions_for_quote(struct sbk_ctx *, 148 | struct sbk_mention_list **, sqlite3_stmt *, int); 149 | int sbk_insert_mentions(char **, struct sbk_mention_list *); 150 | 151 | /* sbk-message.c */ 152 | int sbk_get_long_message(struct sbk_ctx *, char **, 153 | struct sbk_attachment_list **); 154 | 155 | /* sbk-quote.c */ 156 | void sbk_free_quote(struct sbk_quote *); 157 | int sbk_get_quote(struct sbk_ctx *, struct sbk_quote **, sqlite3_stmt *, 158 | int, int, int, int, struct sbk_message_id *); 159 | 160 | /* sbk-reaction.c */ 161 | void sbk_free_reaction_list(struct sbk_reaction_list *); 162 | int sbk_get_reactions_from_column(struct sbk_ctx *, 163 | struct sbk_reaction_list **, sqlite3_stmt *, int); 164 | int sbk_get_reactions_from_table(struct sbk_ctx *, struct sbk_message *); 165 | 166 | /* sbk-read.c */ 167 | int sbk_decrypt_final(struct sbk_ctx *, size_t *, const unsigned char *); 168 | int sbk_decrypt_init(struct sbk_ctx *, uint32_t); 169 | int sbk_decrypt_update(struct sbk_ctx *, size_t, size_t *); 170 | int sbk_grow_buffers(struct sbk_ctx *, size_t); 171 | int sbk_read(struct sbk_ctx *, void *, size_t); 172 | 173 | /* sbk-recipient-tree.c */ 174 | void sbk_free_recipient_tree(struct sbk_ctx *); 175 | struct sbk_recipient *sbk_get_recipient_from_aci(struct sbk_ctx *, 176 | const char *); 177 | struct sbk_recipient *sbk_get_recipient_from_id(struct sbk_ctx *, 178 | struct sbk_recipient_id *); 179 | struct sbk_recipient *sbk_get_recipient_from_id_from_column(struct sbk_ctx *, 180 | sqlite3_stmt *, int); 181 | 182 | /* sbk-sqlite.c */ 183 | int sbk_sqlite_bind_blob(struct sbk_ctx *, sqlite3_stmt *, int, 184 | const void *, size_t); 185 | int sbk_sqlite_bind_double(struct sbk_ctx *, sqlite3_stmt *, int, double); 186 | int sbk_sqlite_bind_int(struct sbk_ctx *, sqlite3_stmt *, int, int); 187 | int sbk_sqlite_bind_int64(struct sbk_ctx *, sqlite3_stmt *, int, 188 | sqlite3_int64); 189 | int sbk_sqlite_bind_null(struct sbk_ctx *, sqlite3_stmt *, int); 190 | int sbk_sqlite_bind_text(struct sbk_ctx *, sqlite3_stmt *, int, 191 | const char *); 192 | int sbk_sqlite_column_text_copy(struct sbk_ctx *, char **, sqlite3_stmt *, 193 | int); 194 | int sbk_sqlite_exec(struct sbk_ctx *, const char *); 195 | int sbk_sqlite_open(sqlite3 **, const char *); 196 | int sbk_sqlite_prepare(struct sbk_ctx *, sqlite3_stmt **, const char *); 197 | int sbk_sqlite_step(struct sbk_ctx *, sqlite3_stmt *); 198 | void sbk_sqlite_warn(struct sbk_ctx *, const char *, ...); 199 | void sbk_sqlite_warnd(sqlite3 *, const char *, ...); 200 | 201 | #endif 202 | -------------------------------------------------------------------------------- /sbk-mention.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | #define SBK_QUERY \ 24 | "SELECT " \ 25 | "recipient_id " \ 26 | "FROM mention " \ 27 | "WHERE message_id = ? " \ 28 | "ORDER BY range_start" 29 | 30 | void 31 | sbk_free_mention_list(struct sbk_mention_list *lst) 32 | { 33 | struct sbk_mention *mnt; 34 | 35 | if (lst != NULL) { 36 | while ((mnt = SIMPLEQ_FIRST(lst)) != NULL) { 37 | SIMPLEQ_REMOVE_HEAD(lst, entries); 38 | free(mnt); 39 | } 40 | free(lst); 41 | } 42 | } 43 | 44 | static struct sbk_mention * 45 | sbk_get_mention(struct sbk_ctx *ctx, sqlite3_stmt *stm) 46 | { 47 | struct sbk_mention *mnt; 48 | 49 | if ((mnt = malloc(sizeof *mnt)) == NULL) { 50 | warn(NULL); 51 | return NULL; 52 | } 53 | 54 | mnt->recipient = sbk_get_recipient_from_id_from_column(ctx, stm, 0); 55 | if (mnt->recipient == NULL) { 56 | free(mnt); 57 | return NULL; 58 | } 59 | 60 | return mnt; 61 | } 62 | 63 | static int 64 | sbk_get_mentions_for_message_id(struct sbk_ctx *ctx, 65 | struct sbk_mention_list **lst, struct sbk_message_id *mid) 66 | { 67 | struct sbk_mention *mnt; 68 | sqlite3_stmt *stm; 69 | int ret; 70 | 71 | if (sbk_sqlite_prepare(ctx, &stm, SBK_QUERY) == -1) 72 | return -1; 73 | 74 | if (sbk_sqlite_bind_int(ctx, stm, 1, mid->row_id) == -1) 75 | goto error; 76 | 77 | if ((*lst = malloc(sizeof **lst)) == NULL) { 78 | warn(NULL); 79 | goto error; 80 | } 81 | 82 | SIMPLEQ_INIT(*lst); 83 | 84 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 85 | if ((mnt = sbk_get_mention(ctx, stm)) == NULL) 86 | goto error; 87 | SIMPLEQ_INSERT_TAIL(*lst, mnt, entries); 88 | } 89 | 90 | if (ret != SQLITE_DONE) 91 | goto error; 92 | 93 | sqlite3_finalize(stm); 94 | return 0; 95 | 96 | error: 97 | sbk_free_mention_list(*lst); 98 | *lst = NULL; 99 | sqlite3_finalize(stm); 100 | return -1; 101 | } 102 | 103 | int 104 | sbk_get_mentions_for_message(struct sbk_ctx *ctx, struct sbk_message *msg) 105 | { 106 | if (msg->id.table == SBK_SMS_TABLE || 107 | ctx->db_version < SBK_DB_VERSION_MENTIONS) 108 | return 0; 109 | 110 | return sbk_get_mentions_for_message_id(ctx, &msg->mentions, &msg->id); 111 | } 112 | 113 | int 114 | sbk_get_mentions_for_edit(struct sbk_ctx *ctx, struct sbk_edit *edit) 115 | { 116 | return sbk_get_mentions_for_message_id(ctx, &edit->mentions, 117 | &edit->id); 118 | } 119 | 120 | static void 121 | sbk_free_quote_mention_list_message(Signal__BodyRangeList *msg) 122 | { 123 | if (msg != NULL) 124 | signal__body_range_list__free_unpacked(msg, NULL); 125 | } 126 | 127 | static Signal__BodyRangeList * 128 | sbk_unpack_quote_mention_list_message(const void *buf, size_t len) 129 | { 130 | Signal__BodyRangeList *msg; 131 | 132 | if ((msg = signal__body_range_list__unpack(NULL, len, buf)) == NULL) 133 | warnx("Cannot unpack quoted mention list"); 134 | 135 | return msg; 136 | } 137 | 138 | int 139 | sbk_get_mentions_for_quote(struct sbk_ctx *ctx, struct sbk_mention_list **lst, 140 | sqlite3_stmt *stm, int idx) 141 | { 142 | Signal__BodyRangeList *msg; 143 | struct sbk_mention *mnt; 144 | struct sbk_recipient *rcp; 145 | const void *blob; 146 | size_t i; 147 | int len; 148 | 149 | *lst = NULL; 150 | 151 | if (sqlite3_column_type(stm, idx) != SQLITE_BLOB) { 152 | /* No mentions */ 153 | return 0; 154 | } 155 | 156 | if ((blob = sqlite3_column_blob(stm, idx)) == NULL) { 157 | sbk_sqlite_warn(ctx, "Cannot get quoted mentions column"); 158 | return -1; 159 | } 160 | 161 | if ((len = sqlite3_column_bytes(stm, idx)) < 0) { 162 | sbk_sqlite_warn(ctx, "Cannot get quoted mentions size"); 163 | return -1; 164 | } 165 | 166 | if ((msg = sbk_unpack_quote_mention_list_message(blob, len)) == NULL) 167 | return -1; 168 | 169 | if ((*lst = malloc(sizeof **lst)) == NULL) { 170 | warn(NULL); 171 | goto error; 172 | } 173 | 174 | SIMPLEQ_INIT(*lst); 175 | 176 | for (i = 0; i < msg->n_ranges; i++) { 177 | if (msg->ranges[i]->associated_value_case != 178 | SIGNAL__BODY_RANGE_LIST__BODY_RANGE__ASSOCIATED_VALUE_MENTION_UUID) 179 | continue; 180 | 181 | if (msg->ranges[i]->mentionuuid == NULL) { 182 | warnx("Quoted mention without uuid"); 183 | goto error; 184 | } 185 | 186 | rcp = sbk_get_recipient_from_aci(ctx, 187 | msg->ranges[i]->mentionuuid); 188 | if (rcp == NULL) { 189 | warnx("Cannot find recipient for quoted mention uuid " 190 | "%s", msg->ranges[i]->mentionuuid); 191 | goto error; 192 | } 193 | 194 | if ((mnt = malloc(sizeof *mnt)) == NULL) { 195 | warn(NULL); 196 | goto error; 197 | } 198 | 199 | mnt->recipient = rcp; 200 | SIMPLEQ_INSERT_TAIL(*lst, mnt, entries); 201 | } 202 | 203 | sbk_free_quote_mention_list_message(msg); 204 | return 0; 205 | 206 | error: 207 | sbk_free_quote_mention_list_message(msg); 208 | sbk_free_mention_list(*lst); 209 | *lst = NULL; 210 | return -1; 211 | } 212 | 213 | int 214 | sbk_insert_mentions(char **text, struct sbk_mention_list *lst) 215 | { 216 | struct sbk_mention *mnt; 217 | char *newtext, *newtextpos, *placeholderpos, *textpos; 218 | const char *name; 219 | size_t copylen, newtextlen, placeholderlen, prefixlen; 220 | 221 | if (lst == NULL || SIMPLEQ_EMPTY(lst)) 222 | return 0; 223 | 224 | newtext = NULL; 225 | placeholderlen = strlen(SBK_MENTION_PLACEHOLDER); 226 | prefixlen = strlen(SBK_MENTION_PREFIX); 227 | 228 | /* Calculate length of new text */ 229 | newtextlen = strlen(*text); 230 | SIMPLEQ_FOREACH(mnt, lst, entries) { 231 | if (newtextlen < placeholderlen) 232 | goto error; 233 | name = sbk_get_recipient_display_name(mnt->recipient); 234 | /* Subtract placeholder, add mention */ 235 | newtextlen = newtextlen - placeholderlen + prefixlen + 236 | strlen(name); 237 | } 238 | 239 | if ((newtext = malloc(newtextlen + 1)) == NULL) { 240 | warn(NULL); 241 | return -1; 242 | } 243 | 244 | textpos = *text; 245 | newtextpos = newtext; 246 | 247 | /* Write new text, replacing placeholders with mentions */ 248 | SIMPLEQ_FOREACH(mnt, lst, entries) { 249 | placeholderpos = strstr(textpos, SBK_MENTION_PLACEHOLDER); 250 | if (placeholderpos == NULL) 251 | goto error; 252 | 253 | copylen = placeholderpos - textpos; 254 | memcpy(newtextpos, textpos, copylen); 255 | textpos += copylen + placeholderlen; 256 | newtextpos += copylen; 257 | 258 | memcpy(newtextpos, SBK_MENTION_PREFIX, prefixlen); 259 | newtextpos += prefixlen; 260 | 261 | name = sbk_get_recipient_display_name(mnt->recipient); 262 | copylen = strlen(name); 263 | memcpy(newtextpos, name, copylen); 264 | newtextpos += copylen; 265 | } 266 | 267 | /* Sanity check: there should be no placeholders left */ 268 | if (strstr(textpos, SBK_MENTION_PLACEHOLDER) != NULL) 269 | goto error; 270 | 271 | copylen = strlen(textpos); 272 | memcpy(newtextpos, textpos, copylen); 273 | newtextpos += copylen; 274 | *newtextpos = '\0'; 275 | 276 | free(*text); 277 | *text = newtext; 278 | 279 | return 0; 280 | 281 | error: 282 | warnx("Invalid mention"); 283 | free(newtext); 284 | return 0; 285 | } 286 | -------------------------------------------------------------------------------- /sbk.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #ifndef SBK_H 18 | #define SBK_H 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #include "backup.pb-c.h" 26 | #include "database.pb-c.h" 27 | 28 | /* 29 | * Message types, based on MessageTypes.java in the Signal-Android repository 30 | */ 31 | 32 | /* Base types */ 33 | #define SBK_BASE_TYPE_MASK 0x1f 34 | 35 | #define SBK_INCOMING_AUDIO_CALL_TYPE 1 36 | #define SBK_OUTGOING_AUDIO_CALL_TYPE 2 37 | #define SBK_MISSED_AUDIO_CALL_TYPE 3 38 | #define SBK_JOINED_TYPE 4 39 | #define SBK_UNSUPPORTED_MESSAGE_TYPE 5 40 | #define SBK_INVALID_MESSAGE_TYPE 6 41 | #define SBK_PROFILE_CHANGE_TYPE 7 42 | #define SBK_MISSED_VIDEO_CALL_TYPE 8 43 | #define SBK_GV1_MIGRATION_TYPE 9 44 | #define SBK_INCOMING_VIDEO_CALL_TYPE 10 45 | #define SBK_OUTGOING_VIDEO_CALL_TYPE 11 46 | #define SBK_GROUP_CALL_TYPE 12 47 | #define SBK_BAD_DECRYPT_TYPE 13 48 | #define SBK_CHANGE_NUMBER_TYPE 14 49 | #define SBK_BOOST_REQUEST_TYPE 15 50 | #define SBK_THREAD_MERGE_TYPE 16 51 | #define SBK_SMS_EXPORT_TYPE 17 52 | #define SBK_SESSION_SWITCHOVER_TYPE 18 53 | 54 | #define SBK_BASE_INBOX_TYPE 20 55 | #define SBK_BASE_OUTBOX_TYPE 21 56 | #define SBK_BASE_SENDING_TYPE 22 57 | #define SBK_BASE_SENT_TYPE 23 58 | #define SBK_BASE_SENT_FAILED_TYPE 24 59 | #define SBK_BASE_PENDING_SECURE_SMS_FALLBACK 25 60 | #define SBK_BASE_PENDING_INSECURE_SMS_FALLBACK 26 61 | #define SBK_BASE_DRAFT_TYPE 27 62 | 63 | /* Message attributes */ 64 | #define SBK_MESSAGE_ATTRIBUTE_MASK 0xe0 65 | #define SBK_MESSAGE_FORCE_SMS_BIT 0x40 66 | 67 | /* Key exchange information */ 68 | #define SBK_KEY_EXCHANGE_MASK 0xff00 69 | #define SBK_KEY_EXCHANGE_BIT 0x8000 70 | #define SBK_KEY_EXCHANGE_IDENTITY_VERIFIED_BIT 0x4000 71 | #define SBK_KEY_EXCHANGE_IDENTITY_DEFAULT_BIT 0x2000 72 | #define SBK_KEY_EXCHANGE_CORRUPTED_BIT 0x1000 73 | #define SBK_KEY_EXCHANGE_INVALID_VERSION_BIT 0x800 74 | #define SBK_KEY_EXCHANGE_BUNDLE_BIT 0x400 75 | #define SBK_KEY_EXCHANGE_IDENTITY_UPDATE_BIT 0x200 76 | #define SBK_KEY_EXCHANGE_CONTENT_FORMAT 0x100 77 | 78 | /* Secure message information */ 79 | #define SBK_SECURE_MESSAGE_BIT 0x800000 80 | #define SBK_END_SESSION_BIT 0x400000 81 | #define SBK_PUSH_MESSAGE_BIT 0x200000 82 | 83 | /* Group message information */ 84 | #define SBK_GROUP_UPDATE_BIT 0x10000 85 | #define SBK_GROUP_QUIT_BIT 0x20000 86 | #define SBK_EXPIRATION_TIMER_UPDATE_BIT 0x40000 87 | 88 | /* Encrypted storage information */ 89 | #define SBK_ENCRYPTION_MASK 0xff000000 90 | #define SBK_ENCRYPTION_SYMMETRIC_BIT 0x80000000 /* Deprecated */ 91 | #define SBK_ENCRYPTION_ASYMMETRIC_BIT 0x40000000 /* Deprecated */ 92 | #define SBK_ENCRYPTION_REMOTE_BIT 0x20000000 93 | #define SBK_ENCRYPTION_REMOTE_FAILED_BIT 0x10000000 94 | #define SBK_ENCRYPTION_REMOTE_NO_SESSION_BIT 0x8000000 95 | #define SBK_ENCRYPTION_REMOTE_DUPLICATE_BIT 0x4000000 96 | #define SBK_ENCRYPTION_REMOTE_LEGACY_BIT 0x2000000 97 | 98 | /* Attachment transfer states */ 99 | #define SBK_ATTACHMENT_TRANSFER_DONE 0 100 | #define SBK_ATTACHMENT_TRANSFER_STARTED 1 101 | #define SBK_ATTACHMENT_TRANSFER_PENDING 2 102 | #define SBK_ATTACHMENT_TRANSFER_FAILED 3 103 | #define SBK_ATTACHMENT_TRANSFER_PERMANENT_FAILURE 4 104 | 105 | /* Content type of the long-text attachment of a long message */ 106 | #define SBK_LONG_TEXT_TYPE "text/x-signal-plain" 107 | 108 | struct sbk_ctx; 109 | 110 | struct sbk_file; 111 | 112 | struct sbk_contact { 113 | char *aci; /* Account Identity */ 114 | char *phone; 115 | char *email; 116 | char *system_joined_name; 117 | char *system_phone_label; 118 | char *profile_given_name; 119 | char *profile_family_name; 120 | char *profile_joined_name; 121 | char *nickname_given_name; 122 | char *nickname_family_name; 123 | char *nickname_joined_name; 124 | }; 125 | 126 | struct sbk_group { 127 | char *name; 128 | }; 129 | 130 | struct sbk_recipient { 131 | enum { 132 | SBK_CONTACT, 133 | SBK_GROUP 134 | } type; 135 | struct sbk_contact *contact; 136 | struct sbk_group *group; 137 | }; 138 | 139 | struct sbk_attachment_id { 140 | int64_t row_id; 141 | int64_t unique_id; /* For older backups */ 142 | }; 143 | 144 | struct sbk_attachment { 145 | struct sbk_attachment_id id; 146 | int status; 147 | char *filename; 148 | char *content_type; 149 | uint64_t size; 150 | uint64_t time_sent; 151 | uint64_t time_recv; 152 | struct sbk_file *file; 153 | TAILQ_ENTRY(sbk_attachment) entries; 154 | }; 155 | 156 | TAILQ_HEAD(sbk_attachment_list, sbk_attachment); 157 | 158 | struct sbk_mention { 159 | struct sbk_recipient *recipient; 160 | SIMPLEQ_ENTRY(sbk_mention) entries; 161 | }; 162 | 163 | SIMPLEQ_HEAD(sbk_mention_list, sbk_mention); 164 | 165 | struct sbk_reaction { 166 | struct sbk_recipient *recipient; 167 | uint64_t time_sent; 168 | uint64_t time_recv; 169 | char *emoji; 170 | SIMPLEQ_ENTRY(sbk_reaction) entries; 171 | }; 172 | 173 | SIMPLEQ_HEAD(sbk_reaction_list, sbk_reaction); 174 | 175 | struct sbk_quote { 176 | uint64_t id; 177 | struct sbk_recipient *recipient; 178 | char *text; 179 | struct sbk_attachment_list *attachments; 180 | struct sbk_mention_list *mentions; 181 | }; 182 | 183 | struct sbk_message_id { 184 | #define SBK_SMS_TABLE 0 185 | #define SBK_MMS_TABLE 1 186 | #define SBK_SINGLE_TABLE 2 187 | int table; 188 | int row_id; 189 | }; 190 | 191 | struct sbk_edit { 192 | struct sbk_message_id id; 193 | int revision; 194 | uint64_t time_sent; 195 | uint64_t time_recv; 196 | char *text; 197 | struct sbk_attachment_list *attachments; 198 | struct sbk_mention_list *mentions; 199 | struct sbk_quote *quote; 200 | TAILQ_ENTRY(sbk_edit) entries; 201 | }; 202 | 203 | TAILQ_HEAD(sbk_edit_list, sbk_edit); 204 | 205 | struct sbk_message { 206 | struct sbk_message_id id; 207 | struct sbk_recipient *recipient; 208 | uint64_t time_sent; 209 | uint64_t time_recv; 210 | int type; 211 | int thread; 212 | char *text; 213 | struct sbk_attachment_list *attachments; 214 | struct sbk_mention_list *mentions; 215 | struct sbk_reaction_list *reactions; 216 | struct sbk_quote *quote; 217 | struct sbk_edit_list *edits; 218 | int nedits; 219 | SIMPLEQ_ENTRY(sbk_message) entries; 220 | }; 221 | 222 | SIMPLEQ_HEAD(sbk_message_list, sbk_message); 223 | 224 | struct sbk_thread { 225 | struct sbk_recipient *recipient; 226 | uint64_t id; 227 | uint64_t date; 228 | uint64_t nmessages; 229 | SIMPLEQ_ENTRY(sbk_thread) entries; 230 | }; 231 | 232 | SIMPLEQ_HEAD(sbk_thread_list, sbk_thread); 233 | 234 | struct sbk_ctx *sbk_ctx_new(void); 235 | void sbk_ctx_free(struct sbk_ctx *); 236 | 237 | int sbk_open(struct sbk_ctx *, const char *, const char *); 238 | void sbk_close(struct sbk_ctx *); 239 | int sbk_eof(struct sbk_ctx *); 240 | int sbk_rewind(struct sbk_ctx *); 241 | 242 | Signal__BackupFrame *sbk_get_frame(struct sbk_ctx *, struct sbk_file **); 243 | int sbk_write_file(struct sbk_ctx *, struct sbk_file *, FILE *); 244 | char *sbk_get_file_data(struct sbk_ctx *, struct sbk_file *, 245 | size_t *); 246 | void sbk_free_frame(Signal__BackupFrame *); 247 | void sbk_free_file(struct sbk_file *); 248 | 249 | struct sbk_attachment_list *sbk_get_attachments_for_thread(struct sbk_ctx *, 250 | struct sbk_thread *); 251 | void sbk_free_attachment_list(struct sbk_attachment_list *); 252 | const char *sbk_attachment_id_to_string(const struct sbk_attachment *); 253 | 254 | struct sbk_message_list *sbk_get_messages_for_thread(struct sbk_ctx *, 255 | struct sbk_thread *); 256 | void sbk_free_message_list(struct sbk_message_list *); 257 | int sbk_is_outgoing_message(const struct sbk_message *); 258 | const char *sbk_message_id_to_string(const struct sbk_message *); 259 | 260 | struct sbk_thread_list *sbk_get_threads(struct sbk_ctx *); 261 | void sbk_free_thread_list(struct sbk_thread_list *); 262 | 263 | const char *sbk_get_recipient_display_name(const struct sbk_recipient *); 264 | 265 | int sbk_write_database(struct sbk_ctx *, const char *); 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /cmd-dump-backup.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "sigbak.h" 27 | 28 | static enum cmd_status cmd_dump_backup(int, char **); 29 | 30 | const struct cmd_entry cmd_dump_backup_entry = { 31 | .name = "dump-backup", 32 | .alias = "dump", 33 | .usage = "[-p passfile] backup", 34 | .exec = cmd_dump_backup 35 | }; 36 | 37 | static void 38 | dump_var(unsigned int ind, const char *name, const char *type, const char *fmt, 39 | ...) 40 | { 41 | va_list ap; 42 | 43 | va_start(ap, fmt); 44 | while (ind-- > 0) 45 | putchar('\t'); 46 | printf("%s (%s):", name, type); 47 | if (fmt != NULL) { 48 | putchar(' '); 49 | vprintf(fmt, ap); 50 | } 51 | putchar('\n'); 52 | va_end(ap); 53 | } 54 | 55 | static void 56 | dump_bool(unsigned int ind, const char *name, int val) 57 | { 58 | dump_var(ind, name, "bool", "%d", val); 59 | } 60 | 61 | static void 62 | dump_int32(unsigned int ind, const char *name, int32_t val) 63 | { 64 | dump_var(ind, name, "int32", "%" PRId32, val); 65 | } 66 | 67 | static void 68 | dump_uint32(unsigned int ind, const char *name, uint32_t val) 69 | { 70 | dump_var(ind, name, "uint32", "%" PRIu32, val); 71 | } 72 | 73 | static void 74 | dump_int64(unsigned int ind, const char *name, int64_t val) 75 | { 76 | dump_var(ind, name, "int64", "%" PRId64, val); 77 | } 78 | 79 | static void 80 | dump_uint64(unsigned int ind, const char *name, uint64_t val) 81 | { 82 | dump_var(ind, name, "uint64", "%" PRIu64, val); 83 | } 84 | 85 | static void 86 | dump_float(unsigned int ind, const char *name, float val) 87 | { 88 | dump_var(ind, name, "float", "%g", val); 89 | } 90 | 91 | static void 92 | dump_double(unsigned int ind, const char *name, double val) 93 | { 94 | dump_var(ind, name, "double", "%g", val); 95 | } 96 | 97 | static void 98 | dump_string(unsigned int ind, const char *name, const char *val) 99 | { 100 | dump_var(ind, name, "string", "%s", val); 101 | } 102 | 103 | static void 104 | dump_binary(unsigned int ind, const char *name, ProtobufCBinaryData *bin) 105 | { 106 | char *hex; 107 | size_t i; 108 | 109 | if (bin->len == SIZE_MAX) { 110 | warnx("Binary data too large"); 111 | return; 112 | } 113 | 114 | if ((hex = reallocarray(NULL, bin->len + 1, 2)) == NULL) { 115 | warn(NULL); 116 | return; 117 | } 118 | 119 | if (bin->len == 0) 120 | hex[0] = '\0'; 121 | else 122 | for (i = 0; i < bin->len; i++) 123 | snprintf(hex + (i * 2), 3, "%02" PRIx8, bin->data[i]); 124 | 125 | dump_var(ind, name, "bytes", "%s", hex); 126 | free(hex); 127 | } 128 | 129 | static void 130 | dump_attachment(unsigned int ind, const char *name, Signal__Attachment *att) 131 | { 132 | dump_var(ind++, name, "Attachment", NULL); 133 | if (att->has_rowid) 134 | dump_uint64(ind, "rowId", att->rowid); 135 | if (att->has_attachmentid) 136 | dump_uint64(ind, "attachmentId", att->attachmentid); 137 | if (att->has_length) 138 | dump_uint32(ind, "length", att->length); 139 | } 140 | 141 | static void 142 | dump_avatar(unsigned int ind, const char *name, Signal__Avatar *avt) 143 | { 144 | dump_var(ind++, name, "Avatar", NULL); 145 | if (avt->name != NULL) 146 | dump_string(ind, "name", avt->name); 147 | if (avt->has_length) 148 | dump_uint32(ind, "length", avt->length); 149 | if (avt->recipientid != NULL) 150 | dump_string(ind, "recipientId", avt->recipientid); 151 | } 152 | 153 | static void 154 | dump_header(unsigned int ind, const char *name, Signal__Header *hdr) 155 | { 156 | dump_var(ind++, name, "Header", NULL); 157 | if (hdr->has_iv) 158 | dump_binary(ind, "iv", &hdr->iv); 159 | if (hdr->has_salt) 160 | dump_binary(ind, "salt", &hdr->salt); 161 | if (hdr->has_version) 162 | dump_uint32(ind, "version", hdr->version); 163 | } 164 | 165 | static void 166 | dump_preference(unsigned int ind, const char *name, 167 | Signal__SharedPreference *prf) 168 | { 169 | size_t i; 170 | 171 | dump_var(ind++, name, "SharedPreference", NULL); 172 | if (prf->file != NULL) 173 | dump_string(ind, "file", prf->file); 174 | if (prf->key != NULL) 175 | dump_string(ind, "key", prf->key); 176 | if (prf->value != NULL) 177 | dump_string(ind, "value", prf->value); 178 | if (prf->has_booleanvalue) 179 | dump_bool(ind, "booleanValue", prf->booleanvalue); 180 | for (i = 0; i < prf->n_stringsetvalue; i++) 181 | dump_string(ind, "stringSetValue", prf->stringsetvalue[i]); 182 | if (prf->has_isstringsetvalue) 183 | dump_bool(ind, "isStringSetValue", prf->isstringsetvalue); 184 | } 185 | 186 | static void 187 | dump_parameter(unsigned int ind, const char *name, 188 | Signal__SqlStatement__SqlParameter *par) 189 | { 190 | dump_var(ind++, name, "SqlParameter", NULL); 191 | if (par->stringparamter != NULL) 192 | dump_string(ind, "stringParamter", par->stringparamter); 193 | if (par->has_integerparameter) 194 | dump_uint64(ind, "integerParameter", par->integerparameter); 195 | if (par->has_doubleparameter) 196 | dump_double(ind, "doubleParameter", par->doubleparameter); 197 | if (par->has_blobparameter) 198 | dump_binary(ind, "blobParameter", &par->blobparameter); 199 | if (par->has_nullparameter) 200 | dump_bool(ind, "nullparameter", par->nullparameter); 201 | } 202 | 203 | static void 204 | dump_statement(unsigned int ind, const char *name, Signal__SqlStatement *stm) 205 | { 206 | size_t i; 207 | 208 | dump_var(ind++, name, "SqlStatement", NULL); 209 | if (stm->statement != NULL) 210 | dump_string(ind, "statement", stm->statement); 211 | for (i = 0; i < stm->n_parameters; i++) 212 | dump_parameter(ind, "parameters", stm->parameters[i]); 213 | } 214 | 215 | static void 216 | dump_sticker(unsigned int ind, const char *name, Signal__Sticker *stk) 217 | { 218 | dump_var(ind++, name, "Sticker", NULL); 219 | if (stk->has_rowid) 220 | dump_uint64(ind, "rowId", stk->rowid); 221 | if (stk->has_length) 222 | dump_uint32(ind, "length", stk->length); 223 | } 224 | 225 | static void 226 | dump_version(unsigned int ind, const char *name, Signal__DatabaseVersion *ver) 227 | { 228 | dump_var(ind++, name, "DatabaseVersion", NULL); 229 | if (ver->has_version) 230 | dump_uint32(ind, "version", ver->version); 231 | } 232 | 233 | static void 234 | dump_keyvalue(unsigned int ind, const char *name, Signal__KeyValue *key) 235 | { 236 | dump_var(ind++, name, "KeyValue", NULL); 237 | if (key->key != NULL) 238 | dump_string(ind, "key", key->key); 239 | if (key->has_blobvalue) 240 | dump_binary(ind, "blobValue", &key->blobvalue); 241 | if (key->has_booleanvalue) 242 | dump_bool(ind, "booleanValue", key->booleanvalue); 243 | if (key->has_floatvalue) 244 | dump_float(ind, "floatValue", key->floatvalue); 245 | if (key->has_integervalue) 246 | dump_int32(ind, "integerValue", key->integervalue); 247 | if (key->has_longvalue) 248 | dump_int64(ind, "longValue", key->longvalue); 249 | if (key->stringvalue != NULL) 250 | dump_string(ind, "stringValue", key->stringvalue); 251 | } 252 | 253 | static void 254 | dump_frame(Signal__BackupFrame *frm) 255 | { 256 | dump_var(0, "frame", "BackupFrame", NULL); 257 | if (frm->header != NULL) 258 | dump_header(1, "header", frm->header); 259 | if (frm->statement != NULL) 260 | dump_statement(1, "statement", frm->statement); 261 | if (frm->preference != NULL) 262 | dump_preference(1, "preference", frm->preference); 263 | if (frm->attachment != NULL) 264 | dump_attachment(1, "attachment", frm->attachment); 265 | if (frm->version != NULL) 266 | dump_version(1, "version", frm->version); 267 | if (frm->has_end) 268 | dump_bool(1, "end", frm->end); 269 | if (frm->avatar != NULL) 270 | dump_avatar(1, "avatar", frm->avatar); 271 | if (frm->sticker != NULL) 272 | dump_sticker(1, "sticker", frm->sticker); 273 | if (frm->keyvalue != NULL) 274 | dump_keyvalue(1, "keyValue", frm->keyvalue); 275 | } 276 | 277 | static enum cmd_status 278 | cmd_dump_backup(int argc, char **argv) 279 | { 280 | struct sbk_ctx *ctx; 281 | Signal__BackupFrame *frm; 282 | char *backup, *passfile, passphr[128]; 283 | int c, ret; 284 | 285 | passfile = NULL; 286 | 287 | while ((c = getopt(argc, argv, "p:")) != -1) 288 | switch (c) { 289 | case 'p': 290 | passfile = optarg; 291 | break; 292 | default: 293 | return CMD_USAGE; 294 | } 295 | 296 | argc -= optind; 297 | argv += optind; 298 | 299 | if (argc != 1) 300 | return CMD_USAGE; 301 | 302 | backup = argv[0]; 303 | 304 | if (unveil(backup, "r") == -1) 305 | err(1, "unveil: %s", backup); 306 | 307 | if (passfile == NULL) { 308 | if (pledge("stdio rpath tty", NULL) == -1) 309 | err(1, "pledge"); 310 | } else { 311 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 312 | err(1, "unveil: %s", passfile); 313 | 314 | if (pledge("stdio rpath", NULL) == -1) 315 | err(1, "pledge"); 316 | } 317 | 318 | if ((ctx = sbk_ctx_new()) == NULL) 319 | return CMD_ERROR; 320 | 321 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 322 | sbk_ctx_free(ctx); 323 | return CMD_ERROR; 324 | } 325 | 326 | if (sbk_open(ctx, backup, passphr) == -1) { 327 | explicit_bzero(passphr, sizeof passphr); 328 | sbk_ctx_free(ctx); 329 | return CMD_ERROR; 330 | } 331 | 332 | explicit_bzero(passphr, sizeof passphr); 333 | 334 | if (pledge("stdio", NULL) == -1) 335 | err(1, "pledge"); 336 | 337 | while ((frm = sbk_get_frame(ctx, NULL)) != NULL) { 338 | dump_frame(frm); 339 | sbk_free_frame(frm); 340 | } 341 | 342 | ret = sbk_eof(ctx) ? CMD_OK : CMD_ERROR; 343 | sbk_close(ctx); 344 | sbk_ctx_free(ctx); 345 | return ret; 346 | } 347 | -------------------------------------------------------------------------------- /cmd-export-attachments.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "sigbak.h" 33 | 34 | #define FLAG_EXPORT_ALL 0x1 35 | #define FLAG_FILENAME_ID 0x2 36 | #define FLAG_MTIME_SENT 0x4 37 | #define FLAG_MTIME_RECV 0x8 38 | 39 | #define FLAG_MTIME_MASK (FLAG_MTIME_SENT | FLAG_MTIME_RECV) 40 | 41 | static enum cmd_status cmd_export_attachments(int, char **); 42 | 43 | const struct cmd_entry cmd_export_attachments_entry = { 44 | .name = "export-attachments", 45 | .alias = "att", 46 | .usage = "[-aIMm] [-p passfile] backup [directory]", 47 | .exec = cmd_export_attachments 48 | }; 49 | 50 | /* 51 | * Create a file with a unique name. If a file with the specified name already 52 | * exists, a new, unique name is used. Given a name of the form "base[.ext]", 53 | * the new name is of the form "base-n[.ext]" where 1 < n < 1000. 54 | */ 55 | static FILE * 56 | create_unique_file(int dfd, const char *name) 57 | { 58 | FILE *fp; 59 | char *buf; 60 | const char *base, *ext, *oldname; 61 | size_t baselen, bufsize, namelen; 62 | int fd, i; 63 | 64 | buf = NULL; 65 | oldname = name; 66 | 67 | for (i = 2; i <= 1000; i++) { 68 | fd = openat(dfd, name, O_WRONLY | O_CREAT | O_EXCL, 0666); 69 | if (fd != -1) 70 | goto out; 71 | if (errno != EEXIST) { 72 | warn("%s", name); 73 | goto out; 74 | } 75 | 76 | if (buf == NULL) { 77 | namelen = strlen(name); 78 | 79 | ext = strrchr(name, '.'); 80 | if (ext != NULL && ext != name && ext[1] != '\0') 81 | baselen = ext - name; 82 | else { 83 | baselen = namelen; 84 | ext = ""; 85 | } 86 | 87 | if (namelen > SIZE_MAX - 5 || baselen > INT_MAX) { 88 | warnx("Attachment filename too long"); 89 | goto out; 90 | } 91 | 92 | /* 4 for the "-n" affix and 1 for the NUL */ 93 | bufsize = namelen + 5; 94 | if ((buf = malloc(bufsize)) == NULL) { 95 | warn(NULL); 96 | goto out; 97 | } 98 | 99 | base = name; 100 | name = buf; 101 | } 102 | 103 | snprintf(buf, bufsize, "%.*s-%d%s", (int)baselen, base, i, 104 | ext); 105 | } 106 | 107 | warnx("%s: Cannot generate unique name", oldname); 108 | 109 | out: 110 | if (fd == -1) 111 | fp = NULL; 112 | else if ((fp = fdopen(fd, "w")) == NULL) { 113 | warn("%s", name); 114 | close(fd); 115 | } 116 | 117 | free(buf); 118 | return fp; 119 | } 120 | 121 | static FILE * 122 | get_file(int dfd, struct sbk_attachment *att, int flags) 123 | { 124 | FILE *fp; 125 | struct tm *tm; 126 | char *name, *tmp; 127 | const char *ext; 128 | time_t tt; 129 | char base[100]; 130 | 131 | if (att->filename != NULL && *att->filename != '\0') { 132 | if ((name = strdup(att->filename)) == NULL) { 133 | warn(NULL); 134 | return NULL; 135 | } 136 | sanitise_filename(name); 137 | } else { 138 | tt = att->time_sent / 1000; 139 | if ((tm = localtime(&tt)) == NULL) { 140 | warnx("localtime() failed"); 141 | return NULL; 142 | } 143 | snprintf(base, sizeof base, 144 | "attachment-%d-%02d-%02d-%02d-%02d-%02d", 145 | tm->tm_year + 1900, 146 | tm->tm_mon + 1, 147 | tm->tm_mday, 148 | tm->tm_hour, 149 | tm->tm_min, 150 | tm->tm_sec); 151 | if (att->content_type == NULL) 152 | ext = NULL; 153 | else 154 | ext = mime_get_extension(att->content_type); 155 | if (ext == NULL) { 156 | if ((name = strdup(base)) == NULL) { 157 | warn(NULL); 158 | return NULL; 159 | } 160 | } else { 161 | if (asprintf(&name, "%s.%s", base, ext) == -1) { 162 | warnx("asprintf() failed"); 163 | return NULL; 164 | } 165 | } 166 | } 167 | 168 | if (flags & FLAG_FILENAME_ID) { 169 | if (asprintf(&tmp, "%s %s", 170 | sbk_attachment_id_to_string(att), name) == -1) { 171 | warnx("asprintf() failed"); 172 | free(name); 173 | return NULL; 174 | } 175 | free(name); 176 | name = tmp; 177 | } 178 | 179 | fp = create_unique_file(dfd, name); 180 | free(name); 181 | return fp; 182 | } 183 | 184 | static int 185 | get_thread_directory(int dfd, struct sbk_thread *thd) 186 | { 187 | char *name; 188 | int thd_dfd; 189 | 190 | if ((name = get_recipient_filename(thd->recipient, NULL)) == NULL) 191 | return -1; 192 | 193 | if (mkdirat(dfd, name, 0777) == -1 && errno != EEXIST) { 194 | warn("%s", name); 195 | free(name); 196 | return -1; 197 | } 198 | 199 | if ((thd_dfd = openat(dfd, name, O_RDONLY | O_DIRECTORY)) == -1) { 200 | warn("%s", name); 201 | free(name); 202 | return -1; 203 | } 204 | 205 | free(name); 206 | return thd_dfd; 207 | } 208 | 209 | static int 210 | set_mtime(FILE *fp, uint64_t msec) 211 | { 212 | struct timespec ts[2]; 213 | 214 | if (fflush(fp) == EOF) { 215 | warn("fflush"); 216 | return -1; 217 | } 218 | 219 | ts[0].tv_nsec = UTIME_OMIT; 220 | ts[1].tv_sec = msec / 1000; 221 | ts[1].tv_nsec = msec % 1000 * 1000000; 222 | 223 | if (futimens(fileno(fp), ts) == -1) { 224 | warn("futimens"); 225 | return -1; 226 | } 227 | 228 | return 0; 229 | } 230 | 231 | static int 232 | export_thread_attachments(struct sbk_ctx *ctx, struct sbk_thread *thd, int dfd, 233 | int flags) 234 | { 235 | struct sbk_attachment_list *lst; 236 | struct sbk_attachment *att, *tmp; 237 | FILE *fp; 238 | int ret, thd_dfd; 239 | 240 | if ((lst = sbk_get_attachments_for_thread(ctx, thd)) == NULL) 241 | return -1; 242 | 243 | if (~flags & FLAG_EXPORT_ALL) { 244 | /* Skip long-message attachments */ 245 | TAILQ_FOREACH_SAFE(att, lst, entries, tmp) 246 | if (att->content_type != NULL && 247 | strcmp(att->content_type, SBK_LONG_TEXT_TYPE) == 0) 248 | TAILQ_REMOVE(lst, att, entries); 249 | } 250 | 251 | if (TAILQ_EMPTY(lst)) { 252 | sbk_free_attachment_list(lst); 253 | return 0; 254 | } 255 | 256 | if ((thd_dfd = get_thread_directory(dfd, thd)) == -1) { 257 | sbk_free_attachment_list(lst); 258 | return -1; 259 | } 260 | 261 | ret = 0; 262 | TAILQ_FOREACH(att, lst, entries) { 263 | if (att->file == NULL) 264 | continue; 265 | 266 | if ((fp = get_file(thd_dfd, att, flags)) == NULL) { 267 | ret = -1; 268 | continue; 269 | } 270 | 271 | if (sbk_write_file(ctx, att->file, fp) == -1) { 272 | fclose(fp); 273 | ret = -1; 274 | continue; 275 | } 276 | 277 | switch (flags & FLAG_MTIME_MASK) { 278 | case FLAG_MTIME_SENT: 279 | if (set_mtime(fp, att->time_sent) == -1) { 280 | fclose(fp); 281 | ret = -1; 282 | continue; 283 | } 284 | break; 285 | case FLAG_MTIME_RECV: 286 | if (set_mtime(fp, att->time_recv) == -1) { 287 | fclose(fp); 288 | ret = -1; 289 | continue; 290 | } 291 | break; 292 | } 293 | 294 | if (fclose(fp) == EOF) { 295 | warn("fclose"); 296 | ret = -1; 297 | } 298 | } 299 | 300 | close(thd_dfd); 301 | sbk_free_attachment_list(lst); 302 | return ret; 303 | } 304 | 305 | static int 306 | export_attachments(struct sbk_ctx *ctx, const char *outdir, int flags) 307 | { 308 | struct sbk_thread_list *lst; 309 | struct sbk_thread *thd; 310 | int dfd, ret; 311 | 312 | if ((dfd = open(outdir, O_RDONLY | O_DIRECTORY)) == -1) { 313 | warn("%s", outdir); 314 | return -1; 315 | } 316 | 317 | if ((lst = sbk_get_threads(ctx)) == NULL) { 318 | close(dfd); 319 | return -1; 320 | } 321 | 322 | ret = 0; 323 | SIMPLEQ_FOREACH(thd, lst, entries) { 324 | if (export_thread_attachments(ctx, thd, dfd, flags) == -1) 325 | ret = -1; 326 | } 327 | 328 | sbk_free_thread_list(lst); 329 | close(dfd); 330 | return ret; 331 | } 332 | 333 | static enum cmd_status 334 | cmd_export_attachments(int argc, char **argv) 335 | { 336 | struct sbk_ctx *ctx; 337 | char *backup, *passfile, passphr[128]; 338 | const char *outdir; 339 | int c, flags, ret; 340 | 341 | flags = 0; 342 | passfile = NULL; 343 | 344 | while ((c = getopt(argc, argv, "aIMmp:")) != -1) 345 | switch (c) { 346 | case 'a': 347 | flags |= FLAG_EXPORT_ALL; 348 | break; 349 | case 'I': 350 | flags |= FLAG_FILENAME_ID; 351 | break; 352 | case 'M': 353 | flags &= ~FLAG_MTIME_RECV; 354 | flags |= FLAG_MTIME_SENT; 355 | break; 356 | case 'm': 357 | flags &= ~FLAG_MTIME_SENT; 358 | flags |= FLAG_MTIME_RECV; 359 | break; 360 | case 'p': 361 | passfile = optarg; 362 | break; 363 | default: 364 | return CMD_USAGE; 365 | } 366 | 367 | argc -= optind; 368 | argv += optind; 369 | 370 | switch (argc) { 371 | case 1: 372 | backup = argv[0]; 373 | outdir = "."; 374 | break; 375 | case 2: 376 | backup = argv[0]; 377 | outdir = argv[1]; 378 | if (mkdir(outdir, 0777) == -1 && errno != EEXIST) { 379 | warn("mkdir: %s", outdir); 380 | return CMD_ERROR; 381 | } 382 | break; 383 | default: 384 | return CMD_USAGE; 385 | } 386 | 387 | if (unveil(backup, "r") == -1) 388 | err(1, "unveil: %s", backup); 389 | 390 | if (unveil(outdir, "rwc") == -1) 391 | err(1, "unveil: %s", outdir); 392 | 393 | /* For SQLite */ 394 | if (unveil("/dev/urandom", "r") == -1) 395 | err(1, "unveil: /dev/urandom"); 396 | 397 | /* For SQLite */ 398 | if (unveil("/tmp", "rwc") == -1) 399 | err(1, "unveil: /tmp"); 400 | 401 | if (passfile == NULL) { 402 | if (pledge("stdio rpath wpath cpath fattr tty", NULL) == -1) 403 | err(1, "pledge"); 404 | } else { 405 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 406 | err(1, "unveil: %s", passfile); 407 | 408 | if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) 409 | err(1, "pledge"); 410 | } 411 | 412 | if ((ctx = sbk_ctx_new()) == NULL) 413 | return CMD_ERROR; 414 | 415 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 416 | sbk_ctx_free(ctx); 417 | return CMD_ERROR; 418 | } 419 | 420 | if (sbk_open(ctx, backup, passphr) == -1) { 421 | explicit_bzero(passphr, sizeof passphr); 422 | sbk_ctx_free(ctx); 423 | return CMD_ERROR; 424 | } 425 | 426 | explicit_bzero(passphr, sizeof passphr); 427 | 428 | if (flags & FLAG_MTIME_MASK) { 429 | if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) 430 | err(1, "pledge"); 431 | } else { 432 | if (pledge("stdio rpath wpath cpath", NULL) == -1) 433 | err(1, "pledge"); 434 | } 435 | 436 | ret = export_attachments(ctx, outdir, flags); 437 | sbk_close(ctx); 438 | sbk_ctx_free(ctx); 439 | return (ret == -1) ? CMD_ERROR : CMD_OK; 440 | } 441 | -------------------------------------------------------------------------------- /sbk-recipient-tree.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | /* For database versions < RECIPIENT_IDS */ 24 | #define SBK_QUERY_1 \ 25 | "SELECT " \ 26 | "r.recipient_ids, " \ 27 | "NULL, " /* e164 */ \ 28 | "NULL, " /* aci */ \ 29 | "NULL, " /* email */ \ 30 | "r.signal_profile_name, " /* profile_given_name */ \ 31 | "NULL, " /* profile_family_name */ \ 32 | "NULL, " /* profile_joined_name */ \ 33 | "r.system_display_name, " /* system_joined_name */ \ 34 | "r.system_phone_label, " \ 35 | "NULL, " /* nickname_given_name */ \ 36 | "NULL, " /* nickname_family_name */ \ 37 | "NULL, " /* nickname_joined_name */ \ 38 | "g.group_id, " \ 39 | "g.title " \ 40 | "FROM recipient_preferences AS r " \ 41 | "LEFT JOIN groups AS g " \ 42 | "ON r.recipient_ids = g.group_id" 43 | 44 | /* For database versions [RECIPIENT_IDS, SPLIT_PROFILE_NAMES) */ 45 | #define SBK_QUERY_2 \ 46 | "SELECT " \ 47 | "r._id, " \ 48 | "r.phone, " /* e164 */ \ 49 | "r.uuid, " /* aci */ \ 50 | "r.email, " \ 51 | "r.signal_profile_name, " /* profile_given_name */ \ 52 | "NULL, " /* profile_family_name */ \ 53 | "NULL, " /* profile_joined_name */ \ 54 | "r.system_display_name, " /* system_joined_name */ \ 55 | "r.system_phone_label, " \ 56 | "NULL, " /* nickname_given_name */ \ 57 | "NULL, " /* nickname_family_name */ \ 58 | "NULL, " /* nickname_joined_name */ \ 59 | "g.group_id, " \ 60 | "g.title " \ 61 | "FROM recipient AS r " \ 62 | "LEFT JOIN groups AS g " \ 63 | "ON r._id = g.recipient_id" 64 | 65 | /* For database versions [SPLIT_PROFILE_NAMES, RESET_PNI_COLUMN) */ 66 | #define SBK_QUERY_3 \ 67 | "SELECT " \ 68 | "r._id, " \ 69 | "r.phone, " /* e164 */ \ 70 | "r.uuid, " /* aci */ \ 71 | "r.email, " \ 72 | "r.signal_profile_name, " /* profile_given_name */ \ 73 | "r.profile_family_name, " \ 74 | "r.profile_joined_name, " \ 75 | "r.system_display_name, " /* system_joined_name */ \ 76 | "r.system_phone_label, " \ 77 | "NULL, " /* nickname_given_name */ \ 78 | "NULL, " /* nickname_family_name */ \ 79 | "NULL, " /* nickname_joined_name */ \ 80 | "g.group_id, " \ 81 | "g.title " \ 82 | "FROM recipient AS r " \ 83 | "LEFT JOIN groups AS g " \ 84 | "ON r._id = g.recipient_id" 85 | 86 | /* For database versions [RESET_PNI_COLUMN, RECIPIENT_TABLE_VALIDATIONS) */ 87 | #define SBK_QUERY_4 \ 88 | "SELECT " \ 89 | "r._id, " \ 90 | "r.phone, " /* e164 */ \ 91 | "r.aci, " \ 92 | "r.email, " \ 93 | "r.signal_profile_name, " /* profile_given_name */ \ 94 | "r.profile_family_name, " \ 95 | "r.profile_joined_name, " \ 96 | "r.system_display_name, " /* system_joined_name */ \ 97 | "r.system_phone_label, " \ 98 | "NULL, " /* nickname_given_name */ \ 99 | "NULL, " /* nickname_family_name */ \ 100 | "NULL, " /* nickname_joined_name */ \ 101 | "g.group_id, " \ 102 | "g.title " \ 103 | "FROM recipient AS r " \ 104 | "LEFT JOIN groups AS g " \ 105 | "ON r._id = g.recipient_id" 106 | 107 | /* 108 | * For database versions [RECIPIENT_TABLE_VALIDATIONS, 109 | * ADD_NICKNAME_AND_NOTE_FIELDS_TO_RECIPIENT_TABLE) 110 | */ 111 | #define SBK_QUERY_5 \ 112 | "SELECT " \ 113 | "r._id, " \ 114 | "r.e164, " \ 115 | "r.aci, " \ 116 | "r.email, " \ 117 | "r.profile_given_name, " \ 118 | "r.profile_family_name, " \ 119 | "r.profile_joined_name, " \ 120 | "r.system_joined_name, " \ 121 | "r.system_phone_label, " \ 122 | "NULL, " /* nickname_given_name */ \ 123 | "NULL, " /* nickname_family_name */ \ 124 | "NULL, " /* nickname_joined_name */ \ 125 | "g.group_id, " \ 126 | "g.title " \ 127 | "FROM recipient AS r " \ 128 | "LEFT JOIN groups AS g " \ 129 | "ON r._id = g.recipient_id" 130 | 131 | /* For database versions >= ADD_NICKNAME_AND_NOTE_FIELDS_TO_RECIPIENT_TABLE */ 132 | #define SBK_QUERY_6 \ 133 | "SELECT " \ 134 | "r._id, " \ 135 | "r.e164, " \ 136 | "r.aci, " \ 137 | "r.email, " \ 138 | "r.profile_given_name, " \ 139 | "r.profile_family_name, " \ 140 | "r.profile_joined_name, " \ 141 | "r.system_joined_name, " \ 142 | "r.system_phone_label, " \ 143 | "r.nickname_given_name, " \ 144 | "r.nickname_family_name, " \ 145 | "r.nickname_joined_name, " \ 146 | "g.group_id, " \ 147 | "g.title " \ 148 | "FROM recipient AS r " \ 149 | "LEFT JOIN groups AS g " \ 150 | "ON r._id = g.recipient_id" 151 | 152 | #define SBK_COLUMN__ID 0 153 | #define SBK_COLUMN_E164 1 154 | #define SBK_COLUMN_ACI 2 155 | #define SBK_COLUMN_EMAIL 3 156 | #define SBK_COLUMN_PROFILE_GIVEN_NAME 4 157 | #define SBK_COLUMN_PROFILE_FAMILY_NAME 5 158 | #define SBK_COLUMN_PROFILE_JOINED_NAME 6 159 | #define SBK_COLUMN_SYSTEM_JOINED_NAME 7 160 | #define SBK_COLUMN_SYSTEM_PHONE_LABEL 8 161 | #define SBK_COLUMN_NICKNAME_GIVEN_NAME 9 162 | #define SBK_COLUMN_NICKNAME_FAMILY_NAME 10 163 | #define SBK_COLUMN_NICKNAME_JOINED_NAME 11 164 | #define SBK_COLUMN_GROUP_ID 12 165 | #define SBK_COLUMN_TITLE 13 166 | 167 | static int sbk_cmp_recipient_entries(struct sbk_recipient_entry *, 168 | struct sbk_recipient_entry *); 169 | 170 | RB_GENERATE_STATIC(sbk_recipient_tree, sbk_recipient_entry, entries, 171 | sbk_cmp_recipient_entries) 172 | 173 | static void 174 | sbk_free_recipient_entry(struct sbk_recipient_entry *ent) 175 | { 176 | if (ent == NULL) 177 | return; 178 | 179 | switch (ent->recipient.type) { 180 | case SBK_CONTACT: 181 | if (ent->recipient.contact != NULL) { 182 | free(ent->recipient.contact->aci); 183 | free(ent->recipient.contact->phone); 184 | free(ent->recipient.contact->email); 185 | free(ent->recipient.contact->system_joined_name); 186 | free(ent->recipient.contact->system_phone_label); 187 | free(ent->recipient.contact->profile_given_name); 188 | free(ent->recipient.contact->profile_family_name); 189 | free(ent->recipient.contact->profile_joined_name); 190 | free(ent->recipient.contact); 191 | } 192 | break; 193 | case SBK_GROUP: 194 | if (ent->recipient.group != NULL) { 195 | free(ent->recipient.group->name); 196 | free(ent->recipient.group); 197 | } 198 | break; 199 | } 200 | 201 | free(ent->id.old); 202 | free(ent); 203 | } 204 | 205 | void 206 | sbk_free_recipient_tree(struct sbk_ctx *ctx) 207 | { 208 | struct sbk_recipient_entry *ent; 209 | 210 | while ((ent = RB_ROOT(&ctx->recipients)) != NULL) { 211 | RB_REMOVE(sbk_recipient_tree, &ctx->recipients, ent); 212 | sbk_free_recipient_entry(ent); 213 | } 214 | } 215 | 216 | static int 217 | sbk_cmp_recipient_entries(struct sbk_recipient_entry *e, 218 | struct sbk_recipient_entry *f) 219 | { 220 | if (e->id.old != NULL) 221 | return strcmp(e->id.old, f->id.old); 222 | else 223 | return (e->id.new < f->id.new) ? -1 : (e->id.new > f->id.new); 224 | } 225 | 226 | static int 227 | sbk_get_recipient_id_from_column(struct sbk_ctx *ctx, 228 | struct sbk_recipient_id *id, sqlite3_stmt *stm, int idx) 229 | { 230 | if (ctx->db_version >= SBK_DB_VERSION_RECIPIENT_IDS) { 231 | id->new = sqlite3_column_int(stm, idx); 232 | id->old = NULL; 233 | } else { 234 | id->new = -1; 235 | if (sbk_sqlite_column_text_copy(ctx, &id->old, stm, idx) == -1) 236 | return -1; 237 | if (id->old == NULL) { 238 | warnx("Invalid recipient id"); 239 | return -1; 240 | } 241 | } 242 | 243 | return 0; 244 | } 245 | 246 | static struct sbk_recipient_entry * 247 | sbk_get_recipient_entry(struct sbk_ctx *ctx, sqlite3_stmt *stm) 248 | { 249 | struct sbk_recipient_entry *ent; 250 | struct sbk_contact *con; 251 | struct sbk_group *grp; 252 | 253 | if ((ent = calloc(1, sizeof *ent)) == NULL) { 254 | warn(NULL); 255 | return NULL; 256 | } 257 | 258 | if (sbk_get_recipient_id_from_column(ctx, &ent->id, stm, 259 | SBK_COLUMN__ID) == -1) 260 | goto error; 261 | 262 | if (sqlite3_column_type(stm, SBK_COLUMN_GROUP_ID) == SQLITE_NULL) 263 | ent->recipient.type = SBK_CONTACT; 264 | else 265 | ent->recipient.type = SBK_GROUP; 266 | 267 | switch (ent->recipient.type) { 268 | case SBK_CONTACT: 269 | con = ent->recipient.contact = calloc(1, sizeof *con); 270 | if (con == NULL) { 271 | warn(NULL); 272 | goto error; 273 | } 274 | 275 | if (ctx->db_version >= SBK_DB_VERSION_RECIPIENT_IDS) { 276 | if (sbk_sqlite_column_text_copy(ctx, &con->phone, 277 | stm, SBK_COLUMN_E164) == -1) 278 | goto error; 279 | 280 | if (sbk_sqlite_column_text_copy(ctx, &con->email, 281 | stm, SBK_COLUMN_EMAIL) == -1) 282 | goto error; 283 | } else { 284 | if (strchr(ent->id.old, '@') != NULL) { 285 | con->email = strdup(ent->id.old); 286 | if (con->email == NULL) { 287 | warn(NULL); 288 | goto error; 289 | } 290 | } else { 291 | con->phone = strdup(ent->id.old); 292 | if (con->phone == NULL) { 293 | warn(NULL); 294 | goto error; 295 | } 296 | } 297 | } 298 | 299 | if (sbk_sqlite_column_text_copy(ctx, &con->aci, 300 | stm, SBK_COLUMN_ACI) == -1) 301 | goto error; 302 | 303 | if (sbk_sqlite_column_text_copy(ctx, &con->system_joined_name, 304 | stm, SBK_COLUMN_SYSTEM_JOINED_NAME) == -1) 305 | goto error; 306 | 307 | if (sbk_sqlite_column_text_copy(ctx, &con->system_phone_label, 308 | stm, SBK_COLUMN_SYSTEM_PHONE_LABEL) == -1) 309 | goto error; 310 | 311 | if (sbk_sqlite_column_text_copy(ctx, &con->profile_given_name, 312 | stm, SBK_COLUMN_PROFILE_GIVEN_NAME) == -1) 313 | goto error; 314 | 315 | if (sbk_sqlite_column_text_copy(ctx, &con->profile_family_name, 316 | stm, SBK_COLUMN_PROFILE_FAMILY_NAME) == -1) 317 | goto error; 318 | 319 | if (sbk_sqlite_column_text_copy(ctx, &con->profile_joined_name, 320 | stm, SBK_COLUMN_PROFILE_JOINED_NAME) == -1) 321 | goto error; 322 | 323 | if (sbk_sqlite_column_text_copy(ctx, &con->nickname_given_name, 324 | stm, SBK_COLUMN_NICKNAME_GIVEN_NAME) == -1) 325 | goto error; 326 | 327 | if (sbk_sqlite_column_text_copy(ctx, 328 | &con->nickname_family_name, stm, 329 | SBK_COLUMN_NICKNAME_FAMILY_NAME) == -1) 330 | goto error; 331 | 332 | if (sbk_sqlite_column_text_copy(ctx, 333 | &con->nickname_joined_name, stm, 334 | SBK_COLUMN_NICKNAME_JOINED_NAME) == -1) 335 | goto error; 336 | 337 | break; 338 | 339 | case SBK_GROUP: 340 | grp = ent->recipient.group = calloc(1, sizeof *grp); 341 | if (grp == NULL) { 342 | warn(NULL); 343 | goto error; 344 | } 345 | 346 | if (sbk_sqlite_column_text_copy(ctx, &grp->name, 347 | stm, SBK_COLUMN_TITLE) == -1) 348 | goto error; 349 | 350 | break; 351 | } 352 | 353 | return ent; 354 | 355 | error: 356 | sbk_free_recipient_entry(ent); 357 | return NULL; 358 | } 359 | 360 | static int 361 | sbk_build_recipient_tree(struct sbk_ctx *ctx) 362 | { 363 | struct sbk_recipient_entry *ent; 364 | sqlite3_stmt *stm; 365 | const char *query; 366 | int ret; 367 | 368 | if (!RB_EMPTY(&ctx->recipients)) 369 | return 0; 370 | 371 | if (sbk_create_database(ctx) == -1) 372 | return -1; 373 | 374 | if (ctx->db_version >= 375 | SBK_DB_VERSION_ADD_NICKNAME_AND_NOTE_FIELDS_TO_RECIPIENT_TABLE) 376 | query = SBK_QUERY_6; 377 | else if (ctx->db_version >= SBK_DB_VERSION_RECIPIENT_TABLE_VALIDATIONS) 378 | query = SBK_QUERY_5; 379 | else if (ctx->db_version >= SBK_DB_VERSION_RESET_PNI_COLUMN) 380 | query = SBK_QUERY_4; 381 | else if (ctx->db_version >= SBK_DB_VERSION_SPLIT_PROFILE_NAMES) 382 | query = SBK_QUERY_3; 383 | else if (ctx->db_version >= SBK_DB_VERSION_RECIPIENT_IDS) 384 | query = SBK_QUERY_2; 385 | else 386 | query = SBK_QUERY_1; 387 | 388 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 389 | return -1; 390 | 391 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 392 | if ((ent = sbk_get_recipient_entry(ctx, stm)) == NULL) 393 | goto error; 394 | RB_INSERT(sbk_recipient_tree, &ctx->recipients, ent); 395 | } 396 | 397 | if (ret != SQLITE_DONE) 398 | goto error; 399 | 400 | sqlite3_finalize(stm); 401 | return 0; 402 | 403 | error: 404 | sbk_free_recipient_tree(ctx); 405 | sqlite3_finalize(stm); 406 | return -1; 407 | } 408 | 409 | struct sbk_recipient * 410 | sbk_get_recipient_from_id(struct sbk_ctx *ctx, struct sbk_recipient_id *id) 411 | { 412 | struct sbk_recipient_entry find, *result; 413 | 414 | if (sbk_build_recipient_tree(ctx) == -1) 415 | return NULL; 416 | 417 | find.id = *id; 418 | result = RB_FIND(sbk_recipient_tree, &ctx->recipients, &find); 419 | 420 | if (result == NULL) { 421 | warnx("Cannot find recipient"); 422 | return NULL; 423 | } 424 | 425 | return &result->recipient; 426 | } 427 | 428 | struct sbk_recipient * 429 | sbk_get_recipient_from_id_from_column(struct sbk_ctx *ctx, sqlite3_stmt *stm, 430 | int idx) 431 | { 432 | struct sbk_recipient *rcp; 433 | struct sbk_recipient_id id; 434 | 435 | if (sbk_get_recipient_id_from_column(ctx, &id, stm, idx) == -1) 436 | return NULL; 437 | 438 | rcp = sbk_get_recipient_from_id(ctx, &id); 439 | free(id.old); 440 | return rcp; 441 | } 442 | 443 | struct sbk_recipient * 444 | sbk_get_recipient_from_aci(struct sbk_ctx *ctx, const char *aci) 445 | { 446 | struct sbk_recipient_entry *ent; 447 | 448 | RB_FOREACH(ent, sbk_recipient_tree, &ctx->recipients) 449 | if (ent->recipient.type == SBK_CONTACT && 450 | ent->recipient.contact->aci != NULL && 451 | strcmp(aci, ent->recipient.contact->aci) == 0) 452 | return &ent->recipient; 453 | 454 | return NULL; 455 | } 456 | -------------------------------------------------------------------------------- /sbk-attachment.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sbk-internal.h" 23 | 24 | /* For database versions < QUOTED_REPLIES */ 25 | #define SBK_SELECT_1 \ 26 | "SELECT " \ 27 | "p._id, " \ 28 | "p.ct, " \ 29 | "p.pending_push, " \ 30 | "p.data_size, " \ 31 | "p.file_name, " \ 32 | "p.unique_id, " \ 33 | "m.date, " \ 34 | "m.date_received, " \ 35 | "0 AS quote, " \ 36 | "NULL AS latest_revision_id " \ 37 | "FROM part AS p " \ 38 | "LEFT JOIN mms AS m " \ 39 | "ON p.mid = m._id " 40 | 41 | /* For database versions [QUOTED_REPLIES, THREAD_AND_MESSAGE_FOREIGN_KEYS) */ 42 | #define SBK_SELECT_2 \ 43 | "SELECT " \ 44 | "p._id, " \ 45 | "p.ct, " \ 46 | "p.pending_push, " \ 47 | "p.data_size, " \ 48 | "p.file_name, " \ 49 | "p.unique_id, " \ 50 | "m.date, " \ 51 | "m.date_received, " \ 52 | "NULL AS latest_revision_id " \ 53 | "FROM part AS p " \ 54 | "LEFT JOIN mms AS m " \ 55 | "ON p.mid = m._id " 56 | 57 | /* 58 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 59 | * REACTION_FOREIGN_KEY_MIGRATION) 60 | */ 61 | #define SBK_SELECT_3 \ 62 | "SELECT " \ 63 | "p._id, " \ 64 | "p.ct, " \ 65 | "p.pending_push, " \ 66 | "p.data_size, " \ 67 | "p.file_name, " \ 68 | "p.unique_id, " \ 69 | "m.date_sent, " \ 70 | "m.date_received, " \ 71 | "NULL AS latest_revision_id " \ 72 | "FROM part AS p " \ 73 | "LEFT JOIN mms AS m " \ 74 | "ON p.mid = m._id " 75 | 76 | /* 77 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 78 | * MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 79 | */ 80 | #define SBK_SELECT_4 \ 81 | "SELECT " \ 82 | "p._id, " \ 83 | "p.ct, " \ 84 | "p.pending_push, " \ 85 | "p.data_size, " \ 86 | "p.file_name, " \ 87 | "p.unique_id, " \ 88 | "m.date_sent, " \ 89 | "m.date_received, " \ 90 | "NULL AS latest_revision_id " \ 91 | "FROM part AS p " \ 92 | "LEFT JOIN message AS m " \ 93 | "ON p.mid = m._id " 94 | 95 | /* 96 | * For database versions [MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION, 97 | * REMOVE_ATTACHMENT_UNIQUE_ID) 98 | */ 99 | #define SBK_SELECT_5 \ 100 | "SELECT " \ 101 | "p._id, " \ 102 | "p.ct, " \ 103 | "p.pending_push, " \ 104 | "p.data_size, " \ 105 | "p.file_name, " \ 106 | "p.unique_id, " \ 107 | "m.date_sent, " \ 108 | "m.date_received " \ 109 | "FROM part AS p " \ 110 | "LEFT JOIN message AS m " \ 111 | "ON p.mid = m._id " 112 | 113 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 114 | #define SBK_SELECT_6 \ 115 | "SELECT " \ 116 | "a._id, " \ 117 | "a.content_type, " \ 118 | "a.transfer_state, " \ 119 | "a.data_size, " \ 120 | "a.file_name, " \ 121 | "0, " /* unique_id */ \ 122 | "m.date_sent, " \ 123 | "m.date_received " \ 124 | "FROM attachment AS a " \ 125 | "LEFT JOIN message AS m " \ 126 | "ON a.message_id = m._id " 127 | 128 | /* For database versions < REACTION_FOREIGN_KEY_MIGRATION */ 129 | #define SBK_WHERE_THREAD_1 \ 130 | "WHERE p.mid IN (SELECT _id FROM mms WHERE thread_id = ?) " \ 131 | "AND latest_revision_id IS NULL " 132 | 133 | /* 134 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 135 | * REMOVE_ATTACHMENT_UNIQUE_ID) 136 | */ 137 | #define SBK_WHERE_THREAD_2 \ 138 | "WHERE p.mid IN (SELECT _id FROM message WHERE thread_id = ?) " \ 139 | "AND latest_revision_id IS NULL " 140 | 141 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 142 | #define SBK_WHERE_THREAD_3 \ 143 | "WHERE a.message_id IN " \ 144 | "(SELECT _id FROM message WHERE thread_id = ?) " \ 145 | "AND latest_revision_id IS NULL " 146 | 147 | /* For database versions < REMOVE_ATTACHMENT_UNIQUE_ID */ 148 | #define SBK_WHERE_MESSAGE_1 \ 149 | "WHERE p.mid = ? AND quote = 0 " 150 | 151 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 152 | #define SBK_WHERE_MESSAGE_2 \ 153 | "WHERE a.message_id = ? AND quote = 0 " 154 | 155 | /* For database versions < REMOVE_ATTACHMENT_UNIQUE_ID */ 156 | #define SBK_WHERE_QUOTE_1 \ 157 | "WHERE p.mid = ? AND quote = 1 " 158 | 159 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 160 | #define SBK_WHERE_QUOTE_2 \ 161 | "WHERE a.message_id = ? AND quote = 1 " 162 | 163 | /* For database versions < REMOVE_ATTACHMENT_UNIQUE_ID */ 164 | #define SBK_ORDER_1 \ 165 | "ORDER BY p.unique_id, p._id" 166 | 167 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 168 | #define SBK_ORDER_2 \ 169 | "ORDER BY a._id" 170 | 171 | /* For database versions < THREAD_AND_MESSAGE_FOREIGN_KEYS */ 172 | #define SBK_QUERY_THREAD_1 \ 173 | SBK_SELECT_2 \ 174 | SBK_WHERE_THREAD_1 \ 175 | SBK_ORDER_1 176 | 177 | /* 178 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 179 | * REACTION_FOREIGN_KEY_MIGRATION) 180 | */ 181 | #define SBK_QUERY_THREAD_2 \ 182 | SBK_SELECT_3 \ 183 | SBK_WHERE_THREAD_1 \ 184 | SBK_ORDER_1 185 | 186 | /* 187 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION 188 | * MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 189 | */ 190 | #define SBK_QUERY_THREAD_3 \ 191 | SBK_SELECT_4 \ 192 | SBK_WHERE_THREAD_2 \ 193 | SBK_ORDER_1 194 | 195 | /* 196 | * For database versions [MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION, 197 | * REMOVE_ATTACHMENT_UNIQUE_ID) 198 | */ 199 | #define SBK_QUERY_THREAD_4 \ 200 | SBK_SELECT_5 \ 201 | SBK_WHERE_THREAD_2 \ 202 | SBK_ORDER_1 203 | 204 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 205 | #define SBK_QUERY_THREAD_5 \ 206 | SBK_SELECT_6 \ 207 | SBK_WHERE_THREAD_3 \ 208 | SBK_ORDER_2 209 | 210 | /* For database versions < QUOTED_REPLIES */ 211 | #define SBK_QUERY_MESSAGE_1 \ 212 | SBK_SELECT_1 \ 213 | SBK_WHERE_MESSAGE_1 \ 214 | SBK_ORDER_1 215 | 216 | /* For database versions [QUOTED_REPLIES, THREAD_AND_MESSAGE_FOREIGN_KEYS) */ 217 | #define SBK_QUERY_MESSAGE_2 \ 218 | SBK_SELECT_2 \ 219 | SBK_WHERE_MESSAGE_1 \ 220 | SBK_ORDER_1 221 | 222 | /* 223 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 224 | * REACTION_FOREIGN_KEY_MIGRATION) 225 | */ 226 | #define SBK_QUERY_MESSAGE_3 \ 227 | SBK_SELECT_3 \ 228 | SBK_WHERE_MESSAGE_1 \ 229 | SBK_ORDER_1 230 | 231 | /* 232 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 233 | * REMOVE_ATTACHMENT_UNIQUE_ID) 234 | */ 235 | #define SBK_QUERY_MESSAGE_4 \ 236 | SBK_SELECT_4 \ 237 | SBK_WHERE_MESSAGE_1 \ 238 | SBK_ORDER_1 239 | 240 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 241 | #define SBK_QUERY_MESSAGE_5 \ 242 | SBK_SELECT_6 \ 243 | SBK_WHERE_MESSAGE_2 \ 244 | SBK_ORDER_2 245 | 246 | /* For database versions < THREAD_AND_MESSAGE_FOREIGN_KEYS */ 247 | #define SBK_QUERY_QUOTE_1 \ 248 | SBK_SELECT_2 \ 249 | SBK_WHERE_QUOTE_1 \ 250 | SBK_ORDER_1 251 | 252 | /* 253 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 254 | * REACTION_FOREIGN_KEY_MIGRATION) 255 | */ 256 | #define SBK_QUERY_QUOTE_2 \ 257 | SBK_SELECT_3 \ 258 | SBK_WHERE_QUOTE_1 \ 259 | SBK_ORDER_1 260 | 261 | /* 262 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 263 | * REMOVE_ATTACHMENT_UNIQUE_ID) 264 | */ 265 | #define SBK_QUERY_QUOTE_3 \ 266 | SBK_SELECT_4 \ 267 | SBK_WHERE_QUOTE_1 \ 268 | SBK_ORDER_1 269 | 270 | /* For database versions >= REMOVE_ATTACHMENT_UNIQUE_ID */ 271 | #define SBK_QUERY_QUOTE_4 \ 272 | SBK_SELECT_6 \ 273 | SBK_WHERE_QUOTE_2 \ 274 | SBK_ORDER_2 275 | 276 | #define SBK_COLUMN__ID 0 277 | #define SBK_COLUMN_CT 1 278 | #define SBK_COLUMN_PENDING_PUSH 2 279 | #define SBK_COLUMN_DATA_SIZE 3 280 | #define SBK_COLUMN_FILE_NAME 4 281 | #define SBK_COLUMN_UNIQUE_ID 5 282 | #define SBK_COLUMN_DATE_SENT 6 283 | #define SBK_COLUMN_DATE_RECEIVED 7 284 | 285 | void 286 | sbk_free_attachment(struct sbk_attachment *att) 287 | { 288 | if (att != NULL) { 289 | free(att->filename); 290 | free(att->content_type); 291 | free(att); 292 | } 293 | } 294 | 295 | void 296 | sbk_free_attachment_list(struct sbk_attachment_list *lst) 297 | { 298 | struct sbk_attachment *att; 299 | 300 | if (lst != NULL) { 301 | while ((att = TAILQ_FIRST(lst)) != NULL) { 302 | TAILQ_REMOVE(lst, att, entries); 303 | sbk_free_attachment(att); 304 | } 305 | free(lst); 306 | } 307 | } 308 | 309 | const char * 310 | sbk_attachment_id_to_string(const struct sbk_attachment *att) 311 | { 312 | static char buf[48]; 313 | 314 | if (att->id.unique_id == 0) 315 | snprintf(buf, sizeof buf, "%" PRId64, att->id.row_id); 316 | else 317 | snprintf(buf, sizeof buf, "%" PRId64 "-%" PRId64, 318 | att->id.row_id, att->id.unique_id); 319 | 320 | return buf; 321 | } 322 | 323 | static struct sbk_attachment * 324 | sbk_get_attachment(struct sbk_ctx *ctx, sqlite3_stmt *stm) 325 | { 326 | struct sbk_attachment *att; 327 | 328 | if ((att = calloc(1, sizeof *att)) == NULL) { 329 | warn(NULL); 330 | return NULL; 331 | } 332 | 333 | if (sbk_sqlite_column_text_copy(ctx, &att->filename, stm, 334 | SBK_COLUMN_FILE_NAME) == -1) 335 | goto error; 336 | 337 | if (sbk_sqlite_column_text_copy(ctx, &att->content_type, stm, 338 | SBK_COLUMN_CT) == -1) 339 | goto error; 340 | 341 | att->id.row_id = sqlite3_column_int64(stm, SBK_COLUMN__ID); 342 | att->id.unique_id = sqlite3_column_int64(stm, SBK_COLUMN_UNIQUE_ID); 343 | att->status = sqlite3_column_int(stm, SBK_COLUMN_PENDING_PUSH); 344 | att->size = sqlite3_column_int64(stm, SBK_COLUMN_DATA_SIZE); 345 | att->time_sent = sqlite3_column_int64(stm, SBK_COLUMN_DATE_SENT); 346 | att->time_recv = sqlite3_column_int64(stm, SBK_COLUMN_DATE_RECEIVED); 347 | att->file = sbk_get_attachment_file(ctx, &att->id); 348 | 349 | if (att->file == NULL) 350 | warnx("Attachment %s not available in backup", 351 | sbk_attachment_id_to_string(att)); 352 | else if (att->size != att->file->len) 353 | warnx("Attachment %s has inconsistent size", 354 | sbk_attachment_id_to_string(att)); 355 | 356 | return att; 357 | 358 | error: 359 | sbk_free_attachment(att); 360 | return NULL; 361 | } 362 | 363 | static struct sbk_attachment_list * 364 | sbk_get_attachments(struct sbk_ctx *ctx, sqlite3_stmt *stm) 365 | { 366 | struct sbk_attachment_list *lst; 367 | struct sbk_attachment *att; 368 | int ret; 369 | 370 | if ((lst = malloc(sizeof *lst)) == NULL) { 371 | warn(NULL); 372 | goto error; 373 | } 374 | 375 | TAILQ_INIT(lst); 376 | 377 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 378 | if ((att = sbk_get_attachment(ctx, stm)) == NULL) 379 | goto error; 380 | TAILQ_INSERT_TAIL(lst, att, entries); 381 | } 382 | 383 | if (ret != SQLITE_DONE) 384 | goto error; 385 | 386 | sqlite3_finalize(stm); 387 | return lst; 388 | 389 | error: 390 | sbk_free_attachment_list(lst); 391 | sqlite3_finalize(stm); 392 | return NULL; 393 | } 394 | 395 | struct sbk_attachment_list * 396 | sbk_get_attachments_for_thread(struct sbk_ctx *ctx, struct sbk_thread *thd) 397 | { 398 | sqlite3_stmt *stm; 399 | const char *query; 400 | 401 | if (sbk_create_database(ctx) == -1) 402 | return NULL; 403 | 404 | if (ctx->db_version >= SBK_DB_VERSION_REMOVE_ATTACHMENT_UNIQUE_ID) 405 | query = SBK_QUERY_THREAD_5; 406 | else if (ctx->db_version >= 407 | SBK_DB_VERSION_MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 408 | query = SBK_QUERY_THREAD_4; 409 | else if (ctx->db_version >= 410 | SBK_DB_VERSION_REACTION_FOREIGN_KEY_MIGRATION) 411 | query = SBK_QUERY_THREAD_3; 412 | else if (ctx->db_version >= 413 | SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS) 414 | query = SBK_QUERY_THREAD_2; 415 | else 416 | query = SBK_QUERY_THREAD_1; 417 | 418 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 419 | return NULL; 420 | 421 | if (sbk_sqlite_bind_int(ctx, stm, 1, thd->id) == -1) { 422 | sqlite3_finalize(stm); 423 | return NULL; 424 | } 425 | 426 | return sbk_get_attachments(ctx, stm); 427 | } 428 | 429 | static int 430 | sbk_get_attachments_for_message_id(struct sbk_ctx *ctx, 431 | struct sbk_attachment_list **lst, struct sbk_message_id *mid) 432 | { 433 | sqlite3_stmt *stm; 434 | const char *query; 435 | 436 | if (mid->table == SBK_SMS_TABLE) 437 | return 0; 438 | 439 | if (ctx->db_version >= SBK_DB_VERSION_REMOVE_ATTACHMENT_UNIQUE_ID) 440 | query = SBK_QUERY_MESSAGE_5; 441 | else if (ctx->db_version >= 442 | SBK_DB_VERSION_REACTION_FOREIGN_KEY_MIGRATION) 443 | query = SBK_QUERY_MESSAGE_4; 444 | else if (ctx->db_version >= 445 | SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS) 446 | query = SBK_QUERY_MESSAGE_3; 447 | else if (ctx->db_version >= SBK_DB_VERSION_QUOTED_REPLIES) 448 | query = SBK_QUERY_MESSAGE_2; 449 | else 450 | query = SBK_QUERY_MESSAGE_1; 451 | 452 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 453 | return -1; 454 | 455 | if (sbk_sqlite_bind_int(ctx, stm, 1, mid->row_id) == -1) { 456 | sqlite3_finalize(stm); 457 | return -1; 458 | } 459 | 460 | if ((*lst = sbk_get_attachments(ctx, stm)) == NULL) 461 | return -1; 462 | 463 | return 0; 464 | } 465 | 466 | int 467 | sbk_get_attachments_for_message(struct sbk_ctx *ctx, struct sbk_message *msg) 468 | { 469 | return sbk_get_attachments_for_message_id(ctx, &msg->attachments, 470 | &msg->id); 471 | } 472 | 473 | int 474 | sbk_get_attachments_for_edit(struct sbk_ctx *ctx, struct sbk_edit *edit) 475 | { 476 | return sbk_get_attachments_for_message_id(ctx, &edit->attachments, 477 | &edit->id); 478 | } 479 | 480 | int 481 | sbk_get_attachments_for_quote(struct sbk_ctx *ctx, struct sbk_quote *qte, 482 | struct sbk_message_id *mid) 483 | { 484 | sqlite3_stmt *stm; 485 | const char *query; 486 | 487 | if (ctx->db_version >= SBK_DB_VERSION_REMOVE_ATTACHMENT_UNIQUE_ID) 488 | query = SBK_QUERY_QUOTE_4; 489 | else if (ctx->db_version >= 490 | SBK_DB_VERSION_REACTION_FOREIGN_KEY_MIGRATION) 491 | query = SBK_QUERY_QUOTE_3; 492 | else if (ctx->db_version >= 493 | SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS) 494 | query = SBK_QUERY_QUOTE_2; 495 | else 496 | query = SBK_QUERY_QUOTE_1; 497 | 498 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 499 | return -1; 500 | 501 | if (sbk_sqlite_bind_int(ctx, stm, 1, mid->row_id) == -1) { 502 | sqlite3_finalize(stm); 503 | return -1; 504 | } 505 | 506 | if ((qte->attachments = sbk_get_attachments(ctx, stm)) == NULL) 507 | return -1; 508 | 509 | return 0; 510 | } 511 | -------------------------------------------------------------------------------- /cmd-export-messages.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "sigbak.h" 32 | 33 | static enum cmd_status cmd_export_messages(int, char **); 34 | 35 | const struct cmd_entry cmd_export_messages_entry = { 36 | .name = "export-messages", 37 | .alias = "msg", 38 | .usage = "[-f format] [-p passfile] backup [directory]", 39 | .exec = cmd_export_messages 40 | }; 41 | 42 | enum { 43 | FORMAT_CSV, 44 | FORMAT_TEXT, 45 | FORMAT_TEXT_SHORT 46 | }; 47 | 48 | static FILE * 49 | get_thread_file(struct sbk_thread *thd, int dfd, const char *ext) 50 | { 51 | FILE *fp; 52 | char *name; 53 | int fd; 54 | 55 | if ((name = get_recipient_filename(thd->recipient, ext)) == NULL) 56 | return NULL; 57 | 58 | fd = openat(dfd, name, O_WRONLY | O_CREAT | O_EXCL, 0666); 59 | if (fd == -1) { 60 | warn("%s", name); 61 | free(name); 62 | return NULL; 63 | } 64 | 65 | if ((fp = fdopen(fd, "w")) == NULL) { 66 | warn("%s", name); 67 | close(fd); 68 | free(name); 69 | return NULL; 70 | } 71 | 72 | free(name); 73 | return fp; 74 | } 75 | 76 | static void 77 | csv_print_quoted_string(FILE *fp, const char *str) 78 | { 79 | const char *c; 80 | 81 | if (str == NULL || str[0] == '\0') 82 | return; 83 | 84 | putc('"', fp); 85 | 86 | for (c = str; *c != '\0'; c++) { 87 | putc(*c, fp); 88 | if (*c == '"') 89 | putc('"', fp); 90 | } 91 | 92 | putc('"', fp); 93 | } 94 | 95 | static void 96 | csv_write_record(FILE *fp, uint64_t time_sent, uint64_t time_recv, 97 | int thread, int type, int nattachments, const char *addr, 98 | const char *name, const char *text) 99 | { 100 | fprintf(fp, "%" PRIu64 ",%" PRIu64 ",%d,%d,%d,", 101 | time_sent, time_recv, thread, type, nattachments); 102 | 103 | csv_print_quoted_string(fp, addr); 104 | putc(',', fp); 105 | csv_print_quoted_string(fp, name); 106 | putc(',', fp); 107 | csv_print_quoted_string(fp, text); 108 | putc('\n', fp); 109 | } 110 | 111 | static int 112 | csv_write_message(FILE *fp, struct sbk_message *msg) 113 | { 114 | struct sbk_attachment *att; 115 | struct sbk_reaction *rct; 116 | const char *addr; 117 | int nattachments; 118 | 119 | addr = (msg->recipient->type == SBK_CONTACT) ? 120 | msg->recipient->contact->phone : "group"; 121 | 122 | nattachments = 0; 123 | if (msg->attachments != NULL) 124 | TAILQ_FOREACH(att, msg->attachments, entries) 125 | nattachments++; 126 | 127 | csv_write_record(fp, 128 | msg->time_sent, 129 | msg->time_recv, 130 | msg->thread, 131 | sbk_is_outgoing_message(msg), 132 | nattachments, 133 | addr, 134 | sbk_get_recipient_display_name(msg->recipient), 135 | msg->text); 136 | 137 | if (msg->reactions != NULL) 138 | SIMPLEQ_FOREACH(rct, msg->reactions, entries) 139 | csv_write_record(fp, 140 | rct->time_sent, 141 | rct->time_recv, 142 | msg->thread, 143 | 2, 144 | 0, 145 | rct->recipient->contact->phone, 146 | sbk_get_recipient_display_name(rct->recipient), 147 | rct->emoji); 148 | 149 | return 0; 150 | } 151 | 152 | static int 153 | csv_export_thread(struct sbk_ctx *ctx, struct sbk_thread *thd, int dfd) 154 | { 155 | struct sbk_message_list *lst; 156 | struct sbk_message *msg; 157 | FILE *fp; 158 | int ret; 159 | 160 | if ((lst = sbk_get_messages_for_thread(ctx, thd)) == NULL) 161 | return -1; 162 | 163 | if (SIMPLEQ_EMPTY(lst)) { 164 | sbk_free_message_list(lst); 165 | return 0; 166 | } 167 | 168 | if ((fp = get_thread_file(thd, dfd, ".csv")) == NULL) { 169 | sbk_free_message_list(lst); 170 | return -1; 171 | } 172 | 173 | ret = 0; 174 | SIMPLEQ_FOREACH(msg, lst, entries) 175 | if (csv_write_message(fp, msg) == -1) 176 | ret = -1; 177 | 178 | fclose(fp); 179 | sbk_free_message_list(lst); 180 | return ret; 181 | } 182 | 183 | static void 184 | text_write_recipient_field(FILE *fp, const char *field, 185 | struct sbk_recipient *rcp) 186 | { 187 | fprintf(fp, "%s: %s", field, sbk_get_recipient_display_name(rcp)); 188 | 189 | if (rcp != NULL) { 190 | if (rcp->type == SBK_GROUP) 191 | fputs(" (group)", fp); 192 | else if (rcp->contact->phone != NULL) 193 | fprintf(fp, " (%s)", rcp->contact->phone); 194 | } 195 | 196 | putc('\n', fp); 197 | } 198 | 199 | static void 200 | text_write_time_field(FILE *fp, const char *field, int64_t msec) 201 | { 202 | static const char *days[] = { 203 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; 204 | static const char *months[] = { 205 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", 206 | "Oct", "Nov", "Dec" }; 207 | 208 | struct tm *tm; 209 | time_t tt; 210 | 211 | tt = msec / 1000; 212 | 213 | if ((tm = localtime(&tt)) == NULL) { 214 | warnx("localtime() failed"); 215 | return; 216 | } 217 | 218 | fprintf(fp, "%s: %s, %d %s %d %02d:%02d:%02d %c%02ld%02ld\n", 219 | field, 220 | days[tm->tm_wday], 221 | tm->tm_mday, 222 | months[tm->tm_mon], 223 | tm->tm_year + 1900, 224 | tm->tm_hour, 225 | tm->tm_min, 226 | tm->tm_sec, 227 | (tm->tm_gmtoff < 0) ? '-' : '+', 228 | labs(tm->tm_gmtoff) / 3600, 229 | labs(tm->tm_gmtoff) % 3600 / 60); 230 | } 231 | 232 | static void 233 | text_write_attachment_field(FILE *fp, struct sbk_attachment *att) 234 | { 235 | fputs("Attachment: ", fp); 236 | 237 | if (att->filename == NULL || *att->filename == '\0') 238 | fputs("no filename", fp); 239 | else 240 | fprintf(fp, "%s", att->filename); 241 | 242 | fprintf(fp, " (%s, %" PRIu64 " bytes, id %s)\n", 243 | (att->content_type != NULL) ? 244 | att->content_type : "", 245 | att->size, 246 | sbk_attachment_id_to_string(att)); 247 | } 248 | 249 | static void 250 | text_write_quote(FILE *fp, struct sbk_quote *qte) 251 | { 252 | struct sbk_attachment *att; 253 | char *s, *t; 254 | 255 | fputs("\n> ", fp); 256 | text_write_recipient_field(fp, "From", qte->recipient); 257 | 258 | fputs("> ", fp); 259 | text_write_time_field(fp, "Sent", qte->id); 260 | 261 | if (qte->attachments != NULL) 262 | TAILQ_FOREACH(att, qte->attachments, entries) { 263 | fputs("> ", fp); 264 | text_write_attachment_field(fp, att); 265 | } 266 | 267 | if (qte->text != NULL && *qte->text != '\0') { 268 | fputs(">\n", fp); 269 | for (s = qte->text; (t = strchr(s, '\n')) != NULL; s = t + 1) 270 | fprintf(fp, "> %.*s\n", (int)(t - s), s); 271 | fprintf(fp, "> %s\n", s); 272 | } 273 | } 274 | 275 | static void 276 | text_write_edits(FILE *fp, struct sbk_message *msg) 277 | { 278 | struct sbk_attachment *att; 279 | struct sbk_edit *edit; 280 | 281 | TAILQ_FOREACH_REVERSE(edit, msg->edits, sbk_edit_list, entries) { 282 | fprintf(fp, "Version: %d\n", edit->revision + 1); 283 | text_write_time_field(fp, "Sent", edit->time_sent); 284 | 285 | if (!sbk_is_outgoing_message(msg)) 286 | text_write_time_field(fp, "Received", edit->time_recv); 287 | 288 | if (edit->attachments != NULL) 289 | TAILQ_FOREACH(att, edit->attachments, entries) 290 | text_write_attachment_field(fp, att); 291 | 292 | if (edit->quote != NULL) 293 | text_write_quote(fp, edit->quote); 294 | 295 | if (edit->text != NULL && *edit->text != '\0') 296 | fprintf(fp, "\n%s\n\n", edit->text); 297 | else 298 | putc('\n', fp); 299 | } 300 | } 301 | 302 | static int 303 | text_write_message(FILE *fp, struct sbk_message *msg) 304 | { 305 | struct sbk_attachment *att; 306 | struct sbk_reaction *rct; 307 | 308 | if (sbk_is_outgoing_message(msg)) 309 | fputs("From: You\n", fp); 310 | else 311 | text_write_recipient_field(fp, "From", msg->recipient); 312 | 313 | text_write_time_field(fp, "Sent", msg->time_sent); 314 | 315 | if (!sbk_is_outgoing_message(msg)) 316 | text_write_time_field(fp, "Received", msg->time_recv); 317 | 318 | if (msg->attachments != NULL) 319 | TAILQ_FOREACH(att, msg->attachments, entries) 320 | text_write_attachment_field(fp, att); 321 | 322 | if (msg->reactions != NULL) 323 | SIMPLEQ_FOREACH(rct, msg->reactions, entries) 324 | fprintf(fp, "Reaction: %s from %s\n", 325 | rct->emoji, 326 | sbk_get_recipient_display_name(rct->recipient)); 327 | 328 | if (msg->edits != NULL) { 329 | fprintf(fp, "Edited: %d versions\n\n", msg->nedits); 330 | text_write_edits(fp, msg); 331 | } else { 332 | if (msg->quote != NULL) 333 | text_write_quote(fp, msg->quote); 334 | 335 | if (msg->text != NULL && *msg->text != '\0') 336 | fprintf(fp, "\n%s\n\n", msg->text); 337 | else 338 | fputs("\n", fp); 339 | } 340 | 341 | return 0; 342 | } 343 | 344 | static int 345 | text_export_thread(struct sbk_ctx *ctx, struct sbk_thread *thd, int dfd) 346 | { 347 | struct sbk_message_list *lst; 348 | struct sbk_message *msg; 349 | FILE *fp; 350 | int ret; 351 | 352 | if ((lst = sbk_get_messages_for_thread(ctx, thd)) == NULL) 353 | return -1; 354 | 355 | if (SIMPLEQ_EMPTY(lst)) { 356 | sbk_free_message_list(lst); 357 | return 0; 358 | } 359 | 360 | if ((fp = get_thread_file(thd, dfd, ".txt")) == NULL) { 361 | sbk_free_message_list(lst); 362 | return -1; 363 | } 364 | 365 | text_write_recipient_field(fp, "Conversation", thd->recipient); 366 | putc('\n', fp); 367 | 368 | ret = 0; 369 | SIMPLEQ_FOREACH(msg, lst, entries) 370 | if (text_write_message(fp, msg) == -1) 371 | ret = -1; 372 | 373 | fclose(fp); 374 | sbk_free_message_list(lst); 375 | return ret; 376 | } 377 | 378 | static const char * 379 | text_short_format_time(int64_t msec) 380 | { 381 | struct tm *tm; 382 | time_t tt; 383 | static char buf[64]; 384 | 385 | tt = msec / 1000; 386 | 387 | if ((tm = localtime(&tt)) == NULL) { 388 | warnx("localtime() failed"); 389 | buf[0] = '\0'; 390 | return buf; 391 | } 392 | 393 | snprintf(buf, sizeof buf, "%d-%02d-%02d %02d:%02d", 394 | tm->tm_year + 1900, 395 | tm->tm_mon + 1, 396 | tm->tm_mday, 397 | tm->tm_hour, 398 | tm->tm_min); 399 | return buf; 400 | } 401 | 402 | static int 403 | text_short_write_message(FILE *fp, struct sbk_message *msg) 404 | { 405 | struct sbk_attachment *att; 406 | const char *name, *time; 407 | int details, natts; 408 | 409 | time = text_short_format_time(msg->time_sent); 410 | name = sbk_is_outgoing_message(msg) ? "You" : 411 | sbk_get_recipient_display_name(msg->recipient); 412 | fprintf(fp, "%s %s:", time, name); 413 | 414 | details = 0; 415 | if (msg->quote != NULL) { 416 | name = sbk_get_recipient_display_name(msg->quote->recipient); 417 | time = text_short_format_time(msg->quote->id); 418 | fprintf(fp, " [reply to %s on %s", name, time); 419 | details = 1; 420 | } 421 | if (msg->edits != NULL) { 422 | fprintf(fp, "%sedited", details ? ", " : " ["); 423 | details = 1; 424 | } 425 | if (msg->attachments != NULL) { 426 | natts = 0; 427 | TAILQ_FOREACH(att, msg->attachments, entries) 428 | natts++; 429 | if (natts > 0) { 430 | fprintf(fp, "%s%d attachment%s", details ? ", " : " [", 431 | natts, (natts > 1) ? "s" : ""); 432 | details = 1; 433 | } 434 | } 435 | if (details) 436 | putc(']', fp); 437 | 438 | if (msg->text != NULL && *msg->text != '\0') 439 | fprintf(fp, " %s\n", msg->text); 440 | else 441 | putc('\n', fp); 442 | 443 | return 0; 444 | } 445 | 446 | static int 447 | text_short_export_thread(struct sbk_ctx *ctx, struct sbk_thread *thd, int dfd) 448 | { 449 | struct sbk_message_list *lst; 450 | struct sbk_message *msg; 451 | FILE *fp; 452 | int ret; 453 | 454 | if ((lst = sbk_get_messages_for_thread(ctx, thd)) == NULL) 455 | return -1; 456 | 457 | if (SIMPLEQ_EMPTY(lst)) { 458 | sbk_free_message_list(lst); 459 | return 0; 460 | } 461 | 462 | if ((fp = get_thread_file(thd, dfd, ".txt")) == NULL) { 463 | sbk_free_message_list(lst); 464 | return -1; 465 | } 466 | 467 | ret = 0; 468 | SIMPLEQ_FOREACH(msg, lst, entries) 469 | if (text_short_write_message(fp, msg) == -1) 470 | ret = -1; 471 | 472 | fclose(fp); 473 | sbk_free_message_list(lst); 474 | return ret; 475 | } 476 | 477 | static int 478 | export_messages(struct sbk_ctx *ctx, const char *outdir, int format) 479 | { 480 | struct sbk_thread_list *lst; 481 | struct sbk_thread *thd; 482 | int dfd, ret; 483 | 484 | if ((dfd = open(outdir, O_RDONLY | O_DIRECTORY)) == -1) { 485 | warn("%s", outdir); 486 | return -1; 487 | } 488 | 489 | if ((lst = sbk_get_threads(ctx)) == NULL) { 490 | close(dfd); 491 | return -1; 492 | } 493 | 494 | ret = 0; 495 | SIMPLEQ_FOREACH(thd, lst, entries) { 496 | switch (format) { 497 | case FORMAT_CSV: 498 | if (csv_export_thread(ctx, thd, dfd) == -1) 499 | ret = -1; 500 | break; 501 | case FORMAT_TEXT: 502 | if (text_export_thread(ctx, thd, dfd) == -1) 503 | ret = -1; 504 | break; 505 | case FORMAT_TEXT_SHORT: 506 | if (text_short_export_thread(ctx, thd, dfd) == -1) 507 | ret = -1; 508 | break; 509 | } 510 | } 511 | 512 | sbk_free_thread_list(lst); 513 | close(dfd); 514 | return ret; 515 | } 516 | 517 | static enum cmd_status 518 | cmd_export_messages(int argc, char **argv) 519 | { 520 | struct sbk_ctx *ctx; 521 | char *backup, *outdir, *passfile, passphr[128]; 522 | int c, format, ret; 523 | 524 | format = FORMAT_TEXT; 525 | passfile = NULL; 526 | 527 | while ((c = getopt(argc, argv, "f:p:")) != -1) 528 | switch (c) { 529 | case 'f': 530 | if (strcmp(optarg, "csv") == 0) 531 | format = FORMAT_CSV; 532 | else if (strcmp(optarg, "text") == 0) 533 | format = FORMAT_TEXT; 534 | else if (strcmp(optarg, "text-short") == 0) 535 | format = FORMAT_TEXT_SHORT; 536 | else { 537 | warnx("%s: Invalid format", optarg); 538 | return CMD_ERROR; 539 | } 540 | break; 541 | case 'p': 542 | passfile = optarg; 543 | break; 544 | default: 545 | return CMD_USAGE; 546 | } 547 | 548 | argc -= optind; 549 | argv += optind; 550 | 551 | switch (argc) { 552 | case 1: 553 | backup = argv[0]; 554 | outdir = "."; 555 | break; 556 | case 2: 557 | backup = argv[0]; 558 | outdir = argv[1]; 559 | if (mkdir(outdir, 0777) == -1 && errno != EEXIST) { 560 | warn("mkdir: %s", outdir); 561 | return CMD_ERROR; 562 | } 563 | break; 564 | default: 565 | return CMD_USAGE; 566 | } 567 | 568 | if (unveil(backup, "r") == -1) 569 | err(1, "unveil: %s", backup); 570 | 571 | if (unveil(outdir, "rwc") == -1) 572 | err(1, "unveil: %s", outdir); 573 | 574 | /* For SQLite */ 575 | if (unveil("/dev/urandom", "r") == -1) 576 | err(1, "unveil: /dev/urandom"); 577 | 578 | /* For SQLite */ 579 | if (unveil("/tmp", "rwc") == -1) 580 | err(1, "unveil: /tmp"); 581 | 582 | if (passfile == NULL) { 583 | if (pledge("stdio rpath wpath cpath tty", NULL) == -1) 584 | err(1, "pledge"); 585 | } else { 586 | if (strcmp(passfile, "-") != 0 && unveil(passfile, "r") == -1) 587 | err(1, "unveil: %s", passfile); 588 | 589 | if (pledge("stdio rpath wpath cpath", NULL) == -1) 590 | err(1, "pledge"); 591 | } 592 | 593 | if ((ctx = sbk_ctx_new()) == NULL) 594 | return CMD_ERROR; 595 | 596 | if (get_passphrase(passfile, passphr, sizeof passphr) == -1) { 597 | sbk_ctx_free(ctx); 598 | return CMD_ERROR; 599 | } 600 | 601 | if (sbk_open(ctx, backup, passphr) == -1) { 602 | explicit_bzero(passphr, sizeof passphr); 603 | sbk_ctx_free(ctx); 604 | return CMD_ERROR; 605 | } 606 | 607 | explicit_bzero(passphr, sizeof passphr); 608 | 609 | if (passfile == NULL && pledge("stdio rpath wpath cpath", NULL) == -1) 610 | err(1, "pledge"); 611 | 612 | ret = export_messages(ctx, outdir, format); 613 | sbk_close(ctx); 614 | sbk_ctx_free(ctx); 615 | return (ret == -1) ? CMD_ERROR : CMD_OK; 616 | } 617 | -------------------------------------------------------------------------------- /sbk-message.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Tim van der Molen 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "sbk-internal.h" 22 | 23 | /* For database versions < REACTIONS */ 24 | #define SBK_SELECT_SMS_1 \ 25 | "SELECT " \ 26 | STRINGIFY(SBK_SMS_TABLE) ", " \ 27 | "_id, " \ 28 | "date_sent, " \ 29 | "date AS date_received, " \ 30 | "thread_id, " \ 31 | "address, " /* recipient_id */ \ 32 | "type, " \ 33 | "body, " \ 34 | "0, " /* mms.quote_id */ \ 35 | "NULL, " /* mms.quote_author */ \ 36 | "NULL, " /* mms.quote_body */ \ 37 | "NULL, " /* mms.quote_mentions */ \ 38 | "NULL AS latest_revision_id, " \ 39 | "NULL, " /* original_message_id */ \ 40 | "0, " /* revision_number */ \ 41 | "NULL " /* reactions */ \ 42 | "FROM sms " 43 | 44 | /* For database versions [REACTIONS, THREAD_AND_MESSAGE_FOREIGN_KEYS) */ 45 | #define SBK_SELECT_SMS_2 \ 46 | "SELECT " \ 47 | STRINGIFY(SBK_SMS_TABLE) ", " \ 48 | "_id, " \ 49 | "date_sent, " \ 50 | "date AS date_received, " \ 51 | "thread_id, " \ 52 | "address, " /* recipient_id */ \ 53 | "type, " \ 54 | "body, " \ 55 | "0, " /* mms.quote_id */ \ 56 | "NULL, " /* mms.quote_author */ \ 57 | "NULL, " /* mms.quote_body */ \ 58 | "NULL, " /* mms.quote_mentions */ \ 59 | "NULL AS latest_revision_id, " \ 60 | "NULL, " /* original_message_id */ \ 61 | "0, " /* revision_number */ \ 62 | "reactions " \ 63 | "FROM sms " 64 | 65 | /* 66 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 67 | * SINGLE_MESSAGE_TABLE_MIGRATION) 68 | */ 69 | #define SBK_SELECT_SMS_3 \ 70 | "SELECT " \ 71 | STRINGIFY(SBK_SMS_TABLE) ", " \ 72 | "_id, " \ 73 | "date_sent, " \ 74 | "date_received, " \ 75 | "thread_id, " \ 76 | "recipient_id, " \ 77 | "type, " \ 78 | "body, " \ 79 | "0, " /* mms.quote_id */ \ 80 | "NULL, " /* mms.quote_author */ \ 81 | "NULL, " /* mms.quote_body */ \ 82 | "NULL, " /* mms.quote_mentions */ \ 83 | "NULL AS latest_revision_id, " \ 84 | "NULL, " /* original_message_id */ \ 85 | "0, " /* revision_number */ \ 86 | "NULL " /* reactions */ \ 87 | "FROM sms " 88 | 89 | /* For database versions < QUOTED_REPLIES */ 90 | #define SBK_SELECT_MMS_1 \ 91 | "SELECT " \ 92 | STRINGIFY(SBK_MMS_TABLE) ", " \ 93 | "_id, " \ 94 | "date, " /* date_sent */ \ 95 | "date_received, " \ 96 | "thread_id, " \ 97 | "address, " /* recipient_id */ \ 98 | "msg_box, " /* type */ \ 99 | "body, " \ 100 | "0, " /* quote_id */ \ 101 | "NULL, " /* quote_author */ \ 102 | "NULL, " /* quote_body */ \ 103 | "NULL, " /* quote_mentions */ \ 104 | "NULL AS latest_revision_id, " \ 105 | "NULL, " /* original_message_id */ \ 106 | "0, " /* revision_number */ \ 107 | "NULL " /* reactions */ \ 108 | "FROM mms " 109 | 110 | /* For database versions [QUOTED_REPLIES, REACTIONS) */ 111 | #define SBK_SELECT_MMS_2 \ 112 | "SELECT " \ 113 | STRINGIFY(SBK_MMS_TABLE) ", " \ 114 | "_id, " \ 115 | "date, " /* date_sent */ \ 116 | "date_received, " \ 117 | "thread_id, " \ 118 | "address, " /* recipient_id */ \ 119 | "msg_box, " /* type */ \ 120 | "body, " \ 121 | "quote_id, " \ 122 | "quote_author, " \ 123 | "quote_body, " \ 124 | "NULL, " /* quote_mentions */ \ 125 | "NULL AS latest_revision_id, " \ 126 | "NULL, " /* original_message_id */ \ 127 | "0, " /* revision_number */ \ 128 | "NULL " /* reactions */ \ 129 | "FROM mms " 130 | 131 | /* For database versions [REACTIONS, MENTIONS) */ 132 | #define SBK_SELECT_MMS_3 \ 133 | "SELECT " \ 134 | STRINGIFY(SBK_MMS_TABLE) ", " \ 135 | "_id, " \ 136 | "date, " /* date_sent */ \ 137 | "date_received, " \ 138 | "thread_id, " \ 139 | "address, " /* recipient_id */ \ 140 | "msg_box, " /* type */ \ 141 | "body, " \ 142 | "quote_id, " \ 143 | "quote_author, " \ 144 | "quote_body, " \ 145 | "NULL, " /* quote_mentions */ \ 146 | "NULL AS latest_revision_id, " \ 147 | "NULL, " /* original_message_id */ \ 148 | "0, " /* revision_number */ \ 149 | "reactions " \ 150 | "FROM mms " 151 | 152 | /* For database versions [MENTIONS, THREAD_AND_MESSAGE_FOREIGN_KEYS) */ 153 | #define SBK_SELECT_MMS_4 \ 154 | "SELECT " \ 155 | STRINGIFY(SBK_MMS_TABLE) ", " \ 156 | "_id, " \ 157 | "date, " /* date_sent */ \ 158 | "date_received, " \ 159 | "thread_id, " \ 160 | "address, " /* recipient_id */ \ 161 | "msg_box, " /* type */ \ 162 | "body, " \ 163 | "quote_id, " \ 164 | "quote_author, " \ 165 | "quote_body, " \ 166 | "quote_mentions, " \ 167 | "NULL AS latest_revision_id, " \ 168 | "NULL, " /* original_message_id */ \ 169 | "0, " /* revision_number */ \ 170 | "reactions " \ 171 | "FROM mms " 172 | 173 | /* 174 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 175 | * SINGLE_MESSAGE_TABLE_MIGRATION) 176 | */ 177 | #define SBK_SELECT_MMS_5 \ 178 | "SELECT " \ 179 | STRINGIFY(SBK_MMS_TABLE) ", " \ 180 | "_id, " \ 181 | "date_sent, " \ 182 | "date_received, " \ 183 | "thread_id, " \ 184 | "recipient_id, " \ 185 | "type, " \ 186 | "body, " \ 187 | "quote_id, " \ 188 | "quote_author, " \ 189 | "quote_body, " \ 190 | "quote_mentions, " \ 191 | "NULL AS latest_revision_id, " \ 192 | "NULL, " /* original_message_id */ \ 193 | "0, " /* revision_number */ \ 194 | "NULL " /* reactions */ \ 195 | "FROM mms " 196 | 197 | /* 198 | * For database versions [SINGLE_MESSAGE_TABLE_MIGRATION, 199 | * REACTION_FOREIGN_KEY_MIGRATION) 200 | */ 201 | #define SBK_SELECT_1 \ 202 | "SELECT " \ 203 | STRINGIFY(SBK_SINGLE_TABLE) ", " \ 204 | "_id, " \ 205 | "date_sent, " \ 206 | "date_received, " \ 207 | "thread_id, " \ 208 | "recipient_id, " \ 209 | "type, " \ 210 | "body, " \ 211 | "quote_id, " \ 212 | "quote_author, " \ 213 | "quote_body, " \ 214 | "quote_mentions, " \ 215 | "NULL AS latest_revision_id, " \ 216 | "NULL, " /* original_message_id */ \ 217 | "0, " /* revision_number */ \ 218 | "NULL " /* reactions */ \ 219 | "FROM mms " 220 | 221 | /* 222 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 223 | * MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 224 | */ 225 | #define SBK_SELECT_2 \ 226 | "SELECT " \ 227 | STRINGIFY(SBK_SINGLE_TABLE) ", " \ 228 | "_id, " \ 229 | "date_sent, " \ 230 | "date_received, " \ 231 | "thread_id, " \ 232 | "recipient_id, " \ 233 | "type, " \ 234 | "body, " \ 235 | "quote_id, " \ 236 | "quote_author, " \ 237 | "quote_body, " \ 238 | "quote_mentions, " \ 239 | "NULL AS latest_revision_id, " \ 240 | "NULL, " /* original_message_id */ \ 241 | "0, " /* revision_number */ \ 242 | "NULL " /* reactions */ \ 243 | "FROM message " 244 | 245 | /* 246 | * For database versions >= MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION 247 | * 248 | * The CASE expression below is based on the outgoingClause variable in 249 | * V185_MessageRecipientsAndEditMessageMigration.kt in the Signal-Android 250 | * repository. 251 | */ 252 | #define SBK_SELECT_3 \ 253 | "SELECT " \ 254 | STRINGIFY(SBK_SINGLE_TABLE) ", " \ 255 | "_id, " \ 256 | "date_sent, " \ 257 | "date_received, " \ 258 | "thread_id, " \ 259 | "CASE WHEN type & " STRINGIFY(SBK_BASE_TYPE_MASK) " IN (" \ 260 | STRINGIFY(SBK_OUTGOING_AUDIO_CALL_TYPE) ", " \ 261 | STRINGIFY(SBK_OUTGOING_VIDEO_CALL_TYPE) ", " \ 262 | STRINGIFY(SBK_BASE_OUTBOX_TYPE) ", " \ 263 | STRINGIFY(SBK_BASE_SENDING_TYPE) ", " \ 264 | STRINGIFY(SBK_BASE_SENT_TYPE) ", " \ 265 | STRINGIFY(SBK_BASE_SENT_FAILED_TYPE) ", " \ 266 | STRINGIFY(SBK_BASE_PENDING_SECURE_SMS_FALLBACK) ", " \ 267 | STRINGIFY(SBK_BASE_PENDING_INSECURE_SMS_FALLBACK) ") " \ 268 | "THEN to_recipient_id ELSE from_recipient_id END, " \ 269 | "type, " \ 270 | "body, " \ 271 | "quote_id, " \ 272 | "quote_author, " \ 273 | "quote_body, " \ 274 | "quote_mentions, " \ 275 | "latest_revision_id, " \ 276 | "original_message_id, " \ 277 | "revision_number, " \ 278 | "NULL " /* reactions */ \ 279 | "FROM message " 280 | 281 | #define SBK_WHERE_THREAD \ 282 | "WHERE thread_id = ?1 AND latest_revision_id IS NULL " 283 | 284 | #define SBK_ORDER \ 285 | "ORDER BY date_received" 286 | 287 | /* For database versions < QUOTED_REPLIES */ 288 | #define SBK_QUERY_1 \ 289 | SBK_SELECT_SMS_1 \ 290 | SBK_WHERE_THREAD \ 291 | "UNION ALL " \ 292 | SBK_SELECT_MMS_1 \ 293 | SBK_WHERE_THREAD \ 294 | SBK_ORDER 295 | 296 | /* For database versions [QUOTED_REPLIES, REACTIONS) */ 297 | #define SBK_QUERY_2 \ 298 | SBK_SELECT_SMS_1 \ 299 | SBK_WHERE_THREAD \ 300 | "UNION ALL " \ 301 | SBK_SELECT_MMS_2 \ 302 | SBK_WHERE_THREAD \ 303 | SBK_ORDER 304 | 305 | /* For database versions [REACTIONS, MENTIONS) */ 306 | #define SBK_QUERY_3 \ 307 | SBK_SELECT_SMS_2 \ 308 | SBK_WHERE_THREAD \ 309 | "UNION ALL " \ 310 | SBK_SELECT_MMS_3 \ 311 | SBK_WHERE_THREAD \ 312 | SBK_ORDER 313 | 314 | /* For database versions [MENTIONS, THREAD_AND_MESSAGE_FOREIGN_KEYS) */ 315 | #define SBK_QUERY_4 \ 316 | SBK_SELECT_SMS_2 \ 317 | SBK_WHERE_THREAD \ 318 | "UNION ALL " \ 319 | SBK_SELECT_MMS_4 \ 320 | SBK_WHERE_THREAD \ 321 | SBK_ORDER 322 | 323 | /* 324 | * For database versions [THREAD_AND_MESSAGE_FOREIGN_KEYS, 325 | * SINGLE_MESSAGE_TABLE_MIGRATION) 326 | */ 327 | #define SBK_QUERY_5 \ 328 | SBK_SELECT_SMS_3 \ 329 | SBK_WHERE_THREAD \ 330 | "UNION ALL " \ 331 | SBK_SELECT_MMS_5 \ 332 | SBK_WHERE_THREAD \ 333 | SBK_ORDER 334 | 335 | /* 336 | * For database versions [SINGLE_MESSAGE_TABLE_MIGRATION, 337 | * REACTION_FOREIGN_KEY_MIGRATION) 338 | */ 339 | #define SBK_QUERY_6 \ 340 | SBK_SELECT_1 \ 341 | SBK_WHERE_THREAD \ 342 | SBK_ORDER 343 | 344 | /* 345 | * For database versions [REACTION_FOREIGN_KEY_MIGRATION, 346 | * MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 347 | */ 348 | #define SBK_QUERY_7 \ 349 | SBK_SELECT_2 \ 350 | SBK_WHERE_THREAD \ 351 | SBK_ORDER 352 | 353 | /* For database versions >= MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION */ 354 | #define SBK_QUERY_8 \ 355 | SBK_SELECT_3 \ 356 | SBK_WHERE_THREAD \ 357 | SBK_ORDER 358 | 359 | #define SBK_COLUMN_TABLE 0 360 | #define SBK_COLUMN__ID 1 361 | #define SBK_COLUMN_DATE_SENT 2 362 | #define SBK_COLUMN_DATE_RECEIVED 3 363 | #define SBK_COLUMN_THREAD_ID 4 364 | #define SBK_COLUMN_RECIPIENT_ID 5 365 | #define SBK_COLUMN_TYPE 6 366 | #define SBK_COLUMN_BODY 7 367 | #define SBK_COLUMN_QUOTE_ID 8 368 | #define SBK_COLUMN_QUOTE_AUTHOR 9 369 | #define SBK_COLUMN_QUOTE_BODY 10 370 | #define SBK_COLUMN_QUOTE_MENTIONS 11 371 | #define SBK_COLUMN_LATEST_REVISION_ID 12 372 | #define SBK_COLUMN_ORIGINAL_MESSAGE_ID 13 373 | #define SBK_COLUMN_REVISION_NUMBER 14 374 | #define SBK_COLUMN_REACTIONS 15 375 | 376 | #define SBK_MESSAGE_HAS_EDITS(stm) \ 377 | (sqlite3_column_type((stm), SBK_COLUMN_ORIGINAL_MESSAGE_ID) \ 378 | != SQLITE_NULL) 379 | 380 | static void 381 | sbk_free_message(struct sbk_message *msg) 382 | { 383 | if (msg != NULL) { 384 | free(msg->text); 385 | sbk_free_attachment_list(msg->attachments); 386 | sbk_free_mention_list(msg->mentions); 387 | sbk_free_reaction_list(msg->reactions); 388 | sbk_free_quote(msg->quote); 389 | sbk_free_edit_list(msg->edits); 390 | free(msg); 391 | } 392 | } 393 | 394 | void 395 | sbk_free_message_list(struct sbk_message_list *lst) 396 | { 397 | struct sbk_message *msg; 398 | 399 | if (lst != NULL) { 400 | while ((msg = SIMPLEQ_FIRST(lst)) != NULL) { 401 | SIMPLEQ_REMOVE_HEAD(lst, entries); 402 | sbk_free_message(msg); 403 | } 404 | free(lst); 405 | } 406 | } 407 | 408 | const char * 409 | sbk_message_id_to_string(const struct sbk_message *msg) 410 | { 411 | static char buf[32]; 412 | 413 | if (msg->id.table == SBK_SINGLE_TABLE) 414 | snprintf(buf, sizeof buf, "%d", msg->id.row_id); 415 | else 416 | snprintf(buf, sizeof buf, "%d-%d", msg->id.table, 417 | msg->id.row_id); 418 | 419 | return buf; 420 | } 421 | 422 | int 423 | sbk_is_outgoing_message(const struct sbk_message *msg) 424 | { 425 | switch (msg->type & SBK_BASE_TYPE_MASK) { 426 | case SBK_OUTGOING_AUDIO_CALL_TYPE: 427 | case SBK_OUTGOING_VIDEO_CALL_TYPE: 428 | case SBK_BASE_OUTBOX_TYPE: 429 | case SBK_BASE_SENDING_TYPE: 430 | case SBK_BASE_SENT_TYPE: 431 | case SBK_BASE_SENT_FAILED_TYPE: 432 | case SBK_BASE_PENDING_SECURE_SMS_FALLBACK: 433 | case SBK_BASE_PENDING_INSECURE_SMS_FALLBACK: 434 | return 1; 435 | default: 436 | return 0; 437 | } 438 | } 439 | 440 | static int 441 | sbk_get_body(struct sbk_message *msg) 442 | { 443 | const char *fmt; 444 | 445 | fmt = NULL; 446 | 447 | if (msg->type & SBK_ENCRYPTION_REMOTE_FAILED_BIT) 448 | fmt = "Bad encrypted message"; 449 | else if (msg->type & SBK_ENCRYPTION_REMOTE_NO_SESSION_BIT) 450 | fmt = "Message encrypted for non-existing session"; 451 | else if (msg->type & SBK_ENCRYPTION_REMOTE_DUPLICATE_BIT) 452 | fmt = "Duplicate message"; 453 | else if ((msg->type & SBK_ENCRYPTION_REMOTE_LEGACY_BIT) || 454 | (msg->type & SBK_ENCRYPTION_REMOTE_BIT)) 455 | fmt = "Encrypted message sent from an older version of Signal " 456 | "that is no longer supported"; 457 | else if (msg->type & SBK_GROUP_UPDATE_BIT) { 458 | if (sbk_is_outgoing_message(msg)) 459 | fmt = "You updated the group"; 460 | else 461 | fmt = "%s updated the group"; 462 | } else if (msg->type & SBK_GROUP_QUIT_BIT) { 463 | if (sbk_is_outgoing_message(msg)) 464 | fmt = "You have left the group"; 465 | else 466 | fmt = "%s has left the group"; 467 | } else if (msg->type & SBK_END_SESSION_BIT) { 468 | if (sbk_is_outgoing_message(msg)) 469 | fmt = "You reset the secure session"; 470 | else 471 | fmt = "%s reset the secure session"; 472 | } else if (msg->type & SBK_KEY_EXCHANGE_IDENTITY_VERIFIED_BIT) { 473 | if (sbk_is_outgoing_message(msg)) 474 | fmt = "You marked your safety number with %s verified"; 475 | else 476 | fmt = "You marked your safety number with %s verified " 477 | "from another device"; 478 | } else if (msg->type & SBK_KEY_EXCHANGE_IDENTITY_DEFAULT_BIT) { 479 | if (sbk_is_outgoing_message(msg)) 480 | fmt = "You marked your safety number with %s " 481 | "unverified"; 482 | else 483 | fmt = "You marked your safety number with %s " 484 | "unverified from another device"; 485 | } else if (msg->type & SBK_KEY_EXCHANGE_CORRUPTED_BIT) 486 | fmt = "Corrupt key exchange message"; 487 | else if (msg->type & SBK_KEY_EXCHANGE_INVALID_VERSION_BIT) 488 | fmt = "Key exchange message for invalid protocol version"; 489 | else if (msg->type & SBK_KEY_EXCHANGE_BUNDLE_BIT) 490 | fmt = "Message with new safety number"; 491 | else if (msg->type & SBK_KEY_EXCHANGE_IDENTITY_UPDATE_BIT) 492 | fmt = "Your safety number with %s has changed"; 493 | else if (msg->type & SBK_KEY_EXCHANGE_BIT) 494 | fmt = "Key exchange message"; 495 | else 496 | switch (msg->type & SBK_BASE_TYPE_MASK) { 497 | case SBK_INCOMING_AUDIO_CALL_TYPE: 498 | case SBK_INCOMING_VIDEO_CALL_TYPE: 499 | fmt = "%s called you"; 500 | break; 501 | case SBK_OUTGOING_AUDIO_CALL_TYPE: 502 | case SBK_OUTGOING_VIDEO_CALL_TYPE: 503 | fmt = "Called %s"; 504 | break; 505 | case SBK_MISSED_AUDIO_CALL_TYPE: 506 | fmt = "Missed audio call from %s"; 507 | break; 508 | case SBK_JOINED_TYPE: 509 | fmt = "%s is on Signal"; 510 | break; 511 | case SBK_UNSUPPORTED_MESSAGE_TYPE: 512 | fmt = "Unsupported message sent from a newer version " 513 | "of Signal"; 514 | break; 515 | case SBK_INVALID_MESSAGE_TYPE: 516 | fmt = "Invalid message"; 517 | break; 518 | case SBK_PROFILE_CHANGE_TYPE: 519 | fmt = "%s changed their profile"; 520 | break; 521 | case SBK_MISSED_VIDEO_CALL_TYPE: 522 | fmt = "Missed video call from %s"; 523 | break; 524 | case SBK_GV1_MIGRATION_TYPE: 525 | fmt = "This group was updated to a new group"; 526 | break; 527 | case SBK_BOOST_REQUEST_TYPE: 528 | fmt = "Like this new feature? Help support Signal " 529 | "with a one-time donation."; 530 | break; 531 | } 532 | 533 | if (fmt == NULL) 534 | return 0; 535 | 536 | free(msg->text); 537 | 538 | if (asprintf(&msg->text, fmt, 539 | sbk_get_recipient_display_name(msg->recipient)) == -1) { 540 | msg->text = NULL; 541 | return -1; 542 | } 543 | 544 | return 0; 545 | } 546 | 547 | static void 548 | sbk_remove_attachment(struct sbk_attachment_list **lst, 549 | struct sbk_attachment *att) 550 | { 551 | TAILQ_REMOVE(*lst, att, entries); 552 | sbk_free_attachment(att); 553 | if (TAILQ_EMPTY(*lst)) { 554 | sbk_free_attachment_list(*lst); 555 | *lst = NULL; 556 | } 557 | } 558 | 559 | int 560 | sbk_get_long_message(struct sbk_ctx *ctx, char **text, 561 | struct sbk_attachment_list **lst) 562 | { 563 | struct sbk_attachment *att; 564 | char *longtext; 565 | int found; 566 | 567 | /* Look for a long-message attachment */ 568 | found = 0; 569 | TAILQ_FOREACH(att, *lst, entries) 570 | if (att->content_type != NULL && 571 | strcmp(att->content_type, SBK_LONG_TEXT_TYPE) == 0) { 572 | found = 1; 573 | break; 574 | } 575 | 576 | if (!found) 577 | return 0; 578 | 579 | if (att->file == NULL) { 580 | warnx("Long-message attachment not available in backup"); 581 | return 0; 582 | } 583 | 584 | if ((longtext = sbk_get_file_data_as_string(ctx, att->file)) == NULL) 585 | return -1; 586 | 587 | free(*text); 588 | *text = longtext; 589 | 590 | /* Do not expose the long-message attachment */ 591 | sbk_remove_attachment(lst, att); 592 | 593 | return 0; 594 | } 595 | 596 | static int 597 | sbk_get_quote_for_message(struct sbk_ctx *ctx, struct sbk_message *msg, 598 | sqlite3_stmt *stm) 599 | { 600 | return sbk_get_quote(ctx, &msg->quote, stm, SBK_COLUMN_QUOTE_ID, 601 | SBK_COLUMN_QUOTE_AUTHOR, SBK_COLUMN_QUOTE_BODY, 602 | SBK_COLUMN_QUOTE_MENTIONS, &msg->id); 603 | } 604 | 605 | static struct sbk_message * 606 | sbk_get_message(struct sbk_ctx *ctx, sqlite3_stmt *stm) 607 | { 608 | struct sbk_message *msg; 609 | 610 | if ((msg = calloc(1, sizeof *msg)) == NULL) { 611 | warn(NULL); 612 | return NULL; 613 | } 614 | 615 | msg->id.table = sqlite3_column_int(stm, SBK_COLUMN_TABLE); 616 | msg->id.row_id = sqlite3_column_int(stm, SBK_COLUMN__ID); 617 | 618 | msg->recipient = sbk_get_recipient_from_id_from_column(ctx, stm, 619 | SBK_COLUMN_RECIPIENT_ID); 620 | if (msg->recipient == NULL) 621 | goto error; 622 | 623 | if (sbk_sqlite_column_text_copy(ctx, &msg->text, stm, SBK_COLUMN_BODY) 624 | == -1) 625 | goto error; 626 | 627 | msg->time_sent = sqlite3_column_int64(stm, SBK_COLUMN_DATE_SENT); 628 | msg->time_recv = sqlite3_column_int64(stm, SBK_COLUMN_DATE_RECEIVED); 629 | msg->type = sqlite3_column_int(stm, SBK_COLUMN_TYPE); 630 | msg->thread = sqlite3_column_int(stm, SBK_COLUMN_THREAD_ID); 631 | 632 | if (sbk_get_body(msg) == -1) 633 | goto error; 634 | 635 | if (msg->id.table != SBK_SMS_TABLE) { 636 | if (sbk_get_attachments_for_message(ctx, msg) == -1) 637 | goto error; 638 | 639 | if (sbk_get_long_message(ctx, &msg->text, &msg->attachments) == 640 | -1) 641 | goto error; 642 | 643 | if (sbk_get_mentions_for_message(ctx, msg) == -1) 644 | goto error; 645 | 646 | if (sbk_insert_mentions(&msg->text, msg->mentions) == -1) { 647 | warnx("Cannot insert mentions in message %s", 648 | sbk_message_id_to_string(msg)); 649 | goto error; 650 | } 651 | 652 | if (sbk_get_quote_for_message(ctx, msg, stm) == -1) { 653 | warnx("Cannot get quote for message %s", 654 | sbk_message_id_to_string(msg)); 655 | goto error; 656 | } 657 | } 658 | 659 | if (ctx->db_version >= SBK_DB_VERSION_REACTION_REFACTOR) { 660 | if (sbk_get_reactions_from_table(ctx, msg) == -1) 661 | goto error; 662 | } else { 663 | if (sbk_get_reactions_from_column(ctx, &msg->reactions, stm, 664 | SBK_COLUMN_REACTIONS) == -1) 665 | goto error; 666 | } 667 | 668 | if (SBK_MESSAGE_HAS_EDITS(stm) && sbk_get_edits(ctx, msg) == -1) { 669 | warnx("Cannot get edits for message %s", 670 | sbk_message_id_to_string(msg)); 671 | goto error; 672 | } 673 | 674 | return msg; 675 | 676 | error: 677 | sbk_free_message(msg); 678 | return NULL; 679 | } 680 | 681 | static struct sbk_message_list * 682 | sbk_get_messages(struct sbk_ctx *ctx, sqlite3_stmt *stm) 683 | { 684 | struct sbk_message_list *lst; 685 | struct sbk_message *msg; 686 | int ret; 687 | 688 | if ((lst = malloc(sizeof *lst)) == NULL) { 689 | warn(NULL); 690 | goto error; 691 | } 692 | 693 | SIMPLEQ_INIT(lst); 694 | 695 | while ((ret = sbk_sqlite_step(ctx, stm)) == SQLITE_ROW) { 696 | if ((msg = sbk_get_message(ctx, stm)) == NULL) 697 | goto error; 698 | SIMPLEQ_INSERT_TAIL(lst, msg, entries); 699 | } 700 | 701 | if (ret != SQLITE_DONE) 702 | goto error; 703 | 704 | sqlite3_finalize(stm); 705 | return lst; 706 | 707 | error: 708 | sbk_free_message_list(lst); 709 | sqlite3_finalize(stm); 710 | return NULL; 711 | } 712 | 713 | struct sbk_message_list * 714 | sbk_get_messages_for_thread(struct sbk_ctx *ctx, struct sbk_thread *thd) 715 | { 716 | sqlite3_stmt *stm; 717 | const char *query; 718 | 719 | if (sbk_create_database(ctx) == -1) 720 | return NULL; 721 | 722 | if (ctx->db_version >= 723 | SBK_DB_VERSION_MESSAGE_RECIPIENTS_AND_EDIT_MESSAGE_MIGRATION) 724 | query = SBK_QUERY_8; 725 | else if (ctx->db_version >= 726 | SBK_DB_VERSION_REACTION_FOREIGN_KEY_MIGRATION) 727 | query = SBK_QUERY_7; 728 | else if (ctx->db_version >= 729 | SBK_DB_VERSION_SINGLE_MESSAGE_TABLE_MIGRATION) 730 | query = SBK_QUERY_6; 731 | else if (ctx->db_version >= 732 | SBK_DB_VERSION_THREAD_AND_MESSAGE_FOREIGN_KEYS) 733 | query = SBK_QUERY_5; 734 | else if (ctx->db_version >= SBK_DB_VERSION_MENTIONS) 735 | query = SBK_QUERY_4; 736 | else if (ctx->db_version >= SBK_DB_VERSION_REACTIONS) 737 | query = SBK_QUERY_3; 738 | else if (ctx->db_version >= SBK_DB_VERSION_QUOTED_REPLIES) 739 | query = SBK_QUERY_2; 740 | else 741 | query = SBK_QUERY_1; 742 | 743 | if (sbk_sqlite_prepare(ctx, &stm, query) == -1) 744 | return NULL; 745 | 746 | if (sbk_sqlite_bind_int(ctx, stm, 1, thd->id) == -1) { 747 | sqlite3_finalize(stm); 748 | return NULL; 749 | } 750 | 751 | return sbk_get_messages(ctx, stm); 752 | } 753 | --------------------------------------------------------------------------------