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