From d9b8ad5c2c1d2be395e51c1d60df296389228f7e Mon Sep 17 00:00:00 2001 From: Citlali del Rey Date: Sun, 18 Feb 2024 16:45:10 -0800 Subject: [PATCH] Initial commit --- README.md | 43 +++++++++++++ cheogram.sql | 41 ++++++++++++ readceb.c | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++ schema.sql | 87 +++++++++++++++++++++++++ sql2txt.pl | 85 +++++++++++++++++++++++++ 5 files changed, 431 insertions(+) create mode 100644 README.md create mode 100644 cheogram.sql create mode 100644 readceb.c create mode 100644 schema.sql create mode 100644 sql2txt.pl diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7d5b23 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# readceb +A C program and a perl script to extract logs from a CEB backup file +from conversations. + +## How-To +Compile `readceb.c` with OpenSSL, and run it with the first parameter +the CEB file and the 2nd parameter the output sqlgz file. + +``` +./readceb backup.ceb backup.sqlgz +``` + +It will prompt you for the account password. Then, you can use `gz` to +uncompress the file. + +## Converting to TXT +You can use `sqlite3` to create a db: +``` +$ sqlite3 backup.db +> .read schema.sql +(if using Cheogram) +> attach database 'backup-cheogram.db' as cheogram; +> .read cheogram.sql +> .read backup.sql +``` + +Then, you can use the Perl script to create a hierarchy of +date-organized logs: + +``` +$ perl sql2txt.pl backup.db +``` + +# Caveats +The C program is rough around the edges. The gz file will come with some +corruption on the end that doesn't seem to affect the resultant file. + +Also, the perl program doesn't handle special cases like incoming or +outgoing calls. Nevertheless, this is a useful tool if ceb2txt has not +been working for you. + +The perl program uses DBD::Sqlite, DBI, Modern::Perl, and DateTime. + diff --git a/cheogram.sql b/cheogram.sql new file mode 100644 index 0000000..ec70bbd --- /dev/null +++ b/cheogram.sql @@ -0,0 +1,41 @@ +-- Cheogram +create table cheogram.messages ( + uuid text, + subject text, + oobUri text, + fileParams text, + payloads text, + timeReceived NUMBER, + occupant_id text +); + +create table cheogram.blocked_media ( + cid text not null primary key +); + +create table cheogram.cids ( + cid text not null primary key, + path text not null, + url text +); +CREATE TABLE cheogram.webxdc_updates ( + serial INTEGER PRIMARY KEY AUTOINCREMENT, + conversationUuid text not null, + sender TEXT NOT NULL, + thread TEXT NOT NULL, + threadParent TEXT, + info TEXT, + document TEXT, + summary TEXT, + payload text, + message_id text +); + +CREATE TABLE cheogram.muted_participants ( + muc_jid TEXT NOT NULL, + occupant_id TEXT NOT NULL, + nick TEXT NOT NULL, + PRIMARY KEY (muc_jid, occupant_id) +); + + diff --git a/readceb.c b/readceb.c new file mode 100644 index 0000000..26aaf04 --- /dev/null +++ b/readceb.c @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +// crypto.h used for the version +#include +#include + +#define READ_BUFFER 1024 + +struct ceb_header { + uint32_t version; + uint16_t app_len; + char* app; + uint16_t jid_len; + char* jid; + uint64_t timestamp; + uint8_t iv[12]; + uint8_t salt[16]; +}; + +int main(int argc, char* argv[]) { + if (argc < 2){ + printf("Need 1 argument."); + exit(1); + } + + // get filename + char* fn = argv[1]; + + // output file + char* output = "out.sqlgz"; + if (argc > 2) output = argv[2]; + + struct ceb_header header; + + // Open file + FILE* ceb = fopen(fn, "r"); + if (ceb == NULL) { + perror("Couldn't open input file"); + exit(errno); + } + + // Read header + fread(&header.version, sizeof(uint32_t), 1, ceb); + header.version = ntohl(header.version); + + // Read app + fread(&header.app_len, sizeof(uint16_t), 1, ceb); + header.app_len = ntohs(header.app_len); + header.app = malloc(header.app_len+1); + fread(header.app, sizeof(char), header.app_len, ceb); + header.app[header.app_len] = 0; + + // Read JID + fread(&header.jid_len, sizeof(uint16_t), 1, ceb); + header.jid_len = ntohs(header.jid_len); + header.jid = malloc(header.jid_len+1); + fread(header.jid, sizeof(char), header.jid_len, ceb); + header.jid[header.jid_len] = 0; + + // Read the reset of the struct + fread(&header.timestamp, sizeof(uint64_t)+12+16, 1, ceb); + + printf("Version %d\n%s, %s\nCreated at %llu\n", header.version, header.app, header.jid, header.timestamp); + + // Get password + char* pw = getpass("Password: "); + + // Derive key + uint8_t key[128]; + PKCS5_PBKDF2_HMAC_SHA1(pw, strlen(pw), header.salt, 16, 1024, 128, key); + + // Read the whole file now + uint8_t* encrypted = malloc(READ_BUFFER); + if (encrypted == NULL) { + printf("Error allocating..\n"); + return 1; + } + size_t bs = READ_BUFFER; + size_t pos = 0; + size_t nread = 0; + + do { + // Perform read + nread = fread(encrypted+pos, sizeof(uint8_t), READ_BUFFER, ceb); + // Move position over by nread + pos += nread; + + // If the position has reached the buffer size + if (pos >= bs) { + // Reallocate + encrypted = realloc(encrypted, pos+READ_BUFFER); + // Increase tracked buffer size + bs += READ_BUFFER; + if (encrypted == NULL) { + printf("Error allocating..\n"); + exit(1); + } + } + } while(nread != 0); + + printf("Total read %ld bytes\n", pos); + + // Reallocate + encrypted = realloc(encrypted, pos); + + + // Try perform decryption + uint8_t* plaintext = malloc(pos); + + EVP_CIPHER_CTX *ctx; + + // Create cipher context + if(!(ctx = EVP_CIPHER_CTX_new())) { + printf("Error creating decryption context\n"); + ERR_print_errors_fp(stderr); + exit(1); + } + + // Load key and iv + if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, header.iv)){ + printf("Could not DecryptInit!\n"); + ERR_print_errors_fp(stderr); + exit(1); + } + + // No padding + EVP_CIPHER_CTX_set_padding(ctx, 0); + + // decryption + int dec_length = 0; + int plaintext_len = 0; + if (1 != EVP_DecryptUpdate(ctx, plaintext, &dec_length, encrypted, pos)){ + printf("Could not DecryptUpdate!\n"); + ERR_print_errors_fp(stderr); + exit(1); + } + plaintext_len += dec_length; + + + // finalize decryption + //if (1 != EVP_DecryptFinal_ex(ctx, plaintext+plaintext_len, &dec_length)) { + // printf("Could not DecryptFinal!\n"); + // ERR_print_errors_fp(stderr); + // exit(1); + //} + + // Cleanup + EVP_CIPHER_CTX_free(ctx); + + printf("Decrypted %d bytes\n", plaintext_len); + + fclose(ceb); + + // Write output file + FILE* outputf = fopen(output, "w"); + if (outputf == NULL) { + perror("Couldn't open output file"); + exit(errno); + } + + fwrite(plaintext, sizeof(uint8_t), plaintext_len, outputf); + + fclose(outputf); + + return 0; +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..ad86497 --- /dev/null +++ b/schema.sql @@ -0,0 +1,87 @@ +-- Corresponds to Conversations versions 2.11.0-beta to 2.??? +-- (internal database format versions 51 to ???). +-- Reference: https://github.com/iNPUTmice/ceb2txt/blob/8b43f40fbce16a0276c9f82a28ab419aa872156d/src/main/java/im/conversations/ceb2txt/Main.java +-- See also https://codeberg.org/iNPUTmice/Conversations/src/commit/d51682a9bc63048db4536a788ac51cc6ad75b23b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +CREATE TABLE accounts ( + uuid text primary key, + username text, + server text, + password text, + display_name text, + status number, + status_message text, + rosterversion text, + options number, + avatar text, + keys text, + hostname text, + port number, + resource text, + pinned_mechanism text, + pinned_channel_binding text, + fast_mechanism text, + fast_token text +); +CREATE TABLE conversations ( + uuid text, + accountUuid text, + name text, + contactUuid text, + contactJid text, + created number, + status number, + mode number, + attributes text +); +CREATE TABLE messages ( + uuid text, + conversationUuid text, + timeSent number, + counterpart text, + trueCounterpart text, + body text, + encryption number, + status number, + type number, + relativeFilePath text, + serverMsgId text, + axolotl_fingerprint text, + carbon number, + edited number, + read number, + oob number, + errorMsg text, + readByMarkers text, + markable number, + remoteMsgId text, + deleted number, + bodyLanguage text +); +CREATE TABLE prekeys ( + account text, + id text, + key text +); +CREATE TABLE signed_prekeys ( + account text, + id text, + key text +); +CREATE TABLE sessions ( + account text, + name text, + device_id text, + key text +); +CREATE TABLE identities ( + account text, + name text, + ownkey text, + fingerprint text, + certificate text, + trust number, + active number, + last_activation number, + key text +); + diff --git a/sql2txt.pl b/sql2txt.pl new file mode 100644 index 0000000..99cb4d2 --- /dev/null +++ b/sql2txt.pl @@ -0,0 +1,85 @@ +#!/usr/bin/env perl +use DBI; +use Modern::Perl '2024'; +use DateTime; + +my $local_time_zone = DateTime::TimeZone->new( name => 'local' ); + +my $dbh = DBI->connect("dbi:SQLite:dbname=$ARGV[0]","",""); + +my $account = $dbh->selectrow_hashref("select uuid,username,server,resource from accounts limit 1"); + + +say "Generating logs for $account->{username} at $account->{server}..."; + +# Create directory +my $dirname = "$account->{username}"."@"."$account->{server}"; +mkdir $dirname; +mkdir $dirname."/group"; +mkdir $dirname."/1on1"; + +# Get conversations +my $sth_conv = $dbh->prepare("select uuid,mode,contactJid from conversations where accountUuid=?"); +my $sth_msg = $dbh->prepare("select body,status,timeSent,counterpart,type from messages where conversationUuid=? order by timeSent asc"); + +$sth_conv->bind_param(1, $account->{uuid}); +$sth_conv->execute(); + +# Loop through conversations +while(my $conv = $sth_conv->fetchrow_hashref) { + $sth_msg->execute($conv->{uuid}); + + # Conversation directory + my $is_group = $conv->{mode} == 1; + my $barejid = (split '/', $conv->{contactJid})[0]; + my $convdir = $dirname.($is_group ? "/group/" : "/1on1/")."$barejid"; + mkdir $convdir or die "Can't make $convdir: $!"; + + # Loop through messages + my $cur_date = '0'; + my $FH_msg = undef; + + while (my $msg = $sth_msg->fetchrow_hashref) { + # Convert to a datetime + my $msg_dt = DateTime->from_epoch( + epoch => $msg->{timeSent}/1000, + time_zone => $local_time_zone, + ); + + my $date = $msg_dt->ymd('-'); + + # Make new text file... + if ($date ne $cur_date) { + if (defined $FH_msg) { + close ($FH_msg); + } + open ($FH_msg, '>', $convdir."/".$date.".txt") or die "Can't open: $!"; + $cur_date = $date; + } + + # Get timestamp + my $timestamp = $msg_dt->hms(':'); + print $FH_msg '[', $timestamp, '] '; + + # 0 = recieved + if ($msg->{status} == 0) { + if ($is_group) { + print $FH_msg (split '/', $msg->{counterpart})[-1], ': '; + } else { + print $FH_msg $barejid, ': '; + } + } else { + if ($is_group) { + print $FH_msg (split '/', $msg->{counterpart})[-1], ' (you): '; + } else { + print $FH_msg $account->{username}, '@', $account->{server}, ': '; + } + } + + # TODO: Body + print $FH_msg $msg->{body}, "\n"; + + } +} + +