Extract base archive classes into their own module.

This commit is contained in:
Greyson Parrelli
2026-03-21 08:53:47 -04:00
committed by Cody Henthorne
parent 08491579dd
commit 8a887b65a1
64 changed files with 668 additions and 525 deletions

View File

@@ -0,0 +1,27 @@
plugins {
id("signal-library")
id("com.squareup.wire")
}
android {
namespace = "org.signal.archive"
}
wire {
kotlin {
javaInterop = true
}
sourcePath {
srcDir("src/main/protowire")
}
}
dependencies {
implementation(project(":core:util"))
implementation(project(":core:models-jvm"))
implementation(project(":lib:libsignal-service"))
implementation(libs.libsignal.android)
implementation(libs.google.guava.android)
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive
/**
* Metadata about a local backup file available for restore.
*/
data class LocalBackupMetadata(
/** Size of the backup in bytes. */
val sizeBytes: Long,
/** Timestamp when the backup was created, in milliseconds since epoch. */
val timestampMs: Long
)

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive
/**
* Represents the progress of a local backup restore operation.
* Emitted as a flow from the storage controller during restore.
*/
sealed interface LocalBackupRestoreProgress {
/** The restore is being prepared (e.g., reading metadata, validating). */
data object Preparing : LocalBackupRestoreProgress
/** The restore is actively in progress. */
data class InProgress(
val bytesRead: Long,
val totalBytes: Long
) : LocalBackupRestoreProgress {
val progressFraction: Float
get() = if (totalBytes > 0) (bytesRead.toFloat() / totalBytes.toFloat()).coerceIn(0f, 1f) else 0f
}
/** The restore completed successfully. */
data object Complete : LocalBackupRestoreProgress
/** The restore failed with an error. */
data class Error(val cause: Throwable) : LocalBackupRestoreProgress
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.local
import org.signal.archive.local.proto.FilesFrame
import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.readVarInt32
import java.io.EOFException
import java.io.InputStream
/**
* Reads [FilesFrame] protos encoded with their length.
*/
class ArchivedFilesReader(private val inputStream: InputStream) : Iterator<FilesFrame>, AutoCloseable {
private var next: FilesFrame? = null
init {
next = read()
}
override fun hasNext(): Boolean {
return next != null
}
override fun next(): FilesFrame {
next?.let { out ->
next = read()
return out
} ?: throw NoSuchElementException()
}
private fun read(): FilesFrame? {
try {
val length = inputStream.readVarInt32().also { if (it < 0) return null }
val frameBytes: ByteArray = inputStream.readNBytesOrThrow(length)
return FilesFrame.ADAPTER.decode(frameBytes)
} catch (e: EOFException) {
return null
}
}
override fun close() {
inputStream.close()
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.local
import org.signal.archive.local.proto.FilesFrame
import org.signal.core.util.writeVarInt32
import java.io.IOException
import java.io.OutputStream
/**
* Write [FilesFrame] protos encoded with their length.
*/
class ArchivedFilesWriter(private val output: OutputStream) : AutoCloseable {
@Throws(IOException::class)
fun write(frame: FilesFrame) {
val bytes = frame.encode()
output.writeVarInt32(bytes.size)
output.write(bytes)
}
override fun close() {
output.close()
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
interface BackupExportWriter : AutoCloseable {
fun write(header: BackupInfo)
fun write(frame: Frame)
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.Frame
/**
* An interface that lets sub-processors emit [Frame]s as they export data.
*/
fun interface BackupFrameEmitter {
fun emit(frame: Frame)
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
interface BackupImportReader : Iterator<Frame>, AutoCloseable {
fun getHeader(): BackupInfo?
fun getBytesRead(): Long
fun getStreamLength(): Long
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.Frame
interface BackupImportStream {
fun read(): Frame?
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import androidx.annotation.VisibleForTesting
import com.google.common.io.CountingInputStream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.util.readFully
import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.readVarInt32
import org.signal.core.util.stream.LimitedInputStream
import org.signal.core.util.stream.MacInputStream
import org.signal.core.util.writeVarInt32
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
import java.io.ByteArrayOutputStream
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.util.zip.GZIPInputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Provides the ability to read backup frames in a streaming fashion from a target [InputStream].
* As it's being read, it will be both decrypted and uncompressed. Specifically, the data is decrypted,
* that decrypted data is gunzipped, then that data is read as frames.
*/
class EncryptedBackupReader private constructor(
keyMaterial: MessageBackupKey.BackupKeyMaterial,
val length: Long,
dataStream: () -> InputStream
) : BackupImportReader {
@VisibleForTesting
val backupInfo: BackupInfo?
private var next: Frame? = null
private val stream: InputStream
private val countingStream: CountingInputStream
companion object {
const val MAC_SIZE = 32
/**
* Estimated upperbound need to read backup secrecy metadata from the start of a file.
*
* Magic Number size + ~varint size (5) + forward secrecy metadata size estimate (200)
*/
val BACKUP_SECRET_METADATA_UPPERBOUND = EncryptedBackupWriter.MAGIC_NUMBER.size + 5 + 200
/**
* Create a reader for a backup from the archive CDN.
* The key difference is that we require forward secrecy data.
*/
fun createForSignalBackup(
key: MessageBackupKey,
aci: ACI,
forwardSecrecyToken: BackupForwardSecrecyToken,
length: Long,
dataStream: () -> InputStream
): EncryptedBackupReader {
return createForSignalBackup(key, key.deriveBackupId(aci), forwardSecrecyToken, length, dataStream)
}
/**
* Create a reader for a backup from the archive CDN, using a [BackupId] directly
* instead of deriving it from an ACI.
*/
fun createForSignalBackup(
key: MessageBackupKey,
backupId: BackupId,
forwardSecrecyToken: BackupForwardSecrecyToken,
length: Long,
dataStream: () -> InputStream
): EncryptedBackupReader {
return EncryptedBackupReader(
keyMaterial = key.deriveBackupSecrets(backupId, forwardSecrecyToken),
length = length,
dataStream = dataStream
)
}
/**
* Create a reader for a local backup or for a transfer to a linked device. Basically everything that isn't [createForSignalBackup].
* The key difference is that we don't require forward secrecy data.
*/
fun createForLocalOrLinking(key: MessageBackupKey, aci: ACI, length: Long, dataStream: () -> InputStream): EncryptedBackupReader {
return createForLocalOrLinking(key, key.deriveBackupId(aci), length, dataStream)
}
/**
* Create a reader for a local backup or for a transfer to a linked device, using a [BackupId] directly
* instead of deriving it from an ACI.
*/
fun createForLocalOrLinking(key: MessageBackupKey, backupId: BackupId, length: Long, dataStream: () -> InputStream): EncryptedBackupReader {
return EncryptedBackupReader(
keyMaterial = key.deriveBackupSecrets(backupId, forwardSecrecyToken = null),
length = length,
dataStream = dataStream
)
}
/**
* Returns the size of the entire forward secrecy prefix. Includes the magic number, varint, and the length of the forward secrecy metadata itself.
*/
fun getForwardSecrecyPrefixDataLength(stream: InputStream): Int {
val metadataLength = readForwardSecrecyMetadata(stream)?.size ?: return 0
return EncryptedBackupWriter.MAGIC_NUMBER.size + metadataLength.lengthAsVarInt32() + metadataLength
}
fun readForwardSecrecyMetadata(stream: InputStream): ByteArray? {
val potentialMagicNumber = stream.readNBytesOrThrow(8)
if (!EncryptedBackupWriter.MAGIC_NUMBER.contentEquals(potentialMagicNumber)) {
return null
}
val metadataLength = stream.readVarInt32()
return stream.readNBytesOrThrow(metadataLength)
}
private fun validateMac(macKey: ByteArray, streamLength: Long, dataStream: InputStream) {
val mac = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(macKey, "HmacSHA256"))
}
val macStream = MacInputStream(
wrapped = LimitedInputStream(dataStream, maxBytes = streamLength - MAC_SIZE),
mac = mac
)
macStream.readFully(false)
val calculatedMac = macStream.mac.doFinal()
val expectedMac = dataStream.readNBytesOrThrow(MAC_SIZE)
if (!calculatedMac.contentEquals(expectedMac)) {
throw IOException("Invalid MAC!")
}
}
private fun Int.lengthAsVarInt32(): Int {
return ByteArrayOutputStream().apply {
writeVarInt32(this@lengthAsVarInt32)
}.toByteArray().size
}
}
init {
val forwardSecrecyMetadata = dataStream().use { readForwardSecrecyMetadata(it) }
val encryptedLength = if (forwardSecrecyMetadata != null) {
val prefixLength = EncryptedBackupWriter.MAGIC_NUMBER.size + forwardSecrecyMetadata.size + forwardSecrecyMetadata.size.lengthAsVarInt32()
length - prefixLength
} else {
length
}
val prefixSkippingStream = {
if (forwardSecrecyMetadata == null) {
dataStream()
} else {
dataStream().also { readForwardSecrecyMetadata(it) }
}
}
prefixSkippingStream().use { validateMac(keyMaterial.macKey, encryptedLength, it) }
countingStream = CountingInputStream(prefixSkippingStream())
val iv = countingStream.readNBytesOrThrow(16)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
stream = GZIPInputStream(
CipherInputStream(
LimitedInputStream(
wrapped = countingStream,
maxBytes = encryptedLength - MAC_SIZE
),
cipher
)
)
backupInfo = readHeader()
next = read()
}
override fun getHeader(): BackupInfo? {
return backupInfo
}
override fun getBytesRead() = countingStream.count
override fun getStreamLength() = length
override fun hasNext(): Boolean {
return next != null
}
override fun next(): Frame {
next?.let { out ->
next = read()
return out
} ?: throw NoSuchElementException()
}
private fun readHeader(): BackupInfo? {
try {
val length = stream.readVarInt32().takeIf { it >= 0 } ?: return null
val headerBytes: ByteArray = stream.readNBytesOrThrow(length)
return BackupInfo.ADAPTER.decode(headerBytes)
} catch (e: EOFException) {
return null
}
}
private fun read(): Frame? {
try {
val length = stream.readVarInt32().also { if (it < 0) return null }
val frameBytes: ByteArray = stream.readNBytesOrThrow(length)
return Frame.ADAPTER.decode(frameBytes)
} catch (e: EOFException) {
return null
}
}
override fun close() {
stream.close()
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.archive.stream.EncryptedBackupReader.Companion.createForSignalBackup
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.util.Util
import org.signal.core.util.stream.MacOutputStream
import org.signal.core.util.writeVarInt32
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
import java.io.IOException
import java.io.OutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Provides the ability to write backup frames in a streaming fashion to a target [OutputStream].
* As it's being written, it will be both encrypted and compressed. Specifically, the backup frames
* are gzipped, that gzipped data is encrypted, and then an HMAC of the encrypted data is appended
* to the end of the [outputStream].
*/
class EncryptedBackupWriter private constructor(
keyMaterial: MessageBackupKey.BackupKeyMaterial,
forwardSecrecyToken: BackupForwardSecrecyToken?,
forwardSecrecyMetadata: ByteArray?,
private val outputStream: OutputStream,
private val append: (ByteArray) -> Unit
) : BackupExportWriter {
private val mainStream: PaddedGzipOutputStream
private val macStream: MacOutputStream
companion object {
val MAGIC_NUMBER = "SBACKUP".toByteArray(Charsets.UTF_8) + 0x01
/**
* Create a writer for a backup from the archive CDN.
* The key difference is that we require forward secrecy data.
*/
fun createForSignalBackup(
key: MessageBackupKey,
aci: ACI,
forwardSecrecyToken: BackupForwardSecrecyToken,
forwardSecrecyMetadata: ByteArray,
outputStream: OutputStream,
append: (ByteArray) -> Unit
): EncryptedBackupWriter {
return createForSignalBackup(key, key.deriveBackupId(aci), forwardSecrecyToken, forwardSecrecyMetadata, outputStream, append)
}
/**
* Create a writer for a backup from the archive CDN, using a [BackupId] directly
* instead of deriving it from an ACI.
*/
fun createForSignalBackup(
key: MessageBackupKey,
backupId: BackupId,
forwardSecrecyToken: BackupForwardSecrecyToken,
forwardSecrecyMetadata: ByteArray,
outputStream: OutputStream,
append: (ByteArray) -> Unit
): EncryptedBackupWriter {
return EncryptedBackupWriter(
keyMaterial = key.deriveBackupSecrets(backupId, forwardSecrecyToken),
forwardSecrecyToken = forwardSecrecyToken,
forwardSecrecyMetadata = forwardSecrecyMetadata,
outputStream = outputStream,
append = append
)
}
/**
* Create a writer for a local backup or for a transfer to a linked device. Basically everything that isn't [createForSignalBackup].
* The key difference is that we don't require forward secrecy data.
*/
fun createForLocalOrLinking(
key: MessageBackupKey,
aci: ACI,
outputStream: OutputStream,
append: (ByteArray) -> Unit
): EncryptedBackupWriter {
return createForLocalOrLinking(key, key.deriveBackupId(aci), outputStream, append)
}
/**
* Create a writer for a local backup or for a transfer to a linked device, using a [BackupId] directly
* instead of deriving it from an ACI.
*/
fun createForLocalOrLinking(
key: MessageBackupKey,
backupId: BackupId,
outputStream: OutputStream,
append: (ByteArray) -> Unit
): EncryptedBackupWriter {
return EncryptedBackupWriter(
keyMaterial = key.deriveBackupSecrets(backupId, forwardSecrecyToken = null),
forwardSecrecyToken = null,
forwardSecrecyMetadata = null,
outputStream = outputStream,
append = append
)
}
}
init {
check(
(forwardSecrecyToken != null && forwardSecrecyMetadata != null) ||
(forwardSecrecyToken == null && forwardSecrecyMetadata == null)
)
if (forwardSecrecyMetadata != null) {
outputStream.write(MAGIC_NUMBER)
outputStream.writeVarInt32(forwardSecrecyMetadata.size)
outputStream.write(forwardSecrecyMetadata)
outputStream.flush()
}
val iv: ByteArray = Util.getSecretBytes(16)
outputStream.write(iv)
outputStream.flush()
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
}
val mac = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(keyMaterial.macKey, "HmacSHA256"))
update(iv)
}
macStream = MacOutputStream(outputStream, mac)
val cipherStream = CipherOutputStream(macStream, cipher)
mainStream = PaddedGzipOutputStream(cipherStream)
}
override fun write(header: BackupInfo) {
val headerBytes = header.encode()
mainStream.writeVarInt32(headerBytes.size)
mainStream.write(headerBytes)
}
@Throws(IOException::class)
override fun write(frame: Frame) {
val frameBytes: ByteArray = frame.encode()
mainStream.writeVarInt32(frameBytes.size)
mainStream.write(frameBytes)
}
@Throws(IOException::class)
override fun close() {
// We need to close the main stream in order for the gzip and all the cipher operations to fully finish before
// we can calculate the MAC. Unfortunately flush()/finish() is not sufficient. So we have to defer to the
// caller to append the bytes to the end of the data however they see fit (like appending to a file).
mainStream.close()
val mac = macStream.mac.doFinal()
append(mac)
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.FilterOutputStream
import java.io.OutputStream
import java.util.zip.GZIPOutputStream
/**
* GZIPs the content of the provided [outputStream], but also adds padding to the end of the stream using the same algorithm as [PaddingInputStream].
* We do this to fit files into a smaller number of size buckets to avoid fingerprinting. And it turns out that bolting on zeros to the end of a GZIP stream is
* fine, because GZIP is smart enough to ignore it. This means readers of this data don't have to do anything special.
*/
class PaddedGzipOutputStream private constructor(private val outputStream: SizeObservingOutputStream) : GZIPOutputStream(outputStream) {
constructor(outputStream: OutputStream) : this(SizeObservingOutputStream(outputStream))
override fun finish() {
super.finish()
val totalLength = outputStream.size
val paddedSize: Long = PaddingInputStream.getPaddedSize(totalLength)
val paddingToAdd: Int = (paddedSize - totalLength).toInt()
outputStream.write(ByteArray(paddingToAdd))
}
/**
* We need to know the size of the *compressed* stream to know how much padding to add at the end.
*/
private class SizeObservingOutputStream(val wrapped: OutputStream) : FilterOutputStream(wrapped) {
var size: Long = 0L
private set
override fun write(b: Int) {
wrapped.write(b)
size++
}
override fun write(b: ByteArray) {
wrapped.write(b)
size += b.size
}
override fun write(b: ByteArray, off: Int, len: Int) {
wrapped.write(b, off, len)
size += len
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import com.google.common.io.CountingInputStream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.readVarInt32
import java.io.EOFException
import java.io.InputStream
/**
* Reads a plaintext backup import stream one frame at a time.
*/
class PlainTextBackupReader(val dataStream: InputStream, val length: Long) : BackupImportReader {
val backupInfo: BackupInfo?
var next: Frame? = null
val inputStream: CountingInputStream
init {
inputStream = CountingInputStream(dataStream)
backupInfo = readHeader()
next = read()
}
override fun getHeader(): BackupInfo? {
return backupInfo
}
override fun getBytesRead() = inputStream.count
override fun getStreamLength() = length
override fun hasNext(): Boolean {
return next != null
}
override fun next(): Frame {
next?.let { out ->
next = read()
return out
} ?: throw NoSuchElementException()
}
override fun close() {
inputStream.close()
}
private fun readHeader(): BackupInfo? {
try {
val length = inputStream.readVarInt32().takeIf { it >= 0 } ?: return null
val headerBytes: ByteArray = inputStream.readNBytesOrThrow(length)
return BackupInfo.ADAPTER.decode(headerBytes)
} catch (e: EOFException) {
return null
}
}
private fun read(): Frame? {
try {
val length = inputStream.readVarInt32().also { if (it < 0) return null }
val frameBytes: ByteArray = inputStream.readNBytesOrThrow(length)
return Frame.ADAPTER.decode(frameBytes)
} catch (e: EOFException) {
return null
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.archive.stream
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.core.util.writeVarInt32
import java.io.IOException
import java.io.OutputStream
/**
* Writes backup frames to the wrapped stream in plain text. Only for testing!
*/
class PlainTextBackupWriter(private val outputStream: OutputStream) : BackupExportWriter {
@Throws(IOException::class)
override fun write(header: BackupInfo) {
val headerBytes: ByteArray = header.encode()
outputStream.writeVarInt32(headerBytes.size)
outputStream.write(headerBytes)
}
@Throws(IOException::class)
override fun write(frame: Frame) {
val frameBytes: ByteArray = frame.encode()
outputStream.writeVarInt32(frameBytes.size)
outputStream.write(frameBytes)
}
override fun close() {
outputStream.close()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
option java_package = "org.signal.archive.proto";
message BackupDebugInfo {
message AttachmentDetails {
uint32 notStartedCount = 1;
uint32 uploadInProgressCount = 2;
uint32 copyPendingCount = 3;
uint32 finishedCount = 4;
uint32 permanentFailureCount = 5;
uint32 temporaryFailureCount = 6;
}
string debuglogUrl = 1;
AttachmentDetails attachmentDetails = 2;
bool usingPaidTier = 3;
}

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
package signal.backup.local;
option java_package = "org.signal.archive.local.proto";
option swift_prefix = "LocalBackupProto_";
message Metadata {
message EncryptedBackupId {
bytes iv = 1; // 12 bytes, randomly generated
bytes encryptedId = 2; // AES-256-CTR, key = local backup metadata key, message = backup ID bytes
// local backup metadata key = hkdf(input: K_B, info: UTF8("20241011_SIGNAL_LOCAL_BACKUP_METADATA_KEY"), length: 32)
// No hash of the ID; if it's decrypted incorrectly, the main backup will fail to decrypt anyway.
}
uint32 version = 1;
EncryptedBackupId backupId = 2; // used to decrypt the backup file knowing only the Account Entropy Pool
}
message FilesFrame {
oneof item {
string mediaName = 1;
}
}