package org.thoughtcrime.securesms.backup; import android.content.Context; import android.database.Cursor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.annimon.stream.function.Consumer; import com.annimon.stream.function.Predicate; import com.google.protobuf.ByteString; import net.sqlcipher.database.SQLiteDatabase; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.SessionDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.util.Conversions; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.libsignal.util.ByteUtil; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.LinkedList; import java.util.List; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class FullBackupExporter extends FullBackupBase { @SuppressWarnings("unused") private static final String TAG = FullBackupExporter.class.getSimpleName(); public static void export(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase input, @NonNull File output, @NonNull String passphrase) throws IOException { BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase); outputStream.writeDatabaseVersion(input.getVersion()); List tables = exportSchema(input, outputStream); int count = 0; for (String table : tables) { if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) { count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count); } else if (table.equals(AttachmentDatabase.TABLE_NAME)) { count = exportTable(table, input, outputStream, null, cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count); } else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) && !table.equals(OneTimePreKeyDatabase.TABLE_NAME) && !table.equals(SessionDatabase.TABLE_NAME)) { count = exportTable(table, input, outputStream, null, null, count); } } for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); outputStream.write(preference); } outputStream.writeEnd(); outputStream.close(); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count)); } private static List exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream) throws IOException { List tables = new LinkedList<>(); try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master", null)) { while (cursor != null && cursor.moveToNext()) { String sql = cursor.getString(0); String name = cursor.getString(1); String type = cursor.getString(2); if (sql != null) { if ("table".equals(type)) { outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP TABLE IF EXISTS " + name).build()); tables.add(name); } else if ("index".equals(type)) { outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP INDEX IF EXISTS " + name).build()); } outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build()); } } } return tables; } private static int exportTable(@NonNull String table, @NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream, @Nullable Predicate predicate, @Nullable Consumer postProcess, int count) throws IOException { String template = "INSERT INTO " + table + " VALUES "; try (Cursor cursor = input.rawQuery("SELECT * FROM " + table, null)) { while (cursor != null && cursor.moveToNext()) { EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); if (predicate == null || predicate.test(cursor)) { StringBuilder statement = new StringBuilder(template); BackupProtos.SqlStatement.Builder statementBuilder = BackupProtos.SqlStatement.newBuilder(); statement.append('('); for (int i=0;i