mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Use libsignal validator to verify backups.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.ValidationError
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey
|
||||
|
||||
object ArchiveValidator {
|
||||
|
||||
/**
|
||||
* Validates the provided [backupFile] that is encrypted with the provided [backupKey].
|
||||
*/
|
||||
fun validate(backupFile: File, backupKey: MessageBackupKey): ValidationResult {
|
||||
return try {
|
||||
val backupId = backupKey.deriveBackupId(SignalStore.account.requireAci())
|
||||
val libSignalBackupKey = LibSignalBackupKey(backupKey.value)
|
||||
val backupKey = LibSignalMessageBackupKey(libSignalBackupKey, backupId.value)
|
||||
|
||||
MessageBackup.validate(backupKey, MessageBackup.Purpose.REMOTE_BACKUP, { backupFile.inputStream() }, backupFile.length())
|
||||
|
||||
ValidationResult.Success
|
||||
} catch (e: IOException) {
|
||||
ValidationResult.ReadError(e)
|
||||
} catch (e: ValidationError) {
|
||||
ValidationResult.ValidationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidationResult {
|
||||
data object Success : ValidationResult
|
||||
data class ReadError(val exception: IOException) : ValidationResult
|
||||
data class ValidationError(val exception: org.signal.libsignal.messagebackup.ValidationError) : ValidationResult
|
||||
}
|
||||
}
|
||||
@@ -276,6 +276,7 @@ object BackupRepository {
|
||||
fun localExport(
|
||||
main: OutputStream,
|
||||
localBackupProgressEmitter: ExportProgressListener,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
archiveAttachment: (AttachmentTable.LocalArchivableAttachment, () -> InputStream?) -> Unit
|
||||
) {
|
||||
val writer = EncryptedBackupWriter(
|
||||
@@ -285,7 +286,7 @@ object BackupRepository {
|
||||
append = { main.write(it) }
|
||||
)
|
||||
|
||||
export(currentTime = System.currentTimeMillis(), isLocal = true, writer = writer, progressEmitter = localBackupProgressEmitter) { dbSnapshot ->
|
||||
export(currentTime = System.currentTimeMillis(), isLocal = true, writer = writer, progressEmitter = localBackupProgressEmitter, cancellationSignal = cancellationSignal) { dbSnapshot ->
|
||||
val localArchivableAttachments = dbSnapshot
|
||||
.attachmentTable
|
||||
.getLocalArchivableAttachments()
|
||||
@@ -308,10 +309,11 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun export(
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit,
|
||||
messageBackupKey: org.whispersystems.signalservice.api.backup.MessageBackupKey = SignalStore.backup.messageBackupKey,
|
||||
messageBackupKey: MessageBackupKey = SignalStore.backup.messageBackupKey,
|
||||
plaintext: Boolean = false,
|
||||
currentTime: Long = System.currentTimeMillis(),
|
||||
mediaBackupEnabled: Boolean = SignalStore.backup.backsUpMedia,
|
||||
@@ -361,6 +363,7 @@ object BackupRepository {
|
||||
eventTimer.emit("store-db-snapshot")
|
||||
|
||||
val exportState = ExportState(backupTime = currentTime, mediaBackupEnabled = mediaBackupEnabled)
|
||||
val selfRecipientId = dbSnapshot.recipientTable.getByAci(signalStoreSnapshot.accountValues.aci!!).get().toLong().let { RecipientId.from(it) }
|
||||
|
||||
var frameCount = 0L
|
||||
|
||||
@@ -383,18 +386,18 @@ object BackupRepository {
|
||||
frameCount++
|
||||
}
|
||||
if (cancellationSignal()) {
|
||||
Log.w(TAG, "[import] Cancelled! Stopping")
|
||||
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||
return@export
|
||||
}
|
||||
|
||||
progressEmitter?.onRecipient()
|
||||
RecipientArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
|
||||
RecipientArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState, selfRecipientId) {
|
||||
writer.write(it)
|
||||
eventTimer.emit("recipient")
|
||||
frameCount++
|
||||
}
|
||||
if (cancellationSignal()) {
|
||||
Log.w(TAG, "[import] Cancelled! Stopping")
|
||||
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||
return@export
|
||||
}
|
||||
|
||||
@@ -409,11 +412,15 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
progressEmitter?.onCall()
|
||||
AdHocCallArchiveProcessor.export(dbSnapshot) { frame ->
|
||||
AdHocCallArchiveProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("call")
|
||||
frameCount++
|
||||
}
|
||||
if (cancellationSignal()) {
|
||||
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||
return@export
|
||||
}
|
||||
|
||||
progressEmitter?.onSticker()
|
||||
StickerArchiveProcessor.export(dbSnapshot) { frame ->
|
||||
@@ -421,15 +428,23 @@ object BackupRepository {
|
||||
eventTimer.emit("sticker-pack")
|
||||
frameCount++
|
||||
}
|
||||
if (cancellationSignal()) {
|
||||
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||
return@export
|
||||
}
|
||||
|
||||
progressEmitter?.onMessage()
|
||||
ChatItemArchiveProcessor.export(dbSnapshot, exportState, cancellationSignal) { frame ->
|
||||
ChatItemArchiveProcessor.export(dbSnapshot, exportState, selfRecipientId, cancellationSignal) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("message")
|
||||
frameCount++
|
||||
|
||||
if (frameCount % 1000 == 0L) {
|
||||
Log.d(TAG, "[export] Exported $frameCount frames so far.")
|
||||
if (cancellationSignal()) {
|
||||
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||
return@export
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,18 @@ package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.select
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.exporters.ChatItemArchiveExporter
|
||||
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
private val TAG = "MessageTableArchiveExtensions"
|
||||
|
||||
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean): ChatItemArchiveExporter {
|
||||
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean, selfRecipientId: RecipientId, exportState: ExportState): ChatItemArchiveExporter {
|
||||
// We create a covering index for the query to drastically speed up perf here.
|
||||
// Remember that we're working on a temporary snapshot of the database, so we can create an index and not worry about cleaning it up.
|
||||
val startTime = System.currentTimeMillis()
|
||||
@@ -62,11 +65,26 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi
|
||||
)
|
||||
Log.d(TAG, "Creating index took ${System.currentTimeMillis() - startTime} ms")
|
||||
|
||||
// Unfortunately we have some bad legacy data where the from_recipient_id is a group.
|
||||
// This cleans it up. Reminder, this is only a snapshot of the data.
|
||||
db.rawWritableDatabase.execSQL(
|
||||
"""
|
||||
UPDATE ${MessageTable.TABLE_NAME}
|
||||
SET ${MessageTable.FROM_RECIPIENT_ID} = ${selfRecipientId.toLong()}
|
||||
WHERE ${MessageTable.FROM_RECIPIENT_ID} IN (
|
||||
SELECT ${GroupTable.RECIPIENT_ID}
|
||||
FROM ${GroupTable.TABLE_NAME}
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
return ChatItemArchiveExporter(
|
||||
db = db,
|
||||
backupStartTime = backupTime,
|
||||
batchSize = 10_000,
|
||||
mediaArchiveEnabled = mediaBackupEnabled,
|
||||
selfRecipientId = selfRecipientId,
|
||||
exportState = exportState,
|
||||
cursorGenerator = { lastSeenReceivedTime, count ->
|
||||
readableDatabase
|
||||
.select(
|
||||
|
||||
@@ -54,7 +54,7 @@ fun RecipientTable.getContactsForBackup(selfId: Long): ContactArchiveExporter {
|
||||
"""
|
||||
${RecipientTable.TYPE} = ? AND (
|
||||
${RecipientTable.ACI_COLUMN} NOT NULL OR
|
||||
${RecipientTable.PNI_COLUMN} NOT NULL OR
|
||||
(${RecipientTable.PNI_COLUMN} NOT NULL AND ${RecipientTable.E164} NOT NULL) OR
|
||||
${RecipientTable.E164} NOT NULL
|
||||
)
|
||||
""",
|
||||
@@ -84,7 +84,12 @@ fun RecipientTable.getGroupsForBackup(): GroupArchiveExporter {
|
||||
INNER JOIN ${GroupTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
||||
"""
|
||||
)
|
||||
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
|
||||
.where(
|
||||
"""
|
||||
${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL AND
|
||||
${GroupTable.TABLE_NAME}.${GroupTable.V2_REVISION} >= 0
|
||||
"""
|
||||
)
|
||||
.run()
|
||||
|
||||
return GroupArchiveExporter(cursor)
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.forEach
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.select
|
||||
import org.thoughtcrime.securesms.backup.v2.exporters.ChatArchiveExporter
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
@@ -13,7 +19,7 @@ import org.thoughtcrime.securesms.database.ThreadTable
|
||||
fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatArchiveExporter {
|
||||
//language=sql
|
||||
val query = """
|
||||
SELECT
|
||||
SELECT
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.ID},
|
||||
${ThreadTable.RECIPIENT_ID},
|
||||
${ThreadTable.PINNED},
|
||||
@@ -26,11 +32,44 @@ fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatArchiveExporter {
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.CHAT_COLORS},
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID},
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.WALLPAPER}
|
||||
FROM ${ThreadTable.TABLE_NAME}
|
||||
FROM ${ThreadTable.TABLE_NAME}
|
||||
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||
WHERE ${ThreadTable.ACTIVE} = 1
|
||||
WHERE
|
||||
${ThreadTable.ACTIVE} = 1 AND
|
||||
${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} NOT IN (${RecipientTable.RecipientType.DISTRIBUTION_LIST.id}, ${RecipientTable.RecipientType.CALL_LINK.id})
|
||||
"""
|
||||
val cursor = readableDatabase.query(query)
|
||||
|
||||
return ChatArchiveExporter(cursor, db)
|
||||
}
|
||||
|
||||
fun ThreadTable.getThreadGroupStatus(messageIds: Collection<Long>): Map<Long, Boolean> {
|
||||
if (messageIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val out: MutableMap<Long, Boolean> = mutableMapOf()
|
||||
|
||||
val query = SqlUtil.buildFastCollectionQuery("${MessageTable.TABLE_NAME}.${MessageTable.ID}", messageIds)
|
||||
readableDatabase
|
||||
.select(
|
||||
"${MessageTable.TABLE_NAME}.${MessageTable.ID}",
|
||||
"${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE}"
|
||||
)
|
||||
.from(
|
||||
"""
|
||||
${MessageTable.TABLE_NAME}
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
INNER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||
"""
|
||||
)
|
||||
.where(query.where, query.whereArgs)
|
||||
.run()
|
||||
.forEach { cursor ->
|
||||
val messageId = cursor.requireLong(MessageTable.ID)
|
||||
val type = cursor.requireInt(RecipientTable.TYPE)
|
||||
out[messageId] = type != RecipientTable.RecipientType.INDIVIDUAL.id
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -24,12 +24,16 @@ import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getThreadGroupStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ContactMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
@@ -75,6 +79,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.payments.FailureReason
|
||||
import org.thoughtcrime.securesms.payments.State
|
||||
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
@@ -88,6 +93,7 @@ import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Future
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
|
||||
@@ -103,9 +109,11 @@ private val TAG = Log.tag(ChatItemArchiveExporter::class.java)
|
||||
*/
|
||||
class ChatItemArchiveExporter(
|
||||
private val db: SignalDatabase,
|
||||
private val selfRecipientId: RecipientId,
|
||||
private val backupStartTime: Long,
|
||||
private val batchSize: Int,
|
||||
private val mediaArchiveEnabled: Boolean,
|
||||
private val exportState: ExportState,
|
||||
private val cursorGenerator: (Long, Int) -> Cursor
|
||||
) : Iterator<ChatItem?>, Closeable {
|
||||
|
||||
@@ -136,7 +144,11 @@ class ChatItemArchiveExporter(
|
||||
eventTimer.emit("extra-data")
|
||||
|
||||
for ((id, record) in records) {
|
||||
val builder = record.toBasicChatItemBuilder(extraData.groupReceiptsById[id])
|
||||
val builder = record.toBasicChatItemBuilder(selfRecipientId, extraData.isGroupThreadById[id] ?: false, extraData.groupReceiptsById[id], exportState, backupStartTime)
|
||||
|
||||
if (builder == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
when {
|
||||
record.remoteDeleted -> {
|
||||
@@ -209,11 +221,12 @@ class ChatItemArchiveExporter(
|
||||
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn))
|
||||
builder.expireStartDate = 0
|
||||
builder.expiresInMs = 0
|
||||
}
|
||||
|
||||
MessageTypes.isProfileChange(record.type) -> {
|
||||
builder.updateMessage = record.toRemoteProfileChangeUpdate()
|
||||
builder.updateMessage = record.toRemoteProfileChangeUpdate() ?: continue
|
||||
}
|
||||
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
@@ -225,12 +238,20 @@ class ChatItemArchiveExporter(
|
||||
}
|
||||
|
||||
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
|
||||
builder.updateMessage = record.toRemoteGroupUpdate()
|
||||
builder.updateMessage = record.toRemoteGroupUpdate() ?: continue
|
||||
}
|
||||
|
||||
MessageTypes.isGroupV1MigrationEvent(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupChange = GroupChangeChatUpdate(
|
||||
updates = listOf(GroupChangeChatUpdate.Update(groupV2MigrationUpdate = GroupV2MigrationUpdate()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
MessageTypes.isCallLog(record.type) -> {
|
||||
val call = db.callTable.getCallByMessageId(record.id)
|
||||
builder.updateMessage = call?.toRemoteCallUpdate(db, record)
|
||||
builder.updateMessage = call?.toRemoteCallUpdate(db, record) ?: continue
|
||||
}
|
||||
|
||||
MessageTypes.isPaymentsNotification(record.type) -> {
|
||||
@@ -338,16 +359,22 @@ class ChatItemArchiveExporter(
|
||||
db.groupReceiptTable.getGroupReceiptInfoForMessages(messageIds)
|
||||
}
|
||||
|
||||
val isGroupThreadFuture = executor.submitTyped {
|
||||
db.threadTable.getThreadGroupStatus(messageIds)
|
||||
}
|
||||
|
||||
val mentionsResult = mentionsFuture.get()
|
||||
val reactionsResult = reactionsFuture.get()
|
||||
val attachmentsResult = attachmentsFuture.get()
|
||||
val groupReceiptsResult = groupReceiptsFuture.get()
|
||||
val isGroupThreadResult = isGroupThreadFuture.get()
|
||||
|
||||
return ExtraMessageData(
|
||||
mentionsById = mentionsResult,
|
||||
reactionsById = reactionsResult,
|
||||
attachmentsById = attachmentsResult,
|
||||
groupReceiptsById = groupReceiptsResult
|
||||
groupReceiptsById = groupReceiptsResult,
|
||||
isGroupThreadById = isGroupThreadResult
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -356,10 +383,10 @@ private fun simpleUpdate(type: SimpleChatUpdate.Type): ChatUpdateMessage {
|
||||
return ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = type))
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toBasicChatItemBuilder(groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?): ChatItem.Builder {
|
||||
private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: RecipientId, isGroupThread: Boolean, groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?, exportState: ExportState, backupStartTime: Long): ChatItem.Builder? {
|
||||
val record = this
|
||||
|
||||
return ChatItem.Builder().apply {
|
||||
val builder = ChatItem.Builder().apply {
|
||||
chatId = record.threadId
|
||||
authorId = record.fromRecipientId
|
||||
dateSent = record.dateSent
|
||||
@@ -369,19 +396,40 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(groupReceipts: List<Group
|
||||
sms = record.type.isSmsType()
|
||||
if (record.type.isDirectionlessType()) {
|
||||
directionless = ChatItem.DirectionlessMessageDetails()
|
||||
} else if (MessageTypes.isOutgoingMessageType(record.type)) {
|
||||
} else if (MessageTypes.isOutgoingMessageType(record.type) || record.fromRecipientId == selfRecipientId.toLong()) {
|
||||
outgoing = ChatItem.OutgoingMessageDetails(
|
||||
sendStatus = record.toRemoteSendStatus(groupReceipts)
|
||||
sendStatus = record.toRemoteSendStatus(isGroupThread, groupReceipts, exportState)
|
||||
)
|
||||
|
||||
if (expiresInMs > 0 && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true) {
|
||||
Log.w(TAG, "Outgoing expiring message was sent but the timer wasn't started! Fixing.")
|
||||
expireStartDate = record.dateReceived
|
||||
}
|
||||
} else {
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateServerSent = record.dateServer,
|
||||
dateServerSent = max(record.dateServer, 0),
|
||||
dateReceived = record.dateReceived,
|
||||
read = record.read,
|
||||
sealedSender = record.sealedSender
|
||||
)
|
||||
|
||||
if (expiresInMs > 0 && incoming?.read == true && expireStartDate == 0L) {
|
||||
Log.w(TAG, "Incoming expiring message was read but the timer wasn't started! Fixing.")
|
||||
expireStartDate = record.dateReceived
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!MessageTypes.isExpirationTimerUpdate(record.type) && builder.expiresInMs > 0 && builder.expireStartDate + builder.expiresInMs < backupStartTime + 1.days.inWholeMilliseconds) {
|
||||
Log.w(TAG, "Message expires too soon! Must skip.")
|
||||
return null
|
||||
}
|
||||
|
||||
if (builder.expireStartDate > 0 && builder.expiresInMs == 0L) {
|
||||
builder.expireStartDate = 0
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteProfileChangeUpdate(): ChatUpdateMessage? {
|
||||
@@ -492,6 +540,8 @@ private fun CallTable.Call.toRemoteCallUpdate(db: SignalDatabase, messageRecord:
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE
|
||||
CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED
|
||||
CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED
|
||||
CallTable.Event.ONGOING -> IndividualCall.State.ACCEPTED
|
||||
CallTable.Event.DELETE -> return null
|
||||
else -> IndividualCall.State.UNKNOWN_STATE
|
||||
},
|
||||
startedCallTimestamp = this.timestamp,
|
||||
@@ -715,9 +765,10 @@ private fun BackupMessageRecord.toRemoteStandardMessage(db: SignalDatabase, medi
|
||||
?.filterNot { linkPreviewAttachments.contains(it) }
|
||||
?.filterNot { it == longTextAttachment }
|
||||
?: emptyList()
|
||||
val hasVoiceNote = messageAttachments.any { it.voiceNote }
|
||||
return StandardMessage(
|
||||
quote = this.toRemoteQuote(mediaArchiveEnabled, quotedAttachments),
|
||||
text = text,
|
||||
text = text.takeUnless { hasVoiceNote },
|
||||
attachments = messageAttachments.toRemoteAttachments(mediaArchiveEnabled),
|
||||
linkPreview = linkPreviews.map { it.toRemoteLinkPreview(mediaArchiveEnabled) },
|
||||
longText = longTextAttachment?.toRemoteFilePointer(mediaArchiveEnabled),
|
||||
@@ -786,16 +837,22 @@ private fun List<DatabaseAttachment>.toRemoteQuoteAttachments(mediaArchiveEnable
|
||||
Quote.QuotedAttachment(
|
||||
contentType = attachment.contentType,
|
||||
fileName = attachment.fileName,
|
||||
thumbnail = attachment.toRemoteMessageAttachment(mediaArchiveEnabled, contentTypeOverride = "image/jpeg").takeUnless { it.pointer?.invalidAttachmentLocator != null }
|
||||
thumbnail = attachment.toRemoteMessageAttachment(
|
||||
mediaArchiveEnabled = mediaArchiveEnabled,
|
||||
flagOverride = MessageAttachment.Flag.NONE,
|
||||
contentTypeOverride = "image/jpeg"
|
||||
).takeUnless { it.pointer?.invalidAttachmentLocator != null }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toRemoteMessageAttachment(mediaArchiveEnabled: Boolean, contentTypeOverride: String? = null): MessageAttachment {
|
||||
private fun DatabaseAttachment.toRemoteMessageAttachment(mediaArchiveEnabled: Boolean, flagOverride: MessageAttachment.Flag? = null, contentTypeOverride: String? = null): MessageAttachment {
|
||||
return MessageAttachment(
|
||||
pointer = this.toRemoteFilePointer(mediaArchiveEnabled, contentTypeOverride),
|
||||
wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE,
|
||||
flag = if (this.voiceNote) {
|
||||
flag = if (flagOverride != null) {
|
||||
flagOverride
|
||||
} else if (this.voiceNote) {
|
||||
MessageAttachment.Flag.VOICE_MESSAGE
|
||||
} else if (this.videoGif) {
|
||||
MessageAttachment.Flag.GIF
|
||||
@@ -914,14 +971,18 @@ private fun List<ReactionRecord>?.toRemote(): List<Reaction> {
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteSendStatus(groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?): List<SendStatus> {
|
||||
if (!groupReceipts.isNullOrEmpty()) {
|
||||
return groupReceipts.toRemoteSendStatus(this, this.networkFailureRecipientIds, this.identityMismatchRecipientIds)
|
||||
private fun BackupMessageRecord.toRemoteSendStatus(isGroupThread: Boolean, groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?, exportState: ExportState): List<SendStatus> {
|
||||
if (isGroupThread || !groupReceipts.isNullOrEmpty()) {
|
||||
return groupReceipts.toRemoteSendStatus(this, this.networkFailureRecipientIds, this.identityMismatchRecipientIds, exportState)
|
||||
}
|
||||
|
||||
if (!exportState.recipientIds.contains(this.toRecipientId)) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val statusBuilder = SendStatus.Builder()
|
||||
.recipientId(this.toRecipientId)
|
||||
.timestamp(this.receiptTimestamp)
|
||||
.timestamp(max(this.receiptTimestamp, 0))
|
||||
|
||||
when {
|
||||
this.identityMismatchRecipientIds.contains(this.toRecipientId) -> {
|
||||
@@ -970,61 +1031,67 @@ private fun BackupMessageRecord.toRemoteSendStatus(groupReceipts: List<GroupRece
|
||||
return listOf(statusBuilder.build())
|
||||
}
|
||||
|
||||
private fun List<GroupReceiptTable.GroupReceiptInfo>.toRemoteSendStatus(messageRecord: BackupMessageRecord, networkFailureRecipientIds: Set<Long>, identityMismatchRecipientIds: Set<Long>): List<SendStatus> {
|
||||
return this.map {
|
||||
val statusBuilder = SendStatus.Builder()
|
||||
.recipientId(it.recipientId.toLong())
|
||||
.timestamp(it.timestamp)
|
||||
|
||||
when {
|
||||
identityMismatchRecipientIds.contains(it.recipientId.toLong()) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH
|
||||
)
|
||||
}
|
||||
MessageTypes.isFailedMessageType(messageRecord.type) && networkFailureRecipientIds.contains(it.recipientId.toLong()) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.NETWORK
|
||||
)
|
||||
}
|
||||
MessageTypes.isFailedMessageType(messageRecord.type) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.UNKNOWN
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_UNKNOWN -> {
|
||||
statusBuilder.pending = SendStatus.Pending()
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_UNDELIVERED -> {
|
||||
statusBuilder.sent = SendStatus.Sent(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_DELIVERED -> {
|
||||
statusBuilder.delivered = SendStatus.Delivered(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_READ -> {
|
||||
statusBuilder.read = SendStatus.Read(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_VIEWED -> {
|
||||
statusBuilder.viewed = SendStatus.Viewed(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_SKIPPED -> {
|
||||
statusBuilder.skipped = SendStatus.Skipped()
|
||||
}
|
||||
else -> {
|
||||
statusBuilder.pending = SendStatus.Pending()
|
||||
}
|
||||
}
|
||||
|
||||
statusBuilder.build()
|
||||
private fun List<GroupReceiptTable.GroupReceiptInfo>?.toRemoteSendStatus(messageRecord: BackupMessageRecord, networkFailureRecipientIds: Set<Long>, identityMismatchRecipientIds: Set<Long>, exportState: ExportState): List<SendStatus> {
|
||||
if (this == null) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return this
|
||||
.filter { exportState.recipientIds.contains(it.recipientId.toLong()) }
|
||||
.map {
|
||||
val statusBuilder = SendStatus.Builder()
|
||||
.recipientId(it.recipientId.toLong())
|
||||
.timestamp(max(it.timestamp, 0))
|
||||
|
||||
when {
|
||||
identityMismatchRecipientIds.contains(it.recipientId.toLong()) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH
|
||||
)
|
||||
}
|
||||
MessageTypes.isFailedMessageType(messageRecord.type) && networkFailureRecipientIds.contains(it.recipientId.toLong()) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.NETWORK
|
||||
)
|
||||
}
|
||||
MessageTypes.isFailedMessageType(messageRecord.type) -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.UNKNOWN
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_UNKNOWN -> {
|
||||
statusBuilder.pending = SendStatus.Pending()
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_UNDELIVERED -> {
|
||||
statusBuilder.sent = SendStatus.Sent(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_DELIVERED -> {
|
||||
statusBuilder.delivered = SendStatus.Delivered(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_READ -> {
|
||||
statusBuilder.read = SendStatus.Read(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_VIEWED -> {
|
||||
statusBuilder.viewed = SendStatus.Viewed(
|
||||
sealedSender = it.isUnidentified
|
||||
)
|
||||
}
|
||||
it.status == GroupReceiptTable.STATUS_SKIPPED -> {
|
||||
statusBuilder.skipped = SendStatus.Skipped()
|
||||
}
|
||||
else -> {
|
||||
statusBuilder.pending = SendStatus.Pending()
|
||||
}
|
||||
}
|
||||
|
||||
statusBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.parseNetworkFailures(): Set<Long> {
|
||||
@@ -1205,5 +1272,6 @@ data class ExtraMessageData(
|
||||
val mentionsById: Map<Long, List<Mention>>,
|
||||
val reactionsById: Map<Long, List<ReactionRecord>>,
|
||||
val attachmentsById: Map<Long, List<DatabaseAttachment>>,
|
||||
val groupReceiptsById: Map<Long, List<GroupReceiptTable.GroupReceiptInfo>>
|
||||
val groupReceiptsById: Map<Long, List<GroupReceiptTable.GroupReceiptInfo>>,
|
||||
val isGroupThreadById: Map<Long, Boolean>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ object LocalArchiver {
|
||||
/**
|
||||
* Export archive to the provided [snapshotFileSystem] and store new files in [filesFileSystem].
|
||||
*/
|
||||
fun export(snapshotFileSystem: SnapshotFileSystem, filesFileSystem: FilesFileSystem, stopwatch: Stopwatch): ArchiveResult {
|
||||
fun export(snapshotFileSystem: SnapshotFileSystem, filesFileSystem: FilesFileSystem, stopwatch: Stopwatch, cancellationSignal: () -> Boolean = { false }): ArchiveResult {
|
||||
Log.i(TAG, "Starting export")
|
||||
|
||||
var metadataStream: OutputStream? = null
|
||||
@@ -58,7 +58,11 @@ object LocalArchiver {
|
||||
val mediaNames: MutableSet<MediaName> = Collections.synchronizedSet(HashSet())
|
||||
|
||||
Log.i(TAG, "Starting frame export")
|
||||
BackupRepository.localExport(mainStream, LocalExportProgressListener()) { attachment, source ->
|
||||
BackupRepository.localExport(mainStream, LocalExportProgressListener(), cancellationSignal) { attachment, source ->
|
||||
if (cancellationSignal()) {
|
||||
return@localExport
|
||||
}
|
||||
|
||||
val mediaName = MediaName.fromDigest(attachment.remoteDigest)
|
||||
|
||||
mediaNames.add(mediaName)
|
||||
@@ -105,6 +109,10 @@ object LocalArchiver {
|
||||
filesStream?.close()
|
||||
}
|
||||
|
||||
if (cancellationSignal()) {
|
||||
return ArchiveResult.failure(FailureCause.CANCELLED)
|
||||
}
|
||||
|
||||
return ArchiveResult.success(Unit)
|
||||
}
|
||||
|
||||
@@ -135,7 +143,7 @@ object LocalArchiver {
|
||||
get() = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size))
|
||||
|
||||
enum class FailureCause {
|
||||
METADATA_STREAM, MAIN_STREAM, FILES_STREAM
|
||||
METADATA_STREAM, MAIN_STREAM, FILES_STREAM, CANCELLED
|
||||
}
|
||||
|
||||
private class LocalExportProgressListener : BackupRepository.ExportProgressListener {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.processor
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.database.getAdhocCallsForBackup
|
||||
import org.thoughtcrime.securesms.backup.v2.importer.AdHodCallArchiveImporter
|
||||
@@ -21,10 +22,14 @@ object AdHocCallArchiveProcessor {
|
||||
|
||||
val TAG = Log.tag(AdHocCallArchiveProcessor::class.java)
|
||||
|
||||
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
|
||||
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
db.callTable.getAdhocCallsForBackup().use { reader ->
|
||||
for (callLog in reader) {
|
||||
emitter.emit(Frame(adHocCall = callLog))
|
||||
if (exportState.recipientIds.contains(callLog.recipientId)) {
|
||||
emitter.emit(Frame(adHocCall = callLog))
|
||||
} else {
|
||||
Log.w(TAG, "Dropping adhoc call for non-exported recipient.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Handles importing/exporting [ChatItem] frames for an archive.
|
||||
@@ -22,8 +23,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
object ChatItemArchiveProcessor {
|
||||
val TAG = Log.tag(ChatItemArchiveProcessor::class.java)
|
||||
|
||||
fun export(db: SignalDatabase, exportState: ExportState, cancellationSignal: () -> Boolean, emitter: BackupFrameEmitter) {
|
||||
db.messageTable.getMessagesForBackup(db, exportState.backupTime, exportState.mediaBackupEnabled).use { chatItems ->
|
||||
fun export(db: SignalDatabase, exportState: ExportState, selfRecipientId: RecipientId, cancellationSignal: () -> Boolean, emitter: BackupFrameEmitter) {
|
||||
db.messageTable.getMessagesForBackup(db, exportState.backupTime, exportState.mediaBackupEnabled, selfRecipientId, exportState).use { chatItems ->
|
||||
var count = 0
|
||||
while (chatItems.hasNext()) {
|
||||
if (count % 1000 == 0 && cancellationSignal()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Handles importing/exporting [ArchiveRecipient] frames for an archive.
|
||||
@@ -32,8 +33,7 @@ object RecipientArchiveProcessor {
|
||||
|
||||
val TAG = Log.tag(RecipientArchiveProcessor::class.java)
|
||||
|
||||
fun export(db: SignalDatabase, signalStore: SignalStore, exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get().toLong()
|
||||
fun export(db: SignalDatabase, signalStore: SignalStore, exportState: ExportState, selfRecipientId: RecipientId, emitter: BackupFrameEmitter) {
|
||||
val releaseChannelId = signalStore.releaseChannelValues.releaseChannelRecipientId
|
||||
if (releaseChannelId != null) {
|
||||
exportState.recipientIds.add(releaseChannelId.toLong())
|
||||
@@ -49,7 +49,7 @@ object RecipientArchiveProcessor {
|
||||
Log.w(TAG, "Missing release channel id on export!")
|
||||
}
|
||||
|
||||
db.recipientTable.getContactsForBackup(selfId).use { reader ->
|
||||
db.recipientTable.getContactsForBackup(selfRecipientId.toLong()).use { reader ->
|
||||
for (recipient in reader) {
|
||||
if (recipient != null) {
|
||||
exportState.recipientIds.add(recipient.id)
|
||||
|
||||
@@ -123,8 +123,8 @@ fun FilePointer?.toLocalAttachment(
|
||||
fun DatabaseAttachment.toRemoteFilePointer(mediaArchiveEnabled: Boolean, contentTypeOverride: String? = null): FilePointer {
|
||||
val builder = FilePointer.Builder()
|
||||
builder.contentType = contentTypeOverride ?: this.contentType?.takeUnless { it.isBlank() }
|
||||
builder.incrementalMac = this.incrementalDigest?.toByteString()
|
||||
builder.incrementalMacChunkSize = this.incrementalMacChunkSize.takeIf { it > 0 }
|
||||
builder.incrementalMac = this.incrementalDigest?.takeIf { it.isNotEmpty() && this.incrementalMacChunkSize > 0 }?.toByteString()
|
||||
builder.incrementalMacChunkSize = this.incrementalMacChunkSize.takeIf { it > 0 && builder.incrementalMac != null }
|
||||
builder.fileName = this.fileName
|
||||
builder.width = this.width.takeIf { it > 0 }
|
||||
builder.height = this.height.takeIf { it > 0 }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.util
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
|
||||
@@ -22,6 +23,8 @@ import org.thoughtcrime.securesms.wallpaper.GradientChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.SingleColorChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
|
||||
|
||||
private val TAG = Log.tag(ChatStyleConverter::class)
|
||||
|
||||
/**
|
||||
* Contains a collection of methods to chat styles to and from their archive format.
|
||||
* These are in a file of their own just because they're rather long (with all of the various constants to map between) and used in multiple places.
|
||||
@@ -37,6 +40,10 @@ object ChatStyleConverter {
|
||||
return null
|
||||
}
|
||||
|
||||
if (chatColorId == ChatColors.Id.NotSet && chatWallpaper == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val chatStyleBuilder = ChatStyle.Builder()
|
||||
|
||||
if (chatColors != null) {
|
||||
@@ -72,6 +79,16 @@ object ChatStyleConverter {
|
||||
chatStyleBuilder.dimWallpaperInDarkMode = chatWallpaper.dimLevelInDarkTheme > 0
|
||||
}
|
||||
|
||||
if (!chatStyleBuilder.hasBubbleColorSet()) {
|
||||
if (chatStyleBuilder.hasWallpaperSet()) {
|
||||
Log.w(TAG, "Wallpaper is set but no bubble color. Defaulting to automatic.")
|
||||
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
|
||||
} else {
|
||||
Log.w(TAG, "After building the chat style, it's empty. Returning null.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return chatStyleBuilder.build()
|
||||
}
|
||||
}
|
||||
@@ -147,6 +164,7 @@ fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? {
|
||||
ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA
|
||||
ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE
|
||||
}
|
||||
Log.w(TAG, "No matching remote bubble preset for ${this.serialize()}")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -193,7 +211,7 @@ fun ChatStyle.parseChatWallpaper(wallpaperAttachmentId: AttachmentId?): ChatWall
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
|
||||
private fun Int.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset? {
|
||||
return when (this) {
|
||||
SingleColorChatWallpaper.BLUSH.color -> ChatStyle.WallpaperPreset.SOLID_BLUSH
|
||||
SingleColorChatWallpaper.COPPER.color -> ChatStyle.WallpaperPreset.SOLID_COPPER
|
||||
@@ -207,7 +225,10 @@ private fun Int.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
|
||||
SingleColorChatWallpaper.PINK.color -> ChatStyle.WallpaperPreset.SOLID_PINK
|
||||
SingleColorChatWallpaper.EGGPLANT.color -> ChatStyle.WallpaperPreset.SOLID_EGGPLANT
|
||||
SingleColorChatWallpaper.SILVER.color -> ChatStyle.WallpaperPreset.SOLID_SILVER
|
||||
else -> ChatStyle.WallpaperPreset.UNKNOWN_WALLPAPER_PRESET
|
||||
else -> {
|
||||
Log.w(TAG, "No matching remote wallpaper preset for $this")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,3 +253,11 @@ private fun Wallpaper.File.toFilePointer(db: SignalDatabase): FilePointer? {
|
||||
val attachment = db.attachmentTable.getAttachment(attachmentId)
|
||||
return attachment?.toRemoteFilePointer(mediaArchiveEnabled = true)
|
||||
}
|
||||
|
||||
private fun ChatStyle.Builder.hasBubbleColorSet(): Boolean {
|
||||
return this.customColorId != null || this.autoBubbleColor != null || this.bubbleColorPreset != null
|
||||
}
|
||||
|
||||
private fun ChatStyle.Builder.hasWallpaperSet(): Boolean {
|
||||
return this.wallpaperPreset != null || this.wallpaperPhoto != null
|
||||
}
|
||||
|
||||
@@ -539,6 +539,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
|
||||
}
|
||||
|
||||
fun fetchRemoteBackupAndWritePlaintext(outputStream: OutputStream?) {
|
||||
|
||||
@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator;
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet;
|
||||
@@ -162,6 +163,7 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -191,6 +193,10 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -82,9 +83,26 @@ class BackupMessagesJob private constructor(parameters: Parameters) : Job(parame
|
||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
val outputStream = FileOutputStream(tempBackupFile)
|
||||
BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, plaintext = false, cancellationSignal = { this.isCanceled })
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
BackupRepository.export(outputStream = outputStream, messageBackupKey = backupKey, append = { tempBackupFile.appendBytes(it) }, plaintext = false, cancellationSignal = { this.isCanceled })
|
||||
stopwatch.split("export")
|
||||
|
||||
when (val result = ArchiveValidator.validate(tempBackupFile, backupKey)) {
|
||||
ArchiveValidator.ValidationResult.Success -> {
|
||||
Log.d(TAG, "Successfully passed validation.")
|
||||
}
|
||||
is ArchiveValidator.ValidationResult.ReadError -> {
|
||||
Log.w(TAG, "Failed to read the file during validation!", result.exception)
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
is ArchiveValidator.ValidationResult.ValidationError -> {
|
||||
// TODO [backup] UX
|
||||
Log.w(TAG, "The backup file fails validation! Message: " + result.exception.message)
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
stopwatch.split("validate")
|
||||
|
||||
if (isCanceled) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
|
||||
|
||||
try {
|
||||
try {
|
||||
val result = LocalArchiver.export(snapshotFileSystem, archiveFileSystem.filesFileSystem, stopwatch)
|
||||
val result = LocalArchiver.export(snapshotFileSystem, archiveFileSystem.filesFileSystem, stopwatch, cancellationSignal = { isCanceled })
|
||||
Log.i(TAG, "Archive finished with result: $result")
|
||||
if (result !is org.signal.core.util.Result.Success) {
|
||||
return Result.failure()
|
||||
|
||||
@@ -203,70 +203,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
/** Store that lets you interact with media ZK credentials. */
|
||||
val mediaCredentials = CredentialStore(KEY_MEDIA_CREDENTIALS, KEY_MEDIA_CDN_READ_CREDENTIALS, KEY_MEDIA_CDN_READ_CREDENTIALS_TIMESTAMP)
|
||||
|
||||
inner class CredentialStore(val authKey: String, val cdnKey: String, val cdnTimestampKey: String) {
|
||||
/**
|
||||
* Retrieves the stored media credentials, mapped by the day they're valid. The day is represented as
|
||||
* the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials]
|
||||
* type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime].
|
||||
*/
|
||||
val byDay: ArchiveServiceCredentials
|
||||
get() {
|
||||
val serialized = store.getString(authKey, null) ?: return ArchiveServiceCredentials()
|
||||
|
||||
return try {
|
||||
val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay
|
||||
ArchiveServiceCredentials(map)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Invalid JSON! Clearing.", e)
|
||||
putString(authKey, null)
|
||||
ArchiveServiceCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds the given credentials to the existing list of stored credentials. */
|
||||
fun add(credentials: List<ArchiveServiceCredential>) {
|
||||
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
|
||||
current.putAll(credentials.associateBy { it.redemptionTime })
|
||||
putString(authKey, JsonUtil.toJson(SerializedCredentials(current)))
|
||||
}
|
||||
|
||||
/** Trims out any credentials that are for days older than the given timestamp. */
|
||||
fun clearOlderThan(startOfDayInSeconds: Long) {
|
||||
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
|
||||
val updated = current.filterKeys { it < startOfDayInSeconds }
|
||||
putString(authKey, JsonUtil.toJson(SerializedCredentials(updated)))
|
||||
}
|
||||
|
||||
/** Clears all credentials. */
|
||||
fun clearAll() {
|
||||
putString(authKey, null)
|
||||
cdnReadCredentials = null
|
||||
}
|
||||
|
||||
/** Credentials to read from the CDN. */
|
||||
var cdnReadCredentials: GetArchiveCdnCredentialsResponse?
|
||||
get() {
|
||||
val cacheAge = System.currentTimeMillis() - getLong(cdnTimestampKey, 0)
|
||||
val cached = getString(cdnKey, null)
|
||||
|
||||
return if (cached != null && (cacheAge > 0 && cacheAge < cachedCdnCredentialsExpiresIn.inWholeMilliseconds)) {
|
||||
try {
|
||||
JsonUtil.fromJson(cached, GetArchiveCdnCredentialsResponse::class.java)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Invalid JSON! Clearing.", e)
|
||||
putString(cdnKey, null)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
putString(cdnKey, value?.let { JsonUtil.toJson(it) })
|
||||
putLong(cdnTimestampKey, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
fun markMessageBackupFailure() {
|
||||
store.beginWrite()
|
||||
.putBoolean(KEY_BACKUP_FAIL, true)
|
||||
@@ -338,4 +274,69 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
return this[startOfDayInSeconds]
|
||||
}
|
||||
}
|
||||
|
||||
inner class CredentialStore(private val authKey: String, private val cdnKey: String, private val cdnTimestampKey: String) {
|
||||
/**
|
||||
* Retrieves the stored media credentials, mapped by the day they're valid. The day is represented as
|
||||
* the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials]
|
||||
* type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime].
|
||||
*/
|
||||
val byDay: ArchiveServiceCredentials
|
||||
get() {
|
||||
val serialized = store.getString(authKey, null) ?: return ArchiveServiceCredentials()
|
||||
|
||||
return try {
|
||||
val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay
|
||||
ArchiveServiceCredentials(map)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Invalid JSON! Clearing.", e)
|
||||
putString(authKey, null)
|
||||
ArchiveServiceCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds the given credentials to the existing list of stored credentials. */
|
||||
fun add(credentials: List<ArchiveServiceCredential>) {
|
||||
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
|
||||
current.putAll(credentials.associateBy { it.redemptionTime })
|
||||
putString(authKey, JsonUtil.toJson(SerializedCredentials(current)))
|
||||
}
|
||||
|
||||
/** Trims out any credentials that are for days older than the given timestamp. */
|
||||
fun clearOlderThan(startOfDayInSeconds: Long) {
|
||||
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
|
||||
val updated = current.filterKeys { it < startOfDayInSeconds }
|
||||
putString(authKey, JsonUtil.toJson(SerializedCredentials(updated)))
|
||||
}
|
||||
|
||||
/** Clears all credentials. */
|
||||
fun clearAll() {
|
||||
putString(authKey, null)
|
||||
putString(cdnKey, null)
|
||||
putLong(cdnTimestampKey, 0)
|
||||
}
|
||||
|
||||
/** Credentials to read from the CDN. */
|
||||
var cdnReadCredentials: GetArchiveCdnCredentialsResponse?
|
||||
get() {
|
||||
val cacheAge = System.currentTimeMillis() - getLong(cdnTimestampKey, 0)
|
||||
val cached = getString(cdnKey, null)
|
||||
|
||||
return if (cached != null && (cacheAge > 0 && cacheAge < cachedCdnCredentialsExpiresIn.inWholeMilliseconds)) {
|
||||
try {
|
||||
JsonUtil.fromJson(cached, GetArchiveCdnCredentialsResponse::class.java)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Invalid JSON! Clearing.", e)
|
||||
putString(cdnKey, null)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
putString(cdnKey, value?.let { JsonUtil.toJson(it) })
|
||||
putLong(cdnTimestampKey, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -239,6 +240,21 @@ object LinkDeviceRepository {
|
||||
}
|
||||
stopwatch.split("create-backup")
|
||||
|
||||
when (val result = ArchiveValidator.validate(tempBackupFile, ephemeralMessageBackupKey)) {
|
||||
ArchiveValidator.ValidationResult.Success -> {
|
||||
Log.d(TAG, "Successfully passed validation.")
|
||||
}
|
||||
is ArchiveValidator.ValidationResult.ReadError -> {
|
||||
Log.w(TAG, "Failed to read the file during validation!", result.exception)
|
||||
return LinkUploadArchiveResult.BackupCreationFailure(result.exception)
|
||||
}
|
||||
is ArchiveValidator.ValidationResult.ValidationError -> {
|
||||
Log.w(TAG, "The backup file fails validation!", result.exception)
|
||||
return LinkUploadArchiveResult.BackupCreationFailure(result.exception)
|
||||
}
|
||||
}
|
||||
stopwatch.split("validate-backup")
|
||||
|
||||
val uploadForm = when (val result = SignalNetwork.attachments.getAttachmentV4UploadForm()) {
|
||||
is NetworkResult.Success -> result.result
|
||||
is NetworkResult.ApplicationError -> throw result.throwable
|
||||
|
||||
Reference in New Issue
Block a user