Manually calculate attachment offsets

The CipherInputStream skip() method is pretty non-functional

Fixes #7438
This commit is contained in:
Moxie Marlinspike 2018-02-24 11:09:26 -08:00
parent 4324f0b7ec
commit 028c6edd8a
7 changed files with 48 additions and 39 deletions

View file

@ -3,9 +3,10 @@ package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Conversions;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
@ -21,13 +22,13 @@ import javax.crypto.spec.SecretKeySpec;
public class ModernDecryptingPartInputStream { 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 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 throws IOException
{ {
FileInputStream inputStream = new FileInputStream(file); FileInputStream inputStream = new FileInputStream(file);
@ -35,25 +36,37 @@ public class ModernDecryptingPartInputStream {
readFully(inputStream, random); 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 { try {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256")); mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
byte[] iv = new byte[16]; byte[] iv = new byte[16];
byte[] key = mac.doFinal(random); int remainder = (int) (offset % 16);
Conversions.longTo4ByteArray(iv, 12, offset / 16);
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); 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) { } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
private static void readFully(InputStream in, byte[] buffer) throws IOException { private static void readFully(InputStream in, byte[] buffer) throws IOException {

View file

@ -124,10 +124,10 @@ public class AttachmentDatabase extends Database {
this.attachmentSecret = attachmentSecret; this.attachmentSecret = attachmentSecret;
} }
public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId) public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId, long offset)
throws IOException throws IOException
{ {
InputStream dataStream = getDataStream(attachmentId, DATA); InputStream dataStream = getDataStream(attachmentId, DATA, offset);
if (dataStream == null) throw new IOException("No stream for: " + attachmentId); if (dataStream == null) throw new IOException("No stream for: " + attachmentId);
else return dataStream; else return dataStream;
@ -137,7 +137,7 @@ public class AttachmentDatabase extends Database {
throws IOException throws IOException
{ {
Log.w(TAG, "getThumbnailStream(" + attachmentId + ")"); Log.w(TAG, "getThumbnailStream(" + attachmentId + ")");
InputStream dataStream = getDataStream(attachmentId, THUMBNAIL); InputStream dataStream = getDataStream(attachmentId, THUMBNAIL, 0);
if (dataStream != null) { if (dataStream != null) {
return dataStream; return dataStream;
@ -380,7 +380,7 @@ public class AttachmentDatabase extends Database {
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@VisibleForTesting @VisibleForTesting
protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType) protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset)
{ {
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType); DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType);
@ -390,9 +390,17 @@ public class AttachmentDatabase extends Database {
try { try {
if (dataInfo.random != null && dataInfo.random.length == 32) { 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 { } 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) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
@ -590,7 +598,7 @@ public class AttachmentDatabase extends Database {
@Override @Override
public @Nullable InputStream call() throws Exception { public @Nullable InputStream call() throws Exception {
Log.w(TAG, "Executing thumbnail job..."); Log.w(TAG, "Executing thumbnail job...");
final InputStream stream = getDataStream(attachmentId, THUMBNAIL); final InputStream stream = getDataStream(attachmentId, THUMBNAIL, 0);
if (stream != null) { if (stream != null) {
return stream; return stream;
@ -616,7 +624,7 @@ public class AttachmentDatabase extends Database {
updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio()); updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio());
return getDataStream(attachmentId, THUMBNAIL); return getDataStream(attachmentId, THUMBNAIL, 0);
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")

View file

@ -47,7 +47,7 @@ public class PartAuthority {
int match = uriMatcher.match(uri); int match = uriMatcher.match(uri);
try { try {
switch (match) { 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 THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); case PERSISTENT_ROW: return PersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri));

View file

@ -158,10 +158,10 @@ public class PartProvider extends ContentProvider {
} }
private ParcelFileDescriptor getParcelStreamForAttachment(AttachmentId attachmentId) throws IOException { 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)); 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(); OutputStream out = memoryFile.getOutputStream();
Util.copy(in, out); Util.copy(in, out);

View file

@ -160,7 +160,7 @@ public class PersistentBlobProvider {
FileData fileData = getFile(context, id); 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); else return ClassicDecryptingPartInputStream.createFor(attachmentSecret, fileData.file);
} }

View file

@ -58,13 +58,9 @@ public class EncryptedMediaDataSource extends MediaDataSource {
private int readAtModern(long position, byte[] bytes, int offset, int length) throws IOException { private int readAtModern(long position, byte[] bytes, int offset, int length) throws IOException {
assert(random != null); 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(); inputStream.close();
return returnValue; return returnValue;

View file

@ -11,7 +11,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.PartUriParser; import org.thoughtcrime.securesms.mms.PartUriParser;
@ -28,8 +27,7 @@ public class PartDataSource implements DataSource {
private Uri uri; private Uri uri;
private InputStream inputSteam; private InputStream inputSteam;
public PartDataSource(@NonNull Context context, @Nullable TransferListener<? super PartDataSource> listener) PartDataSource(@NonNull Context context, @Nullable TransferListener<? super PartDataSource> listener) {
{
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.listener = listener; this.listener = listener;
} }
@ -44,13 +42,7 @@ public class PartDataSource implements DataSource {
if (attachment == null) throw new IOException("Attachment not found"); if (attachment == null) throw new IOException("Attachment not found");
this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId()); this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position);
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!");
if (listener != null) { if (listener != null) {
listener.onTransferStart(this, dataSpec); listener.onTransferStart(this, dataSpec);