/** * Copyright (C) 2011 Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.thoughtcrime.SMP.database; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.net.Uri; import android.util.Log; import org.thoughtcrime.SMP.crypto.MasterCipher; import org.thoughtcrime.SMP.crypto.MasterSecret; import org.thoughtcrime.SMP.recipients.RecipientFactory; import org.thoughtcrime.SMP.recipients.Recipients; import java.util.StringTokenizer; public class SmsMigrator { private static void addEncryptedStringToStatement(Context context, SQLiteStatement statement, Cursor cursor, MasterSecret masterSecret, int index, String key) { int columnIndex = cursor.getColumnIndexOrThrow(key); if (cursor.isNull(columnIndex)) { statement.bindNull(index); } else { statement.bindString(index, encrypt(masterSecret, cursor.getString(columnIndex))); } } private static void addStringToStatement(SQLiteStatement statement, Cursor cursor, int index, String key) { int columnIndex = cursor.getColumnIndexOrThrow(key); if (cursor.isNull(columnIndex)) { statement.bindNull(index); } else { statement.bindString(index, cursor.getString(columnIndex)); } } private static void addIntToStatement(SQLiteStatement statement, Cursor cursor, int index, String key) { int columnIndex = cursor.getColumnIndexOrThrow(key); if (cursor.isNull(columnIndex)) { statement.bindNull(index); } else { statement.bindLong(index, cursor.getLong(columnIndex)); } } private static void addTranslatedTypeToStatement(SQLiteStatement statement, Cursor cursor, int index, String key) { int columnIndex = cursor.getColumnIndexOrThrow(key); if (cursor.isNull(columnIndex)) { statement.bindLong(index, SmsDatabase.Types.BASE_INBOX_TYPE | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT); } else { long theirType = cursor.getLong(columnIndex); statement.bindLong(index, SmsDatabase.Types.translateFromSystemBaseType(theirType) | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT); } } private static boolean isAppropriateTypeForMigration(Cursor cursor, int columnIndex) { long systemType = cursor.getLong(columnIndex); long ourType = SmsDatabase.Types.translateFromSystemBaseType(systemType); return ourType == MmsSmsColumns.Types.BASE_INBOX_TYPE || ourType == MmsSmsColumns.Types.BASE_SENT_TYPE || ourType == MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE; } private static void getContentValuesForRow(Context context, MasterSecret masterSecret, Cursor cursor, long threadId, SQLiteStatement statement) { addStringToStatement(statement, cursor, 1, SmsDatabase.ADDRESS); addIntToStatement(statement, cursor, 2, SmsDatabase.PERSON); addIntToStatement(statement, cursor, 3, SmsDatabase.DATE_RECEIVED); addIntToStatement(statement, cursor, 4, SmsDatabase.DATE_RECEIVED); addIntToStatement(statement, cursor, 5, SmsDatabase.PROTOCOL); addIntToStatement(statement, cursor, 6, SmsDatabase.READ); addIntToStatement(statement, cursor, 7, SmsDatabase.STATUS); addTranslatedTypeToStatement(statement, cursor, 8, SmsDatabase.TYPE); addIntToStatement(statement, cursor, 9, SmsDatabase.REPLY_PATH_PRESENT); addStringToStatement(statement, cursor, 10, SmsDatabase.SUBJECT); addEncryptedStringToStatement(context, statement, cursor, masterSecret, 11, SmsDatabase.BODY); addStringToStatement(statement, cursor, 12, SmsDatabase.SERVICE_CENTER); statement.bindLong(13, threadId); } private static String getTheirCanonicalAddress(Context context, String theirRecipientId) { Uri uri = Uri.parse("content://mms-sms/canonical-address/" + theirRecipientId); Cursor cursor = null; try { cursor = context.getContentResolver().query(uri, null, null, null, null); if (cursor != null && cursor.moveToFirst()) { return cursor.getString(0); } else { return null; } } catch (IllegalStateException iae) { Log.w("SmsMigrator", iae); return null; } finally { if (cursor != null) cursor.close(); } } private static Recipients getOurRecipients(Context context, String theirRecipients) { StringTokenizer tokenizer = new StringTokenizer(theirRecipients.trim(), " "); StringBuilder sb = new StringBuilder(); while (tokenizer.hasMoreTokens()) { String theirRecipientId = tokenizer.nextToken(); String address = getTheirCanonicalAddress(context, theirRecipientId); if (address == null) continue; if (sb.length() != 0) sb.append(','); sb.append(address); } if (sb.length() == 0) return null; else return RecipientFactory.getRecipientsFromString(context, sb.toString(), true); } private static String encrypt(MasterSecret masterSecret, String body) { MasterCipher masterCipher = new MasterCipher(masterSecret); return masterCipher.encryptBody(body); } private static void migrateConversation(Context context, MasterSecret masterSecret, SmsMigrationProgressListener listener, ProgressDescription progress, long theirThreadId, long ourThreadId) { SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context); Cursor cursor = null; try { Uri uri = Uri.parse("content://sms/conversations/" + theirThreadId); cursor = context.getContentResolver().query(uri, null, null, null, null); SQLiteDatabase transaction = ourSmsDatabase.beginTransaction(); SQLiteStatement statement = ourSmsDatabase.createInsertStatement(transaction); while (cursor != null && cursor.moveToNext()) { int typeColumn = cursor.getColumnIndex(SmsDatabase.TYPE); if (cursor.isNull(typeColumn) || isAppropriateTypeForMigration(cursor, typeColumn)) { getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement); statement.execute(); } listener.progressUpdate(new ProgressDescription(progress, cursor.getCount(), cursor.getPosition())); } ourSmsDatabase.endTransaction(transaction); DatabaseFactory.getThreadDatabase(context).update(ourThreadId); DatabaseFactory.getThreadDatabase(context).notifyConversationListeners(ourThreadId); } finally { if (cursor != null) cursor.close(); } } public static void migrateDatabase(Context context, MasterSecret masterSecret, SmsMigrationProgressListener listener) { // if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false)) // return; ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); Cursor cursor = null; try { Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true"); cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC"); while (cursor != null && cursor.moveToNext()) { long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids")); Recipients ourRecipients = getOurRecipients(context, theirRecipients); ProgressDescription progress = new ProgressDescription(cursor.getCount(), cursor.getPosition(), 100, 0); if (ourRecipients != null) { long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients); migrateConversation(context, masterSecret, listener, progress, theirThreadId, ourThreadId); } progress.incrementPrimaryComplete(); listener.progressUpdate(progress); } } finally { if (cursor != null) cursor.close(); } context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).edit() .putBoolean("migrated", true).apply(); } public interface SmsMigrationProgressListener { public void progressUpdate(ProgressDescription description); } public static class ProgressDescription { public final int primaryTotal; public int primaryComplete; public final int secondaryTotal; public final int secondaryComplete; public ProgressDescription(int primaryTotal, int primaryComplete, int secondaryTotal, int secondaryComplete) { this.primaryTotal = primaryTotal; this.primaryComplete = primaryComplete; this.secondaryTotal = secondaryTotal; this.secondaryComplete = secondaryComplete; } public ProgressDescription(ProgressDescription that, int secondaryTotal, int secondaryComplete) { this.primaryComplete = that.primaryComplete; this.primaryTotal = that.primaryTotal; this.secondaryComplete = secondaryComplete; this.secondaryTotal = secondaryTotal; } public void incrementPrimaryComplete() { primaryComplete += 1; } } }