mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Move to separate message and media backup keys.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
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.ByteArrayOutputStream
|
||||
import java.util.UUID
|
||||
@@ -22,7 +22,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
|
||||
@Test
|
||||
fun `can read back all of the frames we write`() {
|
||||
val key = BackupKey(Util.getSecretBytes(32))
|
||||
val key = MessageBackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
@@ -54,7 +54,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
|
||||
@Test
|
||||
fun `padding limits number of sizes`() {
|
||||
val key = BackupKey(Util.getSecretBytes(32))
|
||||
val key = MessageBackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
|
||||
val uniqueSizes = (1..10)
|
||||
@@ -78,7 +78,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
|
||||
@Test
|
||||
fun `using a different IV every time`() {
|
||||
val key = BackupKey(Util.getSecretBytes(32))
|
||||
val key = MessageBackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
val count = 10
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ dependencyResolutionManagement {
|
||||
version("exoplayer", "2.19.0")
|
||||
version("glide", "4.15.1")
|
||||
version("kotlin", "1.9.20")
|
||||
version("libsignal-client", "0.60.0")
|
||||
version("libsignal-client", "0.60.1")
|
||||
version("mp4parser", "1.9.39")
|
||||
version("android-gradle-plugin", "8.4.0")
|
||||
version("accompanist", "0.28.0")
|
||||
|
||||
@@ -9013,28 +9013,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="cf341613dbc8aa7107f9971bfd25c51fc7ea8e789890d643b0015b8ce33c1558" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-android" version="0.60.0">
|
||||
<artifact name="libsignal-android-0.60.0.aar">
|
||||
<sha256 value="17c7b9aa9e3afde0197a918c32062cb26bf5389e6570e5e438868ac7f040e93b" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-android" version="0.60.1">
|
||||
<artifact name="libsignal-android-0.60.1.aar">
|
||||
<sha256 value="883fe0df856f88889024dc83a14c0ccda8ddb181b4adcfbb087e10eeb1e9a25b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-android-0.60.0.module">
|
||||
<sha256 value="1fc4997b239fb6b7d1292936c8364eb148c03a1a21b1baa7bd7f126832dcba33" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-android-0.60.1.module">
|
||||
<sha256 value="20589d089af667c5fddba272f0bb391413b0b41b157cebb92dfafc15092def02" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-client" version="0.58.2">
|
||||
<artifact name="libsignal-client-0.58.2.jar">
|
||||
<sha256 value="64eba31a2ee0af76558ea64642ab117128ba4ad616232aeaa416c021bfe627ba" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-client" version="0.60.1">
|
||||
<artifact name="libsignal-client-0.60.1.jar">
|
||||
<sha256 value="0d2fe21579b89bd39466df3cf3b4ecce828dcda7f99ddb3d9bbef0a8a0b01173" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-client-0.58.2.module">
|
||||
<sha256 value="c9485d4ec1090038e504f7bdfc24ceb53e955b8241fa4ceeda5092b74fe5aaa0" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-client" version="0.60.0">
|
||||
<artifact name="libsignal-client-0.60.0.jar">
|
||||
<sha256 value="24895ab1bcf6bd36cb5dd6b5a90313532ecf186626326ea58a5bfca93ba711e6" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-client-0.60.0.module">
|
||||
<sha256 value="b58881053be1673628e060f27ce9b25eb768ceb91899f4902c06ac5b1eaf74ac" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-client-0.60.1.module">
|
||||
<sha256 value="ac7b7726438d72e962a0329d481c0d1c5ac6ee73589a1b51b298bbb4eced22bd" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="ringrtc-android" version="2.48.4">
|
||||
|
||||
@@ -12,10 +12,9 @@ import org.signal.core.util.concurrent.ListenableFuture;
|
||||
import org.signal.core.util.concurrent.SettableFuture;
|
||||
import org.signal.core.util.stream.LimitedInputStream;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentDownloadResult;
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||
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.api.crypto.ProfileCipherInputStream;
|
||||
@@ -29,9 +28,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.IdentityCheckRequest;
|
||||
import org.whispersystems.signalservice.internal.push.IdentityCheckResponse;
|
||||
@@ -187,7 +184,7 @@ public class SignalServiceMessageReceiver {
|
||||
*
|
||||
* @return An InputStream that streams the plaintext attachment contents.
|
||||
*/
|
||||
public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
|
||||
public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
|
||||
@Nonnull Map<String, String> readCredentialHeaders,
|
||||
@Nonnull File archiveDestination,
|
||||
@Nonnull SignalServiceAttachmentPointer pointer,
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse.StoredMediaObject
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
@@ -44,6 +46,13 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* credentials is to keep the caller anonymous, but that doesn't help if this authenticated request
|
||||
* happens right before all of the unauthenticated ones, as that would make it easier to correlate
|
||||
* traffic.
|
||||
*
|
||||
* GET /v1/archives/auth
|
||||
*
|
||||
* - 200: Success
|
||||
* - 400: Bad start/end times
|
||||
* - 404: BackupId could not be found
|
||||
* - 429: Rate-limited
|
||||
*/
|
||||
fun getServiceCredentials(currentTime: Long): NetworkResult<ArchiveServiceCredentialsResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
@@ -51,30 +60,46 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<GetArchiveCdnCredentialsResponse> {
|
||||
fun getCdnReadCredentials(cdnNumber: Int, messageBackupKey: MessageBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<GetArchiveCdnCredentialsResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(messageBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that you reserve a backupId on the service. This must be done before any other
|
||||
* Ensures that you reserve backupIds for both messages and media on the service. This must be done before any other
|
||||
* backup-related calls. You only need to do it once, but repeated calls are safe.
|
||||
*
|
||||
* PUT /v1/archives/backupid
|
||||
*
|
||||
* - 204: Success
|
||||
* - 400: Invalid credential
|
||||
* - 429: Rate-limited
|
||||
*
|
||||
*/
|
||||
fun triggerBackupIdReservation(backupKey: BackupKey, aci: ACI): NetworkResult<Unit> {
|
||||
fun triggerBackupIdReservation(messageBackupKey: MessageBackupKey, mediaRootBackupKey: MediaRootBackupKey, aci: ACI): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
|
||||
pushServiceSocket.setArchiveBackupId(backupRequestContext.request)
|
||||
val messageBackupRequestContext = BackupAuthCredentialRequestContext.create(messageBackupKey.value, aci.rawUuid)
|
||||
val mediaBackupRequestContext = BackupAuthCredentialRequestContext.create(mediaRootBackupKey.value, aci.rawUuid)
|
||||
pushServiceSocket.setArchiveBackupId(messageBackupRequestContext.request, mediaBackupRequestContext.request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a public key on the service derived from your [BackupKey]. This key is used to prevent
|
||||
* Sets a public key on the service derived from your [MessageBackupKey]. This key is used to prevent
|
||||
* unauthorized users from changing your backup data. You only need to do it once, but repeated
|
||||
* calls are safe.
|
||||
*
|
||||
* PUT /v1/archives/keys
|
||||
*
|
||||
* - 204: Success
|
||||
* - 400: Bad arguments, or request was made on an authenticated channel
|
||||
* - 401: Bad presentation, invalid public key signature, no matching backupId on teh server, or the credential was of the wrong type (messages/media)
|
||||
* - 403: Forbidden
|
||||
* - 429: Rate-limited
|
||||
*/
|
||||
fun setPublicKey(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
@@ -87,16 +112,16 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
/**
|
||||
* Fetches an upload form you can use to upload your main message backup file to cloud storage.
|
||||
*
|
||||
* Responses
|
||||
* 200: Success
|
||||
* 400: Bad args, or made on an authenticated channel
|
||||
* 403: Insufficient permissions
|
||||
* 429: Rate-limited
|
||||
* GET /v1/archives/upload/form
|
||||
* - 200: Success
|
||||
* - 400: Bad args, or made on an authenticated channel
|
||||
* - 403: Insufficient permissions
|
||||
* - 429: Rate-limited
|
||||
*/
|
||||
fun getMessageBackupUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
|
||||
fun getMessageBackupUploadForm(messageBackupKey: MessageBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(messageBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation())
|
||||
}
|
||||
}
|
||||
@@ -106,10 +131,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a
|
||||
* backup yet.
|
||||
*/
|
||||
fun getBackupInfo(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<ArchiveGetBackupInfoResponse> {
|
||||
fun getBackupInfo(messageBackupKey: MessageBackupKey, aci: ACI, messageServiceCredential: ArchiveServiceCredential): NetworkResult<ArchiveGetBackupInfoResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(messageBackupKey, aci, messageServiceCredential)
|
||||
val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation())
|
||||
}
|
||||
}
|
||||
@@ -117,10 +142,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
/**
|
||||
* Lists the media objects in the backup
|
||||
*/
|
||||
fun listMediaObjects(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
fun listMediaObjects(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor)
|
||||
}
|
||||
}
|
||||
@@ -150,11 +175,18 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* so we can request them more often (which is required for backfilling).
|
||||
*
|
||||
* After uploading, the media still needs to be copied via [copyAttachmentToArchive].
|
||||
*
|
||||
* GET /v1/archives/media/upload/form
|
||||
*
|
||||
* - 200: Success
|
||||
* - 400: Bad request, or made on authenticated channel
|
||||
* - 403: Forbidden
|
||||
* - 429: Rate-limited
|
||||
*/
|
||||
fun getMediaUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
|
||||
fun getMediaUploadForm(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation())
|
||||
}
|
||||
}
|
||||
@@ -163,13 +195,13 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* Retrieves all media items in the user's archive. Note that this could be a very large number of items, making this only suitable for debugging.
|
||||
* Use [getArchiveMediaItemsPage] in production.
|
||||
*/
|
||||
fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<List<StoredMediaObject>> {
|
||||
fun debugGetUploadedMediaItemMetadata(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<List<StoredMediaObject>> {
|
||||
return NetworkResult.fromFetch {
|
||||
val mediaObjects: MutableList<StoredMediaObject> = ArrayList()
|
||||
|
||||
var cursor: String? = null
|
||||
do {
|
||||
val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, aci, serviceCredential, 512, cursor).successOrThrow()
|
||||
val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(mediaRootBackupKey, aci, serviceCredential, 512, cursor).successOrThrow()
|
||||
mediaObjects += response.storedMediaObjects
|
||||
cursor = response.cursor
|
||||
} while (cursor != null)
|
||||
@@ -183,10 +215,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* @param limit The maximum number of items to return.
|
||||
* @param cursor A token that can be read from your previous response, telling the server where to start the next page.
|
||||
*/
|
||||
fun getArchiveMediaItemsPage(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
fun getArchiveMediaItemsPage(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, mediaServiceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, mediaServiceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor)
|
||||
}
|
||||
@@ -204,14 +236,14 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* 429: Rate-limited
|
||||
*/
|
||||
fun copyAttachmentToArchive(
|
||||
backupKey: BackupKey,
|
||||
mediaRootBackupKey: MediaRootBackupKey,
|
||||
aci: ACI,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
item: ArchiveMediaRequest
|
||||
): NetworkResult<ArchiveMediaResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item)
|
||||
}
|
||||
@@ -221,14 +253,14 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
|
||||
*/
|
||||
fun copyAttachmentToArchive(
|
||||
backupKey: BackupKey,
|
||||
mediaRootBackupKey: MediaRootBackupKey,
|
||||
aci: ACI,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
items: List<ArchiveMediaRequest>
|
||||
): NetworkResult<BatchArchiveMediaResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
|
||||
val request = BatchArchiveMediaRequest(items = items)
|
||||
|
||||
@@ -238,16 +270,23 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
|
||||
/**
|
||||
* Delete media from the backup cdn.
|
||||
*
|
||||
* POST /v1/archives/media/delete
|
||||
*
|
||||
* - 400: Bad args or made on an authenticated channel
|
||||
* - 401: Bad presentation, invalid public key signature, no matching backupId on teh server, or the credential was of the wrong type (messages/media)
|
||||
* - 403: Forbidden
|
||||
* - 429: Rate-limited
|
||||
*/
|
||||
fun deleteArchivedMedia(
|
||||
backupKey: BackupKey,
|
||||
mediaRootBackupKey: MediaRootBackupKey,
|
||||
aci: ACI,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
mediaToDelete: List<DeleteArchivedMediaRequest.ArchivedMediaObject>
|
||||
): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams)
|
||||
val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete)
|
||||
|
||||
pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request)
|
||||
|
||||
@@ -15,8 +15,7 @@ class ArchiveMediaRequest(
|
||||
@JsonProperty val objectLength: Int,
|
||||
@JsonProperty val mediaId: String,
|
||||
@JsonProperty val hmacKey: String,
|
||||
@JsonProperty val encryptionKey: String,
|
||||
@JsonProperty val iv: String
|
||||
@JsonProperty val encryptionKey: String
|
||||
) {
|
||||
class SourceAttachment(
|
||||
@JsonProperty val cdn: Int,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
/**
|
||||
* A convenient container for passing around both a message and media archive service credential.
|
||||
*/
|
||||
data class ArchiveServiceCredentialPair(
|
||||
val messageCredential: ArchiveServiceCredential,
|
||||
val mediaCredential: ArchiveServiceCredential
|
||||
)
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import okio.IOException
|
||||
|
||||
/**
|
||||
* Represents the result of fetching archive credentials.
|
||||
@@ -13,5 +14,26 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
*/
|
||||
class ArchiveServiceCredentialsResponse(
|
||||
@JsonProperty
|
||||
val credentials: Array<ArchiveServiceCredential>
|
||||
)
|
||||
val credentials: Map<String, List<ArchiveServiceCredential>>
|
||||
) {
|
||||
companion object {
|
||||
private const val KEY_MESSAGES = "messages"
|
||||
private const val KEY_MEDIA = "media"
|
||||
}
|
||||
|
||||
init {
|
||||
if (!credentials.containsKey(KEY_MESSAGES)) {
|
||||
throw IOException("Missing key '$KEY_MESSAGES'")
|
||||
}
|
||||
|
||||
if (!credentials.containsKey(KEY_MEDIA)) {
|
||||
throw IOException("Missing key '$KEY_MEDIA'")
|
||||
}
|
||||
}
|
||||
|
||||
val messageCredentials: List<ArchiveServiceCredential>
|
||||
get() = credentials[KEY_MESSAGES]!!
|
||||
|
||||
val mediaCredentials: List<ArchiveServiceCredential>
|
||||
get() = credentials[KEY_MEDIA]!!
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest
|
||||
class ArchiveSetBackupIdRequest(
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = BackupAuthCredentialRequestSerializer::class)
|
||||
val backupAuthCredentialRequest: BackupAuthCredentialRequest
|
||||
val messagesBackupAuthCredentialRequest: BackupAuthCredentialRequest,
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = BackupAuthCredentialRequestSerializer::class)
|
||||
val mediaBackupAuthCredentialRequest: BackupAuthCredentialRequest
|
||||
) {
|
||||
class BackupAuthCredentialRequestSerializer : JsonSerializer<BackupAuthCredentialRequest>() {
|
||||
override fun serialize(value: BackupAuthCredentialRequest, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
|
||||
@@ -1,104 +1,22 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.libsignal.protocol.kdf.HKDF
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
|
||||
/**
|
||||
* Safe typing around a backup key, which is a 32-byte array.
|
||||
* Contains the common properties for all "backup keys", namely the [MessageBackupKey] and [MediaRootBackupKey]
|
||||
*/
|
||||
class BackupKey(val value: ByteArray) {
|
||||
init {
|
||||
require(value.size == 32) { "Backup key must be 32 bytes!" }
|
||||
}
|
||||
interface BackupKey {
|
||||
|
||||
/**
|
||||
* Identifies a the location of a user's backup.
|
||||
*/
|
||||
fun deriveBackupId(aci: ACI): BackupId {
|
||||
return BackupId(
|
||||
HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupId".toByteArray(), 16)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The cryptographic material used to encrypt a backup.
|
||||
*/
|
||||
fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial {
|
||||
val backupId = deriveBackupId(aci)
|
||||
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
|
||||
|
||||
return BackupKeyMaterial(
|
||||
id = backupId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64)
|
||||
)
|
||||
}
|
||||
val value: ByteArray
|
||||
|
||||
/**
|
||||
* The private key used to generate anonymous credentials when interacting with the backup service.
|
||||
*/
|
||||
fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey {
|
||||
val material = HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupIdKeyPair".toByteArray(), 32)
|
||||
return Curve.decodePrivatePoint(material)
|
||||
}
|
||||
|
||||
fun deriveMediaId(mediaName: MediaName): MediaId {
|
||||
return MediaId(HKDF.deriveSecrets(value, mediaName.toByteArray(), "20231003_Signal_Backups_Media_ID".toByteArray(), 15))
|
||||
}
|
||||
|
||||
fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial {
|
||||
return deriveMediaSecrets(deriveMediaId(mediaName))
|
||||
}
|
||||
|
||||
fun deriveMediaSecretsFromMediaId(base64MediaId: String): MediaKeyMaterial {
|
||||
return deriveMediaSecrets(MediaId(base64MediaId))
|
||||
}
|
||||
|
||||
fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray {
|
||||
return HKDF.deriveSecrets(value, deriveMediaId(thumbnailMediaName).value, "20240513_Signal_Backups_EncryptThumbnail".toByteArray(), 64)
|
||||
}
|
||||
|
||||
private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial {
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80)
|
||||
|
||||
return MediaKeyMaterial(
|
||||
id = mediaId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||
iv = extendedKey.copyOfRange(64, 80)
|
||||
)
|
||||
}
|
||||
|
||||
class BackupKeyMaterial(
|
||||
val id: BackupId,
|
||||
val macKey: ByteArray,
|
||||
val cipherKey: ByteArray
|
||||
)
|
||||
|
||||
class MediaKeyMaterial(
|
||||
val id: MediaId,
|
||||
val macKey: ByteArray,
|
||||
val cipherKey: ByteArray,
|
||||
val iv: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): MediaKeyMaterial {
|
||||
return MediaKeyMaterial(
|
||||
MediaId(id),
|
||||
keyMac.copyOfRange(32, 64),
|
||||
keyMac.copyOfRange(0, 32),
|
||||
iv
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.libsignal.protocol.kdf.HKDF
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey
|
||||
|
||||
/**
|
||||
* Safe typing around a media root backup key, which is a 32-byte array.
|
||||
* This key is a purely random value.
|
||||
*/
|
||||
class MediaRootBackupKey(override val value: ByteArray) : BackupKey {
|
||||
|
||||
/**
|
||||
* The private key used to generate anonymous credentials when interacting with the backup service.
|
||||
*/
|
||||
override fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey {
|
||||
return LibSignalBackupKey(value).deriveEcKey(aci.libSignalAci)
|
||||
}
|
||||
|
||||
fun deriveMediaId(mediaName: MediaName): MediaId {
|
||||
return MediaId(LibSignalBackupKey(value).deriveMediaId(mediaName.name))
|
||||
}
|
||||
|
||||
fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial {
|
||||
val mediaId = deriveMediaId(mediaName)
|
||||
return deriveMediaSecrets(mediaId)
|
||||
}
|
||||
|
||||
fun deriveMediaSecretsFromMediaId(base64MediaId: String): MediaKeyMaterial {
|
||||
return deriveMediaSecrets(MediaId(base64MediaId))
|
||||
}
|
||||
|
||||
fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray {
|
||||
return HKDF.deriveSecrets(value, deriveMediaId(thumbnailMediaName).value, "20240513_Signal_Backups_EncryptThumbnail".toByteArray(), 64)
|
||||
}
|
||||
|
||||
private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial {
|
||||
val libsignalBackupKey = LibSignalBackupKey(value)
|
||||
val combinedKey = libsignalBackupKey.deriveMediaEncryptionKey(mediaId.value)
|
||||
|
||||
return MediaKeyMaterial(
|
||||
id = mediaId,
|
||||
macKey = combinedKey.copyOfRange(0, 32),
|
||||
aesKey = combinedKey.copyOfRange(32, 64)
|
||||
)
|
||||
}
|
||||
|
||||
class MediaKeyMaterial(
|
||||
val id: MediaId,
|
||||
val macKey: ByteArray,
|
||||
val aesKey: ByteArray
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey
|
||||
|
||||
/**
|
||||
* Safe typing around a backup key, which is a 32-byte array.
|
||||
* This key is derived from the master key.
|
||||
*/
|
||||
class MessageBackupKey(override val value: ByteArray) : BackupKey {
|
||||
init {
|
||||
require(value.size == 32) { "Backup key must be 32 bytes!" }
|
||||
}
|
||||
|
||||
/**
|
||||
* The cryptographic material used to encrypt a backup.
|
||||
*/
|
||||
fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial {
|
||||
val backupId = deriveBackupId(aci)
|
||||
val libsignalBackupKey = LibSignalBackupKey(value)
|
||||
val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value)
|
||||
|
||||
return BackupKeyMaterial(
|
||||
id = backupId,
|
||||
macKey = libsignalMessageMessageBackupKey.hmacKey,
|
||||
aesKey = libsignalMessageMessageBackupKey.aesKey
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The private key used to generate anonymous credentials when interacting with the backup service.
|
||||
*/
|
||||
override fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey {
|
||||
return LibSignalBackupKey(value).deriveEcKey(aci.libSignalAci)
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a the location of a user's backup.
|
||||
*/
|
||||
private fun deriveBackupId(aci: ACI): BackupId {
|
||||
return BackupId(
|
||||
LibSignalBackupKey(value).deriveBackupId(aci.libSignalAci)
|
||||
)
|
||||
}
|
||||
|
||||
class BackupKeyMaterial(
|
||||
val id: BackupId,
|
||||
val macKey: ByteArray,
|
||||
val aesKey: ByteArray
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice;
|
||||
import org.signal.libsignal.protocol.incrementalmac.IncrementalMacInputStream;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -126,7 +126,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
/**
|
||||
* Decrypt archived media to it's original attachment encrypted blob.
|
||||
*/
|
||||
public static LimitedInputStream createForArchivedMedia(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength)
|
||||
public static LimitedInputStream createForArchivedMedia(MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength)
|
||||
throws InvalidMessageException, IOException
|
||||
{
|
||||
Mac mac = initMac(archivedMediaKeyMaterial.getMacKey());
|
||||
@@ -139,7 +139,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
verifyMac(macVerificationStream, file.length(), mac, null);
|
||||
}
|
||||
|
||||
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), archivedMediaKeyMaterial.getCipherKey(), file.length() - BLOCK_SIZE - mac.getMacLength());
|
||||
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), archivedMediaKeyMaterial.getAesKey(), file.length() - BLOCK_SIZE - mac.getMacLength());
|
||||
|
||||
if (originalCipherTextLength != 0) {
|
||||
return new LimitedInputStream(inputStream, originalCipherTextLength);
|
||||
@@ -148,7 +148,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
public static LimitedInputStream createStreamingForArchivedAttachment(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest, int incrementalMacChunkSize)
|
||||
public static LimitedInputStream createStreamingForArchivedAttachment(MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest, int incrementalMacChunkSize)
|
||||
throws InvalidMessageException, IOException
|
||||
{
|
||||
final InputStream archiveStream = createForArchivedMedia(archivedMediaKeyMaterial, file, originalCipherTextLength);
|
||||
@@ -204,7 +204,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength());
|
||||
}
|
||||
|
||||
private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize)
|
||||
private AttachmentCipherInputStream(InputStream inputStream, byte[] aesKey, long totalDataSize)
|
||||
throws IOException
|
||||
{
|
||||
super(inputStream);
|
||||
@@ -214,7 +214,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
readFully(iv);
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
this.done = false;
|
||||
this.totalRead = 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.whispersystems.signalservice.api.kbs;
|
||||
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.signal.core.util.Base64;
|
||||
@@ -46,8 +46,9 @@ public final class MasterKey {
|
||||
return derive("Logging Key");
|
||||
}
|
||||
|
||||
public BackupKey deriveBackupKey() {
|
||||
return new BackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32));
|
||||
public MessageBackupKey derivateMessageBackupKey() {
|
||||
// TODO [backup] Derive from AEP
|
||||
return new MessageBackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32));
|
||||
}
|
||||
|
||||
private byte[] derive(String keyName) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
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.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
@@ -63,7 +63,7 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
profileKey: ProfileKey,
|
||||
masterKey: MasterKey,
|
||||
code: String,
|
||||
ephemeralBackupKey: BackupKey?
|
||||
ephemeralMessageBackupKey: MessageBackupKey?
|
||||
): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
val cipher = PrimaryProvisioningCipher(deviceKey)
|
||||
@@ -79,7 +79,7 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
provisioningCode = code,
|
||||
provisioningVersion = ProvisioningVersion.CURRENT.value,
|
||||
masterKey = masterKey.serialize().toByteString(),
|
||||
ephemeralBackupKey = ephemeralBackupKey?.value?.toByteString()
|
||||
ephemeralBackupKey = ephemeralMessageBackupKey?.value?.toByteString()
|
||||
)
|
||||
val ciphertext = cipher.encrypt(message)
|
||||
|
||||
|
||||
@@ -540,8 +540,8 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
|
||||
}
|
||||
|
||||
public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException {
|
||||
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request));
|
||||
public void setArchiveBackupId(BackupAuthCredentialRequest messageRequest, BackupAuthCredentialRequest mediaRequest) throws IOException {
|
||||
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(messageRequest, mediaRequest));
|
||||
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice;
|
||||
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
@@ -290,13 +290,13 @@ public final class AttachmentCipherTest {
|
||||
|
||||
@Test
|
||||
public void archive_encryptDecrypt() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
byte[] plaintextInput = "Peter Parker".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
byte[] plaintextInput = "Peter Parker".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
@@ -305,13 +305,13 @@ public final class AttachmentCipherTest {
|
||||
|
||||
@Test
|
||||
public void archive_encryptDecryptEmpty() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
@@ -324,11 +324,11 @@ public final class AttachmentCipherTest {
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] badKey = Util.getSecretBytes(64);
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16));
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] badKey = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(badKey);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
@@ -372,9 +372,9 @@ public final class AttachmentCipherTest {
|
||||
|
||||
File cipherFile = writeToFile(encryptedData);
|
||||
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
@@ -397,7 +397,7 @@ public final class AttachmentCipherTest {
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext);
|
||||
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
fail();
|
||||
} catch (InvalidMessageException e) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.crypto
|
||||
|
||||
import org.whispersystems.signalservice.api.backup.MediaId
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey.MediaKeyMaterial
|
||||
import org.whispersystems.signalservice.internal.util.Util
|
||||
|
||||
object AttachmentCipherTestHelper {
|
||||
|
||||
/**
|
||||
* Needed to workaround this bug:
|
||||
* https://youtrack.jetbrains.com/issue/KT-60205/Java-class-has-private-access-in-class-constructor-with-inlinevalue-parameter
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createMediaKeyMaterial(combinedKey: ByteArray): MediaKeyMaterial {
|
||||
val parts = Util.split(combinedKey, 32, 32)
|
||||
|
||||
return MediaKeyMaterial(
|
||||
id = MediaId(Util.getSecretBytes(15)),
|
||||
macKey = parts[1],
|
||||
aesKey = parts[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user