From cd92feb2b7fe3e7f4060da47bec9a7a6a99ad79f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 8 Apr 2026 19:58:14 +0000 Subject: [PATCH] Write profile avatars to temp file before renaming to final location. --- .../securesms/profiles/AvatarHelper.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 3869aeafe8..f1e5d3d4f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -147,13 +147,7 @@ public class AvatarHelper { return; } - OutputStream outputStream = null; - try { - outputStream = getOutputStream(context, recipientId, false); - StreamUtil.copy(inputStream, outputStream); - } finally { - StreamUtil.close(outputStream); - } + setAvatarInternal(context, recipientId, inputStream, false); } public static void setSyncAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream) @@ -164,13 +158,7 @@ public class AvatarHelper { return; } - OutputStream outputStream = null; - try { - outputStream = getOutputStream(context, recipientId, true); - StreamUtil.copy(inputStream, outputStream); - } finally { - StreamUtil.close(outputStream); - } + setAvatarInternal(context, recipientId, inputStream, true); } /** @@ -219,6 +207,38 @@ public class AvatarHelper { return profileAvatar; } + /** + * Writes the avatar to a temporary file first, then renames to the final location only on success. + * This prevents partially-written or unauthenticated data from being persisted if the input stream + * throws during reading (e.g. due to a GCM authentication tag failure). + */ + private static void setAvatarInternal(@NonNull Context context, @NonNull RecipientId recipientId, @NonNull InputStream inputStream, boolean isSyncAvatar) + throws IOException + { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + File targetFile = getAvatarFile(context, recipientId, isSyncAvatar); + File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp"); + + OutputStream outputStream = null; + try { + outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, tempFile, true).getSecond(); + StreamUtil.copy(inputStream, outputStream); + } catch (IOException e) { + StreamUtil.close(outputStream); + outputStream = null; + tempFile.delete(); + throw e; + } finally { + StreamUtil.close(outputStream); + } + + if (!tempFile.renameTo(targetFile)) { + tempFile.delete(); + throw new IOException("Failed to rename temp avatar file to final location"); + } + } + + private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) { return new File(getAvatarDirectory(context), recipientId.serialize() + (isSyncAvatar ? "-sync" : "")); }