mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add initial SVRB support.
This commit is contained in:
committed by
Cody Henthorne
parent
f6ab408fc8
commit
5aeca1deb1
@@ -9,6 +9,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.github.difflib.DiffUtils
|
||||
import com.github.difflib.UnifiedDiffUtils
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -23,6 +26,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.InternalValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -44,6 +48,10 @@ class ArchiveImportExportTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
AppDependencies.jobManager.shutdown()
|
||||
mockkObject(SignalStore)
|
||||
every { SignalStore.internal } returns mockk<InternalValues>(relaxed = true) {
|
||||
every { includeDebuglogInBackup } returns false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -285,7 +293,7 @@ class ArchiveImportExportTests {
|
||||
assertTrue(importResult is ImportResult.Success)
|
||||
val success = importResult as ImportResult.Success
|
||||
|
||||
val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime)
|
||||
val generatedBackupData = BackupRepository.exportInMemoryForTests(plaintext = true, currentTime = success.backupTime)
|
||||
checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it }
|
||||
|
||||
return TestResult.Success(filename)
|
||||
@@ -307,11 +315,10 @@ class ArchiveImportExportTests {
|
||||
}
|
||||
|
||||
private fun import(importData: ByteArray): ImportResult {
|
||||
return BackupRepository.import(
|
||||
return BackupRepository.importPlaintextTest(
|
||||
length = importData.size.toLong(),
|
||||
inputStreamFactory = { ByteArrayInputStream(importData) },
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
|
||||
backupKey = null
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -328,13 +335,14 @@ class ArchiveImportExportTests {
|
||||
return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}")
|
||||
}
|
||||
|
||||
if (importComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages.contentToString()}")
|
||||
}
|
||||
|
||||
if (exportComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages.contentToString()}")
|
||||
}
|
||||
// Do we actually need this?
|
||||
// if (importComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
// return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages.contentToString()}")
|
||||
// }
|
||||
//
|
||||
// if (exportComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
// return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages.contentToString()}")
|
||||
// }
|
||||
|
||||
val canonicalImport = importComparable.comparableString
|
||||
val canonicalExport = exportComparable.comparableString
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.ValidationError
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -22,17 +23,25 @@ import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBa
|
||||
|
||||
object ArchiveValidator {
|
||||
|
||||
fun validateSignalBackup(backupFile: File, backupKey: MessageBackupKey, backupForwardSecrecyToken: BackupForwardSecrecyToken): ValidationResult {
|
||||
return validate(backupFile, backupKey, backupForwardSecrecyToken, forTransfer = false)
|
||||
}
|
||||
|
||||
fun validateLocalOrLinking(backupFile: File, backupKey: MessageBackupKey, forTransfer: Boolean): ValidationResult {
|
||||
return validate(backupFile, backupKey, forwardSecrecyToken = null, forTransfer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided [backupFile] that is encrypted with the provided [backupKey].
|
||||
*/
|
||||
fun validate(backupFile: File, backupKey: MessageBackupKey, forTransfer: Boolean): ValidationResult {
|
||||
fun validate(backupFile: File, backupKey: MessageBackupKey, forwardSecrecyToken: BackupForwardSecrecyToken?, forTransfer: Boolean): ValidationResult {
|
||||
return try {
|
||||
val backupId = backupKey.deriveBackupId(SignalStore.account.requireAci())
|
||||
val libSignalBackupKey = LibSignalBackupKey(backupKey.value)
|
||||
val backupKey = LibSignalMessageBackupKey(libSignalBackupKey, backupId.value)
|
||||
val libSignalMessageBackupKey = LibSignalMessageBackupKey(libSignalBackupKey, backupId.value, forwardSecrecyToken)
|
||||
|
||||
MessageBackup.validate(
|
||||
backupKey,
|
||||
libSignalMessageBackupKey,
|
||||
if (forTransfer) MessageBackup.Purpose.DEVICE_TRANSFER else MessageBackup.Purpose.REMOTE_BACKUP,
|
||||
{ backupFile.inputStream() },
|
||||
backupFile.length()
|
||||
|
||||
@@ -39,12 +39,14 @@ import org.signal.core.util.getAllTableDefinitions
|
||||
import org.signal.core.util.getAllTriggerDefinitions
|
||||
import org.signal.core.util.getForeignKeyViolations
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.requireIntOrNull
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.stream.NonClosingOutputStream
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
@@ -144,8 +146,10 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
@@ -722,20 +726,27 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun localExport(
|
||||
fun exportForLocalBackup(
|
||||
main: OutputStream,
|
||||
localBackupProgressEmitter: ExportProgressListener,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
archiveAttachment: (AttachmentTable.LocalArchivableAttachment, () -> InputStream?) -> Unit
|
||||
) {
|
||||
val writer = EncryptedBackupWriter(
|
||||
val writer = EncryptedBackupWriter.createForLocalOrLinking(
|
||||
key = SignalStore.backup.messageBackupKey,
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = NonClosingOutputStream(main),
|
||||
append = { main.write(it) }
|
||||
)
|
||||
|
||||
export(currentTime = System.currentTimeMillis(), isLocal = true, writer = writer, progressEmitter = localBackupProgressEmitter, cancellationSignal = cancellationSignal, mediaBackupEnabled = false) { dbSnapshot ->
|
||||
export(
|
||||
currentTime = System.currentTimeMillis(),
|
||||
isLocal = true,
|
||||
writer = writer,
|
||||
progressEmitter = localBackupProgressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
forTransfer = false
|
||||
) { dbSnapshot ->
|
||||
val localArchivableAttachments = dbSnapshot
|
||||
.attachmentTable
|
||||
.getLocalArchivableAttachments()
|
||||
@@ -758,24 +769,86 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a backup that will be uploaded to the archive CDN.
|
||||
*/
|
||||
fun exportForSignalBackup(
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit,
|
||||
messageBackupKey: MessageBackupKey,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken,
|
||||
forwardSecrecyMetadata: ByteArray,
|
||||
currentTime: Long,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)?
|
||||
) {
|
||||
val writer = EncryptedBackupWriter.createForSignalBackup(
|
||||
key = messageBackupKey,
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
forwardSecrecyMetadata = forwardSecrecyMetadata,
|
||||
append = append
|
||||
)
|
||||
|
||||
return export(
|
||||
currentTime = currentTime,
|
||||
isLocal = false,
|
||||
writer = writer,
|
||||
forTransfer = false,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
extraExportOperations = extraExportOperations
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a backup that will be uploaded to the archive CDN.
|
||||
*/
|
||||
fun exportForLinkAndSync(
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit,
|
||||
messageBackupKey: MessageBackupKey,
|
||||
currentTime: Long,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false }
|
||||
) {
|
||||
val writer = EncryptedBackupWriter.createForLocalOrLinking(
|
||||
key = messageBackupKey,
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
append = append
|
||||
)
|
||||
|
||||
return export(
|
||||
currentTime = currentTime,
|
||||
isLocal = false,
|
||||
writer = writer,
|
||||
forTransfer = true,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
extraExportOperations = null
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@JvmOverloads
|
||||
fun export(
|
||||
fun exportForDebugging(
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit,
|
||||
messageBackupKey: MessageBackupKey = SignalStore.backup.messageBackupKey,
|
||||
plaintext: Boolean = false,
|
||||
currentTime: Long = System.currentTimeMillis(),
|
||||
skipMediaBackup: Boolean = false,
|
||||
forTransfer: Boolean = false,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
exportExtras: ((SignalDatabase) -> Unit)? = null
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)? = null
|
||||
) {
|
||||
val writer: BackupExportWriter = if (plaintext) {
|
||||
PlainTextBackupWriter(outputStream)
|
||||
} else {
|
||||
EncryptedBackupWriter(
|
||||
EncryptedBackupWriter.createForLocalOrLinking(
|
||||
key = messageBackupKey,
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
@@ -783,21 +856,14 @@ object BackupRepository {
|
||||
)
|
||||
}
|
||||
|
||||
val mediaBackupEnabled = if (skipMediaBackup || !SignalStore.backup.areBackupsEnabled) {
|
||||
false
|
||||
} else {
|
||||
getBackupTier().successOrThrow() == MessageBackupTier.PAID
|
||||
}
|
||||
|
||||
export(
|
||||
currentTime = currentTime,
|
||||
isLocal = false,
|
||||
writer = writer,
|
||||
progressEmitter = progressEmitter,
|
||||
mediaBackupEnabled = mediaBackupEnabled,
|
||||
forTransfer = forTransfer,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
exportExtras = exportExtras
|
||||
extraExportOperations = extraExportOperations
|
||||
)
|
||||
}
|
||||
|
||||
@@ -805,9 +871,9 @@ object BackupRepository {
|
||||
* Exports to a blob in memory. Should only be used for testing.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun debugExport(plaintext: Boolean = false, currentTime: Long = System.currentTimeMillis()): ByteArray {
|
||||
fun exportInMemoryForTests(plaintext: Boolean = false, currentTime: Long = System.currentTimeMillis()): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
export(outputStream = outputStream, append = { mac -> outputStream.write(mac) }, plaintext = plaintext, currentTime = currentTime, skipMediaBackup = true)
|
||||
exportForDebugging(outputStream = outputStream, append = { mac -> outputStream.write(mac) }, plaintext = plaintext, currentTime = currentTime)
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
@@ -816,11 +882,10 @@ object BackupRepository {
|
||||
currentTime: Long,
|
||||
isLocal: Boolean,
|
||||
writer: BackupExportWriter,
|
||||
mediaBackupEnabled: Boolean,
|
||||
forTransfer: Boolean = false,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
exportExtras: ((SignalDatabase) -> Unit)? = null
|
||||
forTransfer: Boolean,
|
||||
progressEmitter: ExportProgressListener?,
|
||||
cancellationSignal: () -> Boolean,
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)?
|
||||
) {
|
||||
val eventTimer = EventTimer()
|
||||
val mainDbName = if (isLocal) LOCAL_MAIN_DB_SNAPSHOT_NAME else REMOTE_MAIN_DB_SNAPSHOT_NAME
|
||||
@@ -833,7 +898,7 @@ object BackupRepository {
|
||||
val signalStoreSnapshot: SignalStore = createSignalStoreSnapshot(keyValueDbName)
|
||||
eventTimer.emit("store-db-snapshot")
|
||||
|
||||
val exportState = ExportState(backupTime = currentTime, mediaBackupEnabled = mediaBackupEnabled, forTransfer = forTransfer)
|
||||
val exportState = ExportState(backupTime = currentTime, forTransfer = forTransfer)
|
||||
val selfAci = signalStoreSnapshot.accountValues.aci!!
|
||||
val selfRecipientId = dbSnapshot.recipientTable.getByAci(selfAci).get().toLong().let { RecipientId.from(it) }
|
||||
|
||||
@@ -953,7 +1018,7 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
exportExtras?.invoke(dbSnapshot)
|
||||
extraExportOperations?.invoke(dbSnapshot)
|
||||
|
||||
Log.d(TAG, "[export] totalFrames: $frameCount | ${eventTimer.stop().summary}")
|
||||
} finally {
|
||||
@@ -962,11 +1027,14 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun localImport(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData): ImportResult {
|
||||
/**
|
||||
* Imports a local backup file that was exported to disk.
|
||||
*/
|
||||
fun importLocal(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData): ImportResult {
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
|
||||
val frameReader = try {
|
||||
EncryptedBackupReader(
|
||||
EncryptedBackupReader.createForLocalOrLinking(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
length = mainStreamLength,
|
||||
@@ -983,9 +1051,39 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a backup stored on the archive CDN.
|
||||
*
|
||||
* @param backupKey The key used to encrypt the backup. If `null`, we assume that the file is plaintext.
|
||||
*/
|
||||
fun import(
|
||||
fun importSignalBackup(
|
||||
length: Long,
|
||||
inputStreamFactory: () -> InputStream,
|
||||
selfData: SelfData,
|
||||
backupKey: MessageBackupKey?,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken,
|
||||
cancellationSignal: () -> Boolean = { false }
|
||||
): ImportResult {
|
||||
val frameReader = if (backupKey == null) {
|
||||
PlainTextBackupReader(inputStreamFactory(), length)
|
||||
} else {
|
||||
EncryptedBackupReader.createForSignalBackup(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
length = length,
|
||||
dataStream = inputStreamFactory
|
||||
)
|
||||
}
|
||||
|
||||
return frameReader.use { reader ->
|
||||
import(reader, selfData, cancellationSignal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a backup that was exported via [exportForDebugging].
|
||||
*/
|
||||
fun importForDebugging(
|
||||
length: Long,
|
||||
inputStreamFactory: () -> InputStream,
|
||||
selfData: SelfData,
|
||||
@@ -995,7 +1093,7 @@ object BackupRepository {
|
||||
val frameReader = if (backupKey == null) {
|
||||
PlainTextBackupReader(inputStreamFactory(), length)
|
||||
} else {
|
||||
EncryptedBackupReader(
|
||||
EncryptedBackupReader.createForLocalOrLinking(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
length = length,
|
||||
@@ -1008,6 +1106,22 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a plaintext backup only used for testing.
|
||||
*/
|
||||
fun importPlaintextTest(
|
||||
length: Long,
|
||||
inputStreamFactory: () -> InputStream,
|
||||
selfData: SelfData,
|
||||
cancellationSignal: () -> Boolean = { false }
|
||||
): ImportResult {
|
||||
val frameReader = PlainTextBackupReader(inputStreamFactory(), length)
|
||||
|
||||
return frameReader.use { reader ->
|
||||
import(reader, selfData, cancellationSignal)
|
||||
}
|
||||
}
|
||||
|
||||
private fun import(
|
||||
frameReader: BackupImportReader,
|
||||
selfData: SelfData,
|
||||
@@ -1732,6 +1846,14 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See [org.whispersystems.signalservice.api.archive.ArchiveApi.getSvrBAuthorization].
|
||||
*/
|
||||
fun getSvrBAuth(): NetworkResult<AuthCredentials> {
|
||||
return initBackupAndFetchAuth()
|
||||
.then { SignalNetwork.archive.getSvrBAuthorization(SignalStore.account.requireAci(), it.messageBackupAccess) }
|
||||
}
|
||||
|
||||
/**
|
||||
* During normal operation, ensures that the backupId has been reserved and that your public key has been set,
|
||||
* while also returning an archive access data. Should be the basis of all backup operations.
|
||||
@@ -1887,10 +2009,50 @@ object BackupRepository {
|
||||
indeterminate = true
|
||||
)
|
||||
|
||||
val forwardSecrecyMetadata = EncryptedBackupReader.readForwardSecrecyMetadata(tempBackupFile.inputStream())
|
||||
if (forwardSecrecyMetadata == null) {
|
||||
Log.w(TAG, "Failed to read forward secrecy metadata!")
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
val messageBackupKey = SignalStore.backup.messageBackupKey
|
||||
|
||||
Log.i(TAG, "[remoteRestore] Fetching SVRB data")
|
||||
val svrBAuth = when (val result = BackupRepository.getSvrBAuth()) {
|
||||
is NetworkResult.Success -> result.result
|
||||
is NetworkResult.NetworkError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Network error when getting SVRB auth.", result.getCause())
|
||||
is NetworkResult.StatusCodeError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Status code error when getting SVRB auth.", result.getCause())
|
||||
is NetworkResult.ApplicationError -> throw result.throwable
|
||||
}
|
||||
|
||||
val forwardSecrecyToken = when (val result = SignalNetwork.svrB.restore(svrBAuth, messageBackupKey, forwardSecrecyMetadata)) {
|
||||
is SvrBApi.RestoreResult.Success -> {
|
||||
SignalStore.backup.nextBackupSecretData = result.data.nextBackupSecretData
|
||||
result.data.forwardSecrecyToken
|
||||
}
|
||||
is SvrBApi.RestoreResult.NetworkError -> {
|
||||
return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Network error during SVRB.", result.exception)
|
||||
}
|
||||
SvrBApi.RestoreResult.DataMissingError,
|
||||
is SvrBApi.RestoreResult.RestoreFailedError,
|
||||
is SvrBApi.RestoreResult.SvrError,
|
||||
is SvrBApi.RestoreResult.UnknownError -> {
|
||||
Log.w(TAG, "[remoteRestore] Failed to fetch SVRB data: $result")
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
}
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
Log.i(TAG, "[remoteRestore] Importing backup")
|
||||
val result = import(length = tempBackupFile.length(), inputStreamFactory = tempBackupFile::inputStream, selfData = selfData, backupKey = SignalStore.backup.messageBackupKey, cancellationSignal = cancellationSignal)
|
||||
val result = importSignalBackup(
|
||||
length = tempBackupFile.length(),
|
||||
inputStreamFactory = tempBackupFile::inputStream,
|
||||
selfData = selfData,
|
||||
backupKey = SignalStore.backup.messageBackupKey,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
cancellationSignal = cancellationSignal
|
||||
)
|
||||
if (result == ImportResult.Failure) {
|
||||
Log.w(TAG, "[remoteRestore] Failed to import backup")
|
||||
return RemoteRestoreResult.Failure
|
||||
@@ -1962,7 +2124,6 @@ data class ArchivedMediaObject(val mediaId: String, val cdn: Int)
|
||||
|
||||
class ExportState(
|
||||
val backupTime: Long,
|
||||
val mediaBackupEnabled: Boolean,
|
||||
val forTransfer: Boolean
|
||||
) {
|
||||
val recipientIds: MutableSet<Long> = hashSetOf()
|
||||
|
||||
@@ -23,7 +23,7 @@ import kotlin.time.Duration.Companion.days
|
||||
|
||||
private val TAG = "MessageTableArchiveExtensions"
|
||||
|
||||
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean, selfRecipientId: RecipientId, exportState: ExportState): ChatItemArchiveExporter {
|
||||
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, selfRecipientId: RecipientId, exportState: ExportState): ChatItemArchiveExporter {
|
||||
// We create a covering index for the query to drastically speed up perf here.
|
||||
// Remember that we're working on a temporary snapshot of the database, so we can create an index and not worry about cleaning it up.
|
||||
val startTime = System.currentTimeMillis()
|
||||
@@ -88,11 +88,10 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi
|
||||
|
||||
return ChatItemArchiveExporter(
|
||||
db = db,
|
||||
backupStartTime = backupTime,
|
||||
batchSize = 10_000,
|
||||
mediaArchiveEnabled = mediaBackupEnabled,
|
||||
selfRecipientId = selfRecipientId,
|
||||
noteToSelfThreadId = db.threadTable.getThreadIdFor(selfRecipientId) ?: -1L,
|
||||
backupStartTime = backupTime,
|
||||
batchSize = 10_000,
|
||||
exportState = exportState,
|
||||
cursorGenerator = { lastSeenReceivedTime, count ->
|
||||
readableDatabase
|
||||
|
||||
@@ -127,7 +127,6 @@ class ChatItemArchiveExporter(
|
||||
private val noteToSelfThreadId: Long,
|
||||
private val backupStartTime: Long,
|
||||
private val batchSize: Int,
|
||||
private val mediaArchiveEnabled: Boolean,
|
||||
private val exportState: ExportState,
|
||||
private val cursorGenerator: (Long, Int) -> Cursor
|
||||
) : Iterator<ChatItem?>, Closeable {
|
||||
@@ -353,7 +352,7 @@ class ChatItemArchiveExporter(
|
||||
}
|
||||
|
||||
!record.sharedContacts.isNullOrEmpty() -> {
|
||||
builder.contactMessage = record.toRemoteContactMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id]) ?: continue
|
||||
builder.contactMessage = record.toRemoteContactMessage(reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id]) ?: continue
|
||||
transformTimer.emit("contact")
|
||||
}
|
||||
|
||||
@@ -367,7 +366,7 @@ class ChatItemArchiveExporter(
|
||||
Log.w(TAG, ExportSkips.directStoryReplyInNoteToSelf(record.dateSent))
|
||||
continue
|
||||
}
|
||||
builder.directStoryReplyMessage = record.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[record.id]) ?: continue
|
||||
builder.directStoryReplyMessage = record.toRemoteDirectStoryReplyMessage(reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[record.id]) ?: continue
|
||||
transformTimer.emit("story")
|
||||
}
|
||||
|
||||
@@ -376,11 +375,10 @@ class ChatItemArchiveExporter(
|
||||
val sticker = attachments?.firstOrNull { dbAttachment -> dbAttachment.isSticker }
|
||||
|
||||
if (sticker?.stickerLocator != null) {
|
||||
builder.stickerMessage = sticker.toRemoteStickerMessage(sentTimestamp = record.dateSent, mediaArchiveEnabled = mediaArchiveEnabled, reactions = extraData.reactionsById[id])
|
||||
builder.stickerMessage = sticker.toRemoteStickerMessage(sentTimestamp = record.dateSent, reactions = extraData.reactionsById[id])
|
||||
} else {
|
||||
val standardMessage = record.toRemoteStandardMessage(
|
||||
exportState = exportState,
|
||||
mediaArchiveEnabled = mediaArchiveEnabled,
|
||||
reactionRecords = extraData.reactionsById[id],
|
||||
mentions = extraData.mentionsById[id],
|
||||
attachments = extraData.attachmentsById[record.id]
|
||||
@@ -856,11 +854,11 @@ private fun BackupMessageRecord.toRemoteLinkPreviews(attachments: List<DatabaseA
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun LinkPreview.toRemoteLinkPreview(mediaArchiveEnabled: Boolean): org.thoughtcrime.securesms.backup.v2.proto.LinkPreview {
|
||||
private fun LinkPreview.toRemoteLinkPreview(): org.thoughtcrime.securesms.backup.v2.proto.LinkPreview {
|
||||
return org.thoughtcrime.securesms.backup.v2.proto.LinkPreview(
|
||||
url = url,
|
||||
title = title.nullIfEmpty(),
|
||||
image = (thumbnail.orNull() as? DatabaseAttachment)?.toRemoteMessageAttachment(mediaArchiveEnabled)?.pointer,
|
||||
image = (thumbnail.orNull() as? DatabaseAttachment)?.toRemoteMessageAttachment()?.pointer,
|
||||
description = description.nullIfEmpty(),
|
||||
date = date.clampToValidBackupRange()
|
||||
)
|
||||
@@ -871,7 +869,7 @@ private fun BackupMessageRecord.toRemoteViewOnceMessage(exportState: ExportState
|
||||
attachments
|
||||
?.firstOrNull()
|
||||
?.takeUnless { !it.hasData && it.size == 0L && it.remoteDigest == null && it.width == 0 && it.height == 0 && it.blurHash == null }
|
||||
?.toRemoteMessageAttachment(mediaArchiveEnabled = false)
|
||||
?.toRemoteMessageAttachment()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -882,13 +880,13 @@ private fun BackupMessageRecord.toRemoteViewOnceMessage(exportState: ExportState
|
||||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteContactMessage(mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): ContactMessage? {
|
||||
private fun BackupMessageRecord.toRemoteContactMessage(reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): ContactMessage? {
|
||||
val sharedContact = toRemoteSharedContact(attachments) ?: return null
|
||||
|
||||
return ContactMessage(
|
||||
contact = ContactAttachment(
|
||||
name = sharedContact.name.toRemote(),
|
||||
avatar = (sharedContact.avatar?.attachment as? DatabaseAttachment)?.toRemoteMessageAttachment(mediaArchiveEnabled)?.pointer,
|
||||
avatar = (sharedContact.avatar?.attachment as? DatabaseAttachment)?.toRemoteMessageAttachment()?.pointer,
|
||||
organization = sharedContact.organization ?: "",
|
||||
number = sharedContact.phoneNumbers.mapNotNull { phone ->
|
||||
ContactAttachment.Phone(
|
||||
@@ -969,7 +967,7 @@ private fun Contact.PostalAddress.Type.toRemote(): ContactAttachment.PostalAddre
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): DirectStoryReplyMessage? {
|
||||
private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): DirectStoryReplyMessage? {
|
||||
if (this.body.isNullOrBlank()) {
|
||||
Log.w(TAG, ExportSkips.directStoryReplyHasNoBody(this.dateSent))
|
||||
return null
|
||||
@@ -991,7 +989,7 @@ private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(mediaArchiveEnab
|
||||
body = bodyText,
|
||||
bodyRanges = this.bodyRanges?.toRemoteBodyRanges(this.dateSent) ?: emptyList()
|
||||
),
|
||||
longText = longTextAttachment?.toRemoteFilePointer(mediaArchiveEnabled)
|
||||
longText = longTextAttachment?.toRemoteFilePointer()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -1000,7 +998,7 @@ private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(mediaArchiveEnab
|
||||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteStandardMessage(exportState: ExportState, mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
|
||||
private fun BackupMessageRecord.toRemoteStandardMessage(exportState: ExportState, reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
|
||||
val linkPreviews = this.toRemoteLinkPreviews(attachments)
|
||||
val linkPreviewAttachments = linkPreviews.mapNotNull { it.thumbnail.orElse(null) }.toSet()
|
||||
val quotedAttachments = attachments?.filter { it.quote } ?: emptyList()
|
||||
@@ -1021,11 +1019,11 @@ private fun BackupMessageRecord.toRemoteStandardMessage(exportState: ExportState
|
||||
}
|
||||
|
||||
return StandardMessage(
|
||||
quote = this.toRemoteQuote(exportState, mediaArchiveEnabled, quotedAttachments),
|
||||
quote = this.toRemoteQuote(exportState, quotedAttachments),
|
||||
text = text.takeUnless { hasVoiceNote },
|
||||
attachments = messageAttachments.toRemoteAttachments(mediaArchiveEnabled).withFixedVoiceNotes(textPresent = text != null || longTextAttachment != null),
|
||||
linkPreview = linkPreviews.map { it.toRemoteLinkPreview(mediaArchiveEnabled) },
|
||||
longText = longTextAttachment?.toRemoteFilePointer(mediaArchiveEnabled),
|
||||
attachments = messageAttachments.toRemoteAttachments().withFixedVoiceNotes(textPresent = text != null || longTextAttachment != null),
|
||||
linkPreview = linkPreviews.map { it.toRemoteLinkPreview() },
|
||||
longText = longTextAttachment?.toRemoteFilePointer(),
|
||||
reactions = reactionRecords.toRemote()
|
||||
)
|
||||
}
|
||||
@@ -1064,7 +1062,7 @@ private fun BackupMessageRecord.getBodyText(attachments: List<DatabaseAttachment
|
||||
return trimmed to null
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteQuote(exportState: ExportState, mediaArchiveEnabled: Boolean, attachments: List<DatabaseAttachment>? = null): Quote? {
|
||||
private fun BackupMessageRecord.toRemoteQuote(exportState: ExportState, attachments: List<DatabaseAttachment>? = null): Quote? {
|
||||
if (this.quoteTargetSentTimestamp == MessageTable.QUOTE_NOT_PRESENT_ID || this.quoteAuthor <= 0 || exportState.groupRecipientIds.contains(this.quoteAuthor)) {
|
||||
return null
|
||||
}
|
||||
@@ -1091,7 +1089,7 @@ private fun BackupMessageRecord.toRemoteQuote(exportState: ExportState, mediaArc
|
||||
val attachments = if (remoteType == Quote.Type.VIEW_ONCE) {
|
||||
emptyList()
|
||||
} else {
|
||||
attachments?.toRemoteQuoteAttachments(mediaArchiveEnabled) ?: emptyList()
|
||||
attachments?.toRemoteQuoteAttachments() ?: emptyList()
|
||||
}
|
||||
|
||||
if (remoteType == Quote.Type.NORMAL && body == null && attachments.isEmpty()) {
|
||||
@@ -1127,7 +1125,7 @@ private fun BackupMessageRecord.toRemoteGiftBadgeUpdate(): BackupGiftBadge? {
|
||||
)
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toRemoteStickerMessage(sentTimestamp: Long, mediaArchiveEnabled: Boolean, reactions: List<ReactionRecord>?): StickerMessage? {
|
||||
private fun DatabaseAttachment.toRemoteStickerMessage(sentTimestamp: Long, reactions: List<ReactionRecord>?): StickerMessage? {
|
||||
val stickerLocator = this.stickerLocator!!
|
||||
|
||||
val packId = try {
|
||||
@@ -1150,19 +1148,18 @@ private fun DatabaseAttachment.toRemoteStickerMessage(sentTimestamp: Long, media
|
||||
packKey = packKey.toByteString(),
|
||||
stickerId = stickerLocator.stickerId,
|
||||
emoji = stickerLocator.emoji,
|
||||
data_ = this.toRemoteMessageAttachment(mediaArchiveEnabled).pointer
|
||||
data_ = this.toRemoteMessageAttachment().pointer
|
||||
),
|
||||
reactions = reactions.toRemote()
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toRemoteQuoteAttachments(mediaArchiveEnabled: Boolean): List<Quote.QuotedAttachment> {
|
||||
private fun List<DatabaseAttachment>.toRemoteQuoteAttachments(): List<Quote.QuotedAttachment> {
|
||||
return this.map { attachment ->
|
||||
Quote.QuotedAttachment(
|
||||
contentType = attachment.contentType,
|
||||
fileName = attachment.fileName,
|
||||
thumbnail = attachment.toRemoteMessageAttachment(
|
||||
mediaArchiveEnabled = mediaArchiveEnabled,
|
||||
flagOverride = MessageAttachment.Flag.NONE,
|
||||
contentTypeOverride = "image/jpeg"
|
||||
)
|
||||
@@ -1170,9 +1167,9 @@ private fun List<DatabaseAttachment>.toRemoteQuoteAttachments(mediaArchiveEnable
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toRemoteMessageAttachment(mediaArchiveEnabled: Boolean, flagOverride: MessageAttachment.Flag? = null, contentTypeOverride: String? = null): MessageAttachment {
|
||||
private fun DatabaseAttachment.toRemoteMessageAttachment(flagOverride: MessageAttachment.Flag? = null, contentTypeOverride: String? = null): MessageAttachment {
|
||||
return MessageAttachment(
|
||||
pointer = this.toRemoteFilePointer(mediaArchiveEnabled, contentTypeOverride),
|
||||
pointer = this.toRemoteFilePointer(contentTypeOverride),
|
||||
wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE,
|
||||
flag = if (flagOverride != null) {
|
||||
flagOverride
|
||||
@@ -1189,9 +1186,9 @@ private fun DatabaseAttachment.toRemoteMessageAttachment(mediaArchiveEnabled: Bo
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toRemoteAttachments(mediaArchiveEnabled: Boolean): List<MessageAttachment> {
|
||||
private fun List<DatabaseAttachment>.toRemoteAttachments(): List<MessageAttachment> {
|
||||
return this.map { attachment ->
|
||||
attachment.toRemoteMessageAttachment(mediaArchiveEnabled)
|
||||
attachment.toRemoteMessageAttachment()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ object ChatArchiveImporter {
|
||||
val chatColor = chat.style?.toLocal(importState)
|
||||
|
||||
val wallpaperAttachmentId: AttachmentId? = chat.style?.wallpaperPhoto?.let { filePointer ->
|
||||
filePointer.toLocalAttachment(importState)?.let {
|
||||
filePointer.toLocalAttachment()?.let {
|
||||
SignalDatabase.attachments.restoreWallpaperAttachment(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ class ChatItemArchiveImporter(
|
||||
address.country
|
||||
)
|
||||
},
|
||||
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true), true)
|
||||
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(voiceNote = false, borderless = false, gif = false, wasDownloaded = true), true)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ class ChatItemArchiveImporter(
|
||||
*/
|
||||
private fun StandardMessage.parseBodyText(importState: ImportState): Pair<String?, Attachment?> {
|
||||
if (this.longText != null) {
|
||||
return null to this.longText.toLocalAttachment(importState, contentType = "text/x-signal-plain")
|
||||
return null to this.longText.toLocalAttachment(contentType = "text/x-signal-plain")
|
||||
}
|
||||
|
||||
if (this.text?.body == null) {
|
||||
@@ -499,7 +499,7 @@ class ChatItemArchiveImporter(
|
||||
*/
|
||||
private fun DirectStoryReplyMessage.parseBodyText(importState: ImportState): Pair<String?, Attachment?> {
|
||||
if (this.textReply?.longText != null) {
|
||||
return null to this.textReply.longText.toLocalAttachment(importState, contentType = "text/x-signal-plain")
|
||||
return null to this.textReply.longText.toLocalAttachment(contentType = "text/x-signal-plain")
|
||||
}
|
||||
|
||||
if (this.textReply?.text == null) {
|
||||
@@ -1113,10 +1113,9 @@ class ChatItemArchiveImporter(
|
||||
if (this == null) return null
|
||||
|
||||
return data_.toLocalAttachment(
|
||||
importState = importState,
|
||||
voiceNote = false,
|
||||
gif = false,
|
||||
borderless = false,
|
||||
gif = false,
|
||||
wasDownloaded = true,
|
||||
stickerLocator = StickerLocator(
|
||||
packId = Hex.toStringCondensed(packId.toByteArray()),
|
||||
@@ -1133,16 +1132,15 @@ class ChatItemArchiveImporter(
|
||||
this.title ?: "",
|
||||
this.description ?: "",
|
||||
this.date ?: 0,
|
||||
Optional.ofNullable(this.image?.toLocalAttachment(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true))
|
||||
Optional.ofNullable(this.image?.toLocalAttachment(voiceNote = false, borderless = false, gif = false, wasDownloaded = true))
|
||||
)
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(quote: Boolean = false, contentType: String? = pointer?.contentType): Attachment? {
|
||||
return pointer?.toLocalAttachment(
|
||||
importState = importState,
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
wasDownloaded = wasDownloaded,
|
||||
contentType = contentType,
|
||||
fileName = pointer.fileName,
|
||||
|
||||
@@ -58,9 +58,9 @@ object LocalArchiver {
|
||||
val mediaNames: MutableSet<MediaName> = Collections.synchronizedSet(HashSet())
|
||||
|
||||
Log.i(TAG, "Starting frame export")
|
||||
BackupRepository.localExport(mainStream, LocalExportProgressListener(), cancellationSignal) { attachment, source ->
|
||||
BackupRepository.exportForLocalBackup(mainStream, LocalExportProgressListener(), cancellationSignal) { attachment, source ->
|
||||
if (cancellationSignal()) {
|
||||
return@localExport
|
||||
return@exportForLocalBackup
|
||||
}
|
||||
|
||||
val mediaName = MediaName.fromPlaintextHashAndRemoteKey(attachment.plaintextHash, attachment.remoteKey)
|
||||
@@ -126,7 +126,7 @@ object LocalArchiver {
|
||||
|
||||
val mainStreamLength = snapshotFileSystem.mainLength() ?: return ArchiveResult.failure(FailureCause.MAIN_STREAM)
|
||||
|
||||
BackupRepository.localImport(
|
||||
BackupRepository.importLocal(
|
||||
mainStreamFactory = { snapshotFileSystem.mainInputStream()!! },
|
||||
mainStreamLength = mainStreamLength,
|
||||
selfData = selfData
|
||||
|
||||
@@ -248,7 +248,7 @@ object AccountDataArchiveProcessor {
|
||||
SignalStore.chatColors.chatColors = chatColors
|
||||
|
||||
val wallpaperAttachmentId: AttachmentId? = settings.defaultChatStyle.wallpaperPhoto?.let { filePointer ->
|
||||
filePointer.toLocalAttachment(importState)?.let {
|
||||
filePointer.toLocalAttachment()?.let {
|
||||
SignalDatabase.attachments.restoreWallpaperAttachment(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ object ChatItemArchiveProcessor {
|
||||
val TAG = Log.tag(ChatItemArchiveProcessor::class.java)
|
||||
|
||||
fun export(db: SignalDatabase, exportState: ExportState, selfRecipientId: RecipientId, cancellationSignal: () -> Boolean, emitter: BackupFrameEmitter) {
|
||||
db.messageTable.getMessagesForBackup(db, exportState.backupTime, exportState.mediaBackupEnabled, selfRecipientId, exportState).use { chatItems ->
|
||||
db.messageTable.getMessagesForBackup(db, exportState.backupTime, selfRecipientId, exportState).use { chatItems ->
|
||||
var count = 0
|
||||
while (chatItems.hasNext()) {
|
||||
if (count % 1000 == 0 && cancellationSignal()) {
|
||||
|
||||
@@ -5,16 +5,20 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.google.common.io.CountingInputStream
|
||||
import org.signal.core.util.readFully
|
||||
import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.readVarInt32
|
||||
import org.signal.core.util.stream.LimitedInputStream
|
||||
import org.signal.core.util.stream.MacInputStream
|
||||
import org.signal.core.util.writeVarInt32
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -30,25 +34,116 @@ import javax.crypto.spec.SecretKeySpec
|
||||
* 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(
|
||||
class EncryptedBackupReader private constructor(
|
||||
keyMaterial: MessageBackupKey.BackupKeyMaterial,
|
||||
val length: Long,
|
||||
dataStream: () -> InputStream
|
||||
) : BackupImportReader {
|
||||
|
||||
@VisibleForTesting
|
||||
val backupInfo: BackupInfo?
|
||||
var next: Frame? = null
|
||||
val stream: InputStream
|
||||
val countingStream: CountingInputStream
|
||||
private var next: Frame? = null
|
||||
private val stream: InputStream
|
||||
private val countingStream: CountingInputStream
|
||||
|
||||
constructor(key: MessageBackupKey, aci: ACI, length: Long, dataStream: () -> InputStream) :
|
||||
this(key.deriveBackupSecrets(aci), length, dataStream) {
|
||||
companion object {
|
||||
const val MAC_SIZE = 32
|
||||
|
||||
/**
|
||||
* Create a reader for a backup from the archive CDN.
|
||||
* The key difference is that we require forward secrecy data.
|
||||
*/
|
||||
fun createForSignalBackup(
|
||||
key: MessageBackupKey,
|
||||
aci: ACI,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken,
|
||||
length: Long,
|
||||
dataStream: () -> InputStream
|
||||
): EncryptedBackupReader {
|
||||
return EncryptedBackupReader(
|
||||
keyMaterial = key.deriveBackupSecrets(aci, forwardSecrecyToken),
|
||||
length = length,
|
||||
dataStream = dataStream
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reader for a local backup or for a transfer to a linked device. Basically everything that isn't [createForSignalBackup].
|
||||
* The key difference is that we don't require forward secrecy data.
|
||||
*/
|
||||
fun createForLocalOrLinking(key: MessageBackupKey, aci: ACI, length: Long, dataStream: () -> InputStream): EncryptedBackupReader {
|
||||
return EncryptedBackupReader(
|
||||
keyMaterial = key.deriveBackupSecrets(aci, forwardSecrecyToken = null),
|
||||
length = length,
|
||||
dataStream = dataStream
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the entire forward secrecy prefix. Includes the magic number, varint, and the length of the forward secrecy metadata itself.
|
||||
*/
|
||||
fun getForwardSecrecyPrefixDataLength(stream: InputStream): Int {
|
||||
val metadataLength = readForwardSecrecyMetadata(stream)?.size ?: return 0
|
||||
return EncryptedBackupWriter.MAGIC_NUMBER.size + metadataLength.lengthAsVarInt32() + metadataLength
|
||||
}
|
||||
|
||||
fun readForwardSecrecyMetadata(stream: InputStream): ByteArray? {
|
||||
val potentialMagicNumber = stream.readNBytesOrThrow(8)
|
||||
if (!EncryptedBackupWriter.MAGIC_NUMBER.contentEquals(potentialMagicNumber)) {
|
||||
return null
|
||||
}
|
||||
val metadataLength = stream.readVarInt32()
|
||||
return stream.readNBytesOrThrow(metadataLength)
|
||||
}
|
||||
|
||||
private fun validateMac(macKey: ByteArray, streamLength: Long, dataStream: InputStream) {
|
||||
val mac = Mac.getInstance("HmacSHA256").apply {
|
||||
init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||
}
|
||||
|
||||
val macStream = MacInputStream(
|
||||
wrapped = LimitedInputStream(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!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.lengthAsVarInt32(): Int {
|
||||
return ByteArrayOutputStream().apply {
|
||||
writeVarInt32(this@lengthAsVarInt32)
|
||||
}.toByteArray().size
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
dataStream().use { validateMac(keyMaterial.macKey, length, it) }
|
||||
val forwardSecrecyMetadata = dataStream().use { readForwardSecrecyMetadata(it) }
|
||||
|
||||
countingStream = CountingInputStream(dataStream())
|
||||
val encryptedLength = if (forwardSecrecyMetadata != null) {
|
||||
val prefixLength = EncryptedBackupWriter.MAGIC_NUMBER.size + forwardSecrecyMetadata.size + forwardSecrecyMetadata.size.lengthAsVarInt32()
|
||||
length - prefixLength
|
||||
} else {
|
||||
length
|
||||
}
|
||||
|
||||
val prefixSkippingStream = {
|
||||
if (forwardSecrecyMetadata == null) {
|
||||
dataStream()
|
||||
} else {
|
||||
dataStream().also { readForwardSecrecyMetadata(it) }
|
||||
}
|
||||
}
|
||||
|
||||
prefixSkippingStream().use { validateMac(keyMaterial.macKey, encryptedLength, it) }
|
||||
|
||||
countingStream = CountingInputStream(prefixSkippingStream())
|
||||
val iv = countingStream.readNBytesOrThrow(16)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
@@ -59,7 +154,7 @@ class EncryptedBackupReader(
|
||||
CipherInputStream(
|
||||
LimitedInputStream(
|
||||
wrapped = countingStream,
|
||||
maxBytes = length - MAC_SIZE
|
||||
maxBytes = encryptedLength - MAC_SIZE
|
||||
),
|
||||
cipher
|
||||
)
|
||||
@@ -112,28 +207,4 @@ class EncryptedBackupReader(
|
||||
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 = LimitedInputStream(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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.signal.core.util.stream.MacOutputStream
|
||||
import org.signal.core.util.writeVarInt32
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader.Companion.createForSignalBackup
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -26,9 +28,11 @@ import javax.crypto.spec.SecretKeySpec
|
||||
* 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(
|
||||
class EncryptedBackupWriter private constructor(
|
||||
key: MessageBackupKey,
|
||||
aci: ACI,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken?,
|
||||
forwardSecrecyMetadata: ByteArray?,
|
||||
private val outputStream: OutputStream,
|
||||
private val append: (ByteArray) -> Unit
|
||||
) : BackupExportWriter {
|
||||
@@ -36,8 +40,66 @@ class EncryptedBackupWriter(
|
||||
private val mainStream: PaddedGzipOutputStream
|
||||
private val macStream: MacOutputStream
|
||||
|
||||
companion object {
|
||||
val MAGIC_NUMBER = "SBACKUP".toByteArray(Charsets.UTF_8) + 0x01
|
||||
|
||||
/**
|
||||
* Create a writer for a backup from the archive CDN.
|
||||
* The key difference is that we require forward secrecy data.
|
||||
*/
|
||||
fun createForSignalBackup(
|
||||
key: MessageBackupKey,
|
||||
aci: ACI,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken,
|
||||
forwardSecrecyMetadata: ByteArray,
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit
|
||||
): EncryptedBackupWriter {
|
||||
return EncryptedBackupWriter(
|
||||
key = key,
|
||||
aci = aci,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
forwardSecrecyMetadata = forwardSecrecyMetadata,
|
||||
outputStream = outputStream,
|
||||
append = append
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a writer for a local backup or for a transfer to a linked device. Basically everything that isn't [createForSignalBackup].
|
||||
* The key difference is that we don't require forward secrecy data.
|
||||
*/
|
||||
fun createForLocalOrLinking(
|
||||
key: MessageBackupKey,
|
||||
aci: ACI,
|
||||
outputStream: OutputStream,
|
||||
append: (ByteArray) -> Unit
|
||||
): EncryptedBackupWriter {
|
||||
return EncryptedBackupWriter(
|
||||
key = key,
|
||||
aci = aci,
|
||||
forwardSecrecyToken = null,
|
||||
forwardSecrecyMetadata = null,
|
||||
outputStream = outputStream,
|
||||
append = append
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val keyMaterial = key.deriveBackupSecrets(aci)
|
||||
check(
|
||||
(forwardSecrecyToken != null && forwardSecrecyMetadata != null) ||
|
||||
(forwardSecrecyToken == null && forwardSecrecyMetadata == null)
|
||||
)
|
||||
|
||||
if (forwardSecrecyMetadata != null) {
|
||||
outputStream.write(MAGIC_NUMBER)
|
||||
outputStream.writeVarInt32(forwardSecrecyMetadata.size)
|
||||
outputStream.write(forwardSecrecyMetadata)
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
val keyMaterial = key.deriveBackupSecrets(aci, forwardSecrecyToken)
|
||||
|
||||
val iv: ByteArray = Util.getSecretBytes(16)
|
||||
outputStream.write(iv)
|
||||
|
||||
@@ -9,7 +9,6 @@ import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.emptyIfNull
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.nullIfBlank
|
||||
import org.signal.core.util.orNull
|
||||
@@ -19,8 +18,6 @@ import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||
import org.thoughtcrime.securesms.backup.v2.getMediaName
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
@@ -35,7 +32,6 @@ import org.thoughtcrime.securesms.backup.v2.proto.AvatarColor as RemoteAvatarCol
|
||||
* Converts a [FilePointer] to a local [Attachment] object for inserting into the database.
|
||||
*/
|
||||
fun FilePointer?.toLocalAttachment(
|
||||
importState: ImportState,
|
||||
voiceNote: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
gif: Boolean = false,
|
||||
@@ -132,7 +128,7 @@ fun FilePointer?.toLocalAttachment(
|
||||
/**
|
||||
* @param mediaArchiveEnabled True if this user has enable media backup, otherwise false.
|
||||
*/
|
||||
fun DatabaseAttachment.toRemoteFilePointer(mediaArchiveEnabled: Boolean, contentTypeOverride: String? = null): FilePointer {
|
||||
fun DatabaseAttachment.toRemoteFilePointer(contentTypeOverride: String? = null): FilePointer {
|
||||
val builder = FilePointer.Builder()
|
||||
builder.contentType = contentTypeOverride ?: this.contentType?.takeUnless { it.isBlank() }
|
||||
builder.incrementalMac = this.incrementalDigest?.takeIf { it.isNotEmpty() && this.incrementalMacChunkSize > 0 }?.toByteString()
|
||||
@@ -143,13 +139,13 @@ fun DatabaseAttachment.toRemoteFilePointer(mediaArchiveEnabled: Boolean, content
|
||||
builder.caption = this.caption
|
||||
builder.blurHash = this.blurHash?.hash
|
||||
|
||||
builder.setLegacyLocators(this, mediaArchiveEnabled)
|
||||
builder.setLegacyLocators(this)
|
||||
builder.locatorInfo = this.toLocatorInfo()
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun FilePointer.Builder.setLegacyLocators(attachment: DatabaseAttachment, mediaArchiveEnabled: Boolean) {
|
||||
fun FilePointer.Builder.setLegacyLocators(attachment: DatabaseAttachment) {
|
||||
if (attachment.remoteKey.isNullOrBlank() || attachment.remoteDigest == null || attachment.size == 0L) {
|
||||
this.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
|
||||
return
|
||||
@@ -160,25 +156,6 @@ fun FilePointer.Builder.setLegacyLocators(attachment: DatabaseAttachment, mediaA
|
||||
return
|
||||
}
|
||||
|
||||
val pending = attachment.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED && (attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && attachment.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED)
|
||||
|
||||
if (mediaArchiveEnabled && !pending) {
|
||||
val transitCdnKey = attachment.remoteLocation?.nullIfBlank()
|
||||
val transitCdnNumber = attachment.cdn.cdnNumber.takeIf { transitCdnKey != null }
|
||||
val archiveMediaName = attachment.getMediaName()?.toString()
|
||||
|
||||
this.backupLocator = FilePointer.BackupLocator(
|
||||
mediaName = archiveMediaName.emptyIfNull(),
|
||||
cdnNumber = attachment.archiveCdn.takeIf { archiveMediaName != null },
|
||||
key = Base64.decode(attachment.remoteKey).toByteString(),
|
||||
size = attachment.size.toInt(),
|
||||
digest = attachment.remoteDigest.toByteString(),
|
||||
transitCdnNumber = transitCdnNumber,
|
||||
transitCdnKey = transitCdnKey
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (attachment.remoteLocation.isNullOrBlank()) {
|
||||
this.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
|
||||
return
|
||||
|
||||
@@ -251,7 +251,7 @@ private fun Wallpaper.LinearGradient.toRemoteWallpaperPreset(): ChatStyle.Wallpa
|
||||
private fun Wallpaper.File.toFilePointer(db: SignalDatabase): FilePointer? {
|
||||
val attachmentId: AttachmentId = UriUtil.parseOrNull(this.uri)?.let { PartUriParser(it).partId } ?: return null
|
||||
val attachment = db.attachmentTable.getAttachment(attachmentId)
|
||||
return attachment?.toRemoteFilePointer(mediaArchiveEnabled = true)
|
||||
return attachment?.toRemoteFilePointer()
|
||||
}
|
||||
|
||||
private fun ChatStyle.Builder.hasBubbleColorSet(): Boolean {
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.backup.v2.local.ArchiveResult
|
||||
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
|
||||
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver.FailureCause
|
||||
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader.Companion.MAC_SIZE
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.DebugAttachmentStats
|
||||
@@ -53,11 +54,13 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -99,7 +102,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting encrypted backup to disk...")
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
BackupRepository.exportForDebugging(
|
||||
outputStream = openStream(),
|
||||
append = { bytes -> appendStream().use { it.write(bytes) } }
|
||||
)
|
||||
@@ -115,7 +118,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting plaintext backup to disk...")
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
BackupRepository.exportForDebugging(
|
||||
outputStream = openStream(),
|
||||
append = { bytes -> appendStream().use { it.write(bytes) } },
|
||||
plaintext = true
|
||||
@@ -134,12 +137,12 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
BackupRepository.exportForDebugging(
|
||||
outputStream = FileOutputStream(tempFile),
|
||||
append = { bytes -> tempFile.appendBytes(bytes) }
|
||||
)
|
||||
_state.value = _state.value.copy(statusMessage = "Export complete! Validating...")
|
||||
ArchiveValidator.validate(tempFile, SignalStore.backup.messageBackupKey, forTransfer = false)
|
||||
ArchiveValidator.validateLocalOrLinking(tempFile, SignalStore.backup.messageBackupKey, forTransfer = false)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -181,7 +184,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
val selfData = BackupRepository.SelfData(aci, self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
val backupKey = customCredentials?.messageBackupKey ?: SignalStore.backup.messageBackupKey
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, backupKey) }
|
||||
disposables += Single.fromCallable { BackupRepository.importForDebugging(length, inputStreamFactory, selfData, backupKey) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
@@ -234,10 +237,27 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedStream = tempBackupFile.inputStream()
|
||||
val forwardSecrecyMetadata = tempBackupFile.inputStream().use { EncryptedBackupReader.readForwardSecrecyMetadata(it) }
|
||||
if (forwardSecrecyMetadata == null) {
|
||||
throw IOException("Failed to read forward secrecy metadata!")
|
||||
}
|
||||
|
||||
val svrBAuth = when (val result = BackupRepository.getSvrBAuth()) {
|
||||
is NetworkResult.Success -> result.result
|
||||
else -> throw IOException("Failed to read forward secrecy metadata!")
|
||||
}
|
||||
|
||||
val forwardSecrecyToken = when (val result = SignalNetwork.svrB.restore(svrBAuth, SignalStore.backup.messageBackupKey, forwardSecrecyMetadata)) {
|
||||
is SvrBApi.RestoreResult.Success -> result.data.forwardSecrecyToken
|
||||
else -> throw IOException("Failed to read forward secrecy metadata!")
|
||||
}
|
||||
|
||||
val encryptedStream = tempBackupFile.inputStream().apply {
|
||||
EncryptedBackupReader.readForwardSecrecyMetadata(this)
|
||||
}
|
||||
val iv = encryptedStream.readNBytesOrThrow(16)
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
|
||||
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get(), forwardSecrecyToken)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
|
||||
}
|
||||
@@ -246,7 +266,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
CipherInputStream(
|
||||
LimitedInputStream(
|
||||
wrapped = encryptedStream,
|
||||
maxBytes = tempBackupFile.length() - MAC_SIZE
|
||||
maxBytes = tempBackupFile.length() - MAC_SIZE - tempBackupFile.inputStream().use { EncryptedBackupReader.getForwardSecrecyPrefixDataLength(it) }
|
||||
),
|
||||
cipher
|
||||
)
|
||||
|
||||
@@ -64,6 +64,7 @@ import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
@@ -326,6 +327,9 @@ object AppDependencies {
|
||||
val usernameApi: UsernameApi
|
||||
get() = networkModule.usernameApi
|
||||
|
||||
val svrBApi: SvrBApi
|
||||
get() = networkModule.svrBApi
|
||||
|
||||
val callingApi: CallingApi
|
||||
get() = networkModule.callingApi
|
||||
|
||||
@@ -448,5 +452,6 @@ object AppDependencies {
|
||||
fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket, clientZkProfileOperations: ClientZkProfileOperations): ProfileApi
|
||||
fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): RemoteConfigApi
|
||||
fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi
|
||||
fun provideSvrBApi(libSignalNetwork: Network): SvrBApi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.HandlerThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.billing.BillingFactory;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.billing.BillingApi;
|
||||
@@ -105,6 +106,7 @@ import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi;
|
||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi;
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||
@@ -573,6 +575,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
return new DonationsApi(authWebSocket, unauthWebSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SvrBApi provideSvrBApi(@NotNull Network libSignalNetwork) {
|
||||
return new SvrBApi(libSignalNetwork);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
@@ -218,6 +219,10 @@ class NetworkDependenciesModule(
|
||||
provider.provideDonationsApi(authWebSocket, unauthWebSocket)
|
||||
}
|
||||
|
||||
val svrBApi: SvrBApi by lazy {
|
||||
provider.provideSvrBApi(libsignalNetwork)
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(StandardUserAgentInterceptor())
|
||||
|
||||
@@ -8,6 +8,9 @@ package org.thoughtcrime.securesms.jobs
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.signal.libsignal.net.SvrBStoreResponse
|
||||
import org.signal.protos.resumableuploads.ResumableUpload
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
@@ -30,6 +33,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@@ -141,6 +145,30 @@ class BackupMessagesJob private constructor(
|
||||
|
||||
val stopwatch = Stopwatch("BackupMessagesJob")
|
||||
|
||||
val auth = when (val result = BackupRepository.getSvrBAuth()) {
|
||||
is NetworkResult.Success -> result.result
|
||||
is NetworkResult.NetworkError -> return Result.retry(defaultBackoff()).logW(TAG, "Network error when getting SVRB auth.", result.getCause())
|
||||
is NetworkResult.StatusCodeError -> return Result.retry(defaultBackoff()).logW(TAG, "Status code error when getting SVRB auth.", result.getCause())
|
||||
is NetworkResult.ApplicationError -> throw result.throwable
|
||||
}
|
||||
|
||||
val backupSecretData = SignalStore.backup.nextBackupSecretData ?: run {
|
||||
Log.i(TAG, "First SVRB backup! Creating new backup chain.")
|
||||
val secretData = SignalNetwork.svrB.createNewBackupChain(auth, SignalStore.backup.messageBackupKey)
|
||||
SignalStore.backup.nextBackupSecretData = secretData
|
||||
secretData
|
||||
}
|
||||
|
||||
val svrBMetadata: SvrBStoreResponse = when (val result = SignalNetwork.svrB.store(auth, SignalStore.backup.messageBackupKey, backupSecretData)) {
|
||||
is SvrBApi.StoreResult.Success -> result.data
|
||||
is SvrBApi.StoreResult.NetworkError -> return Result.retry(defaultBackoff()).logW(TAG, "SVRB transient network error.", result.exception)
|
||||
is SvrBApi.StoreResult.SvrError -> return Result.retry(defaultBackoff()).logW(TAG, "SVRB error.", result.throwable)
|
||||
is SvrBApi.StoreResult.UnknownError -> return Result.fatalFailure(RuntimeException(result.throwable))
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully stored data on SVRB.")
|
||||
stopwatch.split("svrb")
|
||||
|
||||
SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload().takeIf { it > 0 }?.let { count -> Log.w(TAG, "Needed to create $count remote keys.") }
|
||||
stopwatch.split("keygen")
|
||||
|
||||
@@ -148,7 +176,7 @@ class BackupMessagesJob private constructor(
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val (tempBackupFile, currentTime) = when (val generateBackupFileResult = getOrCreateBackupFile(stopwatch)) {
|
||||
val (tempBackupFile, currentTime) = when (val generateBackupFileResult = getOrCreateBackupFile(stopwatch, svrBMetadata.forwardSecrecyToken, svrBMetadata.metadata)) {
|
||||
is BackupFileResult.Success -> generateBackupFileResult
|
||||
BackupFileResult.Failure -> return Result.failure()
|
||||
BackupFileResult.Retry -> return Result.retry(defaultBackoff())
|
||||
@@ -242,6 +270,8 @@ class BackupMessagesJob private constructor(
|
||||
}
|
||||
stopwatch.split("upload")
|
||||
|
||||
SignalStore.backup.nextBackupSecretData = svrBMetadata.nextBackupSecretData
|
||||
|
||||
SignalStore.backup.lastBackupProtoSize = tempBackupFile.length()
|
||||
if (!tempBackupFile.delete()) {
|
||||
Log.e(TAG, "Failed to delete temp backup file")
|
||||
@@ -275,7 +305,9 @@ class BackupMessagesJob private constructor(
|
||||
}
|
||||
|
||||
private fun getOrCreateBackupFile(
|
||||
stopwatch: Stopwatch
|
||||
stopwatch: Stopwatch,
|
||||
forwardSecrecyToken: BackupForwardSecrecyToken,
|
||||
forwardSecrecyMetadata: ByteArray
|
||||
): BackupFileResult {
|
||||
if (System.currentTimeMillis() > syncTime && syncTime > 0L && dataFile.isNotNullOrBlank()) {
|
||||
val file = File(dataFile)
|
||||
@@ -294,7 +326,17 @@ class BackupMessagesJob private constructor(
|
||||
val outputStream = FileOutputStream(tempBackupFile)
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
val currentTime = System.currentTimeMillis()
|
||||
BackupRepository.export(outputStream = outputStream, messageBackupKey = backupKey, progressEmitter = ArchiveUploadProgress.ArchiveBackupProgressListener, append = { tempBackupFile.appendBytes(it) }, plaintext = false, cancellationSignal = { this.isCanceled }, currentTime = currentTime) {
|
||||
|
||||
BackupRepository.exportForSignalBackup(
|
||||
outputStream = outputStream,
|
||||
messageBackupKey = backupKey,
|
||||
forwardSecrecyMetadata = forwardSecrecyMetadata,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
progressEmitter = ArchiveUploadProgress.ArchiveBackupProgressListener,
|
||||
append = { tempBackupFile.appendBytes(it) },
|
||||
cancellationSignal = { this.isCanceled },
|
||||
currentTime = currentTime
|
||||
) {
|
||||
writeMediaCursorToTemporaryTable(it, currentTime = currentTime, mediaBackupEnabled = SignalStore.backup.backsUpMedia)
|
||||
}
|
||||
|
||||
@@ -304,7 +346,7 @@ class BackupMessagesJob private constructor(
|
||||
|
||||
stopwatch.split("export")
|
||||
|
||||
when (val result = ArchiveValidator.validate(tempBackupFile, backupKey, forTransfer = false)) {
|
||||
when (val result = ArchiveValidator.validateSignalBackup(tempBackupFile, backupKey, forwardSecrecyToken)) {
|
||||
ArchiveValidator.ValidationResult.Success -> {
|
||||
Log.d(TAG, "Successfully passed validation.")
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
private const val KEY_HAS_SNOOZED_VERIFY = "backup.has_snoozed_verify"
|
||||
private const val KEY_HAS_VERIFIED_BEFORE = "backup.has_verified_before"
|
||||
|
||||
private const val KEY_NEXT_BACKUP_SECRET_DATA = "backup.next_backup_secret_data"
|
||||
|
||||
private val cachedCdnCredentialsExpiresIn: Duration = 12.hours
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
@@ -367,6 +369,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
/** Checks if they have ever verified their backup key before **/
|
||||
var hasVerifiedBefore by booleanValue(KEY_HAS_VERIFIED_BEFORE, false)
|
||||
|
||||
/** The value from the last successful SVRB operation that must be passed to the next SVRB operation. */
|
||||
var nextBackupSecretData by nullableBlobValue(KEY_NEXT_BACKUP_SECRET_DATA, null)
|
||||
|
||||
/**
|
||||
* If true, it means we have been told that remote storage is full, but we have not yet run any of our "garbage collection" tasks, like committing deletes
|
||||
* or pruning orphaned media.
|
||||
|
||||
@@ -262,12 +262,11 @@ object LinkDeviceRepository {
|
||||
|
||||
try {
|
||||
Log.d(TAG, "[createAndUploadArchive] Starting the export.")
|
||||
BackupRepository.export(
|
||||
BackupRepository.exportForLinkAndSync(
|
||||
currentTime = System.currentTimeMillis(),
|
||||
outputStream = outputStream,
|
||||
append = { tempBackupFile.appendBytes(it) },
|
||||
messageBackupKey = ephemeralMessageBackupKey,
|
||||
skipMediaBackup = true,
|
||||
forTransfer = true,
|
||||
cancellationSignal = cancellationSignal
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
@@ -288,7 +287,7 @@ object LinkDeviceRepository {
|
||||
return LinkUploadArchiveResult.BackupCreationCancelled
|
||||
}
|
||||
|
||||
when (val result = ArchiveValidator.validate(tempBackupFile, ephemeralMessageBackupKey, forTransfer = true)) {
|
||||
when (val result = ArchiveValidator.validateLocalOrLinking(tempBackupFile, ephemeralMessageBackupKey, forTransfer = true)) {
|
||||
ArchiveValidator.ValidationResult.Success -> {
|
||||
Log.d(TAG, "[createAndUploadArchive] Successfully passed validation.")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
|
||||
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi
|
||||
|
||||
/**
|
||||
@@ -94,4 +95,7 @@ object SignalNetwork {
|
||||
@get:JvmName("username")
|
||||
val username: UsernameApi
|
||||
get() = AppDependencies.usernameApi
|
||||
|
||||
val svrB: SvrBApi
|
||||
get() = AppDependencies.svrBApi
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
@@ -28,7 +29,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
val frameCount = 10_000
|
||||
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
EncryptedBackupWriter.createForLocalOrLinking(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
|
||||
|
||||
for (i in 0 until frameCount) {
|
||||
@@ -39,7 +40,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
val ciphertext: ByteArray = outputStream.toByteArray()
|
||||
println(ciphertext.size)
|
||||
|
||||
val frames: List<Frame> = EncryptedBackupReader(key, aci, ciphertext.size.toLong()) { ciphertext.inputStream() }.use { reader ->
|
||||
val frames: List<Frame> = EncryptedBackupReader.createForLocalOrLinking(key, aci, ciphertext.size.toLong()) { ciphertext.inputStream() }.use { reader ->
|
||||
assertEquals(reader.backupInfo?.version, 1L)
|
||||
assertEquals(reader.backupInfo?.backupTimeMs, 1000L)
|
||||
reader.asSequence().toList()
|
||||
@@ -61,7 +62,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
.map { frameCount ->
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
EncryptedBackupWriter.createForLocalOrLinking(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
|
||||
|
||||
for (i in 0 until frameCount) {
|
||||
@@ -86,7 +87,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
.map {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
EncryptedBackupWriter.createForLocalOrLinking(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
|
||||
writer.write(Frame(account = AccountData(username = "static-data")))
|
||||
}
|
||||
@@ -98,4 +99,45 @@ class EncryptedBackupReaderWriterTest {
|
||||
|
||||
assertEquals(count, uniqueOutputs.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can read back all of the frames we write - forward secrecy`() {
|
||||
val key = MessageBackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
val forwardSecrecyToken = BackupForwardSecrecyToken(Util.getSecretBytes(32))
|
||||
|
||||
val frameCount = 10_000
|
||||
EncryptedBackupWriter.createForSignalBackup(
|
||||
key = key,
|
||||
aci = aci,
|
||||
forwardSecrecyToken = forwardSecrecyToken,
|
||||
forwardSecrecyMetadata = Util.getSecretBytes(64),
|
||||
outputStream = outputStream,
|
||||
append = { outputStream.write(it) }
|
||||
).use { writer ->
|
||||
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
|
||||
|
||||
for (i in 0 until frameCount) {
|
||||
writer.write(Frame(account = AccountData(username = "username-$i")))
|
||||
}
|
||||
}
|
||||
|
||||
val ciphertext: ByteArray = outputStream.toByteArray()
|
||||
println(ciphertext.size)
|
||||
|
||||
val frames: List<Frame> = EncryptedBackupReader.createForSignalBackup(key, aci, forwardSecrecyToken, ciphertext.size.toLong()) { ciphertext.inputStream() }.use { reader ->
|
||||
assertEquals(reader.backupInfo?.version, 1L)
|
||||
assertEquals(reader.backupInfo?.backupTimeMs, 1000L)
|
||||
reader.asSequence().toList()
|
||||
}
|
||||
|
||||
assertEquals(frameCount, frames.size)
|
||||
|
||||
for (i in 0 until frameCount) {
|
||||
assertEquals("username-$i", frames[i].account?.username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi
|
||||
import org.whispersystems.signalservice.api.svr.SvrBApi
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
@@ -300,4 +301,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||
override fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
|
||||
override fun provideSvrBApi(libSignalNetwork: Network): SvrBApi {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ androidx-window = "1.3.0"
|
||||
glide = "4.15.1"
|
||||
gradle = "8.9.0"
|
||||
kotlin = "2.1.0"
|
||||
libsignal-client = "0.77.1"
|
||||
libsignal-client = "0.78.0"
|
||||
mp4parser = "1.9.39"
|
||||
android-gradle-plugin = "8.7.2"
|
||||
accompanist = "0.28.0"
|
||||
|
||||
@@ -7374,20 +7374,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="57b3cf8f247f1990211110734a7d1af413db145c8f17eb1b2cdc9b9321188c2b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-android" version="0.77.1">
|
||||
<artifact name="libsignal-android-0.77.1.aar">
|
||||
<sha256 value="0310ef720832957996ef68a5186288418d2c7683d2557b6875970affe146c6dd" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-android" version="0.78.0">
|
||||
<artifact name="libsignal-android-0.78.0.aar">
|
||||
<sha256 value="29b9f18df8b89dbde7ea239475c73aa9518c8dad7b75211ef88e120970a7bb0b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-android-0.77.1.module">
|
||||
<sha256 value="e2ae1c49162078756fbd0dff62c11754d3bee9f5757330ded20ae5578673a81e" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-android-0.78.0.module">
|
||||
<sha256 value="7bf3c5f7dbb417076aca6ca50bc8cb3129397e2d5a50cd5ad408d31536c2b6ed" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-client" version="0.77.1">
|
||||
<artifact name="libsignal-client-0.77.1.jar">
|
||||
<sha256 value="2ca65f3f05b05112fcef61d15db7cc11f3bf844e2f8e126e7713e6e77701d6ea" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-client" version="0.78.0">
|
||||
<artifact name="libsignal-client-0.78.0.jar">
|
||||
<sha256 value="f60d185af7d6f14f316749d4fbcddfaad91cca59cc54acef5cddfdb0a6fad0e4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-client-0.77.1.module">
|
||||
<sha256 value="bc8708d10e89320afdbfbf818b15a164098ffc78eb642ad5c64e6a2d6557f4ab" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-client-0.78.0.module">
|
||||
<sha256 value="d79581c29aadd593486d96c119bcbf23be7accb10ad01962ae069321936e1241" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="ringrtc-android" version="2.56.0">
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.whispersystems.signalservice.internal.delete
|
||||
import org.whispersystems.signalservice.internal.get
|
||||
import org.whispersystems.signalservice.internal.post
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.put
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
@@ -362,6 +363,24 @@ class ArchiveApi(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves auth credentials that can be used to perform SVRB operations.
|
||||
*
|
||||
* GET /v1/archives/auth/svrb
|
||||
* - 200: Success
|
||||
* - 400: Bad arguments, or made on an authenticated channel
|
||||
* - 401: Bad presentation, invalid public key signature, no matching backupId on the server, or the credential was of the wrong type (messages/media)
|
||||
* - 403: Forbidden
|
||||
*/
|
||||
fun getSvrBAuthorization(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<AuthCredentials> {
|
||||
return getCredentialPresentation(aci, archiveServiceAccess)
|
||||
.map { it.toArchiveCredentialPresentation().toHeaders() }
|
||||
.then { headers ->
|
||||
val request = WebSocketRequestMessage.get("/v1/archives/auth/svrb", headers)
|
||||
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, AuthCredentials::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCredentialPresentation(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<CredentialPresentationData> {
|
||||
return NetworkResult.fromLocal {
|
||||
val zkCredential = getZkCredential(aci, archiveServiceAccess)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey
|
||||
@@ -28,11 +29,13 @@ class MessageBackupKey(override val value: ByteArray) : BackupKey {
|
||||
|
||||
/**
|
||||
* The cryptographic material used to encrypt a backup.
|
||||
*
|
||||
* @param forwardSecrecyToken Should be present for any backup located on the archive CDN. Absent for other uses (i.e. link+sync).
|
||||
*/
|
||||
fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial {
|
||||
fun deriveBackupSecrets(aci: ACI, forwardSecrecyToken: BackupForwardSecrecyToken?): BackupKeyMaterial {
|
||||
val backupId = deriveBackupId(aci)
|
||||
val libsignalBackupKey = LibSignalBackupKey(value)
|
||||
val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value)
|
||||
val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value, forwardSecrecyToken)
|
||||
|
||||
return BackupKeyMaterial(
|
||||
id = backupId,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import org.signal.libsignal.attest.AttestationFailedException
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.messagebackup.BackupKey
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.libsignal.net.NetworkException
|
||||
import org.signal.libsignal.net.NetworkProtocolException
|
||||
import org.signal.libsignal.net.SvrB
|
||||
import org.signal.libsignal.net.SvrBRestoreResponse
|
||||
import org.signal.libsignal.net.SvrBStoreResponse
|
||||
import org.signal.libsignal.svr.DataMissingException
|
||||
import org.signal.libsignal.svr.RestoreFailedException
|
||||
import org.signal.libsignal.svr.SvrException
|
||||
import org.whispersystems.signalservice.api.CancelationException
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
* A collection of operations for interacting with SVRB, the SVR enclave that provides forward secrecy for backups.
|
||||
*/
|
||||
class SvrBApi(private val network: Network) {
|
||||
|
||||
/**
|
||||
* See [SvrB.createNewBackupChain].
|
||||
*
|
||||
* Call this the first time you ever interact with SVRB. Gives you a secret data to persist and use for future calls.
|
||||
*
|
||||
* Note that this doesn't actually make a network call, it just needs a [Network] to get the environment.
|
||||
*/
|
||||
fun createNewBackupChain(auth: AuthCredentials, backupKey: MessageBackupKey): ByteArray {
|
||||
return network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.createNewBackupChain(BackupKey(backupKey.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* See [SvrB.store].
|
||||
*
|
||||
* Handling this one is funny because the underlying protocols don't use status codes, instead favoring complex results.
|
||||
* As a result, responses are only [NetworkResult.Success] and [NetworkResult.NetworkError], with errors being accounted for
|
||||
* in the success case via the sealed result class.
|
||||
*/
|
||||
fun store(auth: AuthCredentials, backupKey: MessageBackupKey, previousSecretData: ByteArray): StoreResult {
|
||||
return try {
|
||||
val result = network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.store(BackupKey(backupKey.value), previousSecretData)
|
||||
.get()
|
||||
|
||||
when (val exception = result.exceptionOrNull()) {
|
||||
null -> StoreResult.Success(result.getOrThrow())
|
||||
is NetworkException -> StoreResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> StoreResult.NetworkError(exception)
|
||||
is SvrException -> StoreResult.SvrError(exception)
|
||||
else -> StoreResult.UnknownError(exception)
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
StoreResult.UnknownError(e)
|
||||
} catch (e: ExecutionException) {
|
||||
StoreResult.UnknownError(e)
|
||||
} catch (e: InterruptedException) {
|
||||
StoreResult.UnknownError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See [SvrB.restore]
|
||||
*
|
||||
* Handling this one is funny because the underlying protocols don't use status codes, instead favoring complex results.
|
||||
* As a result, responses are only [NetworkResult.Success] and [NetworkResult.NetworkError], with errors being accounted for
|
||||
* in the success case via the sealed result class.
|
||||
*/
|
||||
fun restore(auth: AuthCredentials, backupKey: MessageBackupKey, forwardSecrecyMetadata: ByteArray): RestoreResult {
|
||||
return try {
|
||||
val result = network
|
||||
.svrB(auth.username(), auth.password())
|
||||
.restore(BackupKey(backupKey.value), forwardSecrecyMetadata)
|
||||
.get()
|
||||
|
||||
when (val exception = result.exceptionOrNull()) {
|
||||
null -> RestoreResult.Success(result.getOrThrow())
|
||||
is NetworkException -> RestoreResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> RestoreResult.NetworkError(exception)
|
||||
is DataMissingException -> RestoreResult.DataMissingError
|
||||
is RestoreFailedException -> RestoreResult.RestoreFailedError(exception.triesRemaining)
|
||||
is SvrException -> RestoreResult.SvrError(exception)
|
||||
is AttestationFailedException -> RestoreResult.SvrError(exception)
|
||||
else -> RestoreResult.UnknownError(exception)
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
} catch (e: ExecutionException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
} catch (e: InterruptedException) {
|
||||
RestoreResult.UnknownError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <ResultType, FinalType> CompletableFuture<Result<ResultType>>.toNetworkResult(resultMapper: (Result<ResultType>) -> FinalType): NetworkResult<FinalType> {
|
||||
return try {
|
||||
val result = this.get()
|
||||
return when (val exception = result.exceptionOrNull()) {
|
||||
is NetworkException -> NetworkResult.NetworkError(exception)
|
||||
is NetworkProtocolException -> NetworkResult.NetworkError(exception)
|
||||
else -> NetworkResult.Success(resultMapper(result))
|
||||
}
|
||||
} catch (e: CancelationException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
} catch (e: ExecutionException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
} catch (e: InterruptedException) {
|
||||
NetworkResult.Success(resultMapper(Result.failure(e)))
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StoreResult {
|
||||
data class Success(val data: SvrBStoreResponse) : StoreResult()
|
||||
data class NetworkError(val exception: IOException) : StoreResult()
|
||||
data class SvrError(val throwable: Throwable) : StoreResult()
|
||||
data class UnknownError(val throwable: Throwable) : StoreResult()
|
||||
}
|
||||
|
||||
sealed class RestoreResult {
|
||||
data class Success(val data: SvrBRestoreResponse) : RestoreResult()
|
||||
data class NetworkError(val exception: IOException) : RestoreResult()
|
||||
data object DataMissingError : RestoreResult()
|
||||
data class RestoreFailedError(val triesRemaining: Int) : RestoreResult()
|
||||
data class SvrError(val throwable: Throwable) : RestoreResult()
|
||||
data class UnknownError(val throwable: Throwable) : RestoreResult()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user