Initial commit

This commit is contained in:
Citlali del Rey 2024-02-18 16:45:10 -08:00
commit d9b8ad5c2c
Signed by: nullobsi
GPG Key ID: 933A1F44222C2634
5 changed files with 431 additions and 0 deletions

43
README.md Normal file
View File

@ -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.

41
cheogram.sql Normal file
View File

@ -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)
);

175
readceb.c Normal file
View File

@ -0,0 +1,175 @@
#include <errno.h>
#include<stdlib.h>
#include<stdio.h>
#include<libgen.h>
#include <sys/syslimits.h>
#include<string.h>
#include<pwd.h>
#include<unistd.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
// crypto.h used for the version
#include <openssl/crypto.h>
#include <openssl/err.h>
#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;
}

87
schema.sql Normal file
View File

@ -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
);

85
sql2txt.pl Normal file
View File

@ -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";
}
}