Backup exporter ported to Kotlin with adjustments.

Important preferences included in the backup.
Private chat sessions get reset after backup import.
This commit is contained in:
Anton Chekulaev 2020-11-04 23:29:47 +11:00
parent 0db5f98cb6
commit 68dbd91152
22 changed files with 742 additions and 494 deletions

View File

@ -49,6 +49,16 @@
android:layout_marginRight="@dimen/massive_spacing"
android:text="@string/activity_landing_restore_button_title" />
<Button
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/restoreBackupButton"
android:layout_width="match_parent"
android:layout_height="@dimen/medium_button_height"
android:layout_marginLeft="@dimen/massive_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/massive_spacing"
android:text="Backup" />
<Button
android:id="@+id/linkButton"
android:layout_width="match_parent"

View File

@ -1545,6 +1545,7 @@
<string name="RegistrationLockDialog_disable_registration_lock_pin">Disable Registration Lock PIN?</string>
<string name="RegistrationLockDialog_disable">Disable</string>
<string name="preferences_chats__backups">Backups</string>
<string name="preferences_chats__backup_export_error">An error occurred during exporting a backup. Please try again later.</string>
<string name="prompt_passphrase_activity__signal_is_locked">Session is Locked</string>
<string name="prompt_passphrase_activity__tap_to_unlock">TAP TO UNLOCK</string>
<string name="RegistrationLockDialog_reminder">Reminder:</string>

View File

@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.loki.api.BackgroundPollListener;
import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller;
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;

View File

@ -29,7 +29,8 @@ public class BackupDialog {
@NonNull SwitchPreferenceCompat preference,
@NonNull BackupDirSelector backupDirSelector) {
String[] password = BackupUtil.generateBackupPassphrase();
// String[] password = BackupUtil.generateBackupPassphrase();
String[] password = new String[]{"00000", "00000", "00000", "00000", "00000", "00000"};
String passwordSt = Util.join(password, " ");
AlertDialog dialog = new AlertDialog.Builder(context)

View File

@ -18,7 +18,7 @@ public abstract class FullBackupBase {
static class BackupStream {
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
EventBus.getDefault().post(BackupEvent.createProgress(0));
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] input = passphrase.replace(" ", "").getBytes();
@ -27,7 +27,7 @@ public abstract class FullBackupBase {
if (salt != null) digest.update(salt);
for (int i=0;i<250000;i++) {
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
if (i % 1000 == 0) EventBus.getDefault().post(BackupEvent.createProgress(0));
digest.update(hash);
hash = digest.digest(input);
}
@ -47,10 +47,12 @@ public abstract class FullBackupBase {
private final Type type;
private final int count;
private final @Nullable Exception exception;
BackupEvent(Type type, int count) {
private BackupEvent(Type type, int count, @Nullable Exception exception) {
this.type = type;
this.count = count;
this.exception = exception;
}
public Type getType() {
@ -60,6 +62,22 @@ public abstract class FullBackupBase {
public int getCount() {
return count;
}
@Nullable
public Exception getException() {
return exception;
}
public static BackupEvent createProgress(int count) {
return new BackupEvent(Type.PROGRESS, count, null);
}
public static BackupEvent createFinished() {
return new BackupEvent(Type.FINISHED, 0, null);
}
public static BackupEvent createFinished(@Nullable Exception e) {
return new BackupEvent(Type.FINISHED, 0, e);
}
}
}

View File

@ -1,429 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
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.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.Flushable;
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 Uri fileUri,
@NonNull String passphrase)
throws IOException
{
OutputStream baseOutputStream = context.getContentResolver().openOutputStream(fileUri);
if (baseOutputStream == null) {
throw new IOException("Cannot open an output stream for the file URI: " + fileUri.toString());
}
try (BackupFrameOutputStream outputStream = new BackupFrameOutputStream(baseOutputStream, passphrase)) {
outputStream.writeDatabaseVersion(input.getVersion());
List<String> 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(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count);
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, cursor -> exportSticker(attachmentSecret, cursor, outputStream), count);
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
!table.equals(SessionDatabase.TABLE_NAME) &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
!table.startsWith("sqlite_"))
{
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);
}
for (File avatar : AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length());
}
outputStream.writeEnd();
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
}
}
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
throws IOException
{
List<String> 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) {
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table".equals(type)) {
tables.add(name);
}
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<Cursor> predicate,
@Nullable Consumer<Cursor> 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<cursor.getColumnCount();i++) {
statement.append('?');
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setStringParamter(cursor.getString(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setDoubleParameter(cursor.getDouble(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setIntegerParameter(cursor.getLong(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
} else {
throw new AssertionError("unknown type?" + cursor.getType(i));
}
if (i < cursor.getColumnCount()-1) {
statement.append(',');
}
}
statement.append(')');
outputStream.write(statementBuilder.setStatement(statement.toString()).build());
if (postProcess != null) postProcess.accept(cursor);
}
}
}
return count;
}
private static void exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
if (!TextUtils.isEmpty(data) && size <= 0) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data);
}
if (!TextUtils.isEmpty(data) && size > 0) {
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
private static void exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) {
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
}
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
long result = 0;
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
int read;
byte[] buffer = new byte[8192];
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
result += read;
}
return result;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.EXPIRES_IN };
String where = MmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return mmsCursor.getLong(0) == 0;
}
}
return false;
}
private static class BackupFrameOutputStream extends BackupStream implements Closeable, Flushable {
private final OutputStream outputStream;
private final Cipher cipher;
private final Mac mac;
private final byte[] cipherKey;
private final byte[] macKey;
private byte[] iv;
private int counter;
private BackupFrameOutputStream(@NonNull OutputStream outputStream, @NonNull String passphrase) throws IOException {
try {
byte[] salt = Util.getSecretBytes(32);
byte[] key = getBackupKey(passphrase, salt);
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0];
this.macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
this.outputStream = outputStream;
this.iv = Util.getSecretBytes(16);
this.counter = Conversions.byteArrayToInt(iv);
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray();
outputStream.write(Conversions.intToByteArray(header.length));
outputStream.write(header);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public void write(BackupProtos.SharedPreference preference) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
}
public void write(BackupProtos.SqlStatement statement) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
}
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAvatar(BackupProtos.Avatar.newBuilder()
.setName(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build());
writeStream(in);
}
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAttachment(BackupProtos.Attachment.newBuilder()
.setRowId(attachmentId.getRowId())
.setAttachmentId(attachmentId.getUniqueId())
.setLength(Util.toIntExact(size))
.build())
.build());
writeStream(in);
}
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setSticker(BackupProtos.Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build());
writeStream(in);
}
void writeDatabaseVersion(int version) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
.build());
}
void writeEnd() throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
}
private void writeStream(@NonNull InputStream inputStream) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
mac.update(iv);
byte[] buffer = new byte[8192];
int read;
while ((read = inputStream.read(buffer)) != -1) {
byte[] ciphertext = cipher.update(buffer, 0, read);
if (ciphertext != null) {
outputStream.write(ciphertext);
mac.update(ciphertext);
}
}
byte[] remainder = cipher.doFinal();
outputStream.write(remainder);
mac.update(remainder);
byte[] attachmentDigest = mac.doFinal();
outputStream.write(attachmentDigest, 0, 10);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
byte[] frameMac = mac.doFinal(frameCiphertext);
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);
out.write(length);
out.write(frameCiphertext);
out.write(frameMac, 0, 10);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
@Override
public void flush() throws IOException {
outputStream.flush();
}
@Override
public void close() throws IOException {
outputStream.close();
}
}
}

View File

@ -0,0 +1,440 @@
package org.thoughtcrime.securesms.backup
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.text.TextUtils
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.backup.BackupProtos.*
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.*
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.util.Conversions
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.libsignal.kdf.HKDFv3
import org.whispersystems.libsignal.util.ByteUtil
import java.io.*
import java.lang.Exception
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.*
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object FullBackupExporter : FullBackupBase() {
private val TAG = FullBackupExporter::class.java.simpleName
@JvmStatic
@Throws(IOException::class)
fun export(context: Context,
attachmentSecret: AttachmentSecret,
input: SQLiteDatabase,
fileUri: Uri,
passphrase: String) {
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
var count = 0
try {
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
outputStream.writeDatabaseVersion(input.version)
val tables = exportSchema(input, outputStream)
for (table in tables) if (shouldExportTable(table)) {
count = when (table) {
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
},
null,
count)
}
GroupReceiptDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
},
null,
count)
}
AttachmentDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
},
{ cursor: Cursor ->
exportAttachment(attachmentSecret, cursor, outputStream)
},
count)
}
StickerDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ true },
{ cursor: Cursor -> exportSticker(attachmentSecret, cursor, outputStream) },
count)
}
else -> {
exportTable(table, input, outputStream, null, null, count)
}
}
}
for (preference in IdentityKeyUtil.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (preference in TextSecurePreferences.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (avatar in AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
}
outputStream.writeEnd()
}
EventBus.getDefault().post(BackupEvent.createFinished())
} catch (e: Exception) {
Log.e(TAG, "Failed to make full backup.", e)
EventBus.getDefault().post(BackupEvent.createFinished(e))
throw e
}
}
private inline fun shouldExportTable(table: String): Boolean {
return table != SignedPreKeyDatabase.TABLE_NAME &&
table != OneTimePreKeyDatabase.TABLE_NAME &&
table != SessionDatabase.TABLE_NAME &&
table != PushDatabase.TABLE_NAME &&
// table != DraftDatabase.TABLE_NAME &&
table != LokiBackupFilesDatabase.TABLE_NAME &&
table != LokiAPIDatabase.openGroupProfilePictureTable &&
table != JobDatabase.Jobs.TABLE_NAME &&
table != JobDatabase.Constraints.TABLE_NAME &&
table != JobDatabase.Dependencies.TABLE_NAME &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
!table.startsWith("sqlite_")
}
@Throws(IOException::class)
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
val tables: MutableList<String> = LinkedList()
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val sql = cursor.getString(0)
val name = cursor.getString(1)
val type = cursor.getString(2)
if (sql != null) {
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table" == type) {
tables.add(name)
}
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
}
}
}
}
return tables
}
@Throws(IOException::class)
private fun exportTable(table: String,
input: SQLiteDatabase,
outputStream: BackupFrameOutputStream,
predicate: Predicate<Cursor>?,
postProcess: Consumer<Cursor>?,
count: Int): Int {
var count = count
val template = "INSERT INTO $table VALUES "
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
if (predicate != null && !predicate.test(cursor)) continue
val statement = StringBuilder(template)
val statementBuilder = SqlStatement.newBuilder()
statement.append('(')
for (i in 0 until cursor.columnCount) {
statement.append('?')
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_STRING -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setStringParamter(cursor.getString(i)))
}
Cursor.FIELD_TYPE_FLOAT -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setDoubleParameter(cursor.getDouble(i)))
}
Cursor.FIELD_TYPE_INTEGER -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setIntegerParameter(cursor.getLong(i)))
}
Cursor.FIELD_TYPE_BLOB -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
}
Cursor.FIELD_TYPE_NULL -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setNullparameter(true))
}
else -> {
throw AssertionError("unknown type?" + cursor.getType(i))
}
}
if (i < cursor.columnCount - 1) {
statement.append(',')
}
}
statement.append(')')
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
postProcess?.accept(cursor)
}
}
return count
}
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
try {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
if (!TextUtils.isEmpty(data) && size <= 0) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
}
if (!TextUtils.isEmpty(data) && size > 0) {
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
private fun exportSticker(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
try {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH))
val data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH))
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM))
if (!TextUtils.isEmpty(data) && size > 0) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0).use { inputStream -> outputStream.writeSticker(rowId, inputStream, size) }
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
@Throws(IOException::class)
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
var result: Long = 0
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
var read: Int
val buffer = ByteArray(8192)
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
result += read.toLong()
}
return result
}
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
val where = MmsDatabase.ID + " = ?"
val args = arrayOf(mmsId.toString())
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return mmsCursor.getLong(0) == 0L
}
}
return false
}
private class BackupFrameOutputStream : BackupStream, Closeable, Flushable {
private val outputStream: OutputStream
private var cipher: Cipher
private var mac: Mac
private val cipherKey: ByteArray
private val macKey: ByteArray
private val iv: ByteArray
private var counter: Int = 0
constructor(outputStream: OutputStream, passphrase: String) : super() {
try {
val salt = Util.getSecretBytes(32)
val key = getBackupKey(passphrase, salt)
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0]
macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding")
mac = Mac.getInstance("HmacSHA256")
this.outputStream = outputStream
iv = Util.getSecretBytes(16)
counter = Conversions.byteArrayToInt(iv)
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray()
outputStream.write(Conversions.intToByteArray(header.size))
outputStream.write(header)
} catch (e: NoSuchAlgorithmException) {
throw AssertionError(e)
} catch (e: NoSuchPaddingException) {
throw AssertionError(e)
} catch (e: InvalidKeyException) {
throw AssertionError(e)
}
}
@Throws(IOException::class)
fun writeSql(statement: SqlStatement) {
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
}
@Throws(IOException::class)
fun writePreferenceEntry(preference: SharedPreference?) {
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
}
@Throws(IOException::class)
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAvatar(Avatar.newBuilder()
.setName(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAttachment(Attachment.newBuilder()
.setRowId(attachmentId.rowId)
.setAttachmentId(attachmentId.uniqueId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setSticker(Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeDatabaseVersion(version: Int) {
write(outputStream, BackupFrame.newBuilder()
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
.build())
}
@Throws(IOException::class)
fun writeEnd() {
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
}
@Throws(IOException::class)
private fun writeStream(inputStream: InputStream) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.update(iv)
val buffer = ByteArray(8192)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
val ciphertext = cipher.update(buffer, 0, read)
if (ciphertext != null) {
outputStream.write(ciphertext)
mac.update(ciphertext)
}
}
val remainder = cipher.doFinal()
outputStream.write(remainder)
mac.update(remainder)
val attachmentDigest = mac.doFinal()
outputStream.write(attachmentDigest, 0, 10)
} catch (e: InvalidKeyException) {
throw AssertionError(e)
} catch (e: InvalidAlgorithmParameterException) {
throw AssertionError(e)
} catch (e: IllegalBlockSizeException) {
throw AssertionError(e)
} catch (e: BadPaddingException) {
throw AssertionError(e)
}
}
@Throws(IOException::class)
private fun write(out: OutputStream, frame: BackupFrame) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
val frameCiphertext = cipher.doFinal(frame.toByteArray())
val frameMac = mac.doFinal(frameCiphertext)
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
out.write(length)
out.write(frameCiphertext)
out.write(frameMac, 0, 10)
} catch (e: InvalidKeyException) {
throw AssertionError(e)
} catch (e: InvalidAlgorithmParameterException) {
throw AssertionError(e)
} catch (e: IllegalBlockSizeException) {
throw AssertionError(e)
} catch (e: BadPaddingException) {
throw AssertionError(e)
}
}
@Throws(IOException::class)
override fun flush() {
outputStream.flush()
}
@Throws(IOException::class)
override fun close() {
outputStream.close()
}
}
}

View File

@ -1,16 +1,15 @@
package org.thoughtcrime.securesms.backup;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import androidx.annotation.NonNull;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
@ -40,7 +39,6 @@ import org.whispersystems.libsignal.util.ByteUtil;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -62,7 +60,12 @@ import javax.crypto.spec.SecretKeySpec;
public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused")
/**
* Because BackupProtos.SharedPreference was made only to serialize string values,
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
*/
public static final String PREF_PREFIX_TYPE_INT = "i__";
private static final String TAG = FullBackupImporter.class.getSimpleName();
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@ -85,7 +88,7 @@ public class FullBackupImporter extends FullBackupBase {
BackupFrame frame;
while (!(frame = inputStream.readFrame()).getEnd()) {
if (count++ % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count));
if (frame.hasVersion()) processVersion(db, frame.getVersion());
else if (frame.hasStatement()) processStatement(db, frame.getStatement());
@ -104,7 +107,7 @@ public class FullBackupImporter extends FullBackupBase {
}
}
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
EventBus.getDefault().post(BackupEvent.createFinished());
}
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
@ -185,7 +188,18 @@ public class FullBackupImporter extends FullBackupBase {
@SuppressLint("ApplySharedPref")
private static void processPreference(@NonNull Context context, SharedPreference preference) {
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
String key = preference.getKey();
String value = preference.getValue();
// See comments next to PREF_PREFIX_TYPE_* constants.
if (key.startsWith(PREF_PREFIX_TYPE_INT)) {
preferences.edit().putInt(
key.substring(3),
Integer.parseInt(value)
).commit();
} else {
preferences.edit().putString(key, value).commit();
}
}
private static void dropAllTables(@NonNull SQLiteDatabase db) {

View File

@ -122,21 +122,38 @@ public class IdentityKeyUtil {
}
}
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return new LinkedList<BackupProtos.SharedPreference>() {{
add(BackupProtos.SharedPreference.newBuilder()
LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PUBLIC_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
.build());
add(BackupProtos.SharedPreference.newBuilder()
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PRIVATE_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
.build());
}};
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(ED25519_PUBLIC_KEY)
.setValue(preferences.getString(ED25519_PUBLIC_KEY, null))
.build());
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(ED25519_SECRET_KEY)
.setValue(preferences.getString(ED25519_SECRET_KEY, null))
.build());
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(LOKI_SEED)
.setValue(preferences.getString(LOKI_SEED, null))
.build());
return prefList;
}
private static boolean hasLegacyIdentityKeys(Context context) {

View File

@ -17,7 +17,7 @@ import java.util.Set;
public class DraftDatabase extends Database {
private static final String TABLE_NAME = "drafts";
public static final String TABLE_NAME = "drafts";
public static final String ID = "_id";
public static final String THREAD_ID = "thread_id";
public static final String DRAFT_TYPE = "type";

View File

@ -22,8 +22,8 @@ public class JobDatabase extends Database {
Constraints.CREATE_TABLE,
Dependencies.CREATE_TABLE };
private static final class Jobs {
private static final String TABLE_NAME = "job_spec";
public static final class Jobs {
public static final String TABLE_NAME = "job_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@ -53,8 +53,8 @@ public class JobDatabase extends Database {
IS_RUNNING + " INTEGER)";
}
private static final class Constraints {
private static final String TABLE_NAME = "constraint_spec";
public static final class Constraints {
public static final String TABLE_NAME = "constraint_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@ -65,8 +65,8 @@ public class JobDatabase extends Database {
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
}
private static final class Dependencies {
private static final String TABLE_NAME = "dependency_spec";
public static final class Dependencies {
public static final String TABLE_NAME = "dependency_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";

View File

@ -20,7 +20,7 @@ public class PushDatabase extends Database {
private static final String TAG = PushDatabase.class.getSimpleName();
private static final String TABLE_NAME = "push";
public static final String TABLE_NAME = "push";
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String SOURCE = "source";

View File

@ -50,12 +50,15 @@ import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nl.komponents.kovenant.combine.Tuple2;
public class ThreadDatabase extends Database {
private static final String TAG = ThreadDatabase.class.getSimpleName();
@ -618,6 +621,34 @@ public class ThreadDatabase extends Database {
}
}
/**
* A lightweight utility method to retrieve the complete list of non-archived threads coupled with their recipient address.
* @return a tuple with non-null values: thread id, recipient address.
*/
public @NonNull List<Tuple2<Long, String>> getConversationListQuick() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
ArrayList<Tuple2<Long, String>> result = new ArrayList<>();
try (Cursor cursor = db.query(
TABLE_NAME,
new String[]{ID, ADDRESS},
ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + ADDRESS + " IS NOT NULL",
null,
null,
null,
null)) {
while (cursor != null && cursor.moveToNext()) {
result.add(new Tuple2<>(
cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
));
}
}
return result;
}
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
MmsMessageRecord record = (MmsMessageRecord) messageRecord;

View File

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
@ -104,6 +105,7 @@ public class WorkManagerFactoryMappings {
put(TypingSendJob.class.getName(), TypingSendJob.KEY);
put(UpdateApkJob.class.getName(), UpdateApkJob.KEY);
put(PrepareAttachmentAudioExtrasJob.class.getName(), PrepareAttachmentAudioExtrasJob.KEY);
put(ResetThreadSessionJob.class.getName(), ResetThreadSessionJob.KEY);
}};
public static @Nullable String getFactoryKey(@NonNull String workManagerClass) {

View File

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
@ -81,6 +82,7 @@ public final class JobManagerFactories {
put(TypingSendJob.KEY, new TypingSendJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
put(ResetThreadSessionJob.KEY, new ResetThreadSessionJob.Factory());
}};
}

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.loki.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
@ -23,17 +22,17 @@ import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.common.util.Strings
import kotlinx.android.synthetic.main.activity_pn_mode.*
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityBackupRestoreBinding
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.RegistrationActivity
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.utilities.show
import org.thoughtcrime.securesms.notifications.NotificationChannels
@ -115,21 +114,6 @@ class BackupRestoreActivity : BaseActionBarActivity() {
private fun restore() {
if (viewModel.backupFile.value == null && Strings.isEmptyOrWhitespace(viewModel.backupPassphrase.value)) return
// val backupFile = viewModel.backupFile.value!!
// val password = viewModel.backupPassphrase.value!!.trim()
//
// try {
// FullBackupImporter.importFile(
// this,
// AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret(),
// DatabaseFactory.getBackupDatabase(this),
// backupFile,
// password
// )
// } catch (e: IOException) {
// Log.e(TAG, "Failed to restore from the backup file \"$backupFile\"", e)
// }
val backupFile = viewModel.backupFile.value!!
val passphrase = viewModel.backupPassphrase.value!!.trim()
@ -147,8 +131,10 @@ class BackupRestoreActivity : BaseActionBarActivity() {
)
DatabaseFactory.upgradeRestored(context, database)
NotificationChannels.restoreContactNotificationChannels(context)
TextSecurePreferences.setBackupEnabled(context, true)
TextSecurePreferences.setBackupPassphrase(context, passphrase)
// TextSecurePreferences.setBackupEnabled(context, true)
// TextSecurePreferences.setBackupPassphrase(context, passphrase)
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
BackupImportResult.SUCCESS
} catch (e: DatabaseDowngradeException) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
@ -163,6 +149,7 @@ class BackupRestoreActivity : BaseActionBarActivity() {
val context = this@BackupRestoreActivity
when (result) {
BackupImportResult.SUCCESS -> {
TextSecurePreferences.setHasViewedSeed(context, true)
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
TextSecurePreferences.setPromptedPushRegistration(context, true)
TextSecurePreferences.setIsUsingFCM(context, true)
@ -172,6 +159,8 @@ class BackupRestoreActivity : BaseActionBarActivity() {
application.setUpStorageAPIIfNeeded()
application.setUpP2PAPIIfNeeded()
HomeActivity.requestResetAllSessionsOnStartup(context)
val intent = Intent(context, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
show(intent)
@ -222,7 +211,7 @@ class RestoreBackupViewModel(application: Application): AndroidViewModel(applica
}
val backupFile = MutableLiveData<Uri>()
val backupPassphrase = MutableLiveData<String>()
val backupPassphrase = MutableLiveData<String>("000000000000000000000000000000")
fun onBackupFileSelected(backupFile: Uri) {
//TODO Check if backup file is correct.

View File

@ -26,13 +26,13 @@ import kotlinx.android.synthetic.main.activity_home.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
@ -46,6 +46,8 @@ import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.TextSecurePreferences.getBooleanPreference
import org.thoughtcrime.securesms.util.TextSecurePreferences.setBooleanPreference
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
@ -57,6 +59,32 @@ import org.whispersystems.signalservice.loki.utilities.toHexString
import java.io.IOException
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
companion object {
private const val PREF_RESET_ALL_SESSIONS_ON_START_UP = "pref_reset_all_sessions_on_start_up"
@JvmStatic
fun requestResetAllSessionsOnStartup(context: Context) {
setBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, true)
}
@JvmStatic
fun scheduleResetAllSessionsIfRequested(context: Context) {
if (!getBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, false)) return
setBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, false)
val jobManager = ApplicationContext.getInstance(context).jobManager
DatabaseFactory.getThreadDatabase(context).conversationListQuick.forEach { tuple ->
val threadId: Long = tuple.first
val recipientAddress: String = tuple.second
jobManager.add(ResetThreadSessionJob(
Address.fromSerialized(recipientAddress),
threadId))
}
}
}
private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
@ -196,6 +224,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
TextSecurePreferences.setWasUnlinked(this, true)
ApplicationContext.getInstance(this).clearData()
}
// Perform chat sessions reset if requested (usually happens after backup restoration).
scheduleResetAllSessionsIfRequested(this)
}
override fun onResume() {

View File

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.loki.api
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import java.util.concurrent.TimeUnit
class ResetThreadSessionJob private constructor(
parameters: Parameters,
private val address: Address,
private val threadId: Long)
: BaseJob(parameters) {
companion object {
const val KEY = "ResetThreadSessionJob"
const val DATA_KEY_ADDRESS = "address"
const val DATA_KEY_THREAD_ID = "thread_id"
}
constructor(address: Address, threadId: Long) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
address,
threadId)
override fun serialize(): Data {
return Data.Builder()
.putParcelable(DATA_KEY_ADDRESS, address)
.putLong(DATA_KEY_THREAD_ID, threadId)
.build()
}
override fun getFactoryKey(): String { return KEY }
public override fun onRun() {
val recipient = Recipient.from(context, address, false)
// Only reset sessions for private chats.
if (recipient.isGroupRecipient) return
Log.v(KEY, "Resetting session for thread: \"$threadId\", recipient: \"${address.serialize()}\"")
val message = OutgoingEndSessionMessage(OutgoingTextMessage(recipient, "TERMINATE", 0, -1))
MessageSender.send(context, message, threadId, false, null)
}
public override fun onShouldRetry(e: Exception): Boolean {
return false
}
override fun onCanceled() { }
class Factory : Job.Factory<ResetThreadSessionJob> {
override fun create(parameters: Parameters, data: Data): ResetThreadSessionJob {
val address = data.getParcelable(DATA_KEY_ADDRESS, Address.CREATOR)
val threadId = data.getLong(DATA_KEY_THREAD_ID)
return ResetThreadSessionJob(parameters, address, threadId)
}
}
}

View File

@ -71,7 +71,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val openGroupPublicKeyTable = "open_group_public_keys"
@JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);"
// Open group profile picture cache
private val openGroupProfilePictureTable = "open_group_avatar_cache"
public val openGroupProfilePictureTable = "open_group_avatar_cache"
private val openGroupProfilePicture = "open_group_avatar"
@JvmStatic val createOpenGroupProfilePictureTableCommand = "CREATE TABLE $openGroupProfilePictureTable ($publicChatID STRING PRIMARY KEY, $openGroupProfilePicture TEXT NULLABLE DEFAULT NULL);"

View File

@ -18,7 +18,7 @@ class LokiBackupFilesDatabase(context: Context, databaseHelper: SQLCipherOpenHel
: Database(context, databaseHelper) {
companion object {
private const val TABLE_NAME = "backup_files"
public const val TABLE_NAME = "backup_files"
private const val COLUMN_ID = "_id"
private const val COLUMN_URI = "uri"
private const val COLUMN_FILE_SIZE = "file_size"

View File

@ -6,6 +6,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -104,7 +105,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(BackupEvent event) {
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
ProgressPreference preference = findPreference(TextSecurePreferences.BACKUP_NOW);
if (event.getType() == BackupEvent.Type.PROGRESS) {
preference.setEnabled(false);
@ -114,6 +115,14 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
preference.setEnabled(true);
preference.setProgressVisible(false);
setBackupSummary();
if (event.getException() != null) {
Toast.makeText(
getActivity(),
getString(R.string.preferences_chats__backup_export_error),
Toast.LENGTH_LONG)
.show();
}
}
}

View File

@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.backup.BackupProtos;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
@ -24,10 +26,14 @@ import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import network.loki.messenger.R;
import static org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT;
public class TextSecurePreferences {
private static final String TAG = TextSecurePreferences.class.getSimpleName();
@ -1334,4 +1340,38 @@ public class TextSecurePreferences {
setBooleanPreference(context, "has_seen_light_theme_intro_sheet", true);
}
// endregion
// region Backup related
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
final String prefsFileName;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
prefsFileName = PreferenceManager.getDefaultSharedPreferencesName(context);
} else {
prefsFileName = context.getPackageName() + "_preferences";
}
final LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefsFileName)
.setKey(PREF_PREFIX_TYPE_INT + LOCAL_REGISTRATION_ID_PREF)
.setValue(String.valueOf(preferences.getInt(LOCAL_REGISTRATION_ID_PREF, 0)))
.build());
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefsFileName)
.setKey(LOCAL_NUMBER_PREF)
.setValue(preferences.getString(LOCAL_NUMBER_PREF, null))
.build());
prefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefsFileName)
.setKey(PROFILE_NAME_PREF)
.setValue(preferences.getString(PROFILE_NAME_PREF, null))
.build());
return prefList;
}
// endregion
}