Initial commit
This commit is contained in:
commit
d9b8ad5c2c
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue