Move to separate message and media backup keys.

This commit is contained in:
Greyson Parrelli
2024-10-31 11:58:40 -04:00
parent 22148550dc
commit 26b9cea88e
36 changed files with 596 additions and 380 deletions

View File

@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.backup.v2
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.Base64
@@ -27,7 +26,6 @@ import org.signal.core.util.stream.NonClosingOutputStream
import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey
@@ -73,11 +71,12 @@ import org.whispersystems.signalservice.api.StatusCodeErrorAction
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredentialPair
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
import org.whispersystems.signalservice.api.push.ServiceId.ACI
@@ -96,6 +95,7 @@ import java.util.Locale
import java.util.concurrent.atomic.AtomicLong
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey
object BackupRepository {
@@ -111,7 +111,8 @@ object BackupRepository {
401 -> {
Log.w(TAG, "Received status 401. Resetting initialized state + auth credentials.", error.exception)
SignalStore.backup.backupsInitialized = false
SignalStore.backup.clearAllCredentials()
SignalStore.backup.messageCredentials.clearAll()
SignalStore.backup.mediaCredentials.clearAll()
}
403 -> {
@@ -278,7 +279,7 @@ object BackupRepository {
archiveAttachment: (AttachmentTable.LocalArchivableAttachment, () -> InputStream?) -> Unit
) {
val writer = EncryptedBackupWriter(
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
key = SignalStore.backup.messageBackupKey,
aci = SignalStore.account.aci!!,
outputStream = NonClosingOutputStream(main),
append = { main.write(it) }
@@ -310,7 +311,7 @@ object BackupRepository {
fun export(
outputStream: OutputStream,
append: (ByteArray) -> Unit,
backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
messageBackupKey: org.whispersystems.signalservice.api.backup.MessageBackupKey = SignalStore.backup.messageBackupKey,
plaintext: Boolean = false,
currentTime: Long = System.currentTimeMillis(),
mediaBackupEnabled: Boolean = SignalStore.backup.backsUpMedia,
@@ -320,7 +321,7 @@ object BackupRepository {
PlainTextBackupWriter(outputStream)
} else {
EncryptedBackupWriter(
key = backupKey,
key = messageBackupKey,
aci = SignalStore.account.aci!!,
outputStream = outputStream,
append = append
@@ -368,7 +369,7 @@ object BackupRepository {
BackupInfo(
version = VERSION,
backupTimeMs = exportState.backupTime,
mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey?.toByteString() ?: ByteString.EMPTY
mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey.value.toByteString()
)
)
frameCount++
@@ -444,7 +445,7 @@ object BackupRepository {
}
fun localImport(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData): ImportResult {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val frameReader = try {
EncryptedBackupReader(
@@ -464,7 +465,7 @@ object BackupRepository {
}
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false, cancellationSignal: () -> Boolean = { false }): ImportResult {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val frameReader = if (plaintext) {
PlainTextBackupReader(inputStreamFactory(), length)
@@ -483,7 +484,7 @@ object BackupRepository {
}
private fun import(
backupKey: BackupKey,
messageBackupKey: MessageBackupKey,
frameReader: BackupImportReader,
selfData: SelfData,
cancellationSignal: () -> Boolean
@@ -557,7 +558,8 @@ object BackupRepository {
return ImportResult.Failure
}
SignalStore.backup.mediaRootBackupKey = header.mediaRootBackupKey.toByteArray()
val mediaRootBackupKey = MediaRootBackupKey(header.mediaRootBackupKey.toByteArray())
SignalStore.backup.mediaRootBackupKey = mediaRootBackupKey
// Add back self after clearing data
val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true)
@@ -567,7 +569,7 @@ object BackupRepository {
// Add back default All Chats chat folder after clearing data
SignalDatabase.chatFolders.insertAllChatFolder()
val importState = ImportState(backupKey)
val importState = ImportState(messageBackupKey, mediaRootBackupKey)
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
Log.d(TAG, "[import] Beginning to read frames.")
@@ -695,32 +697,35 @@ object BackupRepository {
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
val masterKey = SignalStore.svr.getOrCreateMasterKey()
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
val key = LibSignalMessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
}
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getArchiveMediaItemsPage(backupKey, SignalStore.account.requireAci(), credential, limit, cursor)
SignalNetwork.archive.getArchiveMediaItemsPage(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential, limit, cursor)
}
}
fun getRemoteBackupUsedSpace(): NetworkResult<Long?> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential)
.map { it.usedSpace }
}
}
/**
* If backups are enabled, sync with the network. Otherwise, return a 404.
* If backups are enabled, sync with the network. Otherwise, return a 404.a
* Used in instrumentation tests.
*/
fun getBackupTier(): NetworkResult<MessageBackupTier> {
return if (SignalStore.backup.areBackupsEnabled) {
@@ -736,11 +741,12 @@ object BackupRepository {
* to be the case.
*/
private fun getBackupTier(aci: ACI): NetworkResult<MessageBackupTier> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.map { credential ->
val zkCredential = SignalNetwork.archive.getZkCredential(backupKey, aci, credential)
val zkCredential = SignalNetwork.archive.getZkCredential(backupKey, aci, credential.mediaCredential)
if (zkCredential.backupLevel == BackupLevel.PAID) {
MessageBackupTier.PAID
} else {
@@ -753,16 +759,17 @@ object BackupRepository {
* Returns an object with details about the remote backup state.
*/
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential)
.map { it to credential }
}
.then { pair ->
val (info, credential) = pair
SignalNetwork.archive.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.debugGetUploadedMediaItemMetadata(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential)
.also { Log.i(TAG, "MediaItemMetadataResult: $it") }
.map { mediaObjects ->
BackupMetadata(
@@ -779,11 +786,12 @@ object BackupRepository {
* @return True if successful, otherwise false.
*/
fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): NetworkResult<Unit> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getMessageBackupUploadForm(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getMessageBackupUploadForm(backupKey, SignalStore.account.requireAci(), credential.messageCredential)
.also { Log.i(TAG, "UploadFormResult: $it") }
}
.then { form ->
@@ -799,11 +807,12 @@ object BackupRepository {
}
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential)
}
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.map { pair ->
@@ -814,11 +823,12 @@ object BackupRepository {
}
fun getBackupFileLastModified(): NetworkResult<ZonedDateTime?> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential)
}
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.then { pair ->
@@ -833,12 +843,13 @@ object BackupRepository {
/**
* Returns an object with details about the remote backup state.
*/
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
private fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.debugGetUploadedMediaItemMetadata(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential)
}
}
@@ -849,11 +860,12 @@ object BackupRepository {
* It's important to note that in order to get this to the archive cdn, you still need to use [copyAttachmentToArchive].
*/
fun getAttachmentUploadForm(): NetworkResult<AttachmentUploadForm> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getMediaUploadForm(backupKey, SignalStore.account.requireAci(), credential)
SignalNetwork.archive.getMediaUploadForm(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential)
}
}
@@ -861,15 +873,16 @@ object BackupRepository {
* Copies a thumbnail that has been uploaded to the transit cdn to the archive cdn.
*/
fun copyThumbnailToArchive(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), mediaRootBackupKey)
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.copyAttachmentToArchive(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
item = request
)
}
@@ -879,32 +892,34 @@ object BackupRepository {
* Copies an attachment that has been uploaded to the transit cdn to the archive cdn.
*/
fun copyAttachmentToArchive(attachment: DatabaseAttachment): NetworkResult<Unit> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
val mediaName = attachment.getMediaName()
val request = attachment.toArchiveMediaRequest(mediaName, backupKey)
val request = attachment.toArchiveMediaRequest(mediaName, mediaRootBackupKey)
SignalNetwork.archive
.copyAttachmentToArchive(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
item = request
)
.map { Triple(mediaName, request.mediaId, it) }
}
.map { (mediaName, mediaId, response) ->
val thumbnailId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
val thumbnailId = mediaRootBackupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId, archiveThumbnailMediaId = thumbnailId)
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun copyAttachmentToArchive(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
val requests = mutableListOf<ArchiveMediaRequest>()
val mediaIdToAttachmentId = mutableMapOf<String, AttachmentId>()
@@ -912,7 +927,7 @@ object BackupRepository {
databaseAttachments.forEach {
val mediaName = it.getMediaName()
val request = it.toArchiveMediaRequest(mediaName, backupKey)
val request = it.toArchiveMediaRequest(mediaName, mediaRootBackupKey)
requests += request
mediaIdToAttachmentId[request.mediaId] = it.attachmentId
attachmentIdToMediaName[it.attachmentId] = mediaName.name
@@ -920,9 +935,9 @@ object BackupRepository {
SignalNetwork.archive
.copyAttachmentToArchive(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
items = requests
)
.map { BatchArchiveMediaResult(it, mediaIdToAttachmentId, attachmentIdToMediaName) }
@@ -933,7 +948,7 @@ object BackupRepository {
.forEach {
val attachmentId = result.mediaIdToAttachmentId(it.mediaId)
val mediaName = result.attachmentIdToMediaName(attachmentId)
val thumbnailId = backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode()
val thumbnailId = mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode()
SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId, thumbnailId)
}
result
@@ -942,7 +957,8 @@ object BackupRepository {
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val mediaToDelete = attachments
.filter { it.archiveMediaId != null }
@@ -958,12 +974,12 @@ object BackupRepository {
return NetworkResult.Success(Unit)
}
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.deleteArchivedMedia(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
mediaToDelete = mediaToDelete
)
}
@@ -974,7 +990,8 @@ object BackupRepository {
}
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val mediaToDelete = mediaObjects
.map {
@@ -989,12 +1006,12 @@ object BackupRepository {
return NetworkResult.Success(Unit)
}
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.deleteArchivedMedia(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
mediaToDelete = mediaToDelete
)
}
@@ -1002,7 +1019,7 @@ object BackupRepository {
}
fun debugDeleteAllArchivedMedia(): NetworkResult<Unit> {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return debugGetArchivedMediaState()
.then { archivedMedia ->
@@ -1018,12 +1035,12 @@ object BackupRepository {
Log.i(TAG, "No media to delete, quick success")
NetworkResult.Success(Unit)
} else {
getAuthCredential()
getAuthCredentialPair()
.then { credential ->
SignalNetwork.archive.deleteArchivedMedia(
backupKey = backupKey,
mediaRootBackupKey = mediaRootBackupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential,
serviceCredential = credential.mediaCredential,
mediaToDelete = mediaToDelete
)
}
@@ -1044,15 +1061,16 @@ object BackupRepository {
return NetworkResult.Success(cached)
}
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getCdnReadCredentials(
cdnNumber = cdnNumber,
backupKey = backupKey,
messageBackupKey = backupKey,
aci = SignalStore.account.requireAci(),
serviceCredential = credential
serviceCredential = credential.mediaCredential
)
}
.also {
@@ -1107,11 +1125,12 @@ object BackupRepository {
)
}
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
return initBackupAndFetchAuth(backupKey)
return initBackupAndFetchAuth(backupKey, mediaRootBackupKey)
.then { credential ->
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential).map {
SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential).map {
SignalStore.backup.usedBackupMediaSpace = it.usedSpace ?: 0L
BackupDirectories(it.backupDir!!, it.mediaDir!!)
}
@@ -1179,14 +1198,15 @@ object BackupRepository {
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
* Should be the basis of all backup operations.
*/
private fun initBackupAndFetchAuth(backupKey: BackupKey): NetworkResult<ArchiveServiceCredential> {
private fun initBackupAndFetchAuth(messageBackupKey: MessageBackupKey, mediaRootBackupKey: MediaRootBackupKey): NetworkResult<ArchiveServiceCredentialPair> {
return if (SignalStore.backup.backupsInitialized) {
getAuthCredential().runOnStatusCodeError(resetInitializedStateErrorAction)
getAuthCredentialPair().runOnStatusCodeError(resetInitializedStateErrorAction)
} else {
return SignalNetwork.archive
.triggerBackupIdReservation(backupKey, SignalStore.account.requireAci())
.then { getAuthCredential() }
.then { credential -> SignalNetwork.archive.setPublicKey(backupKey, SignalStore.account.requireAci(), credential).map { credential } }
.triggerBackupIdReservation(messageBackupKey, mediaRootBackupKey, SignalStore.account.requireAci())
.then { getAuthCredentialPair() }
.then { credential -> SignalNetwork.archive.setPublicKey(messageBackupKey, SignalStore.account.requireAci(), credential.messageCredential).map { credential } }
.then { credential -> SignalNetwork.archive.setPublicKey(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential).map { credential } }
.runIfSuccessful { SignalStore.backup.backupsInitialized = true }
.runOnStatusCodeError(resetInitializedStateErrorAction)
}
@@ -1195,21 +1215,29 @@ object BackupRepository {
/**
* Retrieves an auth credential, preferring a cached value if available.
*/
private fun getAuthCredential(): NetworkResult<ArchiveServiceCredential> {
private fun getAuthCredentialPair(): NetworkResult<ArchiveServiceCredentialPair> {
val currentTime = System.currentTimeMillis()
val credential = SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)
val messageCredential = SignalStore.backup.messageCredentials.byDay.getForCurrentTime(currentTime.milliseconds)
val mediaCredential = SignalStore.backup.mediaCredentials.byDay.getForCurrentTime(currentTime.milliseconds)
if (credential != null) {
return NetworkResult.Success(credential)
if (messageCredential != null && mediaCredential != null) {
return NetworkResult.Success(ArchiveServiceCredentialPair(messageCredential, mediaCredential))
}
Log.w(TAG, "No credentials found for today, need to fetch new ones! This shouldn't happen under normal circumstances. We should ensure the routine fetch is running properly.")
return SignalNetwork.archive.getServiceCredentials(currentTime).map { result ->
SignalStore.backup.addCredentials(result.credentials.toList())
SignalStore.backup.clearCredentialsOlderThan(currentTime)
SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
SignalStore.backup.messageCredentials.add(result.messageCredentials)
SignalStore.backup.messageCredentials.clearOlderThan(currentTime)
SignalStore.backup.mediaCredentials.add(result.mediaCredentials)
SignalStore.backup.mediaCredentials.clearOlderThan(currentTime)
ArchiveServiceCredentialPair(
messageCredential = SignalStore.backup.messageCredentials.byDay.getForCurrentTime(currentTime.milliseconds)!!,
mediaCredential = SignalStore.backup.mediaCredentials.byDay.getForCurrentTime(currentTime.milliseconds)!!
)
}
}
@@ -1232,8 +1260,8 @@ object BackupRepository {
return MediaName.fromDigestForThumbnail(remoteDigest!!)
}
private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(mediaName)
private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, mediaRootBackupKey: MediaRootBackupKey): ArchiveMediaRequest {
val mediaSecrets = mediaRootBackupKey.deriveMediaSecrets(mediaName)
return ArchiveMediaRequest(
sourceAttachment = ArchiveMediaRequest.SourceAttachment(
@@ -1243,8 +1271,7 @@ object BackupRepository {
objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(),
mediaId = mediaSecrets.id.encode(),
hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey),
encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey),
iv = Base64.encodeWithPadding(mediaSecrets.iv)
encryptionKey = Base64.encodeWithPadding(mediaSecrets.aesKey)
)
}
@@ -1269,7 +1296,7 @@ class ExportState(val backupTime: Long, val mediaBackupEnabled: Boolean) {
val localToRemoteCustomChatColors: MutableMap<Long, Int> = hashMapOf()
}
class ImportState(val backupKey: BackupKey) {
class ImportState(val messageBackupKey: MessageBackupKey, val mediaRootBackupKey: MediaRootBackupKey) {
val remoteToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
val chatIdToLocalThreadId: MutableMap<Long, Long> = hashMapOf()
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()

View File

@@ -34,13 +34,13 @@ fun DatabaseAttachment.createArchiveAttachmentPointer(useArchiveCdn: Boolean): S
return try {
val (remoteId, cdnNumber) = if (useArchiveCdn) {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow()
val id = SignalServiceAttachmentRemoteId.Backup(
backupDir = backupDirectories.backupDir,
mediaDir = backupDirectories.mediaDir,
mediaId = backupKey.deriveMediaId(MediaName(archiveMediaName!!)).encode()
mediaId = mediaRootBackupKey.deriveMediaId(MediaName(archiveMediaName!!)).encode()
)
id to archiveCdn
@@ -91,11 +91,11 @@ fun DatabaseAttachment.createArchiveThumbnailPointer(): SignalServiceAttachmentP
throw InvalidAttachmentException("empty encrypted key")
}
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow()
return try {
val key = backupKey.deriveThumbnailTransitKey(getThumbnailMediaName())
val mediaId = backupKey.deriveMediaId(getThumbnailMediaName()).encode()
val key = mediaRootBackupKey.deriveThumbnailTransitKey(getThumbnailMediaName())
val mediaId = mediaRootBackupKey.deriveMediaId(getThumbnailMediaName()).encode()
SignalServiceAttachmentPointer(
cdnNumber = archiveCdn,
remoteId = SignalServiceAttachmentRemoteId.Backup(

View File

@@ -13,7 +13,7 @@ import org.signal.core.util.stream.LimitedInputStream
import org.signal.core.util.stream.MacInputStream
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.io.EOFException
import java.io.IOException
@@ -31,7 +31,7 @@ import javax.crypto.spec.SecretKeySpec
* that decrypted data is gunzipped, then that data is read as frames.
*/
class EncryptedBackupReader(
key: BackupKey,
key: MessageBackupKey,
aci: ACI,
val length: Long,
dataStream: () -> InputStream
@@ -51,7 +51,7 @@ class EncryptedBackupReader(
val iv = countingStream.readNBytesOrThrow(16)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
stream = GZIPInputStream(

View File

@@ -10,7 +10,7 @@ import org.signal.core.util.writeVarInt32
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
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.backup.MessageBackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.io.IOException
import java.io.OutputStream
@@ -27,7 +27,7 @@ import javax.crypto.spec.SecretKeySpec
* to the end of the [outputStream].
*/
class EncryptedBackupWriter(
key: BackupKey,
key: MessageBackupKey,
aci: ACI,
private val outputStream: OutputStream,
private val append: (ByteArray) -> Unit
@@ -44,7 +44,7 @@ class EncryptedBackupWriter(
outputStream.flush()
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
val mac = Mac.getInstance("HmacSHA256").apply {

View File

@@ -103,7 +103,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
val context = LocalContext.current
MessageBackupsKeyRecordScreen(
backupKey = state.backupKey,
messageBackupKey = state.messageBackupKey,
onNavigationClick = viewModel::goToPreviousStage,
onNextClick = viewModel::goToNextStage,
onCopyToClipboardClick = {

View File

@@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
data class MessageBackupsFlowState(
val hasBackupSubscriberAvailable: Boolean = false,
@@ -18,6 +18,6 @@ data class MessageBackupsFlowState(
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
val startScreen: MessageBackupsStage,
val stage: MessageBackupsStage = startScreen,
val backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
val messageBackupKey: MessageBackupKey = SignalStore.backup.messageBackupKey,
val failure: Throwable? = null
)

View File

@@ -50,7 +50,7 @@ import org.signal.core.ui.SignalPreview
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.Hex
import org.thoughtcrime.securesms.R
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import kotlin.random.Random
/**
@@ -60,7 +60,7 @@ import kotlin.random.Random
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsKeyRecordScreen(
backupKey: BackupKey,
messageBackupKey: MessageBackupKey,
onNavigationClick: () -> Unit = {},
onCopyToClipboardClick: (String) -> Unit = {},
onNextClick: () -> Unit = {}
@@ -104,8 +104,8 @@ fun MessageBackupsKeyRecordScreen(
modifier = Modifier.padding(top = 12.dp)
)
val backupKeyString = remember(backupKey) {
backupKey.value.toList().chunked(2).map { Hex.toStringCondensed(it.toByteArray()) }.joinToString(" ")
val backupKeyString = remember(messageBackupKey) {
messageBackupKey.value.toList().chunked(2).map { Hex.toStringCondensed(it.toByteArray()) }.joinToString(" ")
}
Box(
@@ -258,7 +258,7 @@ private fun BottomSheetContent(
private fun MessageBackupsKeyRecordScreenPreview() {
Previews.Preview {
MessageBackupsKeyRecordScreen(
backupKey = BackupKey(Random.nextBytes(32))
messageBackupKey = MessageBackupKey(Random.nextBytes(32))
)
}
}

View File

@@ -96,8 +96,8 @@ fun FilePointer?.toLocalAttachment(
cdnKey = this.backupLocator.transitCdnKey,
archiveCdn = this.backupLocator.cdnNumber,
archiveMediaName = this.backupLocator.mediaName,
archiveMediaId = importState.backupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(),
archiveMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(),
digest = this.backupLocator.digest.toByteArray(),
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,

View File

@@ -19,7 +19,7 @@ class BackupKeyDisplayFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
messageBackupKey = SignalStore.backup.messageBackupKey,
onNavigationClick = { findNavController().popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it) },
onNextClick = { findNavController().popBackStack() }

View File

@@ -366,6 +366,25 @@ fun Screen(
Dividers.Default()
Buttons.LargeTonal(
onClick = {
SignalStore.backup.backupsInitialized = false
}
) {
Text("Clear backup init flag")
}
Buttons.LargeTonal(
onClick = {
SignalStore.backup.messageCredentials.clearAll()
SignalStore.backup.mediaCredentials.clearAll()
}
) {
Text("Clear backup credentials")
}
Dividers.Default()
Row(
verticalAlignment = Alignment.CenterVertically
) {

View File

@@ -475,14 +475,15 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
attachments: List<BackupAttachment> = this.attachments,
inProgress: Set<AttachmentId> = this.inProgressMediaIds
): MediaState {
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val updatedAttachments = attachments.map {
val state = if (inProgress.contains(it.dbAttachment.attachmentId)) {
BackupAttachment.State.IN_PROGRESS
} else if (it.dbAttachment.archiveMediaName != null) {
if (it.dbAttachment.remoteDigest != null) {
val mediaId = backupKey.deriveMediaId(MediaName(it.dbAttachment.archiveMediaName)).encode()
val mediaId = mediaRootBackupKey.deriveMediaId(MediaName(it.dbAttachment.archiveMediaName)).encode()
if (it.dbAttachment.archiveMediaId == mediaId) {
BackupAttachment.State.UPLOADED_FINAL
} else {
@@ -552,10 +553,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
val encryptedStream = tempBackupFile.inputStream()
val iv = encryptedStream.readNBytesOrThrow(16)
val backupKey = SignalStore.svr.orCreateMasterKey.deriveBackupKey()
val backupKey = SignalStore.backup.messageBackupKey
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
val plaintextStream = GZIPInputStream(

View File

@@ -92,13 +92,13 @@ class ArchiveThumbnailUploadJob private constructor(
return Result.success()
}
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val specResult = BackupRepository
.getAttachmentUploadForm()
.then { form ->
SignalNetwork.attachments.getResumableUploadSpec(
key = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()),
key = mediaRootBackupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()),
iv = attachment.remoteIv!!,
uploadForm = form
)
@@ -133,13 +133,12 @@ class ArchiveThumbnailUploadJob private constructor(
return Result.retry(defaultBackoff())
}
val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow()
val mediaSecrets = backupKey.deriveMediaSecrets(attachment.getThumbnailMediaName())
val mediaSecrets = mediaRootBackupKey.deriveMediaSecrets(attachment.getThumbnailMediaName())
return when (val result = BackupRepository.copyThumbnailToArchive(attachmentPointer, attachment)) {
is NetworkResult.Success -> {
// save attachment thumbnail
val archiveMediaId = attachment.archiveMediaId ?: backupKey.deriveMediaId(attachment.getMediaName()).encode()
val archiveMediaId = attachment.archiveMediaId ?: mediaRootBackupKey.deriveMediaId(attachment.getMediaName()).encode()
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterUpload(attachmentId, archiveMediaId, mediaSecrets.id, thumbnailResult.data)
Log.d(TAG, "Successfully archived thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}")

View File

@@ -225,7 +225,7 @@ class RestoreAttachmentJob private constructor(
messageReceiver
.retrieveArchivedAttachment(
SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(MediaName(attachment.archiveMediaName!!)),
SignalStore.backup.mediaRootBackupKey.deriveMediaSecrets(MediaName(attachment.archiveMediaName!!)),
cdnCredentials,
archiveFile,
pointer,

View File

@@ -123,7 +123,7 @@ class RestoreAttachmentThumbnailJob private constructor(
Log.i(TAG, "Downloading thumbnail for $attachmentId")
val downloadResult = AppDependencies.signalServiceMessageReceiver
.retrieveArchivedAttachment(
SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()),
SignalStore.backup.mediaRootBackupKey.deriveMediaSecrets(attachment.getThumbnailMediaName()),
cdnCredentials,
thumbnailTransferFile,
pointer,

View File

@@ -2,15 +2,20 @@ package org.thoughtcrime.securesms.keyvalue
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.coroutines.flow.Flow
import okio.withLock
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.RestoreState
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.internal.util.JsonUtil
import java.io.IOException
import java.util.concurrent.locks.ReentrantLock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
@@ -19,7 +24,8 @@ import kotlin.time.Duration.Companion.milliseconds
class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
companion object {
val TAG = Log.tag(BackupValues::class.java)
private const val KEY_CREDENTIALS = "backup.credentials"
private const val KEY_MESSAGE_CREDENTIALS = "backup.messageCredentials"
private const val KEY_MEDIA_CREDENTIALS = "backup.mediaCredentials"
private const val KEY_CDN_READ_CREDENTIALS = "backup.cdn.readCredentials"
private const val KEY_CDN_READ_CREDENTIALS_TIMESTAMP = "backup.cdn.readCredentials.timestamp"
private const val KEY_RESTORE_STATE = "backup.restoreState"
@@ -53,6 +59,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey"
private val cachedCdnCredentialsExpiresIn: Duration = 12.hours
private val lock = ReentrantLock()
}
override fun onFirstEverAppLaunch() = Unit
@@ -74,7 +82,35 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1)
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
var mediaRootBackupKey: ByteArray? by nullableBlobValue(KEY_MEDIA_ROOT_BACKUP_KEY, null)
/**
* Key used to backup messages.
*/
val messageBackupKey: MessageBackupKey
get() = SignalStore.svr.getOrCreateMasterKey().derivateMessageBackupKey()
/**
* Key used to backup media. Purely random and separate from the message backup key.
*/
var mediaRootBackupKey: MediaRootBackupKey
get() {
lock.withLock {
val value: ByteArray? = getBlob(KEY_MEDIA_ROOT_BACKUP_KEY, null)
if (value != null) {
return MediaRootBackupKey(value)
}
Log.i(TAG, "Generating MediaRootBackupKey...", Throwable())
val bytes = Util.getSecretBytes(32)
putBlob(KEY_MEDIA_ROOT_BACKUP_KEY, bytes)
return MediaRootBackupKey(bytes)
}
}
set(value) {
lock.withLock {
Log.i(TAG, "Setting MediaRootBackupKey", Throwable())
putBlob(KEY_MEDIA_ROOT_BACKUP_KEY, value.value)
}
}
/**
* This is the 'latest' backup tier. This isn't necessarily the user's current backup tier, so this should only ever
@@ -153,24 +189,11 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
val isRestoreInProgress: Boolean
get() = totalRestorableAttachmentSize > 0
/**
* Retrieves the stored credentials, mapped by the day they're valid. The day is represented as
* the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials]
* type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime].
*/
val credentialsByDay: ArchiveServiceCredentials
get() {
val serialized = store.getString(KEY_CREDENTIALS, null) ?: return ArchiveServiceCredentials()
/** Store that lets you interact with message ZK credentials. */
val messageCredentials = CredentialStore(KEY_MESSAGE_CREDENTIALS)
return try {
val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay
ArchiveServiceCredentials(map)
} catch (e: IOException) {
Log.w(TAG, "Invalid JSON! Clearing.", e)
putString(KEY_CREDENTIALS, null)
ArchiveServiceCredentials()
}
}
/** Store that lets you interact with media ZK credentials. */
val mediaCredentials = CredentialStore(KEY_MEDIA_CREDENTIALS)
var cdnReadCredentials: GetArchiveCdnCredentialsResponse?
get() {
@@ -194,26 +217,44 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
cachedCdnCredentialsTimestamp = System.currentTimeMillis()
}
/**
* Adds the given credentials to the existing list of stored credentials.
*/
fun addCredentials(credentials: List<ArchiveServiceCredential>) {
val current: MutableMap<Long, ArchiveServiceCredential> = credentialsByDay.toMutableMap()
current.putAll(credentials.associateBy { it.redemptionTime })
putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(current)))
}
inner class CredentialStore(val key: String) {
/**
* Retrieves the stored media credentials, mapped by the day they're valid. The day is represented as
* the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials]
* type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime].
*/
val byDay: ArchiveServiceCredentials
get() {
val serialized = store.getString(key, null) ?: return ArchiveServiceCredentials()
/**
* Trims out any credentials that are for days older than the given timestamp.
*/
fun clearCredentialsOlderThan(startOfDayInSeconds: Long) {
val current: MutableMap<Long, ArchiveServiceCredential> = credentialsByDay.toMutableMap()
val updated = current.filterKeys { it < startOfDayInSeconds }
putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(updated)))
}
return try {
val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay
ArchiveServiceCredentials(map)
} catch (e: IOException) {
Log.w(TAG, "Invalid JSON! Clearing.", e)
putString(key, null)
ArchiveServiceCredentials()
}
}
fun clearAllCredentials() {
putString(KEY_CREDENTIALS, null)
/** Adds the given credentials to the existing list of stored credentials. */
fun add(credentials: List<ArchiveServiceCredential>) {
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
current.putAll(credentials.associateBy { it.redemptionTime })
putString(key, JsonUtil.toJson(SerializedCredentials(current)))
}
/** Trims out any credentials that are for days older than the given timestamp. */
fun clearOlderThan(startOfDayInSeconds: Long) {
val current: MutableMap<Long, ArchiveServiceCredential> = byDay.toMutableMap()
val updated = current.filterKeys { it < startOfDayInSeconds }
putString(key, JsonUtil.toJson(SerializedCredentials(updated)))
}
/** Clears all credentials. */
fun clearAll() {
putString(key, null)
}
}
fun markMessageBackupFailure() {

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse
import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
@@ -115,9 +115,9 @@ object LinkDeviceRepository {
/**
* Adds a linked device to the account.
*
* @param ephemeralBackupKey An ephemeral key to provide the linked device to sync existing message content. Do not set if link+sync is unsupported.
* @param ephemeralMessageBackupKey An ephemeral key to provide the linked device to sync existing message content. Do not set if link+sync is unsupported.
*/
fun addDevice(uri: Uri, ephemeralBackupKey: BackupKey?): LinkDeviceResult {
fun addDevice(uri: Uri, ephemeralMessageBackupKey: MessageBackupKey?): LinkDeviceResult {
if (!isValidQr(uri)) {
Log.w(TAG, "Bad URI! $uri")
return LinkDeviceResult.BadCode
@@ -155,7 +155,7 @@ object LinkDeviceRepository {
profileKey = ProfileKeyUtil.getSelfProfileKey(),
masterKey = SignalStore.svr.getOrCreateMasterKey(),
code = verificationCodeResult.verificationCode,
ephemeralBackupKey = ephemeralBackupKey
ephemeralMessageBackupKey = ephemeralMessageBackupKey
)
return when (deviceLinkResult) {
@@ -227,13 +227,13 @@ object LinkDeviceRepository {
/**
* Performs the entire process of creating and uploading an archive for a newly-linked device.
*/
fun createAndUploadArchive(ephemeralBackupKey: BackupKey, deviceId: Int, deviceCreatedAt: Long): LinkUploadArchiveResult {
fun createAndUploadArchive(ephemeralMessageBackupKey: MessageBackupKey, deviceId: Int, deviceCreatedAt: Long): LinkUploadArchiveResult {
val stopwatch = Stopwatch("link-archive")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val outputStream = FileOutputStream(tempBackupFile)
try {
BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, backupKey = ephemeralBackupKey, mediaBackupEnabled = false)
BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, messageBackupKey = ephemeralMessageBackupKey, mediaBackupEnabled = false)
} catch (e: Exception) {
return LinkUploadArchiveResult.BackupCreationFailure(e)
}

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.OneTimeEven
import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.QrCodeState
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse
import kotlin.time.Duration.Companion.seconds
@@ -198,8 +198,8 @@ class LinkDeviceViewModel : ViewModel() {
}
private fun addDeviceWithSync(linkUri: Uri) {
val ephemeralBackupKey = BackupKey(Util.getSecretBytes(32))
val result = LinkDeviceRepository.addDevice(linkUri, ephemeralBackupKey)
val ephemeralMessageBackupKey = MessageBackupKey(Util.getSecretBytes(32))
val result = LinkDeviceRepository.addDevice(linkUri, ephemeralMessageBackupKey)
_state.update {
it.copy(
@@ -235,7 +235,7 @@ class LinkDeviceViewModel : ViewModel() {
}
Log.i(TAG, "Beginning the archive generation process...")
val uploadResult = LinkDeviceRepository.createAndUploadArchive(ephemeralBackupKey, waitResult.id, waitResult.created)
val uploadResult = LinkDeviceRepository.createAndUploadArchive(ephemeralMessageBackupKey, waitResult.id, waitResult.created)
when (uploadResult) {
LinkDeviceRepository.LinkUploadArchiveResult.Success -> {
_state.update {
@@ -258,7 +258,7 @@ class LinkDeviceViewModel : ViewModel() {
}
private fun addDeviceWithoutSync(linkUri: Uri) {
val result = LinkDeviceRepository.addDevice(linkUri, ephemeralBackupKey = null)
val result = LinkDeviceRepository.addDevice(linkUri, ephemeralMessageBackupKey = null)
_state.update {
it.copy(

View File

@@ -19,8 +19,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.PartUriParser;
import org.signal.core.util.Base64;
import org.whispersystems.signalservice.api.backup.BackupKey;
import org.whispersystems.signalservice.api.backup.MediaId;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
@@ -72,8 +71,8 @@ class PartDataSource implements DataSource {
if (attachment.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && attachment.archiveMediaId != null) {
final File archiveFile = attachmentDatabase.getOrCreateArchiveTransferFile(attachment.attachmentId);
try {
BackupKey.MediaKeyMaterial mediaKeyMaterial = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey().deriveMediaSecretsFromMediaId(attachment.archiveMediaId);
long originalCipherLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size));
MediaRootBackupKey.MediaKeyMaterial mediaKeyMaterial = SignalStore.backup().getMediaRootBackupKey().deriveMediaSecretsFromMediaId(attachment.archiveMediaId);
long originalCipherLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size));
this.inputStream = AttachmentCipherInputStream.createStreamingForArchivedAttachment(mediaKeyMaterial, archiveFile, originalCipherLength, attachment.size, attachment.remoteDigest, decode, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize);
} catch (InvalidMessageException e) {