diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.java index 66a49c63d2..5557a9c85b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupEvent.java @@ -11,7 +11,7 @@ public class BackupEvent { private final long count; private final long estimatedTotalCount; - BackupEvent(Type type, long count, long estimatedTotalCount) { + public BackupEvent(Type type, long count, long estimatedTotalCount) { this.type = type; this.count = count; this.estimatedTotalCount = estimatedTotalCount; diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java index 0c870e85a3..49d6699004 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java @@ -22,6 +22,7 @@ public enum BackupFileIOError { FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large), NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space), VERIFICATION_FAILED(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_could_not_be_verified), + ATTACHMENT_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_contains_a_very_large_file), UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups); private static final short BACKUP_FAILED_ID = 31321; @@ -44,6 +45,7 @@ public enum BackupFileIOError { .setSmallIcon(R.drawable.ic_signal_backup) .setContentTitle(context.getString(titleId)) .setContentText(context.getString(messageId)) + .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(messageId))) .setContentIntent(pendingIntent) .build(); @@ -51,22 +53,27 @@ public enum BackupFileIOError { .notify(BACKUP_FAILED_ID, backupFailedNotification); } - public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) { + public static void postNotificationForException(@NonNull Context context, @NonNull IOException e) { BackupFileIOError error = getFromException(e); if (error != null) { error.postNotification(context); } - if (error == null && runAttempt > 0) { + if (error == null) { UNKNOWN.postNotification(context); } } private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) { - if (e.getMessage() != null) { - if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE; - else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE; + if (e instanceof FullBackupExporter.InvalidBackupStreamException) { + return ATTACHMENT_TOO_LARGE; + } else if (e.getMessage() != null) { + if (e.getMessage().contains("EFBIG")) { + return FILE_TOO_LARGE; + } else if (e.getMessage().contains("ENOSPC")) { + return NOT_ENOUGH_SPACE; + } } return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 73221d1af7..de1e524d2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -21,7 +21,7 @@ import org.signal.core.util.CursorUtil; import org.signal.core.util.SetUtil; import org.signal.core.util.Stopwatch; import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.kdf.HKDFv3; +import org.signal.libsignal.protocol.kdf.HKDF; import org.signal.libsignal.protocol.util.ByteUtil; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.AttachmentSecret; @@ -144,7 +144,7 @@ public class FullBackupExporter extends FullBackupBase { { BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase); int count = 0; - long estimatedCountOutside = 0L; + long estimatedCountOutside; try { outputStream.writeDatabaseVersion(input.getVersion()); @@ -351,80 +351,86 @@ public class FullBackupExporter extends FullBackupBase { return count; } - private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { - try { - long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)); - long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)); - long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE)); + private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, + @NonNull Cursor cursor, + @NonNull BackupFrameOutputStream outputStream, + int count, + long estimatedCount) + throws IOException + { + long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)); + long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)); + long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE)); - String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA)); - byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM)); + String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA)); + byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM)); - if (!TextUtils.isEmpty(data)) { - long fileLength = new File(data).length(); - long dbLength = size; + if (!TextUtils.isEmpty(data)) { + long fileLength = new File(data).length(); + long dbLength = size; - if (size <= 0 || fileLength != dbLength) { - size = calculateVeryOldStreamLength(attachmentSecret, random, data); - Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId)); - } + if (size <= 0 || fileLength != dbLength) { + size = calculateVeryOldStreamLength(attachmentSecret, random, data); + Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId)); } + } - if (!TextUtils.isEmpty(data) && size > 0) { - InputStream inputStream; - - if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); - else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); - + if (!TextUtils.isEmpty(data) && size > 0) { + try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) { EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); - inputStream.close(); } - } catch (IOException e) { - Log.w(TAG, e); } return count; } - private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { - try { - long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID)); - long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH)); + private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, + @NonNull Cursor cursor, + @NonNull BackupFrameOutputStream outputStream, + int count, + long estimatedCount) + throws IOException + { + long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID)); + long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH)); - String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH)); - byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); + String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH)); + byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); - if (!TextUtils.isEmpty(data) && size > 0) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); - try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) { - outputStream.writeSticker(rowId, inputStream, size); - } + if (!TextUtils.isEmpty(data) && size > 0) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); + try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) { + outputStream.writeSticker(rowId, inputStream, size); } - } catch (IOException e) { - Log.w(TAG, e); } return count; } private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException { - long result = 0; - InputStream inputStream; + long result = 0; - if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); - else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); + try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) { + int read; + byte[] buffer = new byte[8192]; - int read; - byte[] buffer = new byte[8192]; - - while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { - result += read; + while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { + result += read; + } } return result; } + private static InputStream openAttachmentStream(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException { + if (random != null && random.length == 32) { + return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); + } else { + return ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); + } + } + private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream, @NonNull List keysToIncludeInBackup, int count, @@ -528,20 +534,18 @@ public class FullBackupExporter extends FullBackupBase { private final Mac mac; private final byte[] cipherKey; - private final byte[] macKey; - - private byte[] iv; - private int counter; + private final byte[] iv; + private int counter; private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException { try { byte[] salt = Util.getSecretBytes(32); byte[] key = getBackupKey(passphrase, salt); - byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64); + byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64); byte[][] split = ByteUtil.split(derived, 32, 32); this.cipherKey = split[0]; - this.macKey = split[1]; + byte[] macKey = split[1]; this.cipher = Cipher.getInstance("AES/CTR/NoPadding"); this.mac = Mac.getInstance("HmacSHA256"); @@ -576,12 +580,17 @@ public class FullBackupExporter extends FullBackupBase { } public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException { - write(outputStream, BackupProtos.BackupFrame.newBuilder() - .setAvatar(BackupProtos.Avatar.newBuilder() - .setRecipientId(avatarName) - .setLength(Util.toIntExact(size)) - .build()) - .build()); + try { + write(outputStream, BackupProtos.BackupFrame.newBuilder() + .setAvatar(BackupProtos.Avatar.newBuilder() + .setRecipientId(avatarName) + .setLength(Util.toIntExact(size)) + .build()) + .build()); + } catch (ArithmeticException e) { + Log.w(TAG, "Unable to write avatar to backup", e); + throw new InvalidBackupStreamException(); + } if (writeStream(in) != size) { throw new IOException("Size mismatch!"); @@ -589,13 +598,18 @@ public class FullBackupExporter extends FullBackupBase { } public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException { - write(outputStream, BackupProtos.BackupFrame.newBuilder() - .setAttachment(BackupProtos.Attachment.newBuilder() - .setRowId(attachmentId.getRowId()) - .setAttachmentId(attachmentId.getUniqueId()) - .setLength(Util.toIntExact(size)) - .build()) - .build()); + try { + write(outputStream, BackupProtos.BackupFrame.newBuilder() + .setAttachment(BackupProtos.Attachment.newBuilder() + .setRowId(attachmentId.getRowId()) + .setAttachmentId(attachmentId.getUniqueId()) + .setLength(Util.toIntExact(size)) + .build()) + .build()); + } catch (ArithmeticException e) { + Log.w(TAG, "Unable to write " + attachmentId + " to backup", e); + throw new InvalidBackupStreamException(); + } if (writeStream(in) != size) { throw new IOException("Size mismatch!"); @@ -603,12 +617,17 @@ public class FullBackupExporter extends FullBackupBase { } public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException { - write(outputStream, BackupProtos.BackupFrame.newBuilder() - .setSticker(BackupProtos.Sticker.newBuilder() - .setRowId(rowId) - .setLength(Util.toIntExact(size)) - .build()) - .build()); + try { + write(outputStream, BackupProtos.BackupFrame.newBuilder() + .setSticker(BackupProtos.Sticker.newBuilder() + .setRowId(rowId) + .setLength(Util.toIntExact(size)) + .build()) + .build()); + } catch (ArithmeticException e) { + Log.w(TAG, "Unable to write sticker to backup", e); + throw new InvalidBackupStreamException(); + } if (writeStream(in) != size) { throw new IOException("Size mismatch!"); @@ -687,13 +706,14 @@ public class FullBackupExporter extends FullBackupBase { } public interface PostProcessor { - int postProcess(@NonNull Cursor cursor, int count); + int postProcess(@NonNull Cursor cursor, int count) throws IOException; } public interface BackupCancellationSignal { boolean isCanceled(); } - public static final class BackupCanceledException extends IOException { - } + public static final class BackupCanceledException extends IOException {} + + public static final class InvalidBackupStreamException extends IOException {} } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 41759c400b..6debaad495 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -145,10 +145,13 @@ public final class LocalBackupJob extends BaseJob { BackupFileIOError.VERIFICATION_FAILED.postNotification(context); } } catch (FullBackupExporter.BackupCanceledException e) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0)); Log.w(TAG, "Backup cancelled"); throw e; } catch (IOException e) { - BackupFileIOError.postNotificationForException(context, e, getRunAttempt()); + Log.w(TAG, "Error during backup!", e); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0)); + BackupFileIOError.postNotificationForException(context, e); throw e; } finally { if (tempFile.exists()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java index 4d027cbab7..3970ed9ae5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java @@ -138,11 +138,13 @@ public final class LocalBackupJobApi29 extends BaseJob { BackupFileIOError.VERIFICATION_FAILED.postNotification(context); } } catch (FullBackupExporter.BackupCanceledException e) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0)); Log.w(TAG, "Backup cancelled"); throw e; } catch (IOException e) { Log.w(TAG, "Error during backup!", e); - BackupFileIOError.postNotificationForException(context, e, getRunAttempt()); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0)); + BackupFileIOError.postNotificationForException(context, e); throw e; } finally { DocumentFile fileToCleanUp = backupDirectory.findFile(temporaryName); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb75c54f45..f69aece970 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3286,6 +3286,8 @@ There is not enough space to store your backup. Your recent backup could not be created and verified. Please create a new one. + + Your backup contains a very large file that cannot be backed up. Please delete it and create a new backup. Tap to manage backups. %d messages so far Wrong number