conversations-classic/src/main/java/eu/siacs/conversations/services/ExportBackupService.java

281 lines
12 KiB
Java
Raw Normal View History

2019-01-22 18:25:45 +00:00
package eu.siacs.conversations.services;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
public class ExportBackupService extends Service {
public static final String KEYTYPE = "AES";
public static final String CIPHERMODE = "AES/GCM/NoPadding";
public static final String PROVIDER = "BC";
private static final int NOTIFICATION_ID = 19;
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
private NotificationManager notificationManager;
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
mAccounts = mDatabaseBackend.getAccounts();
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
export();
stopForeground(true);
running.set(false);
stopSelf();
}).start();
}
return START_NOT_STICKY;
}
private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
StringBuilder builder = new StringBuilder();
final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
while (accountCursor != null && accountCursor.moveToNext()) {
builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
builder.append(accountCursor.getColumnName(i));
}
builder.append(") VALUES(");
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
final String value = accountCursor.getString(i);
if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
builder.append("NULL");
} else if (value.matches("\\d+")) {
int intValue = Integer.parseInt(value);
Log.d(Config.LOGTAG,"reading int value. "+intValue);
if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
intValue |= 1 << Account.OPTION_DISABLED;
Log.d(Config.LOGTAG,"modified int value "+intValue);
}
builder.append(intValue);
} else {
DatabaseUtils.appendEscapedSQLString(builder, value);
}
}
builder.append(")");
builder.append(';');
builder.append('\n');
}
Log.d(Config.LOGTAG,builder.toString());
if (accountCursor != null) {
accountCursor.close();
}
writer.append(builder.toString());
}
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " messages");
int i = 0;
int p = 0;
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(Message.TABLENAME, cursor, 20));
if (i + 20 > size) {
i = size;
} else {
i += 20;
}
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID,progress.build(p));
Log.d(Config.LOGTAG, "percentage=" + p);
}
}
if (cursor != null) {
cursor.close();
}
}
private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(table, cursor, 20));
}
if (cursor != null) {
cursor.close();
}
}
private void export() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
try {
int count = 0;
final int max = this.mAccounts.size();
final SecureRandom secureRandom = new SecureRandom();
for (Account account : this.mAccounts) {
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name),account.getJid(),System.currentTimeMillis(),IV,salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(this)+account.getJid().asBareJid().toEscapedString()+".ceb");
if (file.getParentFile().mkdirs()) {
Log.d(Config.LOGTAG,"created backup directory "+file.getParentFile().getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = getKey(account.getPassword(), salt);
Log.d(Config.LOGTAG,backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for(String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT,uuid,writer);
}
writer.flush();
writer.close();
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to create backup ", e);
}
}
public static byte[] getKey(String password, byte[] salt) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new AssertionError(e);
}
}
private static String cursorToString(String tablename, Cursor cursor, int max) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO ").append(tablename).append("(");
for (int i = 0; i < cursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
builder.append(cursor.getColumnName(i));
}
builder.append(") VALUES");
for (int i = 0; i < max; ++i) {
if (i != 0) {
builder.append(',');
}
appendValues(cursor, builder);
if (!cursor.moveToNext()) {
break;
}
}
builder.append(';');
builder.append('\n');
return builder.toString();
}
private static void appendValues(Cursor cursor, StringBuilder builder) {
builder.append("(");
for (int i = 0; i < cursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
final String value = cursor.getString(i);
if (value == null) {
builder.append("NULL");
} else if (value.matches("\\d+")) {
builder.append(value);
} else {
DatabaseUtils.appendEscapedSQLString(builder, value);
}
}
builder.append(")");
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class Progress {
private final NotificationCompat.Builder builder;
private final int max;
private final int count;
private Progress(NotificationCompat.Builder builder, int max, int count) {
this.builder = builder;
this.max = max;
this.count = count;
}
private Notification build(int percentage) {
builder.setProgress(max * 100,count * 100 + percentage,false);
return builder.build();
}
}
}