Store & submit spam reporting token from server.

This commit is contained in:
Nicholas
2023-02-02 10:03:56 -05:00
committed by Nicholas Tinsley
parent 6a8e82ef91
commit 72449fd73e
14 changed files with 128 additions and 21 deletions

View File

@@ -93,7 +93,8 @@ public class PushTable extends DatabaseTable {
cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)), cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)),
"", "",
true, true,
false); false,
null);
} }
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
@@ -175,7 +176,8 @@ public class PushTable extends DatabaseTable {
serverGuid, serverGuid,
"", "",
true, true,
false); false,
null);
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@@ -186,6 +186,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature" private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature"
private const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp" private const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp"
private const val HIDDEN = "hidden" private const val HIDDEN = "hidden"
const val REPORTING_TOKEN = "reporting_token"
@JvmField @JvmField
val CREATE_TABLE = val CREATE_TABLE =
@@ -246,7 +247,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
$DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL, $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL,
$NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0, $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0,
$UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0, $UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0,
$HIDDEN INTEGER DEFAULT 0 $HIDDEN INTEGER DEFAULT 0,
$REPORTING_TOKEN BLOB DEFAULT NULL
) )
""".trimIndent() """.trimIndent()
@@ -308,7 +310,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
BADGES, BADGES,
DISTRIBUTION_LIST_ID, DISTRIBUTION_LIST_ID,
NEEDS_PNI_SIGNATURE, NEEDS_PNI_SIGNATURE,
HIDDEN HIDDEN,
REPORTING_TOKEN
) )
private val ID_PROJECTION = arrayOf(ID) private val ID_PROJECTION = arrayOf(ID)
@@ -1721,6 +1724,31 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return updated 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<RecipientId> { fun getSimilarRecipientIds(recipient: Recipient): List<RecipientId> {
val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name") val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name")
val where = "checked_name = ? AND $HIDDEN = ?" val where = "checked_name = ? AND $HIDDEN = ?"

View File

@@ -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.V175_FixFullTextSearchLink
import org.thoughtcrime.securesms.database.helpers.migration.V176_AddScheduledDateToQuoteIndex 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.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. * 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) val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
const val DATABASE_VERSION = 177 const val DATABASE_VERSION = 178
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -160,6 +161,10 @@ object SignalDatabaseMigrations {
if (oldVersion < 177) { if (oldVersion < 177) {
V177_MessageSendLogTableCleanupMigration.migrate(context, db, oldVersion, newVersion) V177_MessageSendLogTableCleanupMigration.migrate(context, db, oldVersion, newVersion)
} }
if (oldVersion < 178) {
V178_ReportingTokenColumnMigration.migrate(context, db, oldVersion, newVersion)
}
} }
@JvmStatic @JvmStatic

View File

@@ -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")
}
}

View File

@@ -24,6 +24,8 @@ import org.thoughtcrime.securesms.messages.MessageDecryptionUtil;
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult; import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds; 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.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage; import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.LinkedList; import java.util.LinkedList;
@@ -111,6 +114,10 @@ public final class PushDecryptMessageJob extends BaseJob {
if (result.getContent().getSenderKeyDistributionMessage().isPresent()) { if (result.getContent().getSenderKeyDistributionMessage().isPresent()) {
handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get()); 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()) { if (FeatureFlags.phoneNumberPrivacy() && result.getContent().getPniSignatureMessage().isPresent()) {
handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get()); handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get());

View File

@@ -11,13 +11,17 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -68,17 +72,26 @@ public class ReportSpamJob extends BaseJob {
return; return;
} }
int count = 0; int count = 0;
List<ReportSpamData> reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT); List<ReportSpamData> reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT);
SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
for (ReportSpamData data : reportSpamData) { for (ReportSpamData data : reportSpamData) {
Optional<ServiceId> serviceId = Recipient.resolved(data.getRecipientId()).getServiceId(); final RecipientId recipientId = data.getRecipientId();
Optional<ServiceId> serviceId = Recipient.resolved(recipientId).getServiceId();
if (serviceId.isPresent() && !serviceId.get().isUnknown()) { 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++; count++;
} else { } 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"); Log.i(TAG, "Reported " + count + " out of " + reportSpamData.size() + " messages in thread " + threadId + " as spam");

View File

@@ -759,8 +759,8 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getCurrencyConversions(); return this.pushServiceSocket.getCurrencyConversions();
} }
public void reportSpam(ServiceId serviceId, String serverGuid) throws IOException { public void reportSpam(ServiceId serviceId, String serverGuid, String reportingToken) throws IOException {
this.pushServiceSocket.reportSpam(serviceId, serverGuid); this.pushServiceSocket.reportSpam(serviceId, serverGuid, reportingToken);
} }
/** /**

View File

@@ -216,7 +216,8 @@ public class SignalServiceMessageReceiver {
entity.getServerUuid(), entity.getServerUuid(),
entity.getDestinationUuid(), entity.getDestinationUuid(),
entity.isUrgent(), entity.isUrgent(),
entity.isStory()); entity.isStory(),
entity.getReportingToken());
} else { } else {
envelope = new SignalServiceEnvelope(entity.getType(), envelope = new SignalServiceEnvelope(entity.getType(),
entity.getTimestamp(), entity.getTimestamp(),
@@ -226,7 +227,8 @@ public class SignalServiceMessageReceiver {
entity.getServerUuid(), entity.getServerUuid(),
entity.getDestinationUuid(), entity.getDestinationUuid(),
entity.isUrgent(), entity.isUrgent(),
entity.isStory()); entity.isStory(),
entity.getReportingToken());
} }
callback.onMessage(envelope); callback.onMessage(envelope);

View File

@@ -65,7 +65,8 @@ public class SignalServiceEnvelope {
String uuid, String uuid,
String destinationUuid, String destinationUuid,
boolean urgent, boolean urgent,
boolean story) boolean story,
byte[] reportingToken)
{ {
Envelope.Builder builder = Envelope.newBuilder() Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type)) .setType(Envelope.Type.valueOf(type))
@@ -88,6 +89,10 @@ public class SignalServiceEnvelope {
builder.setContent(ByteString.copyFrom(content)); builder.setContent(ByteString.copyFrom(content));
} }
if (reportingToken != null) {
builder.setReportingToken(ByteString.copyFrom(reportingToken));
}
this.envelope = builder.build(); this.envelope = builder.build();
this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp;
} }
@@ -100,7 +105,8 @@ public class SignalServiceEnvelope {
String uuid, String uuid,
String destinationUuid, String destinationUuid,
boolean urgent, boolean urgent,
boolean story) boolean story,
byte[] reportingToken)
{ {
Envelope.Builder builder = Envelope.newBuilder() Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type)) .setType(Envelope.Type.valueOf(type))
@@ -118,6 +124,10 @@ public class SignalServiceEnvelope {
builder.setContent(ByteString.copyFrom(content)); builder.setContent(ByteString.copyFrom(content));
} }
if (reportingToken != null) {
builder.setReportingToken(ByteString.copyFrom(reportingToken));
}
this.envelope = builder.build(); this.envelope = builder.build();
this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp;
} }
@@ -253,6 +263,14 @@ public class SignalServiceEnvelope {
return envelope.getStory(); return envelope.getStory();
} }
public boolean hasReportingToken() {
return envelope.hasReportingToken();
}
public byte[] getReportingToken() {
return envelope.getReportingToken().toByteArray();
}
private SignalServiceEnvelopeProto.Builder serializeToProto() { private SignalServiceEnvelopeProto.Builder serializeToProto() {
SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder() SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder()
.setType(getType()) .setType(getType())
@@ -279,6 +297,10 @@ public class SignalServiceEnvelope {
builder.setDestinationUuid(getDestinationUuid()); builder.setDestinationUuid(getDestinationUuid());
} }
if (hasReportingToken()) {
builder.setReportingToken(ByteString.copyFrom(getReportingToken()));
}
return builder; return builder;
} }
@@ -308,6 +330,7 @@ public class SignalServiceEnvelope {
proto.getServerGuid(), proto.getServerGuid(),
proto.getDestinationUuid(), proto.getDestinationUuid(),
proto.getUrgent(), proto.getUrgent(),
proto.getStory()); proto.getStory(),
proto.getReportingToken().toByteArray());
} }
} }

View File

@@ -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 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 { private static class VerificationCodeResponseHandler implements ResponseCodeHandler {

View File

@@ -43,6 +43,9 @@ public class SignalServiceEnvelopeEntity {
@JsonProperty @JsonProperty
private Boolean story; private Boolean story;
@JsonProperty
private byte[] reportingToken;
public SignalServiceEnvelopeEntity() {} public SignalServiceEnvelopeEntity() {}
public int getType() { public int getType() {
@@ -100,4 +103,8 @@ public class SignalServiceEnvelopeEntity {
public boolean isStory() { public boolean isStory() {
return story != null && story; return story != null && story;
} }
public byte[] getReportingToken() {
return reportingToken;
}
} }

View File

@@ -0,0 +1,5 @@
package org.whispersystems.signalservice.internal.push
import com.fasterxml.jackson.annotation.JsonProperty
data class SpamTokenMessage(@JsonProperty val token: String)

View File

@@ -35,6 +35,7 @@ message SignalServiceEnvelopeProto {
optional string destinationUuid = 11; optional string destinationUuid = 11;
optional bool urgent = 12 [default = true]; optional bool urgent = 12 [default = true];
optional bool story = 13; optional bool story = 13;
optional bytes reportingToken = 14;
} }
message MetadataProto { message MetadataProto {

View File

@@ -36,7 +36,8 @@ message Envelope {
optional bool urgent = 14 [default = true]; optional bool urgent = 14 [default = true];
reserved /*updatedPni*/ 15; // Not used presently, may be used in the future reserved /*updatedPni*/ 15; // Not used presently, may be used in the future
optional bool story = 16; optional bool story = 16;
// NEXT ID: 17 optional bytes reporting_token = 17;
// NEXT ID: 18
} }
message Content { message Content {