From 028c6edd8a4a7497490440875abde89c4c0d036b Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sat, 24 Feb 2018 11:09:26 -0800 Subject: [PATCH] Manually calculate attachment offsets The CipherInputStream skip() method is pretty non-functional Fixes #7438 --- .../ModernDecryptingPartInputStream.java | 35 +++++++++++++------ .../database/AttachmentDatabase.java | 24 ++++++++----- .../securesms/mms/PartAuthority.java | 2 +- .../securesms/providers/PartProvider.java | 4 +-- .../providers/PersistentBlobProvider.java | 2 +- .../video/EncryptedMediaDataSource.java | 8 ++--- .../securesms/video/exo/PartDataSource.java | 12 ++----- 7 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/org/thoughtcrime/securesms/crypto/ModernDecryptingPartInputStream.java b/src/org/thoughtcrime/securesms/crypto/ModernDecryptingPartInputStream.java index f459d5dc8c..bfe0672e73 100644 --- a/src/org/thoughtcrime/securesms/crypto/ModernDecryptingPartInputStream.java +++ b/src/org/thoughtcrime/securesms/crypto/ModernDecryptingPartInputStream.java @@ -3,9 +3,10 @@ package org.thoughtcrime.securesms.crypto; import android.support.annotation.NonNull; +import org.thoughtcrime.securesms.util.Conversions; + import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.InvalidAlgorithmParameterException; @@ -21,13 +22,13 @@ import javax.crypto.spec.SecretKeySpec; public class ModernDecryptingPartInputStream { - public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull File file) + public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull File file, long offset) throws IOException { - return createFor(attachmentSecret, random, new FileInputStream(file)); + return createFor(attachmentSecret, random, new FileInputStream(file), offset); } - public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file) + public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, long offset) throws IOException { FileInputStream inputStream = new FileInputStream(file); @@ -35,25 +36,37 @@ public class ModernDecryptingPartInputStream { readFully(inputStream, random); - return createFor(attachmentSecret, random, inputStream); + return createFor(attachmentSecret, random, inputStream, offset); } - private static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull InputStream inputStream) { + private static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull InputStream inputStream, long offset) throws IOException { try { - Mac mac = Mac.getInstance("HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256")); - byte[] iv = new byte[16]; - byte[] key = mac.doFinal(random); + byte[] iv = new byte[16]; + int remainder = (int) (offset % 16); + Conversions.longTo4ByteArray(iv, 12, offset / 16); + byte[] key = mac.doFinal(random); Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); - return new CipherInputStream(inputStream, cipher); + long skipped = inputStream.skip(offset - remainder); + + if (skipped != offset - remainder) { + throw new IOException("Skip failed: " + skipped + " vs " + (offset - remainder)); + } + + CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); + byte[] remainderBuffer = new byte[remainder]; + + readFully(cipherInputStream, remainderBuffer); + + return cipherInputStream; } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) { throw new AssertionError(e); } - } private static void readFully(InputStream in, byte[] buffer) throws IOException { diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 3537a59edb..8793317d54 100644 --- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -124,10 +124,10 @@ public class AttachmentDatabase extends Database { this.attachmentSecret = attachmentSecret; } - public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId) + public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId, long offset) throws IOException { - InputStream dataStream = getDataStream(attachmentId, DATA); + InputStream dataStream = getDataStream(attachmentId, DATA, offset); if (dataStream == null) throw new IOException("No stream for: " + attachmentId); else return dataStream; @@ -137,7 +137,7 @@ public class AttachmentDatabase extends Database { throws IOException { Log.w(TAG, "getThumbnailStream(" + attachmentId + ")"); - InputStream dataStream = getDataStream(attachmentId, THUMBNAIL); + InputStream dataStream = getDataStream(attachmentId, THUMBNAIL, 0); if (dataStream != null) { return dataStream; @@ -380,7 +380,7 @@ public class AttachmentDatabase extends Database { @SuppressWarnings("WeakerAccess") @VisibleForTesting - protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType) + protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset) { DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType); @@ -390,9 +390,17 @@ public class AttachmentDatabase extends Database { try { if (dataInfo.random != null && dataInfo.random.length == 32) { - return ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file); + return ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file, offset); } else { - return ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file); + InputStream stream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file); + long skipped = stream.skip(offset); + + if (skipped != offset) { + Log.w(TAG, "Skip failed: " + skipped + " vs " + offset); + return null; + } + + return stream; } } catch (IOException e) { Log.w(TAG, e); @@ -590,7 +598,7 @@ public class AttachmentDatabase extends Database { @Override public @Nullable InputStream call() throws Exception { Log.w(TAG, "Executing thumbnail job..."); - final InputStream stream = getDataStream(attachmentId, THUMBNAIL); + final InputStream stream = getDataStream(attachmentId, THUMBNAIL, 0); if (stream != null) { return stream; @@ -616,7 +624,7 @@ public class AttachmentDatabase extends Database { updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio()); - return getDataStream(attachmentId, THUMBNAIL); + return getDataStream(attachmentId, THUMBNAIL, 0); } @SuppressLint("NewApi") diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 9289b2a368..659888f88a 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -47,7 +47,7 @@ public class PartAuthority { int match = uriMatcher.match(uri); try { switch (match) { - case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId()); + case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0); case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId()); case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); diff --git a/src/org/thoughtcrime/securesms/providers/PartProvider.java b/src/org/thoughtcrime/securesms/providers/PartProvider.java index 35bd7bd077..a0410dd271 100644 --- a/src/org/thoughtcrime/securesms/providers/PartProvider.java +++ b/src/org/thoughtcrime/securesms/providers/PartProvider.java @@ -158,10 +158,10 @@ public class PartProvider extends ContentProvider { } private ParcelFileDescriptor getParcelStreamForAttachment(AttachmentId attachmentId) throws IOException { - long plaintextLength = Util.getStreamLength(DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId)); + long plaintextLength = Util.getStreamLength(DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId, 0)); MemoryFile memoryFile = new MemoryFile(attachmentId.toString(), Util.toIntExact(plaintextLength)); - InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId); + InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(attachmentId, 0); OutputStream out = memoryFile.getOutputStream(); Util.copy(in, out); diff --git a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java index 93b33ab818..1d5cb8441f 100644 --- a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java +++ b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java @@ -160,7 +160,7 @@ public class PersistentBlobProvider { FileData fileData = getFile(context, id); - if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file); + if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0); else return ClassicDecryptingPartInputStream.createFor(attachmentSecret, fileData.file); } diff --git a/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java b/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java index 71dc2ceb7c..89956cf67a 100644 --- a/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java +++ b/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java @@ -58,13 +58,9 @@ public class EncryptedMediaDataSource extends MediaDataSource { private int readAtModern(long position, byte[] bytes, int offset, int length) throws IOException { assert(random != null); - InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, mediaFile); + InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, mediaFile, position); + int returnValue = inputStream.read(bytes, offset, length); - if (inputStream.skip(position) != position) { - throw new IOException("Skip failed: " + position); - } - - int returnValue = inputStream.read(bytes, offset, length); inputStream.close(); return returnValue; diff --git a/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java index d9d86ca849..220c733d83 100644 --- a/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java +++ b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java @@ -11,7 +11,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.PartUriParser; @@ -28,8 +27,7 @@ public class PartDataSource implements DataSource { private Uri uri; private InputStream inputSteam; - public PartDataSource(@NonNull Context context, @Nullable TransferListener listener) - { + PartDataSource(@NonNull Context context, @Nullable TransferListener listener) { this.context = context.getApplicationContext(); this.listener = listener; } @@ -44,13 +42,7 @@ public class PartDataSource implements DataSource { if (attachment == null) throw new IOException("Attachment not found"); - this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId()); - - if (inputSteam == null) throw new IOException("InputStream not foudn"); - - long skipped = this.inputSteam.skip(dataSpec.position); - - if (skipped != dataSpec.position) throw new IOException("Skip failed!"); + this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position); if (listener != null) { listener.onTransferStart(this, dataSpec);