mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Export backupV2 using actual desired file format.
This commit is contained in:
committed by
Cody Henthorne
parent
fb69fc5af2
commit
befa396e82
@@ -19,6 +19,7 @@ import org.signal.core.util.requireBlob
|
|||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.toInt
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||||
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
||||||
@@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -176,7 +178,6 @@ class BackupTest {
|
|||||||
SignalStore.settings().setKeepMutedChatsArchived(true)
|
SignalStore.settings().setKeepMutedChatsArchived(true)
|
||||||
|
|
||||||
SignalStore.storyValues().viewedReceiptsEnabled = false
|
SignalStore.storyValues().viewedReceiptsEnabled = false
|
||||||
SignalStore.storyValues().userHasReadOnboardingStory = true
|
|
||||||
SignalStore.storyValues().userHasViewedOnboardingStory = true
|
SignalStore.storyValues().userHasViewedOnboardingStory = true
|
||||||
SignalStore.storyValues().isFeatureDisabled = false
|
SignalStore.storyValues().isFeatureDisabled = false
|
||||||
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
|
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
|
||||||
@@ -227,7 +228,8 @@ class BackupTest {
|
|||||||
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||||
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||||
|
|
||||||
BackupRepository.import(BackupRepository.export())
|
val exported: ByteArray = BackupRepository.export()
|
||||||
|
BackupRepository.import(length = exported.size.toLong(), inputStreamFactory = { ByteArrayInputStream(exported) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||||
|
|
||||||
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||||
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||||
@@ -299,7 +301,7 @@ class BackupTest {
|
|||||||
fun standardMessage(
|
fun standardMessage(
|
||||||
outgoing: Boolean,
|
outgoing: Boolean,
|
||||||
sentTimestamp: Long = System.currentTimeMillis(),
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
receivedTimestamp: Long = sentTimestamp + 1,
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
serverTimestamp: Long = sentTimestamp,
|
serverTimestamp: Long = sentTimestamp,
|
||||||
body: String? = null,
|
body: String? = null,
|
||||||
read: Boolean = true,
|
read: Boolean = true,
|
||||||
@@ -328,7 +330,7 @@ class BackupTest {
|
|||||||
fun remoteDeletedMessage(
|
fun remoteDeletedMessage(
|
||||||
outgoing: Boolean,
|
outgoing: Boolean,
|
||||||
sentTimestamp: Long = System.currentTimeMillis(),
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
receivedTimestamp: Long = sentTimestamp + 1,
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
serverTimestamp: Long = sentTimestamp
|
serverTimestamp: Long = sentTimestamp
|
||||||
): Long {
|
): Long {
|
||||||
return db.insertMessage(
|
return db.insertMessage(
|
||||||
@@ -350,7 +352,7 @@ class BackupTest {
|
|||||||
outgoing: Boolean,
|
outgoing: Boolean,
|
||||||
threadId: Long,
|
threadId: Long,
|
||||||
sentTimestamp: Long = System.currentTimeMillis(),
|
sentTimestamp: Long = System.currentTimeMillis(),
|
||||||
receivedTimestamp: Long = sentTimestamp + 1,
|
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||||
serverTimestamp: Long = sentTimestamp,
|
serverTimestamp: Long = sentTimestamp,
|
||||||
body: String? = null,
|
body: String? = null,
|
||||||
read: Boolean = true,
|
read: Boolean = true,
|
||||||
@@ -390,12 +392,12 @@ class BackupTest {
|
|||||||
|
|
||||||
if (quotes != null) {
|
if (quotes != null) {
|
||||||
val quoteDetails = this.getQuoteDetailsFor(quotes)
|
val quoteDetails = this.getQuoteDetailsFor(quotes)
|
||||||
contentValues.put(MessageTable.QUOTE_ID, quoteDetails.quotedSentTimestamp)
|
contentValues.put(MessageTable.QUOTE_ID, if (quoteTargetMissing) MessageTable.QUOTE_TARGET_MISSING_ID else quoteDetails.quotedSentTimestamp)
|
||||||
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
|
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
|
||||||
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
|
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
|
||||||
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
|
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
|
||||||
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
|
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
|
||||||
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing)
|
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body != null && (randomMention || randomStyling)) {
|
if (body != null && (randomMention || randomStyling)) {
|
||||||
@@ -493,7 +495,7 @@ class BackupTest {
|
|||||||
|
|
||||||
if (!contentEquals(expectedValue, actualValue)) {
|
if (!contentEquals(expectedValue, actualValue)) {
|
||||||
if (!describedRow) {
|
if (!describedRow) {
|
||||||
builder.append("-- ROW $i\n")
|
builder.append("-- ROW ${i + 1}\n")
|
||||||
describedRow = true
|
describedRow = true
|
||||||
}
|
}
|
||||||
builder.append(" [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
|
builder.append(" [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
|
||||||
@@ -502,6 +504,8 @@ class BackupTest {
|
|||||||
|
|
||||||
if (describedRow) {
|
if (describedRow) {
|
||||||
builder.append("\n")
|
builder.append("\n")
|
||||||
|
builder.append("Expected: $expectedRow\n")
|
||||||
|
builder.append("Actual: $actualRow\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,31 +8,46 @@ package org.thoughtcrime.securesms.backup.v2
|
|||||||
import org.signal.core.util.EventTimer
|
import org.signal.core.util.EventTimer
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
||||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportStream
|
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
|
||||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupExportStream
|
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupImportStream
|
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import java.io.ByteArrayInputStream
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
object BackupRepository {
|
object BackupRepository {
|
||||||
|
|
||||||
private val TAG = Log.tag(BackupRepository::class.java)
|
private val TAG = Log.tag(BackupRepository::class.java)
|
||||||
|
|
||||||
fun export(): ByteArray {
|
fun export(plaintext: Boolean = false): ByteArray {
|
||||||
val eventTimer = EventTimer()
|
val eventTimer = EventTimer()
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
val writer: BackupExportStream = PlainTextBackupExportStream(outputStream)
|
val writer: BackupExportWriter = if (plaintext) {
|
||||||
|
PlainTextBackupWriter(outputStream)
|
||||||
|
} else {
|
||||||
|
EncryptedBackupWriter(
|
||||||
|
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||||
|
aci = SignalStore.account().aci!!,
|
||||||
|
outputStream = outputStream,
|
||||||
|
append = { mac -> outputStream.write(mac) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.use {
|
||||||
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
||||||
// writes from other threads are blocked. This is something to think more about.
|
// writes from other threads are blocked. This is something to think more about.
|
||||||
SignalDatabase.rawDatabase.withinTransaction {
|
SignalDatabase.rawDatabase.withinTransaction {
|
||||||
@@ -56,17 +71,26 @@ object BackupRepository {
|
|||||||
eventTimer.emit("message")
|
eventTimer.emit("message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
||||||
|
|
||||||
return outputStream.toByteArray()
|
return outputStream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun import(data: ByteArray) {
|
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
|
||||||
val eventTimer = EventTimer()
|
val eventTimer = EventTimer()
|
||||||
|
|
||||||
val stream = ByteArrayInputStream(data)
|
val frameReader = if (plaintext) {
|
||||||
val frameReader = PlainTextBackupImportStream(stream)
|
PlainTextBackupReader(inputStreamFactory())
|
||||||
|
} else {
|
||||||
|
EncryptedBackupReader(
|
||||||
|
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||||
|
aci = selfData.aci,
|
||||||
|
streamLength = length,
|
||||||
|
dataStream = inputStreamFactory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Without a transaction, bad imports could lead to lost data. But because we have a transaction,
|
// Note: Without a transaction, bad imports could lead to lost data. But because we have a transaction,
|
||||||
// writes from other threads are blocked. This is something to think more about.
|
// writes from other threads are blocked. This is something to think more about.
|
||||||
@@ -76,6 +100,12 @@ object BackupRepository {
|
|||||||
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
|
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
|
||||||
SignalDatabase.threads.clearAllDataForBackupRestore()
|
SignalDatabase.threads.clearAllDataForBackupRestore()
|
||||||
SignalDatabase.messages.clearAllDataForBackupRestore()
|
SignalDatabase.messages.clearAllDataForBackupRestore()
|
||||||
|
SignalDatabase.attachments.clearAllDataForBackupRestore()
|
||||||
|
|
||||||
|
// Add back self after clearing data
|
||||||
|
val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true)
|
||||||
|
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
||||||
|
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
||||||
|
|
||||||
val backupState = BackupState()
|
val backupState = BackupState()
|
||||||
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
|
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
|
||||||
@@ -83,7 +113,7 @@ object BackupRepository {
|
|||||||
for (frame in frameReader) {
|
for (frame in frameReader) {
|
||||||
when {
|
when {
|
||||||
frame.account != null -> {
|
frame.account != null -> {
|
||||||
AccountDataProcessor.import(frame.account)
|
AccountDataProcessor.import(frame.account, selfId)
|
||||||
eventTimer.emit("account")
|
eventTimer.emit("account")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +148,13 @@ object BackupRepository {
|
|||||||
|
|
||||||
Log.d(TAG, "import() ${eventTimer.stop().summary}")
|
Log.d(TAG, "import() ${eventTimer.stop().summary}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SelfData(
|
||||||
|
val aci: ACI,
|
||||||
|
val pni: PNI,
|
||||||
|
val e164: String,
|
||||||
|
val profileKey: ProfileKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackupState {
|
class BackupState {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.database
|
||||||
|
|
||||||
|
import org.signal.core.util.delete
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
|
||||||
|
fun AttachmentTable.clearAllDataForBackupRestore() {
|
||||||
|
writableDatabase.delete(AttachmentTable.TABLE_NAME).run()
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
package org.thoughtcrime.securesms.backup.v2.database
|
package org.thoughtcrime.securesms.backup.v2.database
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.requireBlob
|
import org.signal.core.util.requireBlob
|
||||||
@@ -14,16 +15,16 @@ import org.signal.core.util.requireInt
|
|||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChange
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChange
|
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.RemoteDeletedMessage
|
import org.thoughtcrime.securesms.backup.v2.proto.RemoteDeletedMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleUpdate
|
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Text
|
import org.thoughtcrime.securesms.backup.v2.proto.Text
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.UpdateMessage
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTable
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTypes
|
import org.thoughtcrime.securesms.database.MessageTypes
|
||||||
@@ -35,6 +36,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
|||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||||
import org.thoughtcrime.securesms.util.JsonUtils
|
import org.thoughtcrime.securesms.util.JsonUtils
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
import org.whispersystems.signalservice.api.util.toByteArray
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
@@ -90,31 +93,31 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
record.remoteDeleted -> builder.remoteDeletedMessage = RemoteDeletedMessage()
|
record.remoteDeleted -> builder.remoteDeletedMessage = RemoteDeletedMessage()
|
||||||
MessageTypes.isJoinedType(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.JOINED_SIGNAL))
|
MessageTypes.isJoinedType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.JOINED_SIGNAL))
|
||||||
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.IDENTITY_UPDATE))
|
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
|
||||||
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.IDENTITY_VERIFIED))
|
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
|
||||||
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.IDENTITY_DEFAULT))
|
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
|
||||||
MessageTypes.isChangeNumber(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.CHANGE_NUMBER))
|
MessageTypes.isChangeNumber(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||||
MessageTypes.isBoostRequest(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.BOOST_REQUEST))
|
MessageTypes.isBoostRequest(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||||
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.END_SESSION))
|
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
|
||||||
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.CHAT_SESSION_REFRESH))
|
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
|
||||||
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.BAD_DECRYPT))
|
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
|
||||||
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.PAYMENTS_ACTIVATED))
|
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
|
||||||
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = UpdateMessage(simpleUpdate = SimpleUpdate(type = SimpleUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
|
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
|
||||||
MessageTypes.isExpirationTimerUpdate(record.type) -> builder.updateMessage = UpdateMessage(expirationTimerChange = ExpirationTimerChange((record.expiresIn / 1000).toInt()))
|
MessageTypes.isExpirationTimerUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate((record.expiresIn / 1000).toInt()))
|
||||||
MessageTypes.isProfileChange(record.type) -> {
|
MessageTypes.isProfileChange(record.type) -> {
|
||||||
builder.updateMessage = UpdateMessage(
|
builder.updateMessage = ChatUpdateMessage(
|
||||||
profileChange = try {
|
profileChange = try {
|
||||||
val decoded: ByteArray = Base64.decode(record.body!!)
|
val decoded: ByteArray = Base64.decode(record.body!!)
|
||||||
val profileChangeDetails = ProfileChangeDetails.ADAPTER.decode(decoded)
|
val profileChangeDetails = ProfileChangeDetails.ADAPTER.decode(decoded)
|
||||||
if (profileChangeDetails.profileNameChange != null) {
|
if (profileChangeDetails.profileNameChange != null) {
|
||||||
ProfileChange(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue)
|
ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue)
|
||||||
} else {
|
} else {
|
||||||
ProfileChange()
|
ProfileChangeChatUpdate()
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "Profile name change details could not be read", e)
|
Log.w(TAG, "Profile name change details could not be read", e)
|
||||||
ProfileChange()
|
ProfileChangeChatUpdate()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -142,9 +145,9 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
chatId = record.threadId
|
chatId = record.threadId
|
||||||
authorId = record.fromRecipientId
|
authorId = record.fromRecipientId
|
||||||
dateSent = record.dateSent
|
dateSent = record.dateSent
|
||||||
dateReceived = record.dateReceived
|
sealedSender = record.sealedSender
|
||||||
expireStart = if (record.expireStarted > 0) record.expireStarted else null
|
expireStartDate = if (record.expireStarted > 0) record.expireStarted else null
|
||||||
expiresIn = if (record.expiresIn > 0) record.expiresIn else null
|
expiresInMs = if (record.expiresIn > 0) record.expiresIn else null
|
||||||
revisions = emptyList()
|
revisions = emptyList()
|
||||||
sms = !MessageTypes.isSecureType(record.type)
|
sms = !MessageTypes.isSecureType(record.type)
|
||||||
|
|
||||||
@@ -155,7 +158,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
} else {
|
} else {
|
||||||
incoming = ChatItem.IncomingMessageDetails(
|
incoming = ChatItem.IncomingMessageDetails(
|
||||||
dateServerSent = record.dateServer,
|
dateServerSent = record.dateServer,
|
||||||
sealedSender = record.sealedSender,
|
dateReceived = record.dateReceived,
|
||||||
read = record.read
|
read = record.read
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -169,21 +172,21 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
body = this.body!!,
|
body = this.body!!,
|
||||||
bodyRanges = this.bodyRanges?.toBackupBodyRanges() ?: emptyList()
|
bodyRanges = this.bodyRanges?.toBackupBodyRanges() ?: emptyList()
|
||||||
),
|
),
|
||||||
linkPreview = null,
|
// TODO Link previews!
|
||||||
|
linkPreview = emptyList(),
|
||||||
longText = null,
|
longText = null,
|
||||||
reactions = reactionRecords.toBackupReactions()
|
reactions = reactionRecords.toBackupReactions()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BackupMessageRecord.toQuote(): Quote? {
|
private fun BackupMessageRecord.toQuote(): Quote? {
|
||||||
return if (this.quoteTargetSentTimestamp > 0) {
|
return if (this.quoteTargetSentTimestamp != MessageTable.QUOTE_NOT_PRESENT_ID && this.quoteAuthor > 0) {
|
||||||
// TODO Attachments!
|
// TODO Attachments!
|
||||||
val type = QuoteModel.Type.fromCode(this.quoteType)
|
val type = QuoteModel.Type.fromCode(this.quoteType)
|
||||||
Quote(
|
Quote(
|
||||||
targetSentTimestamp = this.quoteTargetSentTimestamp,
|
targetSentTimestamp = this.quoteTargetSentTimestamp.takeIf { !this.quoteMissing && it != MessageTable.QUOTE_TARGET_MISSING_ID },
|
||||||
authorId = this.quoteAuthor,
|
authorId = this.quoteAuthor,
|
||||||
text = this.quoteBody,
|
text = this.quoteBody,
|
||||||
originalMessageMissing = this.quoteMissing,
|
|
||||||
bodyRanges = this.quoteBodyRanges?.toBackupBodyRanges() ?: emptyList(),
|
bodyRanges = this.quoteBodyRanges?.toBackupBodyRanges() ?: emptyList(),
|
||||||
type = when (type) {
|
type = when (type) {
|
||||||
QuoteModel.Type.NORMAL -> Quote.Type.NORMAL
|
QuoteModel.Type.NORMAL -> Quote.Type.NORMAL
|
||||||
@@ -207,7 +210,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
BackupBodyRange(
|
BackupBodyRange(
|
||||||
start = it.start,
|
start = it.start,
|
||||||
length = it.length,
|
length = it.length,
|
||||||
mentionAci = it.mentionUuid,
|
mentionAci = it.mentionUuid?.let { UuidUtil.parseOrThrow(it) }?.toByteArray()?.toByteString(),
|
||||||
style = it.style?.toBackupBodyRangeStyle()
|
style = it.style?.toBackupBodyRangeStyle()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -245,9 +248,9 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
}
|
}
|
||||||
|
|
||||||
val status: SendStatus.Status = when {
|
val status: SendStatus.Status = when {
|
||||||
this.viewedReceiptCount > 0 -> SendStatus.Status.VIEWED
|
this.viewed -> SendStatus.Status.VIEWED
|
||||||
this.readReceiptCount > 0 -> SendStatus.Status.READ
|
this.hasReadReceipt -> SendStatus.Status.READ
|
||||||
this.deliveryReceiptCount > 0 -> SendStatus.Status.DELIVERED
|
this.hasDeliveryReceipt -> SendStatus.Status.DELIVERED
|
||||||
this.baseType == MessageTypes.BASE_SENT_TYPE -> SendStatus.Status.SENT
|
this.baseType == MessageTypes.BASE_SENT_TYPE -> SendStatus.Status.SENT
|
||||||
MessageTypes.isFailedMessageType(this.type) -> SendStatus.Status.FAILED
|
MessageTypes.isFailedMessageType(this.type) -> SendStatus.Status.FAILED
|
||||||
else -> SendStatus.Status.PENDING
|
else -> SendStatus.Status.PENDING
|
||||||
@@ -257,7 +260,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
SendStatus(
|
SendStatus(
|
||||||
recipientId = this.toRecipientId,
|
recipientId = this.toRecipientId,
|
||||||
deliveryStatus = status,
|
deliveryStatus = status,
|
||||||
timestamp = this.receiptTimestamp,
|
lastStatusUpdateTimestamp = this.receiptTimestamp,
|
||||||
sealedSender = this.sealedSender,
|
sealedSender = this.sealedSender,
|
||||||
networkFailure = this.networkFailureRecipientIds.contains(this.toRecipientId),
|
networkFailure = this.networkFailureRecipientIds.contains(this.toRecipientId),
|
||||||
identityKeyMismatch = this.identityMismatchRecipientIds.contains(this.toRecipientId)
|
identityKeyMismatch = this.identityMismatchRecipientIds.contains(this.toRecipientId)
|
||||||
@@ -271,7 +274,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
recipientId = it.recipientId.toLong(),
|
recipientId = it.recipientId.toLong(),
|
||||||
deliveryStatus = it.status.toBackupDeliveryStatus(),
|
deliveryStatus = it.status.toBackupDeliveryStatus(),
|
||||||
sealedSender = it.isUnidentified,
|
sealedSender = it.isUnidentified,
|
||||||
timestamp = it.timestamp,
|
lastStatusUpdateTimestamp = it.timestamp,
|
||||||
networkFailure = networkFailureRecipientIds.contains(it.recipientId.toLong()),
|
networkFailure = networkFailureRecipientIds.contains(it.recipientId.toLong()),
|
||||||
identityKeyMismatch = identityMismatchRecipientIds.contains(it.recipientId.toLong())
|
identityKeyMismatch = identityMismatchRecipientIds.contains(it.recipientId.toLong())
|
||||||
)
|
)
|
||||||
@@ -337,9 +340,9 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
quoteType = this.requireInt(MessageTable.QUOTE_TYPE),
|
quoteType = this.requireInt(MessageTable.QUOTE_TYPE),
|
||||||
originalMessageId = this.requireLong(MessageTable.ORIGINAL_MESSAGE_ID),
|
originalMessageId = this.requireLong(MessageTable.ORIGINAL_MESSAGE_ID),
|
||||||
latestRevisionId = this.requireLong(MessageTable.LATEST_REVISION_ID),
|
latestRevisionId = this.requireLong(MessageTable.LATEST_REVISION_ID),
|
||||||
deliveryReceiptCount = this.requireInt(MessageTable.DELIVERY_RECEIPT_COUNT),
|
hasDeliveryReceipt = this.requireBoolean(MessageTable.HAS_DELIVERY_RECEIPT),
|
||||||
viewedReceiptCount = this.requireInt(MessageTable.VIEWED_RECEIPT_COUNT),
|
viewed = this.requireBoolean(MessageTable.VIEWED_COLUMN),
|
||||||
readReceiptCount = this.requireInt(MessageTable.READ_RECEIPT_COUNT),
|
hasReadReceipt = this.requireBoolean(MessageTable.HAS_READ_RECEIPT),
|
||||||
read = this.requireBoolean(MessageTable.READ),
|
read = this.requireBoolean(MessageTable.READ),
|
||||||
receiptTimestamp = this.requireLong(MessageTable.RECEIPT_TIMESTAMP),
|
receiptTimestamp = this.requireLong(MessageTable.RECEIPT_TIMESTAMP),
|
||||||
networkFailureRecipientIds = this.requireString(MessageTable.NETWORK_FAILURES).parseNetworkFailures(),
|
networkFailureRecipientIds = this.requireString(MessageTable.NETWORK_FAILURES).parseNetworkFailures(),
|
||||||
@@ -371,9 +374,9 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||||||
val quoteType: Int,
|
val quoteType: Int,
|
||||||
val originalMessageId: Long,
|
val originalMessageId: Long,
|
||||||
val latestRevisionId: Long,
|
val latestRevisionId: Long,
|
||||||
val deliveryReceiptCount: Int,
|
val hasDeliveryReceipt: Boolean,
|
||||||
val readReceiptCount: Int,
|
val hasReadReceipt: Boolean,
|
||||||
val viewedReceiptCount: Int,
|
val viewed: Boolean,
|
||||||
val receiptTimestamp: Long,
|
val receiptTimestamp: Long,
|
||||||
val read: Boolean,
|
val read: Boolean,
|
||||||
val networkFailureRecipientIds: Set<Long>,
|
val networkFailureRecipientIds: Set<Long>,
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import org.signal.core.util.toInt
|
|||||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleUpdate
|
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.UpdateMessage
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTable
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTypes
|
import org.thoughtcrime.securesms.database.MessageTypes
|
||||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.JsonUtils
|
import org.thoughtcrime.securesms.util.JsonUtils
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
|
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
|
||||||
@@ -58,9 +59,9 @@ class ChatItemImportInserter(
|
|||||||
MessageTable.BODY,
|
MessageTable.BODY,
|
||||||
MessageTable.FROM_RECIPIENT_ID,
|
MessageTable.FROM_RECIPIENT_ID,
|
||||||
MessageTable.TO_RECIPIENT_ID,
|
MessageTable.TO_RECIPIENT_ID,
|
||||||
MessageTable.DELIVERY_RECEIPT_COUNT,
|
MessageTable.HAS_DELIVERY_RECEIPT,
|
||||||
MessageTable.READ_RECEIPT_COUNT,
|
MessageTable.HAS_READ_RECEIPT,
|
||||||
MessageTable.VIEWED_RECEIPT_COUNT,
|
MessageTable.VIEWED_COLUMN,
|
||||||
MessageTable.MISMATCHED_IDENTITIES,
|
MessageTable.MISMATCHED_IDENTITIES,
|
||||||
MessageTable.EXPIRES_IN,
|
MessageTable.EXPIRES_IN,
|
||||||
MessageTable.EXPIRE_STARTED,
|
MessageTable.EXPIRE_STARTED,
|
||||||
@@ -173,32 +174,32 @@ class ChatItemImportInserter(
|
|||||||
contentValues.put(MessageTable.FROM_RECIPIENT_ID, fromRecipientId.serialize())
|
contentValues.put(MessageTable.FROM_RECIPIENT_ID, fromRecipientId.serialize())
|
||||||
contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize())
|
contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize())
|
||||||
contentValues.put(MessageTable.THREAD_ID, threadId)
|
contentValues.put(MessageTable.THREAD_ID, threadId)
|
||||||
contentValues.put(MessageTable.DATE_RECEIVED, this.dateReceived)
|
contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.dateSent)
|
||||||
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOf { it.timestamp } ?: 0)
|
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOf { it.lastStatusUpdateTimestamp } ?: 0)
|
||||||
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
|
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
|
||||||
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
|
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
|
||||||
contentValues.put(MessageTable.REVISION_NUMBER, 0)
|
contentValues.put(MessageTable.REVISION_NUMBER, 0)
|
||||||
contentValues.put(MessageTable.EXPIRES_IN, this.expiresIn ?: 0)
|
contentValues.put(MessageTable.EXPIRES_IN, this.expiresInMs ?: 0)
|
||||||
contentValues.put(MessageTable.EXPIRE_STARTED, this.expireStart ?: 0)
|
contentValues.put(MessageTable.EXPIRE_STARTED, this.expireStartDate ?: 0)
|
||||||
|
|
||||||
if (this.outgoing != null) {
|
if (this.outgoing != null) {
|
||||||
val viewReceiptCount = this.outgoing.sendStatus.count { it.deliveryStatus == SendStatus.Status.VIEWED }
|
val viewed = this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.VIEWED }
|
||||||
val readReceiptCount = Integer.max(viewReceiptCount, this.outgoing.sendStatus.count { it.deliveryStatus == SendStatus.Status.READ })
|
val hasReadReceipt = viewed || this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.READ }
|
||||||
val deliveryReceiptCount = Integer.max(readReceiptCount, this.outgoing.sendStatus.count { it.deliveryStatus == SendStatus.Status.DELIVERED })
|
val hasDeliveryReceipt = viewed || hasReadReceipt || this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.DELIVERED }
|
||||||
|
|
||||||
contentValues.put(MessageTable.VIEWED_RECEIPT_COUNT, viewReceiptCount)
|
contentValues.put(MessageTable.VIEWED_COLUMN, viewed.toInt())
|
||||||
contentValues.put(MessageTable.READ_RECEIPT_COUNT, readReceiptCount)
|
contentValues.put(MessageTable.HAS_READ_RECEIPT, hasReadReceipt.toInt())
|
||||||
contentValues.put(MessageTable.DELIVERY_RECEIPT_COUNT, deliveryReceiptCount)
|
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, hasDeliveryReceipt.toInt())
|
||||||
contentValues.put(MessageTable.UNIDENTIFIED, this.outgoing.sendStatus.count { it.sealedSender })
|
contentValues.put(MessageTable.UNIDENTIFIED, this.outgoing.sendStatus.count { it.sealedSender })
|
||||||
contentValues.put(MessageTable.READ, 1)
|
contentValues.put(MessageTable.READ, 1)
|
||||||
|
|
||||||
contentValues.addNetworkFailures(this, backupState)
|
contentValues.addNetworkFailures(this, backupState)
|
||||||
contentValues.addIdentityKeyMismatches(this, backupState)
|
contentValues.addIdentityKeyMismatches(this, backupState)
|
||||||
} else {
|
} else {
|
||||||
contentValues.put(MessageTable.VIEWED_RECEIPT_COUNT, 0)
|
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
|
||||||
contentValues.put(MessageTable.READ_RECEIPT_COUNT, 0)
|
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
|
||||||
contentValues.put(MessageTable.DELIVERY_RECEIPT_COUNT, 0)
|
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
|
||||||
contentValues.put(MessageTable.UNIDENTIFIED, this.incoming?.sealedSender?.toInt() ?: 0)
|
contentValues.put(MessageTable.UNIDENTIFIED, this.sealedSender?.toInt())
|
||||||
contentValues.put(MessageTable.READ, this.incoming?.read?.toInt() ?: 0)
|
contentValues.put(MessageTable.READ, this.incoming?.read?.toInt() ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ class ChatItemImportInserter(
|
|||||||
GroupReceiptTable.MMS_ID to messageId,
|
GroupReceiptTable.MMS_ID to messageId,
|
||||||
GroupReceiptTable.RECIPIENT_ID to recipientId.serialize(),
|
GroupReceiptTable.RECIPIENT_ID to recipientId.serialize(),
|
||||||
GroupReceiptTable.STATUS to sendStatus.deliveryStatus.toLocalSendStatus(),
|
GroupReceiptTable.STATUS to sendStatus.deliveryStatus.toLocalSendStatus(),
|
||||||
GroupReceiptTable.TIMESTAMP to sendStatus.timestamp,
|
GroupReceiptTable.TIMESTAMP to sendStatus.lastStatusUpdateTimestamp,
|
||||||
GroupReceiptTable.UNIDENTIFIED to sendStatus.sealedSender
|
GroupReceiptTable.UNIDENTIFIED to sendStatus.sealedSender
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -308,27 +309,28 @@ class ChatItemImportInserter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ContentValues.addUpdateMessage(updateMessage: UpdateMessage) {
|
private fun ContentValues.addUpdateMessage(updateMessage: ChatUpdateMessage) {
|
||||||
var typeFlags: Long = 0
|
var typeFlags: Long = 0
|
||||||
when {
|
when {
|
||||||
updateMessage.simpleUpdate != null -> {
|
updateMessage.simpleUpdate != null -> {
|
||||||
typeFlags = when (updateMessage.simpleUpdate.type) {
|
typeFlags = when (updateMessage.simpleUpdate.type) {
|
||||||
SimpleUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE
|
SimpleChatUpdate.Type.UNKNOWN -> 0
|
||||||
SimpleUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
|
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE
|
||||||
SimpleUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
|
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
|
||||||
SimpleUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
|
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
|
||||||
SimpleUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
|
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
|
||||||
SimpleUpdate.Type.BOOST_REQUEST -> MessageTypes.BOOST_REQUEST_TYPE
|
SimpleChatUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
|
||||||
SimpleUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT
|
SimpleChatUpdate.Type.BOOST_REQUEST -> MessageTypes.BOOST_REQUEST_TYPE
|
||||||
SimpleUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT
|
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT
|
||||||
SimpleUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE
|
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT
|
||||||
SimpleUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE
|
||||||
SimpleUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
|
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
||||||
|
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateMessage.expirationTimerChange != null -> {
|
updateMessage.expirationTimerChange != null -> {
|
||||||
typeFlags = MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
typeFlags = MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
||||||
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresIn.toLong() * 1000)
|
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresInMs.toLong())
|
||||||
}
|
}
|
||||||
updateMessage.profileChange != null -> {
|
updateMessage.profileChange != null -> {
|
||||||
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
|
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
|
||||||
@@ -341,13 +343,13 @@ class ChatItemImportInserter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ContentValues.addQuote(quote: Quote) {
|
private fun ContentValues.addQuote(quote: Quote) {
|
||||||
this.put(MessageTable.QUOTE_ID, quote.targetSentTimestamp)
|
this.put(MessageTable.QUOTE_ID, quote.targetSentTimestamp ?: MessageTable.QUOTE_TARGET_MISSING_ID)
|
||||||
this.put(MessageTable.QUOTE_AUTHOR, backupState.backupToLocalRecipientId[quote.authorId]!!.serialize())
|
this.put(MessageTable.QUOTE_AUTHOR, backupState.backupToLocalRecipientId[quote.authorId]!!.serialize())
|
||||||
this.put(MessageTable.QUOTE_BODY, quote.text)
|
this.put(MessageTable.QUOTE_BODY, quote.text)
|
||||||
this.put(MessageTable.QUOTE_TYPE, quote.type.toLocalQuoteType())
|
this.put(MessageTable.QUOTE_TYPE, quote.type.toLocalQuoteType())
|
||||||
this.put(MessageTable.QUOTE_BODY_RANGES, quote.bodyRanges.toLocalBodyRanges()?.encode())
|
this.put(MessageTable.QUOTE_BODY_RANGES, quote.bodyRanges.toLocalBodyRanges()?.encode())
|
||||||
// TODO quote attachments
|
// TODO quote attachments
|
||||||
this.put(MessageTable.QUOTE_MISSING, quote.originalMessageMissing.toInt())
|
this.put(MessageTable.QUOTE_MISSING, (quote.targetSentTimestamp == null).toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Quote.Type.toLocalQuoteType(): Int {
|
private fun Quote.Type.toLocalQuoteType(): Int {
|
||||||
@@ -398,7 +400,7 @@ class ChatItemImportInserter(
|
|||||||
return BodyRangeList(
|
return BodyRangeList(
|
||||||
ranges = this.map { bodyRange ->
|
ranges = this.map { bodyRange ->
|
||||||
BodyRangeList.BodyRange(
|
BodyRangeList.BodyRange(
|
||||||
mentionUuid = bodyRange.mentionAci,
|
mentionUuid = bodyRange.mentionAci?.let { UuidUtil.fromByteString(it) }?.toString(),
|
||||||
style = bodyRange.style?.let {
|
style = bodyRange.style?.let {
|
||||||
when (bodyRange.style) {
|
when (bodyRange.style) {
|
||||||
BodyRange.Style.BOLD -> BodyRangeList.BodyRange.Style.BOLD
|
BodyRange.Style.BOLD -> BodyRangeList.BodyRange.Style.BOLD
|
||||||
@@ -418,6 +420,7 @@ class ChatItemImportInserter(
|
|||||||
|
|
||||||
private fun SendStatus.Status.toLocalSendStatus(): Int {
|
private fun SendStatus.Status.toLocalSendStatus(): Int {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
SendStatus.Status.UNKNOWN -> GroupReceiptTable.STATUS_UNKNOWN
|
||||||
SendStatus.Status.FAILED -> GroupReceiptTable.STATUS_UNKNOWN
|
SendStatus.Status.FAILED -> GroupReceiptTable.STATUS_UNKNOWN
|
||||||
SendStatus.Status.PENDING -> GroupReceiptTable.STATUS_UNDELIVERED
|
SendStatus.Status.PENDING -> GroupReceiptTable.STATUS_UNDELIVERED
|
||||||
SendStatus.Status.SENT -> GroupReceiptTable.STATUS_UNDELIVERED
|
SendStatus.Status.SENT -> GroupReceiptTable.STATUS_UNDELIVERED
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ fun DistributionListTables.getAllForBackup(): List<BackupRecipient> {
|
|||||||
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
|
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
|
||||||
allowReplies = record.allowsReplies,
|
allowReplies = record.allowsReplies,
|
||||||
deletionTimestamp = record.deletedAtTimestamp,
|
deletionTimestamp = record.deletedAtTimestamp,
|
||||||
isUnknown = record.isUnknown,
|
|
||||||
privacyMode = record.privacyMode.toBackupPrivacyMode(),
|
privacyMode = record.privacyMode.toBackupPrivacyMode(),
|
||||||
memberRecipientIds = record.members.map { it.toLong() }
|
memberRecipientIds = record.members.map { it.toLong() }
|
||||||
)
|
)
|
||||||
@@ -81,7 +80,6 @@ fun DistributionListTables.restoreFromBackup(dlist: BackupDistributionList, back
|
|||||||
allowsReplies = dlist.allowReplies,
|
allowsReplies = dlist.allowReplies,
|
||||||
deletionTimestamp = dlist.deletionTimestamp,
|
deletionTimestamp = dlist.deletionTimestamp,
|
||||||
storageId = null,
|
storageId = null,
|
||||||
isUnknown = dlist.isUnknown,
|
|
||||||
privacyMode = dlist.privacyMode.toLocalPrivacyMode()
|
privacyMode = dlist.privacyMode.toLocalPrivacyMode()
|
||||||
)!!
|
)!!
|
||||||
|
|
||||||
@@ -108,6 +106,7 @@ private fun DistributionListPrivacyMode.toBackupPrivacyMode(): BackupDistributio
|
|||||||
|
|
||||||
private fun BackupDistributionList.PrivacyMode.toLocalPrivacyMode(): DistributionListPrivacyMode {
|
private fun BackupDistributionList.PrivacyMode.toLocalPrivacyMode(): DistributionListPrivacyMode {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
BackupDistributionList.PrivacyMode.UNKNOWN -> DistributionListPrivacyMode.ALL
|
||||||
BackupDistributionList.PrivacyMode.ONLY_WITH -> DistributionListPrivacyMode.ONLY_WITH
|
BackupDistributionList.PrivacyMode.ONLY_WITH -> DistributionListPrivacyMode.ONLY_WITH
|
||||||
BackupDistributionList.PrivacyMode.ALL -> DistributionListPrivacyMode.ALL
|
BackupDistributionList.PrivacyMode.ALL -> DistributionListPrivacyMode.ALL
|
||||||
BackupDistributionList.PrivacyMode.ALL_EXCEPT -> DistributionListPrivacyMode.ALL_EXCEPT
|
BackupDistributionList.PrivacyMode.ALL_EXCEPT -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
|||||||
MessageTable.QUOTE_TYPE,
|
MessageTable.QUOTE_TYPE,
|
||||||
MessageTable.ORIGINAL_MESSAGE_ID,
|
MessageTable.ORIGINAL_MESSAGE_ID,
|
||||||
MessageTable.LATEST_REVISION_ID,
|
MessageTable.LATEST_REVISION_ID,
|
||||||
MessageTable.DELIVERY_RECEIPT_COUNT,
|
MessageTable.HAS_DELIVERY_RECEIPT,
|
||||||
MessageTable.READ_RECEIPT_COUNT,
|
MessageTable.HAS_READ_RECEIPT,
|
||||||
MessageTable.VIEWED_RECEIPT_COUNT,
|
MessageTable.VIEWED_COLUMN,
|
||||||
MessageTable.RECEIPT_TIMESTAMP,
|
MessageTable.RECEIPT_TIMESTAMP,
|
||||||
MessageTable.READ,
|
MessageTable.READ,
|
||||||
MessageTable.NETWORK_FAILURES,
|
MessageTable.NETWORK_FAILURES,
|
||||||
|
|||||||
@@ -127,9 +127,7 @@ fun RecipientTable.restoreRecipientFromBackup(recipient: BackupRecipient, backup
|
|||||||
/**
|
/**
|
||||||
* Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable].
|
* Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable].
|
||||||
*/
|
*/
|
||||||
fun RecipientTable.restoreSelfFromBackup(accountData: AccountData) {
|
fun RecipientTable.restoreSelfFromBackup(accountData: AccountData, selfId: RecipientId) {
|
||||||
val self = Recipient.trustedPush(ACI.parseOrThrow(accountData.aci.toByteArray()), PNI.parseOrNull(accountData.pni.toByteArray()), accountData.e164.toString())
|
|
||||||
|
|
||||||
val values = ContentValues().apply {
|
val values = ContentValues().apply {
|
||||||
put(RecipientTable.PROFILE_GIVEN_NAME, accountData.givenName.nullIfBlank())
|
put(RecipientTable.PROFILE_GIVEN_NAME, accountData.givenName.nullIfBlank())
|
||||||
put(RecipientTable.PROFILE_FAMILY_NAME, accountData.familyName.nullIfBlank())
|
put(RecipientTable.PROFILE_FAMILY_NAME, accountData.familyName.nullIfBlank())
|
||||||
@@ -152,7 +150,7 @@ fun RecipientTable.restoreSelfFromBackup(accountData: AccountData) {
|
|||||||
writableDatabase
|
writableDatabase
|
||||||
.update(RecipientTable.TABLE_NAME)
|
.update(RecipientTable.TABLE_NAME)
|
||||||
.values(values)
|
.values(values)
|
||||||
.where("${RecipientTable.ID} = ?", self.id)
|
.where("${RecipientTable.ID} = ?", selfId)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +179,7 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
|
|||||||
RecipientTable.HIDDEN to contact.hidden,
|
RecipientTable.HIDDEN to contact.hidden,
|
||||||
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName.nullIfBlank(),
|
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName.nullIfBlank(),
|
||||||
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName.nullIfBlank(),
|
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName.nullIfBlank(),
|
||||||
RecipientTable.PROFILE_JOINED_NAME to contact.profileJoinedName.nullIfBlank(),
|
RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName.nullIfBlank(), contact.profileFamilyName.nullIfBlank()).toString().nullIfBlank(),
|
||||||
RecipientTable.PROFILE_KEY to if (profileKey == null) null else Base64.encodeWithPadding(profileKey),
|
RecipientTable.PROFILE_KEY to if (profileKey == null) null else Base64.encodeWithPadding(profileKey),
|
||||||
RecipientTable.PROFILE_SHARING to contact.profileSharing.toInt(),
|
RecipientTable.PROFILE_SHARING to contact.profileSharing.toInt(),
|
||||||
RecipientTable.REGISTERED to contact.registered.toLocalRegisteredState().id,
|
RecipientTable.REGISTERED to contact.registered.toLocalRegisteredState().id,
|
||||||
@@ -205,12 +203,12 @@ private fun Contact.toLocalExtras(): RecipientExtras {
|
|||||||
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
|
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
|
||||||
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
|
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
|
||||||
*/
|
*/
|
||||||
class BackupContactIterator(private val cursor: Cursor, private val selfId: Long) : Iterator<BackupRecipient>, Closeable {
|
class BackupContactIterator(private val cursor: Cursor, private val selfId: Long) : Iterator<BackupRecipient?>, Closeable {
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
return cursor.count > 0 && !cursor.isLast
|
return cursor.count > 0 && !cursor.isLast
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun next(): BackupRecipient {
|
override fun next(): BackupRecipient? {
|
||||||
if (!cursor.moveToNext()) {
|
if (!cursor.moveToNext()) {
|
||||||
throw NoSuchElementException()
|
throw NoSuchElementException()
|
||||||
}
|
}
|
||||||
@@ -225,10 +223,15 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long
|
|||||||
|
|
||||||
val aci = ACI.parseOrNull(cursor.requireString(RecipientTable.ACI_COLUMN))
|
val aci = ACI.parseOrNull(cursor.requireString(RecipientTable.ACI_COLUMN))
|
||||||
val pni = PNI.parseOrNull(cursor.requireString(RecipientTable.PNI_COLUMN))
|
val pni = PNI.parseOrNull(cursor.requireString(RecipientTable.PNI_COLUMN))
|
||||||
|
val e164 = cursor.requireString(RecipientTable.E164)?.e164ToLong()
|
||||||
val registeredState = RecipientTable.RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED))
|
val registeredState = RecipientTable.RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED))
|
||||||
val profileKey = cursor.requireString(RecipientTable.PROFILE_KEY)
|
val profileKey = cursor.requireString(RecipientTable.PROFILE_KEY)
|
||||||
val extras = RecipientTableCursorUtil.getExtras(cursor)
|
val extras = RecipientTableCursorUtil.getExtras(cursor)
|
||||||
|
|
||||||
|
if (aci == null && pni == null && e164 == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return BackupRecipient(
|
return BackupRecipient(
|
||||||
id = id,
|
id = id,
|
||||||
contact = Contact(
|
contact = Contact(
|
||||||
@@ -244,7 +247,6 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long
|
|||||||
profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
|
profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
|
||||||
profileGivenName = cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME).nullIfBlank(),
|
profileGivenName = cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME).nullIfBlank(),
|
||||||
profileFamilyName = cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME).nullIfBlank(),
|
profileFamilyName = cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME).nullIfBlank(),
|
||||||
profileJoinedName = cursor.requireString(RecipientTable.PROFILE_JOINED_NAME).nullIfBlank(),
|
|
||||||
hideStory = extras?.hideStory() ?: false
|
hideStory = extras?.hideStory() ?: false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,13 +7,20 @@ package org.thoughtcrime.securesms.backup.v2.database
|
|||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.insertInto
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.requireBoolean
|
import org.signal.core.util.requireBoolean
|
||||||
|
import org.signal.core.util.requireInt
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.toInt
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||||
import org.thoughtcrime.securesms.database.ThreadTable
|
import org.thoughtcrime.securesms.database.ThreadTable
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
|
private val TAG = Log.tag(ThreadTable::class.java)
|
||||||
|
|
||||||
fun ThreadTable.getThreadsForBackup(): ChatIterator {
|
fun ThreadTable.getThreadsForBackup(): ChatIterator {
|
||||||
val cursor = readableDatabase
|
val cursor = readableDatabase
|
||||||
.select(
|
.select(
|
||||||
@@ -35,6 +42,17 @@ fun ThreadTable.clearAllDataForBackupRestore() {
|
|||||||
clearCache()
|
clearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
|
||||||
|
return writableDatabase
|
||||||
|
.insertInto(ThreadTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
ThreadTable.RECIPIENT_ID to recipientId.serialize(),
|
||||||
|
ThreadTable.PINNED to chat.pinnedOrder,
|
||||||
|
ThreadTable.ARCHIVED to chat.archived.toInt()
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
|
class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
return cursor.count > 0 && !cursor.isLast
|
return cursor.count > 0 && !cursor.isLast
|
||||||
@@ -49,8 +67,8 @@ class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
|
|||||||
id = cursor.requireLong(ThreadTable.ID),
|
id = cursor.requireLong(ThreadTable.ID),
|
||||||
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
|
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
|
||||||
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
|
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
|
||||||
pinned = cursor.requireBoolean(ThreadTable.PINNED),
|
pinnedOrder = cursor.requireInt(ThreadTable.PINNED),
|
||||||
expirationTimer = cursor.requireLong(ThreadTable.EXPIRES_IN)
|
expirationTimerMs = cursor.requireLong(ThreadTable.EXPIRES_IN)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
|||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||||
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.subscription.Subscriber
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
@@ -35,21 +36,11 @@ object AccountDataProcessor {
|
|||||||
val self = Recipient.self().fresh()
|
val self = Recipient.self().fresh()
|
||||||
val record = recipients.getRecordForSync(self.id)
|
val record = recipients.getRecordForSync(self.id)
|
||||||
|
|
||||||
val pniIdentityKey = SignalStore.account().pniIdentityKey
|
|
||||||
val aciIdentityKey = SignalStore.account().aciIdentityKey
|
|
||||||
|
|
||||||
val subscriber: Subscriber? = SignalStore.donationsValues().getSubscriber()
|
val subscriber: Subscriber? = SignalStore.donationsValues().getSubscriber()
|
||||||
|
|
||||||
emitter.emit(
|
emitter.emit(
|
||||||
Frame(
|
Frame(
|
||||||
account = AccountData(
|
account = AccountData(
|
||||||
aci = SignalStore.account().aci!!.toByteString(),
|
|
||||||
pni = SignalStore.account().pni!!.toByteString(),
|
|
||||||
e164 = SignalStore.account().e164!!.toLong(),
|
|
||||||
pniIdentityPrivateKey = pniIdentityKey.privateKey.serialize().toByteString(),
|
|
||||||
pniIdentityPublicKey = pniIdentityKey.publicKey.serialize().toByteString(),
|
|
||||||
aciIdentityPrivateKey = aciIdentityKey.privateKey.serialize().toByteString(),
|
|
||||||
aciIdentityPublicKey = aciIdentityKey.publicKey.serialize().toByteString(),
|
|
||||||
profileKey = self.profileKey?.toByteString() ?: EMPTY,
|
profileKey = self.profileKey?.toByteString() ?: EMPTY,
|
||||||
givenName = self.profileName.givenName,
|
givenName = self.profileName.givenName,
|
||||||
familyName = self.profileName.familyName,
|
familyName = self.profileName.familyName,
|
||||||
@@ -60,14 +51,12 @@ object AccountDataProcessor {
|
|||||||
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
|
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
|
||||||
accountSettings = AccountData.AccountSettings(
|
accountSettings = AccountData.AccountSettings(
|
||||||
storyViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled,
|
storyViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled,
|
||||||
hasReadOnboardingStory = SignalStore.storyValues().userHasViewedOnboardingStory || SignalStore.storyValues().userHasReadOnboardingStory,
|
|
||||||
noteToSelfArchived = record != null && record.syncExtras.isArchived,
|
|
||||||
noteToSelfMarkedUnread = record != null && record.syncExtras.isForcedUnread,
|
noteToSelfMarkedUnread = record != null && record.syncExtras.isForcedUnread,
|
||||||
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
||||||
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
|
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
|
||||||
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
||||||
linkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
|
linkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
|
||||||
unlistedPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isUnlisted,
|
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isUnlisted,
|
||||||
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
|
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
|
||||||
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
|
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
|
||||||
universalExpireTimer = SignalStore.settings().universalExpireTimer,
|
universalExpireTimer = SignalStore.settings().universalExpireTimer,
|
||||||
@@ -84,16 +73,12 @@ object AccountDataProcessor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun import(accountData: AccountData) {
|
fun import(accountData: AccountData, selfId: RecipientId) {
|
||||||
SignalStore.account().restoreAciIdentityKeyFromBackup(accountData.aciIdentityPublicKey.toByteArray(), accountData.aciIdentityPrivateKey.toByteArray())
|
recipients.restoreSelfFromBackup(accountData, selfId)
|
||||||
SignalStore.account().restorePniIdentityKeyFromBackup(accountData.pniIdentityPublicKey.toByteArray(), accountData.pniIdentityPrivateKey.toByteArray())
|
|
||||||
|
|
||||||
recipients.restoreSelfFromBackup(accountData)
|
|
||||||
|
|
||||||
SignalStore.account().setRegistered(true)
|
SignalStore.account().setRegistered(true)
|
||||||
|
|
||||||
val context = ApplicationDependencies.getApplication()
|
val context = ApplicationDependencies.getApplication()
|
||||||
|
|
||||||
val settings = accountData.accountSettings
|
val settings = accountData.accountSettings
|
||||||
|
|
||||||
if (settings != null) {
|
if (settings != null) {
|
||||||
@@ -101,7 +86,7 @@ object AccountDataProcessor {
|
|||||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
|
||||||
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
|
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
|
||||||
SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews
|
SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews
|
||||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = if (settings.unlistedPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED else PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED
|
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED else PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED
|
||||||
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
|
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
|
||||||
SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars
|
SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars
|
||||||
SignalStore.settings().universalExpireTimer = settings.universalExpireTimer
|
SignalStore.settings().universalExpireTimer = settings.universalExpireTimer
|
||||||
@@ -111,7 +96,6 @@ object AccountDataProcessor {
|
|||||||
SignalStore.storyValues().userHasBeenNotifiedAboutStories = settings.hasSetMyStoriesPrivacy
|
SignalStore.storyValues().userHasBeenNotifiedAboutStories = settings.hasSetMyStoriesPrivacy
|
||||||
SignalStore.storyValues().userHasViewedOnboardingStory = settings.hasViewedOnboardingStory
|
SignalStore.storyValues().userHasViewedOnboardingStory = settings.hasViewedOnboardingStory
|
||||||
SignalStore.storyValues().isFeatureDisabled = settings.storiesDisabled
|
SignalStore.storyValues().isFeatureDisabled = settings.storiesDisabled
|
||||||
SignalStore.storyValues().userHasReadOnboardingStory = settings.hasReadOnboardingStory
|
|
||||||
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
|
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
|
||||||
SignalStore.storyValues().viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
|
SignalStore.storyValues().viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ package org.thoughtcrime.securesms.backup.v2.processor
|
|||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||||
import org.thoughtcrime.securesms.backup.v2.database.getThreadsForBackup
|
import org.thoughtcrime.securesms.backup.v2.database.getThreadsForBackup
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
object ChatBackupProcessor {
|
object ChatBackupProcessor {
|
||||||
val TAG = Log.tag(ChatBackupProcessor::class.java)
|
val TAG = Log.tag(ChatBackupProcessor::class.java)
|
||||||
@@ -27,26 +27,18 @@ object ChatBackupProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun import(chat: Chat, backupState: BackupState) {
|
fun import(chat: Chat, backupState: BackupState) {
|
||||||
// TODO Perf can be improved here by doing a single insert instead of insert + multiple updates
|
|
||||||
|
|
||||||
val recipientId: RecipientId? = backupState.backupToLocalRecipientId[chat.recipientId]
|
val recipientId: RecipientId? = backupState.backupToLocalRecipientId[chat.recipientId]
|
||||||
|
if (recipientId == null) {
|
||||||
if (recipientId != null) {
|
Log.w(TAG, "Missing recipient for chat ${chat.id}")
|
||||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(org.thoughtcrime.securesms.recipients.Recipient.resolved(recipientId))
|
return
|
||||||
|
|
||||||
if (chat.archived) {
|
|
||||||
SignalDatabase.threads.archiveConversation(threadId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chat.pinned) {
|
|
||||||
SignalDatabase.threads.pinConversations(Collections.singleton(threadId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignalDatabase.threads.restoreFromBackup(chat, recipientId)?.let { threadId ->
|
||||||
backupState.chatIdToLocalRecipientId[chat.id] = recipientId
|
backupState.chatIdToLocalRecipientId[chat.id] = recipientId
|
||||||
backupState.chatIdToLocalThreadId[chat.id] = threadId
|
backupState.chatIdToLocalThreadId[chat.id] = threadId
|
||||||
backupState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
|
backupState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
|
||||||
} else {
|
}
|
||||||
Log.w(TAG, "Recipient doesnt exist with id $recipientId")
|
|
||||||
}
|
// TODO there's several fields in the chat that actually need to be restored on the recipient table
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ object RecipientBackupProcessor {
|
|||||||
|
|
||||||
SignalDatabase.recipients.getContactsForBackup(selfId).use { reader ->
|
SignalDatabase.recipients.getContactsForBackup(selfId).use { reader ->
|
||||||
for (backupRecipient in reader) {
|
for (backupRecipient in reader) {
|
||||||
|
if (backupRecipient != null) {
|
||||||
emitter.emit(Frame(recipient = backupRecipient))
|
emitter.emit(Frame(recipient = backupRecipient))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SignalDatabase.recipients.getGroupsForBackup().use { reader ->
|
SignalDatabase.recipients.getGroupsForBackup().use { reader ->
|
||||||
for (backupRecipient in reader) {
|
for (backupRecipient in reader) {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
|
import org.signal.libsignal.protocol.kdf.HKDF
|
||||||
|
import java.io.FilterOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class BackupEncryptedOutputStream(key: ByteArray, backupId: ByteArray, wrapped: OutputStream) : FilterOutputStream(wrapped) {
|
||||||
|
|
||||||
|
val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
val mac: Mac = Mac.getInstance("HmacSHA256")
|
||||||
|
|
||||||
|
var finalMac: ByteArray? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (key.size != 32) {
|
||||||
|
throw IllegalArgumentException("Key must be 32 bytes!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupId.size != 16) {
|
||||||
|
throw IllegalArgumentException("BackupId must be 32 bytes!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val extendedKey = HKDF.deriveSecrets(key, backupId, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
|
||||||
|
val macKey = extendedKey.copyOfRange(0, 32)
|
||||||
|
val cipherKey = extendedKey.copyOfRange(32, 64)
|
||||||
|
val iv = extendedKey.copyOfRange(64, 80)
|
||||||
|
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
||||||
|
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(b: Int) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(data: ByteArray) {
|
||||||
|
write(data, 0, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(data: ByteArray, off: Int, len: Int) {
|
||||||
|
cipher.update(data, off, len)?.let { ciphertext ->
|
||||||
|
mac.update(ciphertext)
|
||||||
|
super.write(ciphertext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() {
|
||||||
|
cipher.doFinal()?.let { ciphertext ->
|
||||||
|
mac.update(ciphertext)
|
||||||
|
super.write(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalMac = mac.doFinal()
|
||||||
|
|
||||||
|
super.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
flush()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMac(): ByteArray {
|
||||||
|
return finalMac ?: throw IllegalStateException("Mac not yet available! You must call flush() before asking for the mac.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.stream
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
|
||||||
interface BackupExportStream {
|
interface BackupExportWriter : AutoCloseable {
|
||||||
fun write(frame: Frame)
|
fun write(frame: Frame)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
|
import org.signal.core.util.readFully
|
||||||
|
import org.signal.core.util.readNBytesOrThrow
|
||||||
|
import org.signal.core.util.readVarInt32
|
||||||
|
import org.signal.core.util.stream.MacInputStream
|
||||||
|
import org.signal.core.util.stream.TruncatingInputStream
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the ability to read backup frames in a streaming fashion from a target [InputStream].
|
||||||
|
* As it's being read, it will be both decrypted and uncompressed. Specifically, the data is decrypted,
|
||||||
|
* that decrypted data is gunzipped, then that data is read as frames.
|
||||||
|
*/
|
||||||
|
class EncryptedBackupReader(
|
||||||
|
key: BackupKey,
|
||||||
|
aci: ACI,
|
||||||
|
streamLength: Long,
|
||||||
|
dataStream: () -> InputStream
|
||||||
|
) : Iterator<Frame>, AutoCloseable {
|
||||||
|
|
||||||
|
var next: Frame? = null
|
||||||
|
val stream: InputStream
|
||||||
|
|
||||||
|
init {
|
||||||
|
val keyMaterial = key.deriveSecrets(aci)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||||
|
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
|
||||||
|
}
|
||||||
|
|
||||||
|
validateMac(keyMaterial.macKey, streamLength, dataStream())
|
||||||
|
|
||||||
|
stream = GZIPInputStream(
|
||||||
|
CipherInputStream(
|
||||||
|
TruncatingInputStream(
|
||||||
|
wrapped = dataStream(),
|
||||||
|
maxBytes = streamLength - MAC_SIZE
|
||||||
|
),
|
||||||
|
cipher
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
next = read()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean {
|
||||||
|
return next != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun next(): Frame {
|
||||||
|
next?.let { out ->
|
||||||
|
next = read()
|
||||||
|
return out
|
||||||
|
} ?: throw NoSuchElementException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun read(): Frame? {
|
||||||
|
try {
|
||||||
|
val length = stream.readVarInt32().also { if (it < 0) return null }
|
||||||
|
val frameBytes: ByteArray = stream.readNBytesOrThrow(length)
|
||||||
|
|
||||||
|
return Frame.ADAPTER.decode(frameBytes)
|
||||||
|
} catch (e: EOFException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
stream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAC_SIZE = 32
|
||||||
|
|
||||||
|
fun validateMac(macKey: ByteArray, streamLength: Long, dataStream: InputStream) {
|
||||||
|
val mac = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val macStream = MacInputStream(
|
||||||
|
wrapped = TruncatingInputStream(dataStream, maxBytes = streamLength - MAC_SIZE),
|
||||||
|
mac = mac
|
||||||
|
)
|
||||||
|
|
||||||
|
macStream.readFully(false)
|
||||||
|
|
||||||
|
val calculatedMac = macStream.mac.doFinal()
|
||||||
|
val expectedMac = dataStream.readNBytesOrThrow(MAC_SIZE)
|
||||||
|
|
||||||
|
if (!calculatedMac.contentEquals(expectedMac)) {
|
||||||
|
throw IOException("Invalid MAC!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
|
import org.signal.core.util.stream.MacOutputStream
|
||||||
|
import org.signal.core.util.writeVarInt32
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherOutputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the ability to write backup frames in a streaming fashion to a target [OutputStream].
|
||||||
|
* As it's being written, it will be both encrypted and compressed. Specifically, the backup frames
|
||||||
|
* are gzipped, that gzipped data is encrypted, and then an HMAC of the encrypted data is appended
|
||||||
|
* to the end of the [outputStream].
|
||||||
|
*/
|
||||||
|
class EncryptedBackupWriter(
|
||||||
|
key: BackupKey,
|
||||||
|
aci: ACI,
|
||||||
|
private val outputStream: OutputStream,
|
||||||
|
private val append: (ByteArray) -> Unit
|
||||||
|
) : BackupExportWriter {
|
||||||
|
|
||||||
|
private val mainStream: GZIPOutputStream
|
||||||
|
private val macStream: MacOutputStream
|
||||||
|
|
||||||
|
init {
|
||||||
|
val keyMaterial = key.deriveSecrets(aci)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||||
|
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(keyMaterial.macKey, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
macStream = MacOutputStream(outputStream, mac)
|
||||||
|
|
||||||
|
mainStream = GZIPOutputStream(
|
||||||
|
CipherOutputStream(
|
||||||
|
macStream,
|
||||||
|
cipher
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun write(frame: Frame) {
|
||||||
|
val frameBytes: ByteArray = frame.encode()
|
||||||
|
|
||||||
|
mainStream.writeVarInt32(frameBytes.size)
|
||||||
|
mainStream.write(frameBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close() {
|
||||||
|
// We need to close the main stream in order for the gzip and all the cipher operations to fully finish before
|
||||||
|
// we can calculate the MAC. Unfortunately flush()/finish() is not sufficient. So we have to defer to the
|
||||||
|
// caller to append the bytes to the end of the data however they see fit (like appending to a file).
|
||||||
|
mainStream.close()
|
||||||
|
val mac = macStream.mac.doFinal()
|
||||||
|
append(mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.stream
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
import org.signal.core.util.Conversions
|
|
||||||
import org.signal.core.util.readNBytesOrThrow
|
import org.signal.core.util.readNBytesOrThrow
|
||||||
|
import org.signal.core.util.readVarInt32
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -14,7 +14,7 @@ import java.io.InputStream
|
|||||||
/**
|
/**
|
||||||
* Reads a plaintext backup import stream one frame at a time.
|
* Reads a plaintext backup import stream one frame at a time.
|
||||||
*/
|
*/
|
||||||
class PlainTextBackupImportStream(val inputStream: InputStream) : BackupImportStream, Iterator<Frame> {
|
class PlainTextBackupReader(val inputStream: InputStream) : Iterator<Frame> {
|
||||||
|
|
||||||
var next: Frame? = null
|
var next: Frame? = null
|
||||||
|
|
||||||
@@ -33,15 +33,12 @@ class PlainTextBackupImportStream(val inputStream: InputStream) : BackupImportSt
|
|||||||
} ?: throw NoSuchElementException()
|
} ?: throw NoSuchElementException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(): Frame? {
|
private fun read(): Frame? {
|
||||||
try {
|
try {
|
||||||
val lengthBytes: ByteArray = inputStream.readNBytesOrThrow(4)
|
val length = inputStream.readVarInt32().also { if (it < 0) return null }
|
||||||
val length = Conversions.byteArrayToInt(lengthBytes)
|
|
||||||
|
|
||||||
val frameBytes: ByteArray = inputStream.readNBytesOrThrow(length)
|
val frameBytes: ByteArray = inputStream.readNBytesOrThrow(length)
|
||||||
val frame: Frame = Frame.ADAPTER.decode(frameBytes)
|
|
||||||
|
|
||||||
return frame
|
return Frame.ADAPTER.decode(frameBytes)
|
||||||
} catch (e: EOFException) {
|
} catch (e: EOFException) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.stream
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
import org.signal.core.util.Conversions
|
import org.signal.core.util.writeVarInt32
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -13,14 +13,17 @@ import java.io.OutputStream
|
|||||||
/**
|
/**
|
||||||
* Writes backup frames to the wrapped stream in plain text. Only for testing!
|
* Writes backup frames to the wrapped stream in plain text. Only for testing!
|
||||||
*/
|
*/
|
||||||
class PlainTextBackupExportStream(private val outputStream: OutputStream) : BackupExportStream {
|
class PlainTextBackupWriter(private val outputStream: OutputStream) : BackupExportWriter {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun write(frame: Frame) {
|
override fun write(frame: Frame) {
|
||||||
val frameBytes: ByteArray = frame.encode()
|
val frameBytes: ByteArray = frame.encode()
|
||||||
val lengthBytes: ByteArray = Conversions.intToByteArray(frameBytes.size)
|
|
||||||
|
|
||||||
outputStream.write(lengthBytes)
|
outputStream.writeVarInt32(frameBytes.size)
|
||||||
outputStream.write(frameBytes)
|
outputStream.write(frameBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,23 +5,70 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.settings.app.internal.backup
|
package org.thoughtcrime.securesms.components.settings.app.internal.backup
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
|
import org.signal.core.ui.theme.SignalTheme
|
||||||
|
import org.signal.core.util.getLength
|
||||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
|
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
|
||||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
|
|
||||||
class InternalBackupPlaygroundFragment : ComposeFragment() {
|
class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||||
|
|
||||||
val viewModel: InternalBackupPlaygroundViewModel by viewModels()
|
private val viewModel: InternalBackupPlaygroundViewModel by viewModels()
|
||||||
|
private lateinit var exportFileLauncher: ActivityResultLauncher<Intent>
|
||||||
|
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
exportFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
requireContext().contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.write(viewModel.backupData!!)
|
||||||
|
Toast.makeText(requireContext(), "Saved successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
} ?: Toast.makeText(requireContext(), "Failed to open output stream", Toast.LENGTH_SHORT).show()
|
||||||
|
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
requireContext().contentResolver.getLength(uri)?.let { length ->
|
||||||
|
viewModel.import(length) { requireContext().contentResolver.openInputStream(uri)!! }
|
||||||
|
}
|
||||||
|
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun FragmentContent() {
|
override fun FragmentContent() {
|
||||||
@@ -30,22 +77,65 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||||||
Screen(
|
Screen(
|
||||||
state = state,
|
state = state,
|
||||||
onExportClicked = { viewModel.export() },
|
onExportClicked = { viewModel.export() },
|
||||||
onImportClicked = { viewModel.import() }
|
onImportMemoryClicked = { viewModel.import() },
|
||||||
|
onImportFileClicked = {
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_GET_CONTENT
|
||||||
|
type = "application/octet-stream"
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
importFileLauncher.launch(intent)
|
||||||
|
},
|
||||||
|
onPlaintextClicked = { viewModel.onPlaintextToggled() },
|
||||||
|
onSaveToDiskClicked = {
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_CREATE_DOCUMENT
|
||||||
|
type = "application/octet-stream"
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "backup-${if (state.plaintext) "plaintext" else "encrypted"}-${System.currentTimeMillis()}.bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFileLauncher.launch(intent)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Screen(
|
fun Screen(
|
||||||
state: ScreenState,
|
state: ScreenState,
|
||||||
onExportClicked: () -> Unit = {},
|
onExportClicked: () -> Unit = {},
|
||||||
onImportClicked: () -> Unit = {}
|
onImportMemoryClicked: () -> Unit = {},
|
||||||
|
onImportFileClicked: () -> Unit = {},
|
||||||
|
onPlaintextClicked: () -> Unit = {},
|
||||||
|
onSaveToDiskClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Surface {
|
Surface {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
StateLabel(text = "Plaintext?")
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Switch(
|
||||||
|
checked = state.plaintext,
|
||||||
|
onCheckedChange = { onPlaintextClicked() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Buttons.LargePrimary(
|
Buttons.LargePrimary(
|
||||||
onClick = onExportClicked,
|
onClick = onExportClicked,
|
||||||
enabled = !state.backupState.inProgress
|
enabled = !state.backupState.inProgress
|
||||||
@@ -53,17 +143,92 @@ fun Screen(
|
|||||||
Text("Export")
|
Text("Export")
|
||||||
}
|
}
|
||||||
Buttons.LargeTonal(
|
Buttons.LargeTonal(
|
||||||
onClick = onImportClicked,
|
onClick = onImportMemoryClicked,
|
||||||
enabled = state.backupState == BackupState.EXPORT_DONE
|
enabled = state.backupState == BackupState.EXPORT_DONE
|
||||||
) {
|
) {
|
||||||
Text("Import")
|
Text("Import from memory")
|
||||||
|
}
|
||||||
|
Buttons.LargeTonal(
|
||||||
|
onClick = onImportFileClicked
|
||||||
|
) {
|
||||||
|
Text("Import from file")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
when (state.backupState) {
|
||||||
|
BackupState.NONE -> {
|
||||||
|
StateLabel("")
|
||||||
|
}
|
||||||
|
BackupState.EXPORT_IN_PROGRESS -> {
|
||||||
|
StateLabel("Export in progress...")
|
||||||
|
}
|
||||||
|
BackupState.EXPORT_DONE -> {
|
||||||
|
StateLabel("Export complete. Sitting in memory. You can click 'Import' to import that data, or you can save it to a file.")
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Buttons.MediumTonal(onClick = onSaveToDiskClicked) {
|
||||||
|
Text("Save to file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackupState.IMPORT_IN_PROGRESS -> {
|
||||||
|
StateLabel("Import in progress...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Composable
|
||||||
|
private fun StateLabel(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewScreen() {
|
fun PreviewScreen() {
|
||||||
Screen(state = ScreenState(backupState = BackupState.NONE))
|
SignalTheme {
|
||||||
|
Surface {
|
||||||
|
Screen(state = ScreenState(backupState = BackupState.NONE, plaintext = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun PreviewScreenExportInProgress() {
|
||||||
|
SignalTheme {
|
||||||
|
Surface {
|
||||||
|
Screen(state = ScreenState(backupState = BackupState.EXPORT_IN_PROGRESS, plaintext = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun PreviewScreenExportDone() {
|
||||||
|
SignalTheme {
|
||||||
|
Surface {
|
||||||
|
Screen(state = ScreenState(backupState = BackupState.EXPORT_DONE, plaintext = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun PreviewScreenImportInProgress() {
|
||||||
|
SignalTheme {
|
||||||
|
Surface {
|
||||||
|
Screen(state = ScreenState(backupState = BackupState.IMPORT_IN_PROGRESS, plaintext = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import io.reactivex.rxjava3.core.Single
|
|||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
class InternalBackupPlaygroundViewModel : ViewModel() {
|
class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||||
|
|
||||||
@@ -22,13 +26,14 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
|
|
||||||
val disposables = CompositeDisposable()
|
val disposables = CompositeDisposable()
|
||||||
|
|
||||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupState = BackupState.NONE))
|
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupState = BackupState.NONE, plaintext = false))
|
||||||
val state: State<ScreenState> = _state
|
val state: State<ScreenState> = _state
|
||||||
|
|
||||||
fun export() {
|
fun export() {
|
||||||
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS)
|
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS)
|
||||||
|
val plaintext = _state.value.plaintext
|
||||||
|
|
||||||
disposables += Single.fromCallable { BackupRepository.export() }
|
disposables += Single.fromCallable { BackupRepository.export(plaintext = plaintext) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { data ->
|
.subscribe { data ->
|
||||||
@@ -40,8 +45,12 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
fun import() {
|
fun import() {
|
||||||
backupData?.let {
|
backupData?.let {
|
||||||
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||||
|
val plaintext = _state.value.plaintext
|
||||||
|
|
||||||
disposables += Single.fromCallable { BackupRepository.import(it) }
|
val self = Recipient.self()
|
||||||
|
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||||
|
|
||||||
|
disposables += Single.fromCallable { BackupRepository.import(it.size.toLong(), { ByteArrayInputStream(it) }, selfData, plaintext = plaintext) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { nothing ->
|
.subscribe { nothing ->
|
||||||
@@ -51,12 +60,33 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun import(length: Long, inputStreamFactory: () -> InputStream) {
|
||||||
|
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||||
|
val plaintext = _state.value.plaintext
|
||||||
|
|
||||||
|
val self = Recipient.self()
|
||||||
|
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||||
|
|
||||||
|
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = plaintext) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { nothing ->
|
||||||
|
backupData = null
|
||||||
|
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPlaintextToggled() {
|
||||||
|
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ScreenState(
|
data class ScreenState(
|
||||||
val backupState: BackupState
|
val backupState: BackupState,
|
||||||
|
val plaintext: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class BackupState(val inProgress: Boolean = false) {
|
enum class BackupState(val inProgress: Boolean = false) {
|
||||||
|
|||||||
@@ -207,6 +207,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||||||
const val ORIGINAL_MESSAGE_ID = "original_message_id"
|
const val ORIGINAL_MESSAGE_ID = "original_message_id"
|
||||||
const val REVISION_NUMBER = "revision_number"
|
const val REVISION_NUMBER = "revision_number"
|
||||||
|
|
||||||
|
const val QUOTE_NOT_PRESENT_ID = 0L
|
||||||
|
const val QUOTE_TARGET_MISSING_ID = -1L
|
||||||
|
|
||||||
const val CREATE_TABLE = """
|
const val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -2316,7 +2319,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||||||
val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.isQuote }.toList()
|
val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.isQuote }.toList()
|
||||||
val quoteMentions: List<Mention> = parseQuoteMentions(cursor)
|
val quoteMentions: List<Mention> = parseQuoteMentions(cursor)
|
||||||
val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
|
val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
|
||||||
val quote: QuoteModel? = if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
|
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
|
||||||
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
|
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -5119,7 +5122,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||||||
val quoteAttachments: List<Attachment> = attachments.filter { it.isQuote }
|
val quoteAttachments: List<Attachment> = attachments.filter { it.isQuote }
|
||||||
val quoteDeck = SlideDeck(quoteAttachments)
|
val quoteDeck = SlideDeck(quoteAttachments)
|
||||||
|
|
||||||
return if (quoteId > 0 && quoteAuthor > 0) {
|
return if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0) {
|
||||||
if (quoteText != null && (quoteMentions.isNotEmpty() || bodyRanges != null)) {
|
if (quoteText != null && (quoteMentions.isNotEmpty() || bodyRanges != null)) {
|
||||||
val updated: UpdatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions)
|
val updated: UpdatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions)
|
||||||
val styledText = SpannableString(updated.body)
|
val styledText = SpannableString(updated.body)
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
|
|
||||||
val foundRecords = queries.flatMap { query ->
|
val foundRecords = queries.flatMap { query ->
|
||||||
readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).readToList { cursor ->
|
readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).readToList { cursor ->
|
||||||
getRecord(context, cursor)
|
RecipientTableCursorUtil.getRecord(context, cursor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ public final class FeatureFlags {
|
|||||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final Map<String, Object> FORCED_VALUES = new HashMap<String, Object>() {{
|
static final Map<String, Object> FORCED_VALUES = new HashMap<String, Object>() {{
|
||||||
put(INTERNAL_USER, true);
|
|
||||||
}};
|
}};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
|
|||||||
|
|
||||||
message BackupInfo {
|
message BackupInfo {
|
||||||
uint64 version = 1;
|
uint64 version = 1;
|
||||||
uint64 backupTime = 2;
|
uint64 backupTimeMs = 2;
|
||||||
|
bytes iv = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Frame {
|
message Frame {
|
||||||
@@ -45,46 +46,36 @@ message AccountData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message AccountSettings {
|
message AccountSettings {
|
||||||
bool noteToSelfArchived = 1;
|
bool readReceipts = 1;
|
||||||
bool readReceipts = 2;
|
bool sealedSenderIndicators = 2;
|
||||||
bool sealedSenderIndicators = 3;
|
bool typingIndicators = 3;
|
||||||
bool typingIndicators = 4;
|
bool noteToSelfMarkedUnread = 4;
|
||||||
bool proxiedLinkPreviews = 5;
|
bool linkPreviews = 5;
|
||||||
bool noteToSelfMarkedUnread = 6;
|
bool notDiscoverableByPhoneNumber = 6;
|
||||||
bool linkPreviews = 7;
|
bool preferContactAvatars = 7;
|
||||||
bool unlistedPhoneNumber = 8;
|
uint32 universalExpireTimer = 8;
|
||||||
bool preferContactAvatars = 9;
|
repeated string preferredReactionEmoji = 9;
|
||||||
uint32 universalExpireTimer = 10;
|
bool displayBadgesOnProfile = 10;
|
||||||
repeated string preferredReactionEmoji = 11;
|
bool keepMutedChatsArchived = 11;
|
||||||
bool displayBadgesOnProfile = 12;
|
bool hasSetMyStoriesPrivacy = 12;
|
||||||
bool keepMutedChatsArchived = 13;
|
bool hasViewedOnboardingStory = 13;
|
||||||
bool hasSetMyStoriesPrivacy = 14;
|
bool storiesDisabled = 14;
|
||||||
bool hasViewedOnboardingStory = 15;
|
optional bool storyViewReceiptsEnabled = 15;
|
||||||
bool storiesDisabled = 16;
|
bool hasSeenGroupStoryEducationSheet = 16;
|
||||||
optional bool storyViewReceiptsEnabled = 17;
|
bool hasCompletedUsernameOnboarding = 17;
|
||||||
bool hasReadOnboardingStory = 18;
|
PhoneNumberSharingMode phoneNumberSharingMode = 18;
|
||||||
bool hasSeenGroupStoryEducationSheet = 19;
|
|
||||||
bool hasCompletedUsernameOnboarding = 20;
|
|
||||||
PhoneNumberSharingMode phoneNumberSharingMode = 21;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes aciIdentityPublicKey = 1;
|
bytes profileKey = 1;
|
||||||
bytes aciIdentityPrivateKey = 2;
|
optional string username = 2;
|
||||||
bytes pniIdentityPublicKey = 3;
|
UsernameLink usernameLink = 3;
|
||||||
bytes pniIdentityPrivateKey = 4;
|
string givenName = 4;
|
||||||
bytes profileKey = 5;
|
string familyName = 5;
|
||||||
optional string username = 6;
|
string avatarUrlPath = 6;
|
||||||
UsernameLink usernameLink = 7;
|
bytes subscriberId = 7;
|
||||||
string givenName = 8;
|
string subscriberCurrencyCode = 8;
|
||||||
string familyName = 9;
|
bool subscriptionManuallyCancelled = 9;
|
||||||
string avatarUrlPath = 10;
|
AccountSettings accountSettings = 10;
|
||||||
bytes subscriberId = 11;
|
|
||||||
string subscriberCurrencyCode = 12;
|
|
||||||
bool subscriptionManuallyCancelled = 13;
|
|
||||||
AccountSettings accountSettings = 14;
|
|
||||||
bytes aci = 15;
|
|
||||||
bytes pni = 16;
|
|
||||||
uint64 e164 = 17;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Recipient {
|
message Recipient {
|
||||||
@@ -94,29 +85,30 @@ message Recipient {
|
|||||||
Group group = 3;
|
Group group = 3;
|
||||||
DistributionList distributionList = 4;
|
DistributionList distributionList = 4;
|
||||||
Self self = 5;
|
Self self = 5;
|
||||||
|
ReleaseNotes releaseNotes = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
|
enum Registered {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
REGISTERED = 1;
|
||||||
|
NOT_REGISTERED = 2;
|
||||||
|
}
|
||||||
|
|
||||||
optional bytes aci = 1; // should be 16 bytes
|
optional bytes aci = 1; // should be 16 bytes
|
||||||
optional bytes pni = 2; // should be 16 bytes
|
optional bytes pni = 2; // should be 16 bytes
|
||||||
optional string username = 3;
|
optional string username = 3;
|
||||||
optional uint64 e164 = 4;
|
optional uint64 e164 = 4;
|
||||||
bool blocked = 5;
|
bool blocked = 5;
|
||||||
bool hidden = 6;
|
bool hidden = 6;
|
||||||
enum Registered {
|
|
||||||
UNKNOWN = 0;
|
|
||||||
REGISTERED = 1;
|
|
||||||
NOT_REGISTERED = 2;
|
|
||||||
}
|
|
||||||
Registered registered = 7;
|
Registered registered = 7;
|
||||||
uint64 unregisteredTimestamp = 8;
|
uint64 unregisteredTimestamp = 8;
|
||||||
optional bytes profileKey = 9;
|
optional bytes profileKey = 9;
|
||||||
bool profileSharing = 10;
|
bool profileSharing = 10;
|
||||||
optional string profileGivenName = 11;
|
optional string profileGivenName = 11;
|
||||||
optional string profileFamilyName = 12;
|
optional string profileFamilyName = 12;
|
||||||
optional string profileJoinedName = 13;
|
bool hideStory = 13;
|
||||||
bool hideStory = 14;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Group {
|
message Group {
|
||||||
@@ -134,30 +126,34 @@ message Group {
|
|||||||
|
|
||||||
message Self {}
|
message Self {}
|
||||||
|
|
||||||
|
message ReleaseNotes {}
|
||||||
|
|
||||||
message Chat {
|
message Chat {
|
||||||
uint64 id = 1; // generated id for reference only within this file
|
uint64 id = 1; // generated id for reference only within this file
|
||||||
uint64 recipientId = 2;
|
uint64 recipientId = 2;
|
||||||
bool archived = 3;
|
bool archived = 3;
|
||||||
bool pinned = 4;
|
uint32 pinnedOrder = 4; // 0 = unpinned, otherwise chat is considered pinned and will be displayed in ascending order
|
||||||
uint64 expirationTimer = 5;
|
uint64 expirationTimerMs = 5;
|
||||||
uint64 muteUntil = 6;
|
uint64 muteUntilMs = 6;
|
||||||
bool markedUnread = 7;
|
bool markedUnread = 7;
|
||||||
bool dontNotifyForMentionsIfMuted = 8;
|
bool dontNotifyForMentionsIfMuted = 8;
|
||||||
|
FilePointer wallpaper = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DistributionList {
|
message DistributionList {
|
||||||
|
enum PrivacyMode {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
ONLY_WITH = 1;
|
||||||
|
ALL_EXCEPT = 2;
|
||||||
|
ALL = 3;
|
||||||
|
}
|
||||||
|
|
||||||
string name = 1;
|
string name = 1;
|
||||||
bytes distributionId = 2; // distribution list ids are uuids
|
bytes distributionId = 2; // distribution list ids are uuids
|
||||||
bool allowReplies = 3;
|
bool allowReplies = 3;
|
||||||
uint64 deletionTimestamp = 4;
|
uint64 deletionTimestamp = 4;
|
||||||
bool isUnknown = 5;
|
PrivacyMode privacyMode = 5;
|
||||||
enum PrivacyMode {
|
repeated uint64 memberRecipientIds = 6; // generated recipient id
|
||||||
ONLY_WITH = 0;
|
|
||||||
ALL_EXCEPT = 1;
|
|
||||||
ALL = 2;
|
|
||||||
}
|
|
||||||
PrivacyMode privacyMode = 6;
|
|
||||||
repeated uint64 memberRecipientIds = 7; // generated recipient id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Identity {
|
message Identity {
|
||||||
@@ -170,38 +166,41 @@ message Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message Call {
|
message Call {
|
||||||
uint64 callId = 1;
|
|
||||||
uint64 peerRecipientId = 2;
|
|
||||||
enum Type {
|
enum Type {
|
||||||
AUDIO_CALL = 0;
|
UNKNOWN_TYPE = 0;
|
||||||
VIDEO_CALL = 1;
|
AUDIO_CALL = 1;
|
||||||
GROUP_CALL = 2;
|
VIDEO_CALL = 2;
|
||||||
AD_HOC_CALL = 3;
|
GROUP_CALL = 3;
|
||||||
|
AD_HOC_CALL = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
UNKNOWN_EVENT = 0;
|
||||||
|
OUTGOING = 1; // 1:1 calls only
|
||||||
|
ACCEPTED = 2; // 1:1 and group calls. Group calls: You accepted a ring.
|
||||||
|
NOT_ACCEPTED = 3; // 1:1 calls only,
|
||||||
|
MISSED = 4; // 1:1 and group. Group calls: The remote ring has expired or was cancelled by the ringer.
|
||||||
|
DELETE = 5; // 1:1 and Group/Ad-Hoc Calls.
|
||||||
|
GENERIC_GROUP_CALL = 6; // Group/Ad-Hoc Calls only. Initial state
|
||||||
|
JOINED = 7; // Group Calls: User has joined the group call.
|
||||||
|
DECLINED = 8; // Group Calls: If you declined a ring.
|
||||||
|
OUTGOING_RING = 9; // Group Calls: If you are ringing a group.
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64 callId = 1;
|
||||||
|
uint64 conversationRecipientId = 2;
|
||||||
Type type = 3;
|
Type type = 3;
|
||||||
bool outgoing = 4;
|
bool outgoing = 4;
|
||||||
uint64 timestamp = 5;
|
uint64 timestamp = 5;
|
||||||
uint64 ringerRecipientId = 6;
|
uint64 ringerRecipientId = 6;
|
||||||
enum Event {
|
|
||||||
OUTGOING = 0; // 1:1 calls only
|
|
||||||
ACCEPTED = 1; // 1:1 and group calls. Group calls: You accepted a ring.
|
|
||||||
NOT_ACCEPTED = 2; // 1:1 calls only,
|
|
||||||
MISSED = 3; // 1:1 and group/ad-hoc calls. Group calls: The remote ring has expired or was cancelled by the ringer.
|
|
||||||
DELETE = 4; // 1:1 and Group/Ad-Hoc Calls.
|
|
||||||
GENERIC_GROUP_CALL = 5; // Group/Ad-Hoc Calls only. Initial state
|
|
||||||
JOINED = 6; // Group Calls: User has joined the group call.
|
|
||||||
RINGING = 7; // Group Calls: If a ring was requested by another user.
|
|
||||||
DECLINED = 8; // Group Calls: If you declined a ring.
|
|
||||||
OUTGOING_RING = 9; // Group Calls: If you are ringing a group.
|
|
||||||
}
|
|
||||||
Event event = 7;
|
Event event = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChatItem {
|
message ChatItem {
|
||||||
message IncomingMessageDetails {
|
message IncomingMessageDetails {
|
||||||
uint64 dateServerSent = 1;
|
uint64 dateReceived = 1;
|
||||||
bool read = 2;
|
uint64 dateServerSent = 2;
|
||||||
bool sealedSender = 3;
|
bool read = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message OutgoingMessageDetails {
|
message OutgoingMessageDetails {
|
||||||
@@ -211,43 +210,45 @@ message ChatItem {
|
|||||||
uint64 chatId = 1; // conversation id
|
uint64 chatId = 1; // conversation id
|
||||||
uint64 authorId = 2; // recipient id
|
uint64 authorId = 2; // recipient id
|
||||||
uint64 dateSent = 3;
|
uint64 dateSent = 3;
|
||||||
uint64 dateReceived = 4;
|
bool sealedSender = 4;
|
||||||
optional uint64 expireStart = 5; // timestamp of when expiration timer started ticking down
|
optional uint64 expireStartDate = 5; // timestamp of when expiration timer started ticking down
|
||||||
optional uint64 expiresIn = 6; // how long timer of message is (ms)
|
optional uint64 expiresInMs = 6; // how long timer of message is (ms)
|
||||||
repeated ChatItem revisions = 7;
|
repeated ChatItem revisions = 7; // ordered from oldest to newest
|
||||||
bool sms = 8;
|
bool sms = 8;
|
||||||
|
|
||||||
oneof directionalDetails {
|
oneof directionalDetails {
|
||||||
IncomingMessageDetails incoming = 9;
|
IncomingMessageDetails incoming = 10;
|
||||||
OutgoingMessageDetails outgoing = 10;
|
OutgoingMessageDetails outgoing = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
oneof item {
|
oneof item {
|
||||||
StandardMessage standardMessage = 11;
|
StandardMessage standardMessage = 13;
|
||||||
ContactMessage contactMessage = 12;
|
ContactMessage contactMessage = 14;
|
||||||
VoiceMessage voiceMessage = 13;
|
VoiceMessage voiceMessage = 15;
|
||||||
StickerMessage stickerMessage = 14;
|
StickerMessage stickerMessage = 16;
|
||||||
RemoteDeletedMessage remoteDeletedMessage = 15;
|
RemoteDeletedMessage remoteDeletedMessage = 17;
|
||||||
UpdateMessage updateMessage = 16;
|
ChatUpdateMessage updateMessage = 18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message SendStatus {
|
message SendStatus {
|
||||||
enum Status {
|
enum Status {
|
||||||
FAILED = 0;
|
UNKNOWN = 0;
|
||||||
PENDING = 1;
|
FAILED = 1;
|
||||||
SENT = 2;
|
PENDING = 2;
|
||||||
DELIVERED = 3;
|
SENT = 3;
|
||||||
READ = 4;
|
DELIVERED = 4;
|
||||||
VIEWED = 5;
|
READ = 5;
|
||||||
SKIPPED = 6; // e.g. user in group was blocked, so we skipped sending to them
|
VIEWED = 6;
|
||||||
|
SKIPPED = 7; // e.g. user in group was blocked, so we skipped sending to them
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64 recipientId = 1;
|
uint64 recipientId = 1;
|
||||||
Status deliveryStatus = 2;
|
Status deliveryStatus = 2;
|
||||||
bool networkFailure = 3;
|
bool networkFailure = 3;
|
||||||
bool identityKeyMismatch = 4;
|
bool identityKeyMismatch = 4;
|
||||||
bool sealedSender = 5;
|
bool sealedSender = 5;
|
||||||
uint64 timestamp = 6;
|
uint64 lastStatusUpdateTimestamp = 6; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt
|
||||||
}
|
}
|
||||||
|
|
||||||
message Text {
|
message Text {
|
||||||
@@ -258,9 +259,9 @@ message Text {
|
|||||||
message StandardMessage {
|
message StandardMessage {
|
||||||
optional Quote quote = 1;
|
optional Quote quote = 1;
|
||||||
optional Text text = 2;
|
optional Text text = 2;
|
||||||
repeated AttachmentPointer attachments = 3;
|
repeated FilePointer attachments = 3;
|
||||||
optional LinkPreview linkPreview = 4;
|
repeated LinkPreview linkPreview = 4;
|
||||||
optional AttachmentPointer longText = 5;
|
optional FilePointer longText = 5;
|
||||||
repeated Reaction reactions = 6;
|
repeated Reaction reactions = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,10 +282,11 @@ message ContactAttachment {
|
|||||||
|
|
||||||
message Phone {
|
message Phone {
|
||||||
enum Type {
|
enum Type {
|
||||||
HOME = 0;
|
UNKNOWN = 0;
|
||||||
MOBILE = 1;
|
HOME = 1;
|
||||||
WORK = 2;
|
MOBILE = 2;
|
||||||
CUSTOM = 3;
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional string value = 1;
|
optional string value = 1;
|
||||||
@@ -294,10 +296,11 @@ message ContactAttachment {
|
|||||||
|
|
||||||
message Email {
|
message Email {
|
||||||
enum Type {
|
enum Type {
|
||||||
HOME = 0;
|
UNKNOWN = 0;
|
||||||
MOBILE = 1;
|
HOME = 1;
|
||||||
WORK = 2;
|
MOBILE = 2;
|
||||||
CUSTOM = 3;
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional string value = 1;
|
optional string value = 1;
|
||||||
@@ -307,9 +310,10 @@ message ContactAttachment {
|
|||||||
|
|
||||||
message PostalAddress {
|
message PostalAddress {
|
||||||
enum Type {
|
enum Type {
|
||||||
HOME = 0;
|
UNKNOWN = 0;
|
||||||
WORK = 1;
|
HOME = 1;
|
||||||
CUSTOM = 2;
|
WORK = 2;
|
||||||
|
CUSTOM = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
@@ -324,8 +328,7 @@ message ContactAttachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message Avatar {
|
message Avatar {
|
||||||
optional AttachmentPointer avatar = 1;
|
FilePointer avatar = 1;
|
||||||
optional bool isProfile = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Name name = 1;
|
optional Name name = 1;
|
||||||
@@ -338,13 +341,13 @@ message ContactAttachment {
|
|||||||
|
|
||||||
message DocumentMessage {
|
message DocumentMessage {
|
||||||
Text text = 1;
|
Text text = 1;
|
||||||
AttachmentPointer document = 2;
|
FilePointer document = 2;
|
||||||
repeated Reaction reactions = 3;
|
repeated Reaction reactions = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoiceMessage {
|
message VoiceMessage {
|
||||||
optional Quote quote = 1;
|
optional Quote quote = 1;
|
||||||
AttachmentPointer audio = 2;
|
FilePointer audio = 2;
|
||||||
repeated Reaction reactions = 3;
|
repeated Reaction reactions = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,11 +359,6 @@ message StickerMessage {
|
|||||||
// Tombstone for remote delete
|
// Tombstone for remote delete
|
||||||
message RemoteDeletedMessage {}
|
message RemoteDeletedMessage {}
|
||||||
|
|
||||||
message ScheduledMessage {
|
|
||||||
ChatItem message = 1;
|
|
||||||
uint64 scheduledTime = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Sticker {
|
message Sticker {
|
||||||
bytes packId = 1;
|
bytes packId = 1;
|
||||||
bytes packKey = 2;
|
bytes packKey = 2;
|
||||||
@@ -371,37 +369,62 @@ message Sticker {
|
|||||||
message LinkPreview {
|
message LinkPreview {
|
||||||
string url = 1;
|
string url = 1;
|
||||||
optional string title = 2;
|
optional string title = 2;
|
||||||
optional AttachmentPointer image = 3;
|
optional FilePointer image = 3;
|
||||||
optional string description = 4;
|
optional string description = 4;
|
||||||
optional uint64 date = 5;
|
optional uint64 date = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AttachmentPointer {
|
message FilePointer {
|
||||||
|
message BackupLocator {
|
||||||
|
string mediaName = 1;
|
||||||
|
uint32 cdnNumber = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AttachmentLocator {
|
||||||
|
string cdnKey = 1;
|
||||||
|
uint32 cdnNumber = 2;
|
||||||
|
uint64 uploadTimestamp = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LegacyAttachmentLocator {
|
||||||
|
fixed64 cdnId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An attachment that was backed up without being downloaded.
|
||||||
|
// Its MediaName should be generated as “{sender_aci}_{cdn_attachment_key}”,
|
||||||
|
// but should eventually transition to a BackupLocator with mediaName
|
||||||
|
// being the content hash once it is downloaded.
|
||||||
|
message UndownloadedBackupLocator {
|
||||||
|
bytes senderAci = 1;
|
||||||
|
string cdnKey = 2;
|
||||||
|
uint32 cdnNumber = 3;
|
||||||
|
}
|
||||||
|
|
||||||
enum Flags {
|
enum Flags {
|
||||||
VOICE_MESSAGE = 0;
|
VOICE_MESSAGE = 0;
|
||||||
BORDERLESS = 1;
|
BORDERLESS = 1;
|
||||||
GIF = 2;
|
GIF = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
oneof attachmentIdentifier {
|
oneof locator {
|
||||||
fixed64 cdnId = 1;
|
BackupLocator backupLocator = 1;
|
||||||
string cdnKey = 2;
|
AttachmentLocator attachmentLocator= 2;
|
||||||
|
LegacyAttachmentLocator legacyAttachmentLocator = 3;
|
||||||
|
UndownloadedBackupLocator undownloadedBackupLocator = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional string contentType = 3;
|
optional bytes key = 5;
|
||||||
optional bytes key = 4;
|
optional string contentType = 6;
|
||||||
optional uint32 size = 5;
|
optional uint32 size = 7;
|
||||||
optional bytes digest = 6;
|
optional bytes digest = 8;
|
||||||
optional bytes incrementalMac = 7;
|
optional bytes incrementalMac = 9;
|
||||||
optional bytes incrementalMacChunkSize = 8;
|
optional bytes incrementalMacChunkSize = 10;
|
||||||
optional string fileName = 9;
|
optional string fileName = 11;
|
||||||
optional uint32 flags = 10;
|
optional uint32 flags = 12;
|
||||||
optional uint32 width = 11;
|
optional uint32 width = 13;
|
||||||
optional uint32 height = 12;
|
optional uint32 height = 14;
|
||||||
optional string caption = 13;
|
optional string caption = 15;
|
||||||
optional string blurHash = 14;
|
optional string blurHash = 16;
|
||||||
optional uint64 uploadTimestamp = 15;
|
|
||||||
optional uint32 cdnNumber = 16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Quote {
|
message Quote {
|
||||||
@@ -414,16 +437,15 @@ message Quote {
|
|||||||
message QuotedAttachment {
|
message QuotedAttachment {
|
||||||
optional string contentType = 1;
|
optional string contentType = 1;
|
||||||
optional string fileName = 2;
|
optional string fileName = 2;
|
||||||
optional AttachmentPointer thumbnail = 3;
|
optional FilePointer thumbnail = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64 targetSentTimestamp = 1;
|
optional uint64 targetSentTimestamp = 1; // null if the target message could not be found at time of quote insert
|
||||||
uint64 authorId = 2;
|
uint64 authorId = 2;
|
||||||
optional string text = 3;
|
optional string text = 3;
|
||||||
repeated QuotedAttachment attachments = 4;
|
repeated QuotedAttachment attachments = 4;
|
||||||
repeated BodyRange bodyRanges = 5;
|
repeated BodyRange bodyRanges = 5;
|
||||||
Type type = 6;
|
Type type = 6;
|
||||||
bool originalMessageMissing = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message BodyRange {
|
message BodyRange {
|
||||||
@@ -440,7 +462,7 @@ message BodyRange {
|
|||||||
optional uint32 length = 2;
|
optional uint32 length = 2;
|
||||||
|
|
||||||
oneof associatedValue {
|
oneof associatedValue {
|
||||||
string mentionAci = 3;
|
bytes mentionAci = 3;
|
||||||
Style style = 4;
|
Style style = 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,82 +471,85 @@ message Reaction {
|
|||||||
string emoji = 1;
|
string emoji = 1;
|
||||||
uint64 authorId = 2;
|
uint64 authorId = 2;
|
||||||
uint64 sentTimestamp = 3;
|
uint64 sentTimestamp = 3;
|
||||||
uint64 receivedTimestamp = 4;
|
optional uint64 receivedTimestamp = 4;
|
||||||
|
uint64 sortOrder = 5; // A higher sort order means that a reaction is more recent
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateMessage {
|
message ChatUpdateMessage {
|
||||||
oneof update {
|
oneof update {
|
||||||
SimpleUpdate simpleUpdate = 1;
|
SimpleChatUpdate simpleUpdate = 1;
|
||||||
GroupDescriptionUpdate groupDescription = 2;
|
GroupDescriptionChatUpdate groupDescription = 2;
|
||||||
ExpirationTimerChange expirationTimerChange = 3;
|
ExpirationTimerChatUpdate expirationTimerChange = 3;
|
||||||
ProfileChange profileChange = 4;
|
ProfileChangeChatUpdate profileChange = 4;
|
||||||
ThreadMergeEvent threadMerge = 5;
|
ThreadMergeChatUpdate threadMerge = 5;
|
||||||
SessionSwitchoverEvent sessionSwitchover = 6;
|
SessionSwitchoverChatUpdate sessionSwitchover = 6;
|
||||||
CallingMessage callingMessage = 7;
|
CallChatUpdate callingMessage = 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message CallingMessage {
|
message CallChatUpdate{
|
||||||
oneof call {
|
oneof call {
|
||||||
uint64 callId = 1; // maps to id of Call from call log
|
uint64 callId = 1; // maps to id of Call from call log
|
||||||
CallMessage callMessage = 2;
|
IndividualCallChatUpdate callMessage = 2;
|
||||||
GroupCallMessage groupCall = 3;
|
GroupCallChatUpdate groupCall = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message CallMessage {
|
message IndividualCallChatUpdate {
|
||||||
enum Type {
|
enum Type {
|
||||||
INCOMING_AUDIO_CALL = 0;
|
UNKNOWN = 0;
|
||||||
INCOMING_VIDEO_CALL = 1;
|
INCOMING_AUDIO_CALL = 1;
|
||||||
OUTGOING_AUDIO_CALL = 2;
|
INCOMING_VIDEO_CALL = 2;
|
||||||
OUTGOING_VIDEO_CALL = 3;
|
OUTGOING_AUDIO_CALL = 3;
|
||||||
MISSED_AUDIO_CALL = 4;
|
OUTGOING_VIDEO_CALL = 4;
|
||||||
MISSED_VIDEO_CALL = 5;
|
MISSED_AUDIO_CALL = 5;
|
||||||
|
MISSED_VIDEO_CALL = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupCallMessage {
|
message GroupCallChatUpdate {
|
||||||
bytes startedCallUuid = 1;
|
bytes startedCallAci = 1;
|
||||||
uint64 startedCallTimestamp = 2;
|
uint64 startedCallTimestamp = 2;
|
||||||
repeated bytes inCallUuids = 3;
|
repeated bytes inCallAcis = 3;
|
||||||
bool isCallFull = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SimpleUpdate {
|
message SimpleChatUpdate {
|
||||||
enum Type {
|
enum Type {
|
||||||
JOINED_SIGNAL = 0;
|
UNKNOWN = 0;
|
||||||
IDENTITY_UPDATE = 1;
|
JOINED_SIGNAL = 1;
|
||||||
IDENTITY_VERIFIED = 2;
|
IDENTITY_UPDATE = 2;
|
||||||
IDENTITY_DEFAULT = 3; // marking as unverified
|
IDENTITY_VERIFIED = 3;
|
||||||
CHANGE_NUMBER = 4;
|
IDENTITY_DEFAULT = 4; // marking as unverified
|
||||||
BOOST_REQUEST = 5;
|
CHANGE_NUMBER = 5;
|
||||||
END_SESSION = 6;
|
BOOST_REQUEST = 6;
|
||||||
CHAT_SESSION_REFRESH = 7;
|
END_SESSION = 7;
|
||||||
BAD_DECRYPT = 8;
|
CHAT_SESSION_REFRESH = 8;
|
||||||
PAYMENTS_ACTIVATED = 9;
|
BAD_DECRYPT = 9;
|
||||||
PAYMENT_ACTIVATION_REQUEST = 10;
|
PAYMENTS_ACTIVATED = 10;
|
||||||
|
PAYMENT_ACTIVATION_REQUEST = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
Type type = 1;
|
Type type = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupDescriptionUpdate {
|
message GroupDescriptionChatUpdate {
|
||||||
string body = 1;
|
string newDescription = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExpirationTimerChange {
|
message ExpirationTimerChatUpdate {
|
||||||
uint32 expiresIn = 1;
|
uint32 expiresInMs = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ProfileChange {
|
message ProfileChangeChatUpdate {
|
||||||
string previousName = 1;
|
string previousName = 1;
|
||||||
string newName = 2;
|
string newName = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ThreadMergeEvent {
|
message ThreadMergeChatUpdate {
|
||||||
uint64 previousE164 = 1;
|
uint64 previousE164 = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SessionSwitchoverEvent {
|
message SessionSwitchoverChatUpdate {
|
||||||
uint64 e164 = 1;
|
uint64 e164 = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,6 +562,6 @@ message StickerPack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message StickerPackSticker {
|
message StickerPackSticker {
|
||||||
AttachmentPointer data = 1;
|
FilePointer data = 1;
|
||||||
string emoji = 2;
|
string emoji = 2;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.stream
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class EncryptedBackupReaderWriterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can read back all of the frames we write`() {
|
||||||
|
val key = BackupKey(Util.getSecretBytes(32))
|
||||||
|
val aci = ACI.from(UUID.randomUUID())
|
||||||
|
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
val frameCount = 10_000
|
||||||
|
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||||
|
for (i in 0 until frameCount) {
|
||||||
|
writer.write(Frame(account = AccountData(username = "username-$i")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ciphertext: ByteArray = outputStream.toByteArray()
|
||||||
|
|
||||||
|
val frames: List<Frame> = EncryptedBackupReader(key, aci, ciphertext.size.toLong()) { ciphertext.inputStream() }.use { reader ->
|
||||||
|
reader.asSequence().toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(frameCount, frames.size)
|
||||||
|
|
||||||
|
for (i in 0 until frameCount) {
|
||||||
|
assertEquals("username-$i", frames[i].account?.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,5 @@ java {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(testLibs.junit.junit)
|
testImplementation(testLibs.junit.junit)
|
||||||
|
testImplementation(testLibs.assertj.core)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 32-bit variable-length integer from the stream.
|
||||||
|
*
|
||||||
|
* The format uses one byte for each 7 bits of the integer, with the most significant bit (MSB) of each byte indicating whether more bytes need to be read.
|
||||||
|
* If the MSB is 0, it indicates the final byte. The actual integer value is constructed from the remaining 7 bits of each byte.
|
||||||
|
*/
|
||||||
|
fun InputStream.readVarInt32(): Int {
|
||||||
|
var result = 0
|
||||||
|
|
||||||
|
// We read 7 bits of the integer at a time, up to the full size of an integer (32 bits).
|
||||||
|
for (shift in 0 until 32 step 7) {
|
||||||
|
// Despite returning an int, the range of the returned value is 0..255, so it's just a byte.
|
||||||
|
// I believe it's an int just so it can return -1 when the stream ends.
|
||||||
|
val byte: Int = read()
|
||||||
|
if (byte < 0) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
val lowestSevenBits = byte and 0x7F
|
||||||
|
val shiftedBits = lowestSevenBits shl shift
|
||||||
|
|
||||||
|
result = result or shiftedBits
|
||||||
|
|
||||||
|
// If the MSB is 0, that means the varint is finished, and we have our full result
|
||||||
|
if (byte and 0x80 == 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IOException("Malformed varint!")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the entire stream into a [ByteArray].
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun InputStream.readFully(autoClose: Boolean = true): ByteArray {
|
||||||
|
return StreamUtil.readFully(this, Integer.MAX_VALUE, autoClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills reads data from the stream into the [buffer] until it is full.
|
||||||
|
* Throws an [IOException] if the stream doesn't have enough data to fill the buffer.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun InputStream.readFully(buffer: ByteArray) {
|
||||||
|
return StreamUtil.readFully(this, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the specified number of bytes from the stream and returns it as a [ByteArray].
|
||||||
|
* Throws an [IOException] if the stream doesn't have that many bytes.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun InputStream.readNBytesOrThrow(length: Int): ByteArray {
|
||||||
|
val buffer = ByteArray(length)
|
||||||
|
this.readFully(buffer)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun InputStream.readLength(): Long? {
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
var count = 0L
|
||||||
|
|
||||||
|
while (this.read(buffer).also { if (it > 0) count += it } != -1) {
|
||||||
|
// do nothing, all work is in the while condition
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util
|
||||||
|
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a 32-bit variable-length integer to the stream.
|
||||||
|
*
|
||||||
|
* The format uses one byte for each 7 bits of the integer, with the most significant bit (MSB) of each byte indicating whether more bytes need to be read.
|
||||||
|
*/
|
||||||
|
fun OutputStream.writeVarInt32(value: Int) {
|
||||||
|
var remaining = value
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// We write 7 bits of the integer at a time
|
||||||
|
val lowestSevenBits = remaining and 0x7F
|
||||||
|
remaining = remaining ushr 7
|
||||||
|
|
||||||
|
if (remaining == 0) {
|
||||||
|
// If there are no more bits to write, we're done
|
||||||
|
write(lowestSevenBits)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Otherwise, we need to write the next 7 bits, and set the MSB to 1 to indicate that there are more bits to come
|
||||||
|
write(lowestSevenBits or 0x80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.signal.core.util;
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
package org.signal.core.util;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
|
||||||
@@ -20,7 +23,7 @@ public final class StreamUtil {
|
|||||||
|
|
||||||
private StreamUtil() {}
|
private StreamUtil() {}
|
||||||
|
|
||||||
public static void close(@Nullable Closeable closeable) {
|
public static void close(Closeable closeable) {
|
||||||
if (closeable == null) return;
|
if (closeable == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -64,6 +67,10 @@ public final class StreamUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] readFully(InputStream in, int maxBytes) throws IOException {
|
public static byte[] readFully(InputStream in, int maxBytes) throws IOException {
|
||||||
|
return readFully(in, maxBytes, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] readFully(InputStream in, int maxBytes, boolean closeWhenDone) throws IOException {
|
||||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int totalRead = 0;
|
int totalRead = 0;
|
||||||
@@ -77,7 +84,9 @@ public final class StreamUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (closeWhenDone) {
|
||||||
in.close();
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
return bout.toByteArray();
|
return bout.toByteArray();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import java.io.FilterInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a [Mac] as data is read from the target [InputStream].
|
||||||
|
* To get the final MAC, read the [mac] property after the stream has been fully read.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```kotlin
|
||||||
|
* val stream = MacInputStream(myStream, myMac)
|
||||||
|
* stream.readFully()
|
||||||
|
* val mac = stream.mac.doFinal()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class MacInputStream(val wrapped: InputStream, val mac: Mac) : FilterInputStream(wrapped) {
|
||||||
|
override fun read(): Int {
|
||||||
|
return wrapped.read().also { byte ->
|
||||||
|
if (byte >= 0) {
|
||||||
|
mac.update(byte.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(destination: ByteArray): Int {
|
||||||
|
return read(destination, 0, destination.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(destination: ByteArray, offset: Int, length: Int): Int {
|
||||||
|
return wrapped.read(destination, offset, length).also { bytesRead ->
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
mac.update(destination, offset, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import java.io.FilterOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a [Mac] as data is written to the target [OutputStream].
|
||||||
|
* To get the final MAC, read the [mac] property after the stream has been fully written.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```kotlin
|
||||||
|
* val stream = MacOutputStream(myStream, myMac)
|
||||||
|
* // write data to stream
|
||||||
|
* val mac = stream.mac.doFinal()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class MacOutputStream(val wrapped: OutputStream, val mac: Mac) : FilterOutputStream(wrapped) {
|
||||||
|
override fun write(byte: Int) {
|
||||||
|
wrapped.write(byte)
|
||||||
|
mac.update(byte.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(data: ByteArray) {
|
||||||
|
write(data, 0, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(data: ByteArray, offset: Int, length: Int) {
|
||||||
|
wrapped.write(data, offset, length)
|
||||||
|
mac.update(data, offset, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import java.io.FilterInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.lang.UnsupportedOperationException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [InputStream] that will read from the target [InputStream] until it reaches the end, or until it has read [maxBytes] bytes.
|
||||||
|
*/
|
||||||
|
class TruncatingInputStream(private val wrapped: InputStream, private val maxBytes: Long) : FilterInputStream(wrapped) {
|
||||||
|
|
||||||
|
private var bytesRead: Long = 0
|
||||||
|
|
||||||
|
override fun read(): Int {
|
||||||
|
if (bytesRead >= maxBytes) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapped.read().also {
|
||||||
|
if (it >= 0) {
|
||||||
|
bytesRead++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(destination: ByteArray): Int {
|
||||||
|
return read(destination, 0, destination.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(destination: ByteArray, offset: Int, length: Int): Int {
|
||||||
|
if (bytesRead >= maxBytes) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
val bytesRemaining: Long = maxBytes - bytesRead
|
||||||
|
val bytesToRead: Int = if (bytesRemaining > length) length else Math.toIntExact(bytesRemaining)
|
||||||
|
val bytesRead = wrapped.read(destination, offset, bytesToRead)
|
||||||
|
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
this.bytesRead += bytesRead
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesRead
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun skip(n: Long): Long {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class VarInt32Tests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests a random sampling of integers. The faster and more practical version of [testAll].
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testRandomSampling() {
|
||||||
|
val randomInts = (0..100_000).map { Random.nextInt() }
|
||||||
|
|
||||||
|
val bytes = ByteArrayOutputStream().use { outputStream ->
|
||||||
|
for (value in randomInts) {
|
||||||
|
outputStream.writeVarInt32(value)
|
||||||
|
}
|
||||||
|
outputStream
|
||||||
|
}.toByteArray()
|
||||||
|
|
||||||
|
bytes.inputStream().use { inputStream ->
|
||||||
|
for (value in randomInts) {
|
||||||
|
val read = inputStream.readVarInt32()
|
||||||
|
assertEquals(value, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exhaustively checks reading and writing a varint for all possible integers.
|
||||||
|
* We can't keep everything in memory, so instead we use sequences to grab a million at a time,
|
||||||
|
* then run smaller chunks of those in parallel.
|
||||||
|
*/
|
||||||
|
@Ignore("This test is very slow (over a minute). It was run once to verify correctness, but the random sampling test should be sufficient for catching regressions.")
|
||||||
|
@Test
|
||||||
|
fun testAll() {
|
||||||
|
val counter = AtomicInteger(0)
|
||||||
|
|
||||||
|
(Int.MIN_VALUE..Int.MAX_VALUE)
|
||||||
|
.asSequence()
|
||||||
|
.chunked(1_000_000)
|
||||||
|
.forEach { bigChunk ->
|
||||||
|
bigChunk
|
||||||
|
.chunked(100_000)
|
||||||
|
.parallelStream()
|
||||||
|
.forEach { smallChunk ->
|
||||||
|
println("Chunk ${counter.addAndGet(1)}")
|
||||||
|
|
||||||
|
val bytes = ByteArrayOutputStream().use { outputStream ->
|
||||||
|
for (value in smallChunk) {
|
||||||
|
outputStream.writeVarInt32(value)
|
||||||
|
}
|
||||||
|
outputStream
|
||||||
|
}.toByteArray()
|
||||||
|
|
||||||
|
bytes.inputStream().use { inputStream ->
|
||||||
|
for (value in smallChunk) {
|
||||||
|
val read = inputStream.readVarInt32()
|
||||||
|
assertEquals(value, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.readFully
|
||||||
|
import java.io.InputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class MacInputStreamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stream mac matches normal mac when reading via buffer`() {
|
||||||
|
testMacEquality { inputStream ->
|
||||||
|
inputStream.readFully()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stream mac matches normal mac when reading one byte at a time`() {
|
||||||
|
testMacEquality { inputStream ->
|
||||||
|
var lastRead = inputStream.read()
|
||||||
|
while (lastRead != -1) {
|
||||||
|
lastRead = inputStream.read()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testMacEquality(read: (InputStream) -> Unit) {
|
||||||
|
val data = Random.nextBytes(1_000)
|
||||||
|
val key = Random.nextBytes(32)
|
||||||
|
|
||||||
|
val mac1 = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac2 = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedMac = mac1.doFinal(data)
|
||||||
|
|
||||||
|
val actualMac = MacInputStream(data.inputStream(), mac2).use { stream ->
|
||||||
|
read(stream)
|
||||||
|
stream.mac.doFinal()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertArrayEquals(expectedMac, actualMac)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.StreamUtil
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class MacOutputStreamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stream mac matches normal mac when writing via buffer`() {
|
||||||
|
testMacEquality { data, outputStream ->
|
||||||
|
StreamUtil.copy(data.inputStream(), outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stream mac matches normal mac when writing one byte at a time`() {
|
||||||
|
testMacEquality { data, outputStream ->
|
||||||
|
for (byte in data) {
|
||||||
|
outputStream.write(byte.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testMacEquality(write: (ByteArray, OutputStream) -> Unit) {
|
||||||
|
val data = Random.nextBytes(1_000)
|
||||||
|
val key = Random.nextBytes(32)
|
||||||
|
|
||||||
|
val mac1 = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac2 = Mac.getInstance("HmacSHA256").apply {
|
||||||
|
init(SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedMac = mac1.doFinal(data)
|
||||||
|
|
||||||
|
val actualMac = MacOutputStream(ByteArrayOutputStream(), mac2).use { stream ->
|
||||||
|
write(data, stream)
|
||||||
|
stream.mac.doFinal()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertArrayEquals(expectedMac, actualMac)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.stream
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.readFully
|
||||||
|
|
||||||
|
class TruncatingInputStreamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when I fully read the stream via a buffer, I should only get maxBytes`() {
|
||||||
|
val inputStream = TruncatingInputStream(ByteArray(100).inputStream(), maxBytes = 75)
|
||||||
|
val data = inputStream.readFully()
|
||||||
|
|
||||||
|
assertEquals(75, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when I fully read the stream one byte at a time, I should only get maxBytes`() {
|
||||||
|
val inputStream = TruncatingInputStream(ByteArray(100).inputStream(), maxBytes = 75)
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
var lastRead = inputStream.read()
|
||||||
|
while (lastRead != -1) {
|
||||||
|
count++
|
||||||
|
lastRead = inputStream.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(75, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import okio.IOException
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun ContentResolver.getLength(uri: Uri): Long? {
|
||||||
|
return this.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
cursor.requireLongOrNull(OpenableColumns.SIZE)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: openInputStream(uri)?.use { it.readLength() }
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.signal.core.util
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the entire stream into a [ByteArray].
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun InputStream.readFully(): ByteArray {
|
|
||||||
return StreamUtil.readFully(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills reads data from the stream into the [buffer] until it is full.
|
|
||||||
* Throws an [IOException] if the stream doesn't have enough data to fill the buffer.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun InputStream.readFully(buffer: ByteArray) {
|
|
||||||
return StreamUtil.readFully(this, buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the specified number of bytes from the stream and returns it as a [ByteArray].
|
|
||||||
* Throws an [IOException] if the stream doesn't have that many bytes.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun InputStream.readNBytesOrThrow(length: Int): ByteArray {
|
|
||||||
val buffer: ByteArray = ByteArray(length)
|
|
||||||
this.readFully(buffer)
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class InputStreamExtensionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when I call readLength, it returns the correct length`() {
|
||||||
|
for (i in 1..10) {
|
||||||
|
val bytes = ByteArray(Random.nextInt(from = 512, until = 8092))
|
||||||
|
val length = bytes.inputStream().readLength()
|
||||||
|
assertEquals(bytes.size.toLong(), length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.backup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe typing around a backupId, which is a 16-byte array.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class BackupId(val value: ByteArray) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(value.size == 16) { "BackupId must be 16 bytes!" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.backup
|
||||||
|
|
||||||
|
import org.signal.libsignal.protocol.kdf.HKDF
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe typing around a backup key, which is a 32-byte array.
|
||||||
|
*/
|
||||||
|
class BackupKey(val value: ByteArray) {
|
||||||
|
init {
|
||||||
|
require(value.size == 32) { "Backup key must be 32 bytes!" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deriveSecrets(aci: ACI): KeyMaterial {
|
||||||
|
val backupId = BackupId(
|
||||||
|
HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupId".toByteArray(), 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
|
||||||
|
|
||||||
|
return KeyMaterial(
|
||||||
|
backupId = backupId,
|
||||||
|
macKey = extendedKey.copyOfRange(0, 32),
|
||||||
|
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||||
|
iv = extendedKey.copyOfRange(64, 80)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyMaterial(
|
||||||
|
val backupId: BackupId,
|
||||||
|
val macKey: ByteArray,
|
||||||
|
val cipherKey: ByteArray,
|
||||||
|
val iv: ByteArray
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.whispersystems.signalservice.api.kbs;
|
package org.whispersystems.signalservice.api.kbs;
|
||||||
|
|
||||||
|
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||||
|
import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
@@ -44,6 +46,10 @@ public final class MasterKey {
|
|||||||
return derive("Logging Key");
|
return derive("Logging Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BackupKey deriveBackupKey() {
|
||||||
|
return new BackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32));
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] derive(String keyName) {
|
private byte[] derive(String keyName) {
|
||||||
return hmacSha256(masterKey, StringUtil.utf8(keyName));
|
return hmacSha256(masterKey, StringUtil.utf8(keyName));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user