From d1e457713253d1329e9a2198e1f3b200885a256d Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 4 Jun 2019 11:05:03 +1000 Subject: [PATCH 1/4] Implement LokiAPIDatabase --- .../database/helpers/SQLCipherOpenHelper.java | 4 +- .../securesms/loki/LokiAPIDatabase.kt | 82 +++++++++++++++---- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index a1c4adb04a..3d3bae2bf5 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -113,7 +113,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { for (String sql : JobDatabase.CREATE_TABLE) { db.execSQL(sql); } - db.execSQL(LokiAPIDatabase.getCreateTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSwarmCacheTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index ff2a4b70b5..6d08fff367 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -1,44 +1,94 @@ package org.thoughtcrime.securesms.loki +import android.content.ContentValues import android.content.Context +import android.database.Cursor import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.api.LokiAPITarget +import org.whispersystems.signalservice.loki.api.LokiSwarmAPI class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { companion object { - private val tableKey = "loki_api_database" - private val swarmCacheKey = "swarm_cache" - private val lastMessageHashValueKey = "last_message_hash_value" - private val receivedMessageHashValuesKey = "received_message_hash_values" - - @JvmStatic - val createTableCommand = "CREATE TABLE $tableKey ($swarmCacheKey TEXT, $lastMessageHashValueKey TEXT, $receivedMessageHashValuesKey TEXT);" + // Swarm cache + private val swarmCache = "loki_api_swarm_cache" + private val hexEncodedPublicKey = "hex_encoded_public_key" + private val swarm = "swarm" + @JvmStatic val createSwarmCacheTableCommand = "CREATE TABLE $swarmCache ($hexEncodedPublicKey TEXT, $swarm TEXT);" + // Last message hash value cache + private val lastMessageHashValueCache = "loki_api_last_message_hash_value_cache" + private val target = "target" + private val lastMessageHashValue = "last_message_hash_value" + @JvmStatic val createLastMessageHashValueTableCommand = "CREATE TABLE $lastMessageHashValueCache ($target TEXT, $lastMessageHashValue TEXT);" + // Received message hash values cache + private val receivedMessageHashValuesCache = "loki_api_received_message_hash_values_cache" + private val userID = "user_id" + private val receivedMessageHashValues = "received_message_hash_values" + @JvmStatic val createReceivedMessageHashValuesTableCommand = "CREATE TABLE $receivedMessageHashValuesCache ($userID TEXT, $receivedMessageHashValues TEXT);" } - override fun getSwarmCache(): Map>? { - return null + override fun getSwarmCache(hexEncodedPublicKey: String): List? { + return get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor -> + val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) + swarmAsString.split(",").map { LokiAPITarget(it, LokiSwarmAPI.defaultSnodePort) } + } } - override fun setSwarmCache(newValue: Map>) { - // TODO: Implement + override fun setSwarmCache(hexEncodedPublicKey: String, newValue: List) { + val database = databaseHelper.writableDatabase + val swarmAsString = newValue.joinToString(",") { it.address } + database.update(swarmCache, wrap(mapOf( swarm to swarmAsString )), "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) } override fun getLastMessageHashValue(target: LokiAPITarget): String? { - return null + return get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(target.address)) { cursor -> + cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue)) + } } override fun setLastMessageHashValue(target: LokiAPITarget, newValue: String) { - // TODO: Implement + val database = databaseHelper.writableDatabase + database.update(lastMessageHashValueCache, wrap(mapOf( lastMessageHashValue to newValue )), "${Companion.target} = ?", wrap(target.address)) } override fun getReceivedMessageHashValues(): Set? { - return null + return get(receivedMessageHashValuesCache, "$userID = ?", wrap("0")) { cursor -> + val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) + receivedMessageHashValuesAsString.split(",").toSet() + } } override fun setReceivedMessageHashValues(newValue: Set) { - // TODO: Implement + val database = databaseHelper.writableDatabase + val receivedMessageHashValuesAsString = newValue.joinToString(",") + database.update(receivedMessageHashValuesCache, wrap(mapOf( receivedMessageHashValues to receivedMessageHashValuesAsString )), "$userID = ?", wrap("0")) } -} \ No newline at end of file + + // region Convenience + private fun get(table: String, query: String, arguments: Array, get: (Cursor) -> T): T? { + val database = databaseHelper.readableDatabase + var cursor: Cursor? = null + try { + cursor = database.query(table, null, query, arguments, null, null, null) + if (cursor != null && cursor.moveToFirst()) { return get(cursor) } + } catch (e: Exception) { + // Do nothing + } finally { + cursor?.close() + } + return null + } +} + +private inline fun wrap(x: T): Array { + return Array(1) { x } +} + +private fun wrap(x: Map): ContentValues { + val result = ContentValues(x.size) + x.forEach { result.put(it.key, it.value) } + return result +} +// endregion \ No newline at end of file From ab99db60594e77cf8ec9d0ecd9c03e8c1cacac59 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 4 Jun 2019 11:07:20 +1000 Subject: [PATCH 2/4] Make received message hash values cache user specific --- src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 6d08fff367..72d77160c7 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -9,7 +9,7 @@ import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.api.LokiAPITarget import org.whispersystems.signalservice.loki.api.LokiSwarmAPI -class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { +class LokiAPIDatabase(private val userPublicKey: String, context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { companion object { // Swarm cache @@ -54,7 +54,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } override fun getReceivedMessageHashValues(): Set? { - return get(receivedMessageHashValuesCache, "$userID = ?", wrap("0")) { cursor -> + return get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor -> val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) receivedMessageHashValuesAsString.split(",").toSet() } @@ -63,7 +63,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setReceivedMessageHashValues(newValue: Set) { val database = databaseHelper.writableDatabase val receivedMessageHashValuesAsString = newValue.joinToString(",") - database.update(receivedMessageHashValuesCache, wrap(mapOf( receivedMessageHashValues to receivedMessageHashValuesAsString )), "$userID = ?", wrap("0")) + database.update(receivedMessageHashValuesCache, wrap(mapOf( receivedMessageHashValues to receivedMessageHashValuesAsString )), "$userID = ?", wrap(userPublicKey)) } // region Convenience From 158182baa005e74f56e8fe79f9dde17ee6325fa2 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 4 Jun 2019 11:23:44 +1000 Subject: [PATCH 3/4] Make swarm cache port specific Last message hash value cache is intentionally not port specific so that if a snode switches to a different port the information about which messages it has stored isn't lost --- src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 72d77160c7..9c847adc16 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.api.LokiAPITarget -import org.whispersystems.signalservice.loki.api.LokiSwarmAPI class LokiAPIDatabase(private val userPublicKey: String, context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { @@ -32,13 +31,18 @@ class LokiAPIDatabase(private val userPublicKey: String, context: Context, helpe override fun getSwarmCache(hexEncodedPublicKey: String): List? { return get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor -> val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) - swarmAsString.split(",").map { LokiAPITarget(it, LokiSwarmAPI.defaultSnodePort) } + swarmAsString.split(",").map { targetAsString -> + val components = targetAsString.split("?port=") + LokiAPITarget(components[0], components[1].toInt()) + } } } override fun setSwarmCache(hexEncodedPublicKey: String, newValue: List) { val database = databaseHelper.writableDatabase - val swarmAsString = newValue.joinToString(",") { it.address } + val swarmAsString = newValue.joinToString(",") { target -> + "${target.address}?port=${target.port}" + } database.update(swarmCache, wrap(mapOf( swarm to swarmAsString )), "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) } From 4b1c5dfce2f7a7a30827947f4e3a05c1213f585b Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 4 Jun 2019 11:45:29 +1000 Subject: [PATCH 4/4] Quick fix compilation issues --- .../SignalCommunicationModule.java | 2 +- .../securesms/jobs/AttachmentUploadJob.java | 2 +- .../securesms/jobs/PushDecryptJob.java | 2 +- .../mms/AttachmentStreamLocalUriFetcher.java | 2 +- .../securesms/util/RealtimeSleepTimer.java | 89 +++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/RealtimeSleepTimer.java diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 0b3d82c796..9fecccd64f 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -46,13 +46,13 @@ import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.WebRtcCallService; +import org.thoughtcrime.securesms.util.RealtimeSleepTimer; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.util.CredentialsProvider; -import org.whispersystems.signalservice.api.util.RealtimeSleepTimer; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.ConnectivityListener; diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index c9866c390a..8157ae06d5 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -85,7 +85,7 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType { MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment); SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment); - SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream()); + SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), false); Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment)).get(); database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 388f1ee9eb..ebfc17d8a7 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -106,7 +106,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; -import org.whispersystems.signalservice.loki.messages.LokiServiceMessage; +import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage; import java.security.MessageDigest; import java.util.ArrayList; diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java index 8475761428..53056eef3c 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java @@ -37,7 +37,7 @@ class AttachmentStreamLocalUriFetcher implements DataFetcher { public void loadData(Priority priority, DataCallback callback) { try { if (!digest.isPresent()) throw new InvalidMessageException("No attachment digest!"); - is = AttachmentCipherInputStream.createFor(attachment, plaintextLength, key, digest.get()); + is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get()); callback.onDataReady(is); } catch (IOException | InvalidMessageException e) { callback.onLoadFailed(e); diff --git a/src/org/thoughtcrime/securesms/util/RealtimeSleepTimer.java b/src/org/thoughtcrime/securesms/util/RealtimeSleepTimer.java new file mode 100644 index 0000000000..99e244e8dd --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/RealtimeSleepTimer.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.util; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; + +import org.whispersystems.signalservice.api.util.SleepTimer; + +import java.util.concurrent.TimeUnit; + +/** + * A sleep timer that is based on elapsed realtime, so + * that it works properly, even in low-power sleep modes. + * + */ +public class RealtimeSleepTimer implements SleepTimer { + private static final String TAG = RealtimeSleepTimer.class.getSimpleName(); + + private final AlarmReceiver alarmReceiver; + private final Context context; + + public RealtimeSleepTimer(Context context) { + this.context = context; + alarmReceiver = new RealtimeSleepTimer.AlarmReceiver(); + } + + @Override + public void sleep(long millis) { + context.registerReceiver(alarmReceiver, + new IntentFilter(AlarmReceiver.WAKE_UP_THREAD_ACTION)); + + final long startTime = System.currentTimeMillis(); + alarmReceiver.setAlarm(millis); + + while (System.currentTimeMillis() - startTime < millis) { + try { + synchronized (this) { + wait(millis - System.currentTimeMillis() + startTime); + } + } catch (InterruptedException e) { + Log.w(TAG, e); + } + } + + context.unregisterReceiver(alarmReceiver); + } + + private class AlarmReceiver extends BroadcastReceiver { + private static final String WAKE_UP_THREAD_ACTION = "org.whispersystems.signalservice.api.util.RealtimeSleepTimer.AlarmReceiver.WAKE_UP_THREAD"; + + private void setAlarm(long millis) { + final Intent intent = new Intent(WAKE_UP_THREAD_ACTION); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); + final AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + + Log.w(TAG, "Setting alarm to wake up in " + millis + "ms."); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } else { + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.w(TAG, "Waking up."); + + synchronized (RealtimeSleepTimer.this) { + RealtimeSleepTimer.this.notifyAll(); + } + } + } +} +