Do most of the proto and database groundwork for the new mediaName.

This commit is contained in:
Greyson Parrelli
2025-06-20 11:47:54 -04:00
committed by Cody Henthorne
parent e705495638
commit 38c8f852bf
431 changed files with 600 additions and 781 deletions

View File

@@ -7,10 +7,8 @@
package org.whispersystems.signalservice.api;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.stream.LimitedInputStream;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.attachment.AttachmentDownloadResult;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
@@ -27,7 +25,6 @@ import org.whispersystems.signalservice.internal.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZonedDateTime;
@@ -68,7 +65,7 @@ public class SignalServiceMessageReceiver {
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes)
throws IOException, InvalidMessageException, MissingConfigurationException {
return retrieveAttachment(pointer, destination, maxSizeBytes, null).getDataStream();
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
@@ -99,9 +96,9 @@ public class SignalServiceMessageReceiver {
* @throws IOException
* @throws InvalidMessageException
*/
public AttachmentDownloadResult retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener)
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
if (pointer.getDigest().isEmpty()) throw new InvalidMessageException("No attachment digest!");
if (pointer.getKey() == null) throw new InvalidMessageException("No key!");
socket.retrieveAttachment(pointer.getCdnNumber(), Collections.emptyMap(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
@@ -111,16 +108,13 @@ public class SignalServiceMessageReceiver {
StreamUtil.readFully(tempStream, iv);
}
return new AttachmentDownloadResult(
AttachmentCipherInputStream.createForAttachment(
destination,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
),
iv
return AttachmentCipherInputStream.createForAttachment(
destination,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
);
}
@@ -136,13 +130,13 @@ public class SignalServiceMessageReceiver {
*
* @return An InputStream that streams the plaintext attachment contents.
*/
public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
public InputStream retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException
{
if (pointer.getDigest().isEmpty()) {
@@ -160,29 +154,16 @@ public class SignalServiceMessageReceiver {
.map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s)))
.orElse(0L);
// There's two layers of encryption -- one from the backup, and one from the attachment. This only strips the outermost backup encryption layer.
try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) {
try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) {
// TODO [backup] I don't think we should be doing the full copy here. This is basically doing the entire download inline in this single line.
StreamUtil.copy(backupDecrypted, fos);
}
}
byte[] iv = new byte[16];
try (InputStream tempStream = new FileInputStream(attachmentDestination)) {
StreamUtil.readFully(tempStream, iv);
}
LimitedInputStream dataStream = AttachmentCipherInputStream.createForAttachment(
return AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial,
attachmentDestination,
originalCipherLength,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
);
return new AttachmentDownloadResult(dataStream, iv);
}
/**
@@ -197,13 +178,13 @@ public class SignalServiceMessageReceiver {
*
* @return An InputStream that streams the plaintext attachment contents.
*/
public AttachmentDownloadResult retrieveArchivedThumbnail(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
public InputStream retrieveArchivedThumbnail(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException
{
if (pointer.getKey() == null) {
@@ -217,26 +198,13 @@ public class SignalServiceMessageReceiver {
.map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s)))
.orElse(0L);
// There's two layers of encryption -- one from the backup, and one from the attachment. This only strips the outermost backup encryption layer.
try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) {
try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) {
// TODO [backup] I don't think we should be doing the full copy here. This is basically doing the entire download inline in this single line.
StreamUtil.copy(backupDecrypted, fos);
}
}
byte[] iv = new byte[16];
try (InputStream tempStream = new FileInputStream(attachmentDestination)) {
StreamUtil.readFully(tempStream, iv);
}
LimitedInputStream dataStream = AttachmentCipherInputStream.createForArchiveThumbnailInnerLayer(
return AttachmentCipherInputStream.createForArchivedThumbnail(
archivedMediaKeyMaterial,
attachmentDestination,
originalCipherLength,
pointer.getSize().orElse(0),
pointer.getKey()
);
return new AttachmentDownloadResult(dataStream, iv);
}
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, ProgressListener listener) throws MissingConfigurationException, IOException {

View File

@@ -91,7 +91,6 @@ class AttachmentApi(
remoteId = SignalServiceAttachmentRemoteId.V4(attachmentData.resumableUploadSpec.cdnKey),
cdnNumber = attachmentData.resumableUploadSpec.cdnNumber,
key = resumableUploadSpec.attachmentKey,
iv = resumableUploadSpec.attachmentIv,
digest = digestInfo.digest,
incrementalDigest = digestInfo.incrementalDigest,
incrementalDigestChunkSize = digestInfo.incrementalMacChunkSize,

View File

@@ -14,7 +14,6 @@ class AttachmentUploadResult(
val remoteId: SignalServiceAttachmentRemoteId,
val cdnNumber: Int,
val key: ByteArray,
val iv: ByteArray,
val digest: ByteArray,
val incrementalDigest: ByteArray?,
val incrementalDigestChunkSize: Int,

View File

@@ -14,8 +14,8 @@ import org.signal.core.util.Hex
value class MediaName(val name: String) {
companion object {
fun fromDigest(digest: ByteArray) = MediaName(Hex.toStringCondensed(digest))
fun fromDigestForThumbnail(digest: ByteArray) = MediaName("${Hex.toStringCondensed(digest)}_thumbnail")
fun fromPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey))
fun fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey) + "_thumbnail")
fun forThumbnailFromMediaName(mediaName: String) = MediaName("${mediaName}_thumbnail")
/**

View File

@@ -96,56 +96,6 @@ object AttachmentCipherInputStream {
)
}
/**
* After removing the server layer of encryption using [createForArchivedMediaOuterLayer], use this to decrypt the inner layer of the attachment.
* Thumbnails have a special path because we don't do any additional digest/hash validation on them.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchiveThumbnailInnerLayer(
file: File,
plaintextLength: Long,
combinedKeyMaterial: ByteArray
): LimitedInputStream {
return create(
streamSupplier = { FileInputStream(file) },
streamLength = file.length(),
plaintextLength = plaintextLength,
combinedKeyMaterial = combinedKeyMaterial,
digest = null,
incrementalDigest = null,
incrementalMacChunkSize = 0,
ignoreDigest = true
)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
* This will return a stream that unwraps the server's layer of encryption, giving you a stream that contains a "normally-encrypted" attachment.
*
* Because we're validating the encryptedDigest/plaintextHash of the inner layer, there's no additional out-of-band validation of this outer layer.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedMediaOuterLayer(archivedMediaKeyMaterial: MediaKeyMaterial, file: File, originalCipherTextLength: Long): LimitedInputStream {
val mac = initMac(archivedMediaKeyMaterial.macKey)
if (file.length() <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
FileInputStream(file).use { macVerificationStream ->
verifyMac(macVerificationStream, file.length(), mac, null)
}
val encryptedStream = FileInputStream(file)
val encryptedStreamExcludingMac = LimitedInputStream(encryptedStream, file.length() - mac.macLength)
val cipher = createCipher(encryptedStreamExcludingMac, archivedMediaKeyMaterial.aesKey)
val inputStream: InputStream = BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
return LimitedInputStream(inputStream, originalCipherTextLength)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
*
@@ -156,7 +106,7 @@ object AttachmentCipherInputStream {
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedMediaOuterAndInnerLayers(
fun createForArchivedMedia(
archivedMediaKeyMaterial: MediaKeyMaterial,
file: File,
originalCipherTextLength: Long,
@@ -185,6 +135,42 @@ object AttachmentCipherInputStream {
)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
*
* This creates a stream decrypt both the inner and outer layers of an archived attachment at the same time by basically double-decrypting it.
*
* @param incrementalDigest If null, incremental mac validation is disabled.
* @param incrementalMacChunkSize If 0, incremental mac validation is disabled.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedThumbnail(
archivedMediaKeyMaterial: MediaKeyMaterial,
file: File,
originalCipherTextLength: Long,
plaintextLength: Long,
combinedKeyMaterial: ByteArray
): LimitedInputStream {
val keyMaterial = CombinedKeyMaterial.from(combinedKeyMaterial)
val mac = initMac(keyMaterial.macKey)
if (originalCipherTextLength <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
return create(
streamSupplier = { createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, file, originalCipherTextLength) },
streamLength = originalCipherTextLength,
plaintextLength = plaintextLength,
combinedKeyMaterial = combinedKeyMaterial,
digest = null,
incrementalDigest = null,
incrementalMacChunkSize = 0,
ignoreDigest = true
)
}
/**
* Creates a stream to decrypt sticker data. Stickers have a special path because the key material is derived from the pack key.
*/
@@ -209,6 +195,33 @@ object AttachmentCipherInputStream {
return BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
* This will return a stream that unwraps the server's layer of encryption, giving you a stream that contains a "normally-encrypted" attachment.
*
* Because we're validating the encryptedDigest/plaintextHash of the inner layer, there's no additional out-of-band validation of this outer layer.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
private fun createForArchivedMediaOuterLayer(archivedMediaKeyMaterial: MediaKeyMaterial, file: File, originalCipherTextLength: Long): LimitedInputStream {
val mac = initMac(archivedMediaKeyMaterial.macKey)
if (file.length() <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
FileInputStream(file).use { macVerificationStream ->
verifyMac(macVerificationStream, file.length(), mac, null)
}
val encryptedStream = FileInputStream(file)
val encryptedStreamExcludingMac = LimitedInputStream(encryptedStream, file.length() - mac.macLength)
val cipher = createCipher(encryptedStreamExcludingMac, archivedMediaKeyMaterial.aesKey)
val inputStream: InputStream = BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
return LimitedInputStream(inputStream, originalCipherTextLength)
}
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
private fun create(