diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java index 12e5170a8a..55070a771f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java @@ -93,7 +93,8 @@ public class PushTable extends DatabaseTable { cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)), "", true, - false); + false, + null); } } catch (IOException e) { Log.w(TAG, e); @@ -175,7 +176,8 @@ public class PushTable extends DatabaseTable { serverGuid, "", true, - false); + false, + null); } catch (IOException e) { throw new AssertionError(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 0770219ee3..dfbeb6537d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -186,6 +186,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature" private const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp" private const val HIDDEN = "hidden" + const val REPORTING_TOKEN = "reporting_token" @JvmField val CREATE_TABLE = @@ -246,7 +247,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL, $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0, $UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0, - $HIDDEN INTEGER DEFAULT 0 + $HIDDEN INTEGER DEFAULT 0, + $REPORTING_TOKEN BLOB DEFAULT NULL ) """.trimIndent() @@ -308,7 +310,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da BADGES, DISTRIBUTION_LIST_ID, NEEDS_PNI_SIGNATURE, - HIDDEN + HIDDEN, + REPORTING_TOKEN ) private val ID_PROJECTION = arrayOf(ID) @@ -1721,6 +1724,31 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return updated } + fun setReportingToken(id: RecipientId, reportingToken: ByteArray) { + val values = ContentValues(1).apply { + put(REPORTING_TOKEN, reportingToken) + } + + if (update(id, values)) { + ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) + } + } + + fun getReportingToken(id: RecipientId): ByteArray? { + readableDatabase + .select(REPORTING_TOKEN) + .from(TABLE_NAME) + .where(ID_WHERE, id) + .run() + .use { cursor -> + if (cursor.moveToFirst()) { + return cursor.requireBlob(REPORTING_TOKEN) + } else { + return null + } + } + } + fun getSimilarRecipientIds(recipient: Recipient): List { val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name") val where = "checked_name = ? AND $HIDDEN = ?" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 49af660ddf..10287a7fba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V174_ReactionForeig import org.thoughtcrime.securesms.database.helpers.migration.V175_FixFullTextSearchLink import org.thoughtcrime.securesms.database.helpers.migration.V176_AddScheduledDateToQuoteIndex import org.thoughtcrime.securesms.database.helpers.migration.V177_MessageSendLogTableCleanupMigration +import org.thoughtcrime.securesms.database.helpers.migration.V178_ReportingTokenColumnMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -41,7 +42,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 177 + const val DATABASE_VERSION = 178 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -160,6 +161,10 @@ object SignalDatabaseMigrations { if (oldVersion < 177) { V177_MessageSendLogTableCleanupMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 178) { + V178_ReportingTokenColumnMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt new file mode 100644 index 0000000000..4f8cb91852 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * This adds a column to the Recipients table to store a spam reporting token. + */ +object V178_ReportingTokenColumnMigration : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE recipient ADD COLUMN reporting_token BLOB DEFAULT NULL") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index bcebe9bb1c..698545e79c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -24,6 +24,8 @@ import org.thoughtcrime.securesms.messages.MessageDecryptionUtil; import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationIds; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -31,6 +33,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage; import org.whispersystems.signalservice.api.push.PNI; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.LinkedList; @@ -111,6 +114,10 @@ public final class PushDecryptMessageJob extends BaseJob { if (result.getContent().getSenderKeyDistributionMessage().isPresent()) { handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get()); } + result.getContent(); + if (envelope.hasReportingToken()) { + SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken()); + } if (FeatureFlags.phoneNumberPrivacy() && result.getContent().getPniSignatureMessage().isPresent()) { handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java index 9ed1ac81ca..07ff29471d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java @@ -11,13 +11,17 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -68,17 +72,26 @@ public class ReportSpamJob extends BaseJob { return; } - int count = 0; - List reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT); - SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); + int count = 0; + List reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT); + SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); for (ReportSpamData data : reportSpamData) { - Optional serviceId = Recipient.resolved(data.getRecipientId()).getServiceId(); + final RecipientId recipientId = data.getRecipientId(); + + Optional serviceId = Recipient.resolved(recipientId).getServiceId(); if (serviceId.isPresent() && !serviceId.get().isUnknown()) { - signalServiceAccountManager.reportSpam(serviceId.get(), data.getServerGuid()); + String reportingTokenEncoded = ""; + + byte[] reportingTokenBytes = SignalDatabase.recipients().getReportingToken(recipientId); + if (reportingTokenBytes != null) { + reportingTokenEncoded = Base64.encodeBytes(reportingTokenBytes); + } + + signalServiceAccountManager.reportSpam(serviceId.get(), data.getServerGuid(), reportingTokenEncoded); count++; } else { - Log.w(TAG, "Unable to report spam without an ACI for " + data.getRecipientId()); + Log.w(TAG, "Unable to report spam without an ACI for " + recipientId); } } Log.i(TAG, "Reported " + count + " out of " + reportSpamData.size() + " messages in thread " + threadId + " as spam"); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 745271fd3e..ab87d047ea 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -759,8 +759,8 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getCurrencyConversions(); } - public void reportSpam(ServiceId serviceId, String serverGuid) throws IOException { - this.pushServiceSocket.reportSpam(serviceId, serverGuid); + public void reportSpam(ServiceId serviceId, String serverGuid, String reportingToken) throws IOException { + this.pushServiceSocket.reportSpam(serviceId, serverGuid, reportingToken); } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 9199d5ffb1..19be68a99d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -216,7 +216,8 @@ public class SignalServiceMessageReceiver { entity.getServerUuid(), entity.getDestinationUuid(), entity.isUrgent(), - entity.isStory()); + entity.isStory(), + entity.getReportingToken()); } else { envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(), @@ -226,7 +227,8 @@ public class SignalServiceMessageReceiver { entity.getServerUuid(), entity.getDestinationUuid(), entity.isUrgent(), - entity.isStory()); + entity.isStory(), + entity.getReportingToken()); } callback.onMessage(envelope); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 97a75a5a41..4a8106f221 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -65,7 +65,8 @@ public class SignalServiceEnvelope { String uuid, String destinationUuid, boolean urgent, - boolean story) + boolean story, + byte[] reportingToken) { Envelope.Builder builder = Envelope.newBuilder() .setType(Envelope.Type.valueOf(type)) @@ -88,6 +89,10 @@ public class SignalServiceEnvelope { builder.setContent(ByteString.copyFrom(content)); } + if (reportingToken != null) { + builder.setReportingToken(ByteString.copyFrom(reportingToken)); + } + this.envelope = builder.build(); this.serverDeliveredTimestamp = serverDeliveredTimestamp; } @@ -100,7 +105,8 @@ public class SignalServiceEnvelope { String uuid, String destinationUuid, boolean urgent, - boolean story) + boolean story, + byte[] reportingToken) { Envelope.Builder builder = Envelope.newBuilder() .setType(Envelope.Type.valueOf(type)) @@ -118,6 +124,10 @@ public class SignalServiceEnvelope { builder.setContent(ByteString.copyFrom(content)); } + if (reportingToken != null) { + builder.setReportingToken(ByteString.copyFrom(reportingToken)); + } + this.envelope = builder.build(); this.serverDeliveredTimestamp = serverDeliveredTimestamp; } @@ -253,6 +263,14 @@ public class SignalServiceEnvelope { return envelope.getStory(); } + public boolean hasReportingToken() { + return envelope.hasReportingToken(); + } + + public byte[] getReportingToken() { + return envelope.getReportingToken().toByteArray(); + } + private SignalServiceEnvelopeProto.Builder serializeToProto() { SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder() .setType(getType()) @@ -279,6 +297,10 @@ public class SignalServiceEnvelope { builder.setDestinationUuid(getDestinationUuid()); } + if (hasReportingToken()) { + builder.setReportingToken(ByteString.copyFrom(getReportingToken())); + } + return builder; } @@ -308,6 +330,7 @@ public class SignalServiceEnvelope { proto.getServerGuid(), proto.getDestinationUuid(), proto.getUrgent(), - proto.getStory()); + proto.getStory(), + proto.getReportingToken().toByteArray()); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index cb0991e182..a97eb1fdfd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -2487,10 +2487,10 @@ public class PushServiceSocket { } } - public void reportSpam(ServiceId serviceId, String serverGuid) + public void reportSpam(ServiceId serviceId, String serverGuid, String reportingToken) throws NonSuccessfulResponseCodeException, MalformedResponseException, PushNetworkException { - makeServiceRequest(String.format(REPORT_SPAM, serviceId.toString(), serverGuid), "POST", ""); + makeServiceRequest(String.format(REPORT_SPAM, serviceId.toString(), serverGuid), "POST", JsonUtil.toJson(new SpamTokenMessage(reportingToken))); } private static class VerificationCodeResponseHandler implements ResponseCodeHandler { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceEnvelopeEntity.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceEnvelopeEntity.java index 345444697e..27d9041652 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceEnvelopeEntity.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceEnvelopeEntity.java @@ -43,6 +43,9 @@ public class SignalServiceEnvelopeEntity { @JsonProperty private Boolean story; + @JsonProperty + private byte[] reportingToken; + public SignalServiceEnvelopeEntity() {} public int getType() { @@ -100,4 +103,8 @@ public class SignalServiceEnvelopeEntity { public boolean isStory() { return story != null && story; } + + public byte[] getReportingToken() { + return reportingToken; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SpamTokenMessage.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SpamTokenMessage.kt new file mode 100644 index 0000000000..efc96a5ea1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SpamTokenMessage.kt @@ -0,0 +1,5 @@ +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SpamTokenMessage(@JsonProperty val token: String) diff --git a/libsignal/service/src/main/proto/InternalSerialization.proto b/libsignal/service/src/main/proto/InternalSerialization.proto index c94b89e578..b3f9df0f63 100644 --- a/libsignal/service/src/main/proto/InternalSerialization.proto +++ b/libsignal/service/src/main/proto/InternalSerialization.proto @@ -35,6 +35,7 @@ message SignalServiceEnvelopeProto { optional string destinationUuid = 11; optional bool urgent = 12 [default = true]; optional bool story = 13; + optional bytes reportingToken = 14; } message MetadataProto { diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 27677636e7..3c0f67434f 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -36,7 +36,8 @@ message Envelope { optional bool urgent = 14 [default = true]; reserved /*updatedPni*/ 15; // Not used presently, may be used in the future optional bool story = 16; - // NEXT ID: 17 + optional bytes reporting_token = 17; + // NEXT ID: 18 } message Content {