mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 15:11:42 +01:00
Add incremental digests to attachment sending.
This commit is contained in:
@@ -154,7 +154,7 @@ public class SignalServiceMessageReceiver {
|
||||
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
|
||||
|
||||
socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
|
||||
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get());
|
||||
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), pointer.getincrementalDigest().orElse(new byte[0]));
|
||||
}
|
||||
|
||||
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
|
||||
|
||||
@@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.AttachmentDigest;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
@@ -762,7 +763,7 @@ public class SignalServiceMessageSender {
|
||||
v2UploadAttributes = socket.getAttachmentV2UploadAttributes();
|
||||
}
|
||||
|
||||
Pair<Long, byte[]> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes);
|
||||
Pair<Long, AttachmentDigest> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes);
|
||||
|
||||
return new SignalServiceAttachmentPointer(0,
|
||||
new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()),
|
||||
@@ -771,7 +772,8 @@ public class SignalServiceMessageSender {
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(), attachment.getHeight(),
|
||||
Optional.of(attachmentIdAndDigest.second()),
|
||||
Optional.of(attachmentIdAndDigest.second().getDigest()),
|
||||
Optional.of(attachmentIdAndDigest.second().getIncrementalDigest()),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.isBorderless(),
|
||||
@@ -811,7 +813,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException {
|
||||
byte[] digest = socket.uploadAttachment(attachmentData);
|
||||
AttachmentDigest digest = socket.uploadAttachment(attachmentData);
|
||||
return new SignalServiceAttachmentPointer(attachmentData.getResumableUploadSpec().getCdnNumber(),
|
||||
new SignalServiceAttachmentRemoteId(attachmentData.getResumableUploadSpec().getCdnKey()),
|
||||
attachment.getContentType(),
|
||||
@@ -820,7 +822,8 @@ public class SignalServiceMessageSender {
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.of(digest),
|
||||
Optional.of(digest.getDigest()),
|
||||
Optional.ofNullable(digest.getIncrementalDigest()),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.isBorderless(),
|
||||
|
||||
@@ -8,6 +8,8 @@ package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.signal.libsignal.protocol.InvalidMacException;
|
||||
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.HKDFv3;
|
||||
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
@@ -51,7 +53,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
private long totalRead;
|
||||
private byte[] overflowBuffer;
|
||||
|
||||
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
|
||||
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest)
|
||||
throws InvalidMessageException, IOException
|
||||
{
|
||||
try {
|
||||
@@ -71,7 +73,18 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
verifyMac(fin, file.length(), mac, digest);
|
||||
}
|
||||
|
||||
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
|
||||
final FileInputStream innerStream = new FileInputStream(file);
|
||||
|
||||
boolean hasIncrementalMac = incrementalDigest != null && incrementalDigest.length > 0;
|
||||
|
||||
InputStream wrap = !hasIncrementalMac ? innerStream
|
||||
: new IncrementalMacInputStream(
|
||||
innerStream,
|
||||
parts[1],
|
||||
ChunkSizeChoice.inferChunkSize(Math.max(Math.toIntExact(file.length()), 1)),
|
||||
incrementalDigest);
|
||||
|
||||
InputStream inputStream = new AttachmentCipherInputStream(wrap, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
|
||||
|
||||
if (plaintextLength != 0) {
|
||||
inputStream = new ContentLengthInputStream(inputStream, plaintextLength);
|
||||
|
||||
@@ -25,6 +25,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
private final Optional<Integer> size;
|
||||
private final Optional<byte[]> preview;
|
||||
private final Optional<byte[]> digest;
|
||||
private final Optional<byte[]> incrementalDigest;
|
||||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final boolean borderless;
|
||||
@@ -44,6 +45,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
int width,
|
||||
int height,
|
||||
Optional<byte[]> digest,
|
||||
Optional<byte[]> incrementalDigest,
|
||||
Optional<String> fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
@@ -53,21 +55,22 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType);
|
||||
this.cdnNumber = cdnNumber;
|
||||
this.remoteId = remoteId;
|
||||
this.key = key;
|
||||
this.size = size;
|
||||
this.preview = preview;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.digest = digest;
|
||||
this.fileName = fileName;
|
||||
this.voiceNote = voiceNote;
|
||||
this.borderless = borderless;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.uploadTimestamp = uploadTimestamp;
|
||||
this.gif = gif;
|
||||
this.cdnNumber = cdnNumber;
|
||||
this.remoteId = remoteId;
|
||||
this.key = key;
|
||||
this.size = size;
|
||||
this.preview = preview;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.digest = digest;
|
||||
this.incrementalDigest = incrementalDigest;
|
||||
this.fileName = fileName;
|
||||
this.voiceNote = voiceNote;
|
||||
this.borderless = borderless;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.uploadTimestamp = uploadTimestamp;
|
||||
this.gif = gif;
|
||||
}
|
||||
|
||||
public int getCdnNumber() {
|
||||
@@ -108,6 +111,10 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
return digest;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getincrementalDigest() {
|
||||
return incrementalDigest;
|
||||
}
|
||||
|
||||
public boolean getVoiceNote() {
|
||||
return voiceNote;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public final class AttachmentPointerUtil {
|
||||
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.empty(),
|
||||
pointer.getWidth(), pointer.getHeight(),
|
||||
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.empty(),
|
||||
pointer.hasIncrementalDigest() ? Optional.of(pointer.getIncrementalDigest().toByteArray()) : Optional.empty(),
|
||||
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.empty(),
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE)) != 0,
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE)) != 0,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.crypto
|
||||
|
||||
data class AttachmentDigest(val digest: ByteArray, val incrementalDigest: ByteArray?)
|
||||
@@ -112,6 +112,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalUrl;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.AttachmentDigest;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException;
|
||||
@@ -1345,7 +1346,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes)
|
||||
public AttachmentDigest uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes)
|
||||
throws IOException
|
||||
{
|
||||
return uploadToCdn0(AVATAR_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(),
|
||||
@@ -1358,17 +1359,17 @@ public class PushServiceSocket {
|
||||
null, null);
|
||||
}
|
||||
|
||||
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes)
|
||||
public Pair<Long, AttachmentDigest> uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
long id = Long.parseLong(uploadAttributes.getAttachmentId());
|
||||
byte[] digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(),
|
||||
uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(),
|
||||
uploadAttributes.getCredential(), uploadAttributes.getDate(),
|
||||
uploadAttributes.getSignature(), attachment.getData(),
|
||||
"application/octet-stream", attachment.getDataSize(),
|
||||
attachment.getOutputStreamFactory(), attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
long id = Long.parseLong(uploadAttributes.getAttachmentId());
|
||||
AttachmentDigest digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(),
|
||||
uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(),
|
||||
uploadAttributes.getCredential(), uploadAttributes.getDate(),
|
||||
uploadAttributes.getSignature(), attachment.getData(),
|
||||
"application/octet-stream", attachment.getDataSize(),
|
||||
attachment.getOutputStreamFactory(), attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
|
||||
return new Pair<>(id, digest);
|
||||
}
|
||||
@@ -1382,7 +1383,7 @@ public class PushServiceSocket {
|
||||
System.currentTimeMillis() + CDN2_RESUMABLE_LINK_LIFETIME_MILLIS);
|
||||
}
|
||||
|
||||
public byte[] uploadAttachment(PushAttachmentData attachment) throws IOException {
|
||||
public AttachmentDigest uploadAttachment(PushAttachmentData attachment) throws IOException {
|
||||
|
||||
if (attachment.getResumableUploadSpec() == null || attachment.getResumableUploadSpec().getExpirationTimestamp() < System.currentTimeMillis()) {
|
||||
throw new ResumeLocationInvalidException();
|
||||
@@ -1472,11 +1473,11 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] uploadToCdn0(String path, String acl, String key, String policy, String algorithm,
|
||||
String credential, String date, String signature,
|
||||
InputStream data, String contentType, long length,
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener progressListener,
|
||||
CancelationSignal cancelationSignal)
|
||||
private AttachmentDigest uploadToCdn0(String path, String acl, String key, String policy, String algorithm,
|
||||
String credential, String date, String signature,
|
||||
InputStream data, String contentType, long length,
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener progressListener,
|
||||
CancelationSignal cancelationSignal)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(0), random);
|
||||
@@ -1516,7 +1517,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
try (Response response = call.execute()) {
|
||||
if (response.isSuccessful()) return file.getTransmittedDigest();
|
||||
if (response.isSuccessful()) return file.getAttachmentDigest();
|
||||
else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
||||
} catch (PushNetworkException | NonSuccessfulResponseCodeException e) {
|
||||
throw e;
|
||||
@@ -1577,7 +1578,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException {
|
||||
private AttachmentDigest uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException {
|
||||
ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(2), random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
@@ -1593,7 +1594,7 @@ public class PushServiceSocket {
|
||||
try (NowhereBufferedSink buffer = new NowhereBufferedSink()) {
|
||||
file.writeTo(buffer);
|
||||
}
|
||||
return file.getTransmittedDigest();
|
||||
return file.getAttachmentDigest();
|
||||
}
|
||||
|
||||
Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, resumableUrl))
|
||||
@@ -1611,7 +1612,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
try (Response response = call.execute()) {
|
||||
if (response.isSuccessful()) return file.getTransmittedDigest();
|
||||
if (response.isSuccessful()) return file.getAttachmentDigest();
|
||||
else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
||||
} catch (PushNetworkException | NonSuccessfulResponseCodeException e) {
|
||||
throw e;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.whispersystems.signalservice.internal.push.http;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory {
|
||||
|
||||
private final byte[] key;
|
||||
private final byte[] iv;
|
||||
|
||||
public AttachmentCipherOutputStreamFactory(byte[] key, byte[] iv) {
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigestingOutputStream createFor(OutputStream wrap) throws IOException {
|
||||
return new AttachmentCipherOutputStream(key, iv, wrap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.signalservice.internal.push.http
|
||||
|
||||
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice
|
||||
import org.signal.libsignal.protocol.incrementalmac.IncrementalMacOutputStream
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
|
||||
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Creates [AttachmentCipherOutputStream] using the provided [key] and [iv].
|
||||
*
|
||||
* [createFor] is straightforward, and is the legacy behavior.
|
||||
* [createIncrementalFor] first wraps the stream in an [IncrementalMacOutputStream] to calculate MAC digests on chunks as the stream is written to.
|
||||
*
|
||||
* @property key
|
||||
* @property iv
|
||||
*/
|
||||
class AttachmentCipherOutputStreamFactory(private val key: ByteArray, private val iv: ByteArray) : OutputStreamFactory {
|
||||
companion object {
|
||||
private const val AES_KEY_LENGTH = 32
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createFor(wrap: OutputStream): DigestingOutputStream {
|
||||
return AttachmentCipherOutputStream(key, iv, wrap)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun createIncrementalFor(wrap: OutputStream?, length: Long, incrementalDigestOut: OutputStream?): DigestingOutputStream {
|
||||
if (length > Int.MAX_VALUE) {
|
||||
throw IllegalArgumentException("Attachment length overflows int!")
|
||||
}
|
||||
|
||||
val privateKey = key.sliceArray(AES_KEY_LENGTH until key.size)
|
||||
val chunkSizeChoice = ChunkSizeChoice.inferChunkSize(length.toInt().coerceAtLeast(1))
|
||||
val incrementalStream = IncrementalMacOutputStream(wrap, privateKey, chunkSizeChoice, incrementalDigestOut)
|
||||
return createFor(incrementalStream)
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.whispersystems.signalservice.internal.push.http;
|
||||
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.SkippingOutputStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okio.BufferedSink;
|
||||
|
||||
public class DigestingRequestBody extends RequestBody {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final OutputStreamFactory outputStreamFactory;
|
||||
private final String contentType;
|
||||
private final long contentLength;
|
||||
private final ProgressListener progressListener;
|
||||
private final CancelationSignal cancelationSignal;
|
||||
private final long contentStart;
|
||||
|
||||
private byte[] digest;
|
||||
|
||||
public DigestingRequestBody(InputStream inputStream,
|
||||
OutputStreamFactory outputStreamFactory,
|
||||
String contentType, long contentLength,
|
||||
ProgressListener progressListener,
|
||||
CancelationSignal cancelationSignal,
|
||||
long contentStart)
|
||||
{
|
||||
Preconditions.checkArgument(contentLength >= contentStart);
|
||||
Preconditions.checkArgument(contentStart >= 0);
|
||||
|
||||
this.inputStream = inputStream;
|
||||
this.outputStreamFactory = outputStreamFactory;
|
||||
this.contentType = contentType;
|
||||
this.contentLength = contentLength;
|
||||
this.progressListener = progressListener;
|
||||
this.cancelationSignal = cancelationSignal;
|
||||
this.contentStart = contentStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return MediaType.parse(contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
DigestingOutputStream outputStream = outputStreamFactory.createFor(new SkippingOutputStream(contentStart, sink.outputStream()));
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
int read;
|
||||
long total = 0;
|
||||
|
||||
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
if (cancelationSignal != null && cancelationSignal.isCanceled()) {
|
||||
throw new IOException("Canceled!");
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
total += read;
|
||||
|
||||
if (progressListener != null) {
|
||||
progressListener.onAttachmentProgress(contentLength, total);
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
digest = outputStream.getTransmittedDigest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
if (contentLength > 0) return contentLength - contentStart;
|
||||
else return -1;
|
||||
}
|
||||
|
||||
public byte[] getTransmittedDigest() {
|
||||
return digest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.whispersystems.signalservice.internal.push.http
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.BufferedSink
|
||||
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream
|
||||
import org.whispersystems.signalservice.api.crypto.SkippingOutputStream
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.internal.crypto.AttachmentDigest
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* This [RequestBody] encrypts the data written to it before it is sent.
|
||||
*/
|
||||
class DigestingRequestBody(
|
||||
private val inputStream: InputStream,
|
||||
private val outputStreamFactory: OutputStreamFactory,
|
||||
private val contentType: String,
|
||||
private val contentLength: Long,
|
||||
private val progressListener: SignalServiceAttachment.ProgressListener?,
|
||||
private val cancelationSignal: CancelationSignal?,
|
||||
private val contentStart: Long
|
||||
) : RequestBody() {
|
||||
lateinit var transmittedDigest: ByteArray
|
||||
private set
|
||||
var incrementalDigest: ByteArray? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
require(contentLength >= contentStart)
|
||||
require(contentStart >= 0)
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return MediaType.parse(contentType)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val digestStream = ByteArrayOutputStream()
|
||||
val inner = SkippingOutputStream(contentStart, sink.outputStream())
|
||||
val outputStream: DigestingOutputStream = if (outputStreamFactory is AttachmentCipherOutputStreamFactory) {
|
||||
outputStreamFactory.createIncrementalFor(inner, contentLength, digestStream)
|
||||
} else {
|
||||
outputStreamFactory.createFor(inner)
|
||||
}
|
||||
|
||||
val buffer = ByteArray(8192)
|
||||
var read: Int
|
||||
var total: Long = 0
|
||||
|
||||
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
|
||||
if (cancelationSignal?.isCanceled == true) {
|
||||
throw IOException("Canceled!")
|
||||
}
|
||||
outputStream.write(buffer, 0, read)
|
||||
total += read.toLong()
|
||||
progressListener?.onAttachmentProgress(contentLength, total)
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
digestStream.close()
|
||||
|
||||
incrementalDigest = digestStream.toByteArray()
|
||||
transmittedDigest = outputStream.transmittedDigest
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return if (contentLength > 0) contentLength - contentStart else -1
|
||||
}
|
||||
|
||||
fun getAttachmentDigest() = AttachmentDigest(transmittedDigest, incrementalDigest)
|
||||
|
||||
companion object {
|
||||
const val TAG = "DigestingRequestBody"
|
||||
}
|
||||
}
|
||||
@@ -667,20 +667,21 @@ message AttachmentPointer {
|
||||
fixed64 cdnId = 1;
|
||||
string cdnKey = 15;
|
||||
}
|
||||
optional string contentType = 2;
|
||||
optional bytes key = 3;
|
||||
optional uint32 size = 4;
|
||||
optional bytes thumbnail = 5;
|
||||
optional bytes digest = 6;
|
||||
optional string fileName = 7;
|
||||
optional uint32 flags = 8;
|
||||
optional uint32 width = 9;
|
||||
optional uint32 height = 10;
|
||||
optional string caption = 11;
|
||||
optional string blurHash = 12;
|
||||
optional uint64 uploadTimestamp = 13;
|
||||
optional uint32 cdnNumber = 14;
|
||||
// Next ID: 16
|
||||
optional string contentType = 2;
|
||||
optional bytes key = 3;
|
||||
optional uint32 size = 4;
|
||||
optional bytes thumbnail = 5;
|
||||
optional bytes digest = 6;
|
||||
optional bytes incrementalDigest = 16;
|
||||
optional string fileName = 7;
|
||||
optional uint32 flags = 8;
|
||||
optional uint32 width = 9;
|
||||
optional uint32 height = 10;
|
||||
optional string caption = 11;
|
||||
optional string blurHash = 12;
|
||||
optional uint64 uploadTimestamp = 13;
|
||||
optional uint32 cdnNumber = 14;
|
||||
// Next ID: 17
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.crypto;
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
@@ -17,9 +18,11 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS;
|
||||
|
||||
public final class AttachmentCipherTest {
|
||||
@@ -32,9 +35,9 @@ public final class AttachmentCipherTest {
|
||||
public void attachment_encryptDecrypt() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Peter Parker".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
@@ -46,9 +49,9 @@ public final class AttachmentCipherTest {
|
||||
public void attachment_encryptDecryptEmpty() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
@@ -57,19 +60,19 @@ public final class AttachmentCipherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_decryptFailOnBadKey() throws IOException{
|
||||
public void attachment_decryptFailOnBadKey() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
byte[] badKey = new byte[64];
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badKey = new byte[64];
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest);
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest, encryptResult.incrementalDigest);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
@@ -82,19 +85,19 @@ public final class AttachmentCipherTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_decryptFailOnBadDigest() throws IOException{
|
||||
public void attachment_decryptFailOnBadDigest() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Mary Jane Watson".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
byte[] badDigest = new byte[32];
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Mary Jane Watson".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badDigest = new byte[32];
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest);
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest, encryptResult.incrementalDigest);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
@@ -106,9 +109,42 @@ public final class AttachmentCipherTest {
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_decryptFailOnBadIncrementalDigest() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = new byte[1000000];
|
||||
|
||||
new Random().nextBytes(plaintextInput);
|
||||
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.length);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, badDigest);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
fail();
|
||||
} catch (InvalidMacException e) {
|
||||
hitCorrectException = true;
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = false;
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecryptPaddedContent() throws IOException, InvalidMessageException {
|
||||
int[] lengths = { 531, 600, 724, 1019, 1024 };
|
||||
int[] lengths = { 531, 600, 724, 1019, 1024 };
|
||||
|
||||
for (int length : lengths) {
|
||||
byte[] plaintextInput = new byte[length];
|
||||
@@ -117,24 +153,26 @@ public final class AttachmentCipherTest {
|
||||
plaintextInput[i] = (byte) 0x97;
|
||||
}
|
||||
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput);
|
||||
InputStream dataStream = new PaddingInputStream(inputStream, length);
|
||||
ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream();
|
||||
DigestingOutputStream digestStream = new AttachmentCipherOutputStreamFactory(key, null).createFor(encryptedStream);
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput);
|
||||
InputStream paddedInputStream = new PaddingInputStream(inputStream, length);
|
||||
ByteArrayOutputStream destinationOutputStream = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream incrementalDigestOutputStream = new ByteArrayOutputStream();
|
||||
DigestingOutputStream encryptingOutputStream = new AttachmentCipherOutputStreamFactory(key, iv).createIncrementalFor(destinationOutputStream, length, incrementalDigestOutputStream);
|
||||
|
||||
Util.copy(dataStream, digestStream);
|
||||
digestStream.flush();
|
||||
Util.copy(paddedInputStream, encryptingOutputStream);
|
||||
|
||||
byte[] digest = digestStream.getTransmittedDigest();
|
||||
byte[] encryptedData = encryptedStream.toByteArray();
|
||||
encryptingOutputStream.flush();
|
||||
encryptingOutputStream.close();
|
||||
|
||||
encryptedStream.close();
|
||||
inputStream.close();
|
||||
byte[] encryptedData = destinationOutputStream.toByteArray();
|
||||
byte[] digest = encryptingOutputStream.getTransmittedDigest();
|
||||
byte[] incrementalDigest = incrementalDigestOutputStream.toByteArray();
|
||||
|
||||
File cipherFile = writeToFile(encryptedData);
|
||||
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest);
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest, incrementalDigest);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
@@ -149,13 +187,13 @@ public final class AttachmentCipherTest {
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Aunt May".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Aunt May".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null);
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null, encryptResult.incrementalDigest);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
@@ -175,14 +213,14 @@ public final class AttachmentCipherTest {
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "Uncle Ben".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
|
||||
|
||||
badMacCiphertext[badMacCiphertext.length - 1] += 1;
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
@@ -200,7 +238,7 @@ public final class AttachmentCipherTest {
|
||||
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "Peter Parker".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
@@ -213,7 +251,7 @@ public final class AttachmentCipherTest {
|
||||
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
@@ -227,10 +265,10 @@ public final class AttachmentCipherTest {
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
|
||||
byte[] badPackKey = new byte[32];
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
byte[] badPackKey = new byte[32];
|
||||
|
||||
AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey);
|
||||
} catch (InvalidMessageException e) {
|
||||
@@ -249,7 +287,7 @@ public final class AttachmentCipherTest {
|
||||
try {
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "Uncle Ben".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
|
||||
|
||||
badMacCiphertext[badMacCiphertext.length - 1] += 1;
|
||||
@@ -262,15 +300,26 @@ public final class AttachmentCipherTest {
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
private static EncryptResult encryptData(byte[] data, byte[] keyMaterial) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
AttachmentCipherOutputStream encryptStream = new AttachmentCipherOutputStream(keyMaterial, null, outputStream);
|
||||
private static EncryptResult encryptData(byte[] data, byte[] keyMaterial, boolean withIncremental) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream incrementalDigestOut = new ByteArrayOutputStream();
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
AttachmentCipherOutputStreamFactory factory = new AttachmentCipherOutputStreamFactory(keyMaterial, iv);
|
||||
|
||||
DigestingOutputStream encryptStream;
|
||||
if (withIncremental) {
|
||||
encryptStream = factory.createIncrementalFor(outputStream, data.length, incrementalDigestOut);
|
||||
} else {
|
||||
encryptStream = factory.createFor(outputStream);
|
||||
}
|
||||
|
||||
|
||||
encryptStream.write(data);
|
||||
encryptStream.flush();
|
||||
encryptStream.close();
|
||||
incrementalDigestOut.close();
|
||||
|
||||
return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest());
|
||||
return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest(), incrementalDigestOut.toByteArray());
|
||||
}
|
||||
|
||||
private static File writeToFile(byte[] data) throws IOException {
|
||||
@@ -296,10 +345,12 @@ public final class AttachmentCipherTest {
|
||||
private static class EncryptResult {
|
||||
final byte[] ciphertext;
|
||||
final byte[] digest;
|
||||
final byte[] incrementalDigest;
|
||||
|
||||
private EncryptResult(byte[] ciphertext, byte[] digest) {
|
||||
private EncryptResult(byte[] ciphertext, byte[] digest, byte[] incrementalDigest) {
|
||||
this.ciphertext = ciphertext;
|
||||
this.digest = digest;
|
||||
this.incrementalDigest = incrementalDigest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DigestingRequestBodyTest {
|
||||
private final OutputStreamFactory outputStreamFactory = new AttachmentCipherOutputStreamFactory(attachmentKey, attachmentIV);
|
||||
|
||||
@Test
|
||||
public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameTransmittedDigest() throws Exception {
|
||||
public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameDigests() throws Exception {
|
||||
DigestingRequestBody fromStart = getBody(0);
|
||||
DigestingRequestBody fromMiddle = getBody(CONTENT_LENGTH / 2);
|
||||
|
||||
@@ -36,6 +36,7 @@ public class DigestingRequestBodyTest {
|
||||
}
|
||||
|
||||
assertArrayEquals(fromStart.getTransmittedDigest(), fromMiddle.getTransmittedDigest());
|
||||
assertArrayEquals(fromStart.getIncrementalDigest(), fromMiddle.getIncrementalDigest());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user