mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 11:15:44 +00:00
Update file format for backupV2.
This commit is contained in:
committed by
Cody Henthorne
parent
2e7279c72f
commit
34dbd11db0
@@ -41,18 +41,21 @@ class EncryptedBackupReader(
|
||||
val stream: InputStream
|
||||
|
||||
init {
|
||||
val keyMaterial = key.deriveSecrets(aci)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
|
||||
}
|
||||
val keyMaterial = key.deriveBackupSecrets(aci)
|
||||
|
||||
validateMac(keyMaterial.macKey, streamLength, dataStream())
|
||||
|
||||
val inputStream = dataStream()
|
||||
val iv = inputStream.readNBytesOrThrow(16)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
|
||||
}
|
||||
|
||||
stream = GZIPInputStream(
|
||||
CipherInputStream(
|
||||
TruncatingInputStream(
|
||||
wrapped = dataStream(),
|
||||
wrapped = inputStream,
|
||||
maxBytes = streamLength - MAC_SIZE
|
||||
),
|
||||
cipher
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.signal.core.util.stream.MacOutputStream
|
||||
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.push.ServiceId.ACI
|
||||
import java.io.IOException
|
||||
@@ -36,14 +37,19 @@ class EncryptedBackupWriter(
|
||||
private val macStream: MacOutputStream
|
||||
|
||||
init {
|
||||
val keyMaterial = key.deriveSecrets(aci)
|
||||
val keyMaterial = key.deriveBackupSecrets(aci)
|
||||
|
||||
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.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
|
||||
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
|
||||
}
|
||||
|
||||
val mac = Mac.getInstance("HmacSHA256").apply {
|
||||
init(SecretKeySpec(keyMaterial.macKey, "HmacSHA256"))
|
||||
update(iv)
|
||||
}
|
||||
|
||||
macStream = MacOutputStream(outputStream, mac)
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
@@ -56,7 +57,7 @@ class EncryptedBackupReaderWriterTest {
|
||||
val key = BackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
|
||||
val sizes = (1..10)
|
||||
val uniqueSizes = (1..10)
|
||||
.map { frameCount ->
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
@@ -72,6 +73,29 @@ class EncryptedBackupReaderWriterTest {
|
||||
}
|
||||
.toSet()
|
||||
|
||||
assertEquals(1, sizes.size)
|
||||
assertEquals(1, uniqueSizes.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `using a different IV every time`() {
|
||||
val key = BackupKey(Util.getSecretBytes(32))
|
||||
val aci = ACI.from(UUID.randomUUID())
|
||||
val count = 10
|
||||
|
||||
val uniqueOutputs = (0 until count)
|
||||
.map {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
|
||||
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
|
||||
writer.write(Frame(account = AccountData(username = "static-data")))
|
||||
}
|
||||
|
||||
outputStream.toByteArray()
|
||||
}
|
||||
.map { Base64.encodeWithPadding(it) }
|
||||
.toSet()
|
||||
|
||||
assertEquals(count, uniqueOutputs.size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ public class SignalServiceMessageReceiver {
|
||||
*
|
||||
* @return An InputStream that streams the plaintext attachment contents.
|
||||
*/
|
||||
public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.KeyMaterial<MediaId> archivedMediaKeyMaterial,
|
||||
public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
|
||||
@Nonnull Map<String, String> readCredentialHeaders,
|
||||
@Nonnull File archiveDestination,
|
||||
@Nonnull SignalServiceAttachmentPointer pointer,
|
||||
|
||||
@@ -22,16 +22,15 @@ class BackupKey(val value: ByteArray) {
|
||||
)
|
||||
}
|
||||
|
||||
fun deriveSecrets(aci: ACI): KeyMaterial<BackupId> {
|
||||
fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial {
|
||||
val backupId = deriveBackupId(aci)
|
||||
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
|
||||
|
||||
return KeyMaterial(
|
||||
return BackupKeyMaterial(
|
||||
id = backupId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||
iv = extendedKey.copyOfRange(64, 80)
|
||||
cipherKey = extendedKey.copyOfRange(32, 64)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,14 +38,14 @@ class BackupKey(val value: ByteArray) {
|
||||
return MediaId(HKDF.deriveSecrets(value, mediaName.toByteArray(), "Media ID".toByteArray(), 15))
|
||||
}
|
||||
|
||||
fun deriveMediaSecrets(mediaName: MediaName): KeyMaterial<MediaId> {
|
||||
fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial {
|
||||
return deriveMediaSecrets(deriveMediaId(mediaName))
|
||||
}
|
||||
|
||||
fun deriveMediaSecrets(mediaId: MediaId): KeyMaterial<MediaId> {
|
||||
private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial {
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80)
|
||||
|
||||
return KeyMaterial(
|
||||
return MediaKeyMaterial(
|
||||
id = mediaId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||
@@ -54,16 +53,22 @@ class BackupKey(val value: ByteArray) {
|
||||
)
|
||||
}
|
||||
|
||||
class KeyMaterial<Id> (
|
||||
val id: Id,
|
||||
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): KeyMaterial<MediaId> {
|
||||
return KeyMaterial(
|
||||
fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): MediaKeyMaterial {
|
||||
return MediaKeyMaterial(
|
||||
MediaId(id),
|
||||
keyMac.copyOfRange(32, 64),
|
||||
keyMac.copyOfRange(0, 32),
|
||||
|
||||
@@ -110,7 +110,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
/**
|
||||
* Decrypt archived media to it's original attachment encrypted blob.
|
||||
*/
|
||||
public static InputStream createForArchivedMedia(BackupKey.KeyMaterial<MediaId> archivedMediaKeyMaterial, File file, long originalCipherTextLength)
|
||||
public static InputStream createForArchivedMedia(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength)
|
||||
throws InvalidMessageException, IOException
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -93,7 +93,7 @@ public final class AttachmentCipherTest {
|
||||
@Test
|
||||
public void archive_encryptDecrypt() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
BackupKey.KeyMaterial<MediaId> keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
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);
|
||||
@@ -108,7 +108,7 @@ public final class AttachmentCipherTest {
|
||||
@Test
|
||||
public void archive_encryptDecryptEmpty() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
BackupKey.KeyMaterial<MediaId> keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
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);
|
||||
@@ -128,7 +128,7 @@ public final class AttachmentCipherTest {
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] badKey = Util.getSecretBytes(64);
|
||||
BackupKey.KeyMaterial<MediaId> keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16));
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16));
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
|
||||
@@ -270,7 +270,7 @@ public final class AttachmentCipherTest {
|
||||
|
||||
File cipherFile = writeToFile(encryptedData);
|
||||
|
||||
BackupKey.KeyMaterial<MediaId> keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
|
||||
@@ -348,7 +348,7 @@ public final class AttachmentCipherTest {
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext);
|
||||
|
||||
BackupKey.KeyMaterial<MediaId> keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16));
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
fail();
|
||||
} catch (InvalidMessageException e) {
|
||||
|
||||
Reference in New Issue
Block a user