code clean up in HttpDownloadConnection

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Transferable.java                    |  31 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java              | 308 
src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java       |  32 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java |   5 
4 files changed, 217 insertions(+), 159 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Transferable.java 🔗

@@ -5,26 +5,27 @@ import java.util.List;
 
 public interface Transferable {
 
-	List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
-	List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
+    List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
+    List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
 
-	int STATUS_UNKNOWN = 0x200;
-	int STATUS_CHECKING = 0x201;
-	int STATUS_FAILED = 0x202;
-	int STATUS_OFFER = 0x203;
-	int STATUS_DOWNLOADING = 0x204;
-	int STATUS_OFFER_CHECK_FILESIZE = 0x206;
-	int STATUS_UPLOADING = 0x207;
-	int STATUS_CANCELLED = 0x208;
+    int GCM_AUTHENTICATION_TAG_LENGTH = 16;
 
+    int STATUS_UNKNOWN = 0x200;
+    int STATUS_CHECKING = 0x201;
+    int STATUS_FAILED = 0x202;
+    int STATUS_OFFER = 0x203;
+    int STATUS_DOWNLOADING = 0x204;
+    int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+    int STATUS_UPLOADING = 0x207;
+    int STATUS_CANCELLED = 0x208;
 
-	boolean start();
+    boolean start();
 
-	int getStatus();
+    int getStatus();
 
-	Long getFileSize();
+    Long getFileSize();
 
-	int getProgress();
+    int getProgress();
 
-	void cancel();
+    void cancel();
 }

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java 🔗

@@ -1,21 +1,12 @@
 package eu.siacs.conversations.http;
 
-import android.util.Log;
+import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
 
+import android.util.Log;
 import androidx.annotation.Nullable;
-
 import com.google.common.base.Strings;
 import com.google.common.io.ByteStreams;
 import com.google.common.primitives.Longs;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Locale;
-
-import javax.net.ssl.SSLHandshakeException;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.DownloadableFile;
@@ -27,13 +18,23 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.FileWriterException;
 import eu.siacs.conversations.utils.MimeUtils;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+import javax.net.ssl.SSLHandshakeException;
 import okhttp3.Call;
 import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Response;
-
-import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
 
 public class HttpDownloadConnection implements Transferable {
 
@@ -88,30 +89,36 @@ public class HttpDownloadConnection implements Transferable {
             } else {
                 mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
             }
-            final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
+            final AbstractConnectionManager.Extension extension =
+                    AbstractConnectionManager.Extension.of(mUrl.encodedPath());
             if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
                 this.message.setEncryption(Message.ENCRYPTION_PGP);
             } else if (message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
                 this.message.setEncryption(Message.ENCRYPTION_NONE);
             }
             final String ext = extension.getExtension();
-            final String filename = Strings.isNullOrEmpty(ext) ? message.getUuid() : String.format("%s.%s", message.getUuid(), ext);
+            final String filename =
+                    Strings.isNullOrEmpty(ext)
+                            ? message.getUuid()
+                            : String.format("%s.%s", message.getUuid(), ext);
             mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename);
             setupFile();
-            if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
+            if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL
+                    && this.file.getKey() == null) {
                 this.message.setEncryption(Message.ENCRYPTION_NONE);
             }
             final Long knownFileSize;
-            if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+            if (message.getEncryption() == Message.ENCRYPTION_PGP
+                    || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
                 knownFileSize = null;
             } else {
                 knownFileSize = message.getFileParams().size;
             }
-            Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getBody());
+            Log.d(Config.LOGTAG, "knownFileSize: " + knownFileSize + ", body=" + message.getBody());
             if (knownFileSize != null && interactive) {
                 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL
                         && this.file.getKey() != null) {
-                    this.file.setExpectedSize(knownFileSize + 16);
+                    this.file.setExpectedSize(knownFileSize + GCM_AUTHENTICATION_TAG_LENGTH);
                 } else {
                     this.file.setExpectedSize(knownFileSize);
                 }
@@ -127,9 +134,16 @@ public class HttpDownloadConnection implements Transferable {
     private void setupFile() {
         final String reference = mUrl.fragment();
         if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
-            this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid());
+            this.file =
+                    new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid());
             this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
-            Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
+            Log.d(
+                    Config.LOGTAG,
+                    "create temporary OMEMO encrypted file: "
+                            + this.file.getAbsolutePath()
+                            + "("
+                            + message.getMimeType()
+                            + ")");
         } else {
             this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
         }
@@ -158,29 +172,30 @@ public class HttpDownloadConnection implements Transferable {
     }
 
     private void decryptFile() throws IOException {
-        final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
+        final DownloadableFile outputFile =
+                mXmppConnectionService.getFileBackend().getFile(message, true);
 
-        if (outputFile.getParentFile().mkdirs()) {
+        final var directory = outputFile.getParentFile();
+        if (directory != null && directory.mkdirs()) {
             Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
         }
 
         if (!outputFile.createNewFile()) {
             Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath());
         }
+        final var cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
+        cipher.init(
+                false, new AEADParameters(new KeyParameter(this.file.getKey()), 128, file.getIv()));
+        try (final InputStream is = new FileInputStream(this.file);
+                final CipherOutputStream outputStream =
+                        new CipherOutputStream(new FileOutputStream(outputFile), cipher)) {
+            ByteStreams.copy(is, outputStream);
+        }
 
-        final InputStream is = new FileInputStream(this.file);
-
-        outputFile.setKey(this.file.getKey());
-        outputFile.setIv(this.file.getIv());
-        final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true);
-
-        ByteStreams.copy(is, os);
-
-        FileBackend.close(is);
-        FileBackend.close(os);
-
-        if (!file.delete()) {
-            Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
+        if (file.delete()) {
+            Log.w(
+                    Config.LOGTAG,
+                    "deleted temporary OMEMO encrypted file " + file.getAbsolutePath());
         }
     }
 
@@ -189,16 +204,25 @@ public class HttpDownloadConnection implements Transferable {
         mHttpConnectionManager.finishConnection(this);
         boolean notify = acceptedAutomatically && !message.isRead();
         if (message.getEncryption() == Message.ENCRYPTION_PGP) {
-            notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
+            notify =
+                    message.getConversation()
+                            .getAccount()
+                            .getPgpDecryptionService()
+                            .decrypt(message, notify);
         }
         mHttpConnectionManager.updateConversationUi(true);
         final boolean notifyAfterScan = notify;
-        final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true);
-        mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
-            if (notifyAfterScan) {
-                mXmppConnectionService.getNotificationService().push(message);
-            }
-        });
+        final DownloadableFile file =
+                mXmppConnectionService.getFileBackend().getFile(message, true);
+        mXmppConnectionService
+                .getFileBackend()
+                .updateMediaScanner(
+                        file,
+                        () -> {
+                            if (notifyAfterScan) {
+                                mXmppConnectionService.getNotificationService().push(message);
+                            }
+                        });
     }
 
     private void decryptIfNeeded() throws IOException {
@@ -223,7 +247,8 @@ public class HttpDownloadConnection implements Transferable {
         } else if (e instanceof java.net.ConnectException) {
             mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
         } else if (e instanceof FileWriterException) {
-            mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
+            mXmppConnectionService.showErrorToastInUi(
+                    R.string.download_failed_could_not_write_file);
         } else if (e instanceof InvalidFileException) {
             mXmppConnectionService.showErrorToastInUi(R.string.download_failed_invalid_file);
         } else {
@@ -267,7 +292,6 @@ public class HttpDownloadConnection implements Transferable {
             this.interactive = interactive;
         }
 
-
         @Override
         public void run() {
             check();
@@ -279,7 +303,10 @@ public class HttpDownloadConnection implements Transferable {
                 showToastForException(e);
             } else {
                 HttpDownloadConnection.this.acceptedAutomatically = false;
-                HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                HttpDownloadConnection.this
+                        .mXmppConnectionService
+                        .getNotificationService()
+                        .push(message);
             }
             cancel();
         }
@@ -289,12 +316,11 @@ public class HttpDownloadConnection implements Transferable {
             try {
                 size = retrieveFileSize();
             } catch (final Exception e) {
-                Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
+                Log.d(Config.LOGTAG, "could not retrieve file size", e);
                 retrieveFailed(e);
                 return;
             }
-            final Message.FileParams fileParams = message.getFileParams();
-            FileBackend.updateFileParams(message, fileParams.url, size);
+            persistFileSize(size);
             message.setOob(true);
             mXmppConnectionService.databaseBackend.updateMessage(message, true);
             file.setExpectedSize(size);
@@ -307,54 +333,69 @@ public class HttpDownloadConnection implements Transferable {
             } else {
                 changeStatus(STATUS_OFFER);
                 HttpDownloadConnection.this.acceptedAutomatically = false;
-                HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                HttpDownloadConnection.this
+                        .mXmppConnectionService
+                        .getNotificationService()
+                        .push(message);
             }
         }
 
         private long retrieveFileSize() throws IOException {
             Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
             changeStatus(STATUS_CHECKING);
-            final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
-                    mUrl,
-                    message.getConversation().getAccount(),
-                    interactive
-            );
-            final Request request = new Request.Builder()
-                    .url(URL.stripFragment(mUrl))
-                    .addHeader("Accept-Encoding", "identity")
-                    .head()
-                    .build();
+            final OkHttpClient client =
+                    mHttpConnectionManager.buildHttpClient(
+                            mUrl, message.getConversation().getAccount(), interactive);
+            final Request request =
+                    new Request.Builder()
+                            .url(URL.stripFragment(mUrl))
+                            .addHeader("Accept-Encoding", "identity")
+                            .head()
+                            .build();
             mostRecentCall = client.newCall(request);
-            try {
-                final Response response = mostRecentCall.execute();
+            try (final Response response = mostRecentCall.execute()) {
                 throwOnInvalidCode(response);
                 final String contentLength = response.header("Content-Length");
                 final String contentType = response.header("Content-Type");
-                final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
+                final AbstractConnectionManager.Extension extension =
+                        AbstractConnectionManager.Extension.of(mUrl.encodedPath());
                 if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
                     final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
                     if (fileExtension != null) {
-                        mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType);
-                        Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type");
+                        mXmppConnectionService
+                                .getFileBackend()
+                                .setupRelativeFilePath(
+                                        message,
+                                        String.format("%s.%s", message.getUuid(), fileExtension),
+                                        contentType);
+                        Log.d(
+                                Config.LOGTAG,
+                                "rewriting name after not finding extension in url but in content"
+                                        + " type");
                         setupFile();
                     }
                 }
-                if (Strings.isNullOrEmpty(contentLength)) {
+                final Long size = Longs.tryParse(Strings.nullToEmpty(contentLength));
+                if (size == null || size < 0) {
                     throw new IOException("no content-length found in HEAD response");
                 }
-                final long size = Long.parseLong(contentLength, 10);
-                if (size < 0) {
-                    throw new IOException("Server reported negative file size");
-                }
                 return size;
-            } catch (final IOException e) {
-                Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
-                throw e;
-            } catch (final NumberFormatException e) {
-                throw new IOException(e);
             }
         }
+    }
 
+    private void persistFileSize(final long size) {
+        final Message.FileParams fileParams = message.getFileParams();
+        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && file.getKey() != null) {
+            // store the file size of the clear text file. If we resume the download we will add the
+            // auth tag size again
+            // this is equivalent to use updating file params *after* download (which would take the
+            // clear text size as well)
+            FileBackend.updateFileParams(
+                    message, fileParams.url, size - GCM_AUTHENTICATION_TAG_LENGTH);
+        } else {
+            FileBackend.updateFileParams(message, fileParams.url, size);
+        }
     }
 
     private class FileDownloader implements Runnable {
@@ -376,65 +417,110 @@ public class HttpDownloadConnection implements Transferable {
             } catch (final SSLHandshakeException e) {
                 changeStatus(STATUS_OFFER);
             } catch (final Exception e) {
-                Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": unable to download file", e);
+                Log.d(
+                        Config.LOGTAG,
+                        message.getConversation().getAccount().getJid().asBareJid()
+                                + ": unable to download file",
+                        e);
                 if (interactive) {
                     showToastForException(e);
                 } else {
                     HttpDownloadConnection.this.acceptedAutomatically = false;
-                    HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                    HttpDownloadConnection.this
+                            .mXmppConnectionService
+                            .getNotificationService()
+                            .push(message);
                 }
                 cancel();
             }
         }
 
         private void download() throws Exception {
-            final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
-                    mUrl,
-                    message.getConversation().getAccount(),
-                    interactive
-            );
+            final OkHttpClient client =
+                    mHttpConnectionManager.buildHttpClient(
+                            mUrl, message.getConversation().getAccount(), interactive);
 
-            final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
+            final Request.Builder requestBuilder =
+                    new Request.Builder().url(URL.stripFragment(mUrl));
 
             final long expected = file.getExpectedSize();
-            final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
+            // TODO potentially just skip if file exists and size == expected? (this can happen if
+            // the decryption failed)
+            final boolean tryResume =
+                    file.exists() && file.getSize() > 0 && file.getSize() < expected;
             final long resumeSize;
             if (tryResume) {
                 resumeSize = file.getSize();
-                Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
-                requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
+                Log.d(
+                        Config.LOGTAG,
+                        "http download trying resume after " + resumeSize + " of " + expected);
+                requestBuilder.addHeader(
+                        "Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
             } else {
                 resumeSize = 0;
             }
             final Request request = requestBuilder.build();
             mostRecentCall = client.newCall(request);
-            final Response response = mostRecentCall.execute();
-            throwOnInvalidCode(response);
-            final String contentRange = response.header("Content-Range");
-            final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
-            final InputStream inputStream = response.body().byteStream();
-            final OutputStream outputStream;
-            long transmitted = 0;
-            if (tryResume && serverResumed) {
-                Log.d(Config.LOGTAG, "server resumed");
-                transmitted = file.getSize();
-                updateProgress(Math.round(((double) transmitted / expected) * 100));
-                outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
-            } else {
-                final String contentLength = response.header("Content-Length");
-                final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
-                if (expected != size) {
-                    Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
+            try (final Response response = mostRecentCall.execute()) {
+                throwOnInvalidCode(response);
+                final String contentRange = response.header("Content-Range");
+                final boolean serverResumed =
+                        tryResume
+                                && contentRange != null
+                                && contentRange.startsWith("bytes " + resumeSize + "-");
+                final var body = response.body();
+                if (body == null) {
+                    throw new IOException("response body was null");
                 }
-                file.getParentFile().mkdirs();
-                Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath());
-                if (!file.exists() && !file.createNewFile()) {
-                    throw new FileWriterException(file);
+                final InputStream inputStream = body.byteStream();
+                if (tryResume && serverResumed) {
+                    Log.d(Config.LOGTAG, "server resumed");
+                    final var offset = file.getSize();
+                    try (final OutputStream os = new FileOutputStream(file, true)) {
+                        copy(inputStream, os, offset, expected);
+                    }
+                } else {
+                    final String contentLength = response.header("Content-Length");
+                    final Long size = Longs.tryParse(Strings.nullToEmpty(contentLength));
+                    if (size == null) {
+                        Log.d(Config.LOGTAG, "no content-length in GET response (probably gzip)");
+                    } else {
+                        if (expected != size) {
+                            if (expected == 0) {
+                                // this means we got 0 (unknown) on HEAD. We won't download the file
+                                // but we update the file size so the user can try it again now that
+                                // the actual file size is known
+                                persistFileSize(size);
+                            }
+                            throw new IOException(
+                                    "Content-Length in GET response did not match expected size");
+                        }
+                    }
+                    final var directory = file.getParentFile();
+                    if (directory != null && directory.mkdirs()) {
+                        Log.d(Config.LOGTAG, "create directory " + directory.getAbsolutePath());
+                    }
+                    Log.d(Config.LOGTAG, "creating file: " + file.getAbsolutePath());
+                    if (!file.exists() && !file.createNewFile()) {
+                        throw new FileWriterException(file);
+                    }
+                    try (final OutputStream os = new FileOutputStream(file)) {
+                        copy(inputStream, os, 0, expected);
+                    }
                 }
-                outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
             }
+        }
+
+        private void copy(
+                final InputStream inputStream,
+                final OutputStream outputStream,
+                final long offset,
+                final long expected)
+                throws IOException, FileWriterException {
+            long transmitted = offset;
             int count;
             final byte[] buffer = new byte[4096];
+            updateProgress(Math.round(((double) transmitted / expected) * 100));
             while ((count = inputStream.read(buffer)) != -1) {
                 transmitted += count;
                 try {
@@ -443,11 +529,11 @@ public class HttpDownloadConnection implements Transferable {
                     throw new FileWriterException(file);
                 }
                 if (transmitted > expected) {
-                    throw new InvalidFileException(String.format("File exceeds expected size of %d", expected));
+                    throw new InvalidFileException(
+                            String.format("File exceeds expected size of %d", expected));
                 }
                 updateProgress(Math.round(((double) transmitted / expected) * 100));
             }
-            outputStream.flush();
         }
 
         private void updateImageBounds() {
@@ -463,7 +549,6 @@ public class HttpDownloadConnection implements Transferable {
             mXmppConnectionService.getFileBackend().updateFileParams(message, url);
             mXmppConnectionService.updateMessage(message);
         }
-
     }
 
     private static void throwOnInvalidCode(final Response response) throws IOException {
@@ -478,6 +563,5 @@ public class HttpDownloadConnection implements Transferable {
         private InvalidFileException(final String message) {
             super(message);
         }
-
     }
 }

src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java 🔗

@@ -4,19 +4,15 @@ import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIO
 
 import android.os.PowerManager;
 import android.os.SystemClock;
-import android.util.Log;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.content.ContextCompat;
-import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.utils.Compatibility;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.concurrent.atomic.AtomicLong;
 import okhttp3.MediaType;
 import okhttp3.RequestBody;
@@ -25,7 +21,6 @@ import okio.Okio;
 import okio.Source;
 import org.bouncycastle.crypto.engines.AESEngine;
 import org.bouncycastle.crypto.io.CipherInputStream;
-import org.bouncycastle.crypto.io.CipherOutputStream;
 import org.bouncycastle.crypto.modes.AEADBlockCipher;
 import org.bouncycastle.crypto.modes.GCMBlockCipher;
 import org.bouncycastle.crypto.params.AEADParameters;
@@ -71,7 +66,7 @@ public class AbstractConnectionManager {
             }
 
             @Override
-            public void writeTo(final BufferedSink sink) throws IOException {
+            public void writeTo(@NonNull final BufferedSink sink) throws IOException {
                 long transmitted = 0;
                 try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
                     long read;
@@ -89,29 +84,6 @@ public class AbstractConnectionManager {
         void onProgress(long progress);
     }
 
-    public static OutputStream createOutputStream(
-            DownloadableFile file, boolean append, boolean decrypt) {
-        FileOutputStream os;
-        try {
-            os = new FileOutputStream(file, append);
-            if (file.getKey() == null || !decrypt) {
-                return os;
-            }
-        } catch (FileNotFoundException e) {
-            Log.d(Config.LOGTAG, "unable to create output stream", e);
-            return null;
-        }
-        try {
-            AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
-            cipher.init(
-                    false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
-            return new CipherOutputStream(os, cipher);
-        } catch (Exception e) {
-            Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
-            return null;
-        }
-    }
-
     public XmppConnectionService getXmppConnectionService() {
         return this.mXmppConnectionService;
     }

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java 🔗

@@ -1303,7 +1303,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             this.file = file;
             this.transportSecurity = transportSecurity;
             this.transportTerminationLatch = transportTerminationLatch;
-            this.total = transportSecurity == null ? total : (total + 16);
+            this.total =
+                    transportSecurity == null ? total : (total + GCM_AUTHENTICATION_TAG_LENGTH);
             this.updateRunnable = updateRunnable;
         }
 
@@ -1445,7 +1446,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             if (this.transportSecurity == null) {
                 return fileOutputStream;
             } else {
-                final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+                final var cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
                 cipher.init(
                         false,
                         new AEADParameters(