Add determinte progress and foreground service for sqlcipher migration

This commit is contained in:
Moxie Marlinspike 2018-02-01 16:01:24 -08:00
parent bdd4b456c4
commit 23aee53c7d
7 changed files with 139 additions and 16 deletions

View File

@ -443,6 +443,8 @@
</intent-filter>
</service>
<service android:name=".service.GenericForegroundService"/>
<receiver android:name=".gcm.GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />

View File

@ -34,7 +34,7 @@
android:layout_gravity="center"/>
<ProgressBar android:id="@+id/determinate_progress"
style="@android:style/Widget.ProgressBar.Horizontal"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:indeterminate="false"

View File

@ -1317,6 +1317,7 @@
<string name="Permissions_not_now">Not now</string>
<string name="ConversationListActivity_signal_needs_contacts_permission_in_order_to_search_your_contacts_but_it_has_been_permanently_denied">Signal needs Contacts permission in order to search your contacts, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Contacts\".</string>
<string name="conversation_activity__enable_signal_messages">ENABLE SIGNAL MESSAGES</string>
<string name="SQLCipherMigrationHelper_migrating_signal_database">Migrating Signal database</string>
<!-- EOF -->

View File

@ -156,7 +156,8 @@ public class DatabaseFactory {
SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret,
legacyOpenHelper.getWritableDatabase(),
databaseHelper.getWritableDatabase());
databaseHelper.getWritableDatabase(),
listener);
}
}

View File

@ -10,13 +10,16 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.annimon.stream.function.Function;
import com.annimon.stream.function.BiFunction;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidMessageException;
@ -30,11 +33,13 @@ public class SQLCipherMigrationHelper {
private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
static void migratePlaintext(@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
static void migratePlaintext(@NonNull Context context,
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
{
modernDb.beginTransaction();
try {
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
copyTable("identities", legacyDb, modernDb, null);
copyTable("push", legacyDb, modernDb, null);
copyTable("groups", legacyDb, modernDb, null);
@ -43,13 +48,15 @@ public class SQLCipherMigrationHelper {
modernDb.setTransactionSuccessful();
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context);
}
}
public static void migrateCiphertext(@NonNull Context context,
@NonNull MasterSecret masterSecret,
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
@Nullable DatabaseUpgradeActivity.DatabaseUpgradeListener listener)
{
MasterCipher legacyCipher = new MasterCipher(masterSecret);
AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
@ -57,7 +64,10 @@ public class SQLCipherMigrationHelper {
modernDb.beginTransaction();
try {
copyTable("sms", legacyDb, modernDb, (row) -> {
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
int total = 5000;
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
row.getAsLong("type"),
row.getAsString("body"));
@ -65,10 +75,14 @@ public class SQLCipherMigrationHelper {
row.put("body", plaintext.second);
row.put("type", plaintext.first);
if (listener != null && (progress.first % 1000 == 0)) {
listener.setProgress(getTotalProgress(0, progress.first, progress.second), total);
}
return row;
});
copyTable("mms", legacyDb, modernDb, (row) -> {
copyTable("mms", legacyDb, modernDb, (row, progress) -> {
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
row.getAsLong("msg_box"),
row.getAsString("body"));
@ -76,10 +90,14 @@ public class SQLCipherMigrationHelper {
row.put("body", plaintext.second);
row.put("msg_box", plaintext.first);
if (listener != null && (progress.first % 1000 == 0)) {
listener.setProgress(getTotalProgress(1000, progress.first, progress.second), total);
}
return row;
});
copyTable("part", legacyDb, modernDb, (row) -> {
copyTable("part", legacyDb, modernDb, (row, progress) -> {
String fileName = row.getAsString("file_name");
String mediaKey = row.getAsString("cd");
@ -107,11 +125,14 @@ public class SQLCipherMigrationHelper {
Log.w(TAG, e);
}
if (listener != null && (progress.first % 1000 == 0)) {
listener.setProgress(getTotalProgress(2000, progress.first, progress.second), total);
}
return row;
});
copyTable("thread", legacyDb, modernDb, (row) -> {
copyTable("thread", legacyDb, modernDb, (row, progress) -> {
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
row.getAsLong("snippet_type"),
row.getAsString("snippet"));
@ -119,11 +140,15 @@ public class SQLCipherMigrationHelper {
row.put("snippet", plaintext.second);
row.put("snippet_type", plaintext.first);
if (listener != null && (progress.first % 1000 == 0)) {
listener.setProgress(getTotalProgress(3000, progress.first, progress.second), total);
}
return row;
});
copyTable("drafts", legacyDb, modernDb, (row) -> {
copyTable("drafts", legacyDb, modernDb, (row, progress) -> {
String draftType = row.getAsString("type");
String draft = row.getAsString("value");
@ -134,24 +159,31 @@ public class SQLCipherMigrationHelper {
Log.w(TAG, e);
}
if (listener != null && (progress.first % 1000 == 0)) {
listener.setProgress(getTotalProgress(4000, progress.first, progress.second), total);
}
return row;
});
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
modernDb.setTransactionSuccessful();
} finally {
modernDb.endTransaction();
GenericForegroundService.stopForegroundTask(context);
}
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
}
private static void copyTable(@NonNull String tableName,
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
@Nullable Function<ContentValues, ContentValues> transformer)
@Nullable BiFunction<ContentValues, Pair<Integer, Integer>, ContentValues> transformer)
{
try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) {
int count = (cursor != null) ? cursor.getCount() : 0;
int progress = 1;
while (cursor != null && cursor.moveToNext()) {
ContentValues row = new ContentValues();
@ -167,7 +199,7 @@ public class SQLCipherMigrationHelper {
}
if (transformer != null) {
row = transformer.apply(row);
row = transformer.apply(row, new Pair<>(progress++, count));
}
modernDb.insert(tableName, null, row);
@ -194,4 +226,9 @@ public class SQLCipherMigrationHelper {
return new Pair<>(type, body);
}
private static int getTotalProgress(int sectionOffset, int sectionProgress, int sectionTotal) {
double percentOfSectionComplete = ((double)sectionProgress) / ((double)sectionTotal);
return sectionOffset + (int)(((double)1000) * percentOfSectionComplete);
}
}

View File

@ -78,11 +78,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
android.database.sqlite.SQLiteDatabase legacyDb = legacyHelper.getWritableDatabase();
SQLCipherMigrationHelper.migratePlaintext(legacyDb, db);
SQLCipherMigrationHelper.migratePlaintext(context, legacyDb, db);
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db);
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null);
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
}
}

View File

@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import java.util.concurrent.atomic.AtomicInteger;
public class GenericForegroundService extends Service {
private static final int NOTIFICATION_ID = 827353982;
private static final String EXTRA_TITLE = "extra_title";
private static final String ACTION_START = "start";
private static final String ACTION_STOP = "stop";
private final AtomicInteger foregroundCount = new AtomicInteger(0);
@Override
public void onCreate() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && ACTION_START.equals(intent.getAction())) handleStart(intent);
else if (intent != null && ACTION_STOP.equals(intent.getAction())) handleStop();
return START_NOT_STICKY;
}
private void handleStart(@NonNull Intent intent) {
String title = intent.getStringExtra(EXTRA_TITLE);
assert title != null;
if (foregroundCount.getAndIncrement() == 0) {
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_signal_grey_24dp)
.setContentTitle(title)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0))
.build());
}
}
private void handleStop() {
if (foregroundCount.decrementAndGet() == 0) {
stopForeground(true);
stopSelf();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
public static void startForegroundTask(@NonNull Context context, @NonNull String task) {
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_START);
intent.putExtra(EXTRA_TITLE, task);
context.startService(intent);
}
public static void stopForegroundTask(@NonNull Context context) {
Intent intent = new Intent(context, GenericForegroundService.class);
intent.setAction(ACTION_STOP);
context.startService(intent);
}
}