Simple encrypted glide disk cache

fork-5.53.8
Moxie Marlinspike 2018-03-18 14:52:49 -07:00
rodzic 7e1e666172
commit 95d76638dc
15 zmienionych plików z 420 dodań i 18 usunięć

Wyświetl plik

@ -142,7 +142,7 @@ public class ThumbnailView extends FrameLayout {
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(new RoundedCorners(radius))
.transition(withCrossFade())
.centerCrop()
@ -173,7 +173,7 @@ public class ThumbnailView extends FrameLayout {
private RequestBuilder buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
RequestBuilder builder = glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(new RoundedCorners(radius))
.centerCrop()
.transition(withCrossFade());

Wyświetl plik

@ -65,7 +65,7 @@ public class AttachmentSecret {
}
@JsonIgnore
byte[] getModernKey() {
public byte[] getModernKey() {
return modernKey;
}

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.giph.ui;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@ -19,6 +20,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@ -98,6 +100,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
this.stickerFragment.setLayoutManager(type);
}
@SuppressLint("StaticFieldLeak")
@Override
public void onClick(final GiphyAdapter.GiphyViewHolder viewHolder) {
if (finishingImage != null) finishingImage.gifProgress.setVisibility(View.GONE);
@ -108,7 +111,9 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
@Override
protected Uri doInBackground(Void... params) {
try {
return Uri.fromFile(viewHolder.getFile(forMms));
byte[] data = viewHolder.getData(forMms);
return PersistentBlobProvider.getInstance(GiphyActivity.this).create(GiphyActivity.this, data, "image/gif", null);
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
return null;

Wyświetl plik

@ -13,13 +13,16 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.GifDrawableEncoder;
import com.bumptech.glide.load.resource.gif.GifDrawableResource;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.ByteBufferUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
@ -32,7 +35,10 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.concurrent.ExecutionException;
@ -94,17 +100,20 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
}
public File getFile(boolean forMms) throws ExecutionException, InterruptedException {
public byte[] getData(boolean forMms) throws ExecutionException, InterruptedException {
synchronized (this) {
while (!modelReady) {
Util.wait(this, 0);
}
}
return glideRequests.load(forMms ? new GiphyPaddedUrl(image.getGifMmsUrl(), image.getMmsGifSize()) :
new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()))
.downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
GifDrawable drawable = glideRequests.asGif()
.load(forMms ? new GiphyPaddedUrl(image.getGifMmsUrl(), image.getMmsGifSize()) :
new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()))
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
return ByteBufferUtil.toBytes(drawable.getBuffer());
}
public synchronized void setModelReady() {

Wyświetl plik

@ -45,7 +45,7 @@ class GiphyPaddedUrlFetcher implements DataFetcher<InputStream> {
}
@Override
public void loadData(Priority priority, DataCallback<? super InputStream> callback) {
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
bodies = new LinkedList<>();
rangeStreams = new LinkedList<>();
stream = null;

Wyświetl plik

@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, Bitmap> {
private static final String TAG = EncryptedBitmapCacheDecoder.class.getSimpleName();
private final StreamBitmapDecoder streamBitmapDecoder;
private final byte[] secret;
public EncryptedBitmapCacheDecoder(@NonNull byte[] secret, @NonNull StreamBitmapDecoder streamBitmapDecoder) {
this.secret = secret;
this.streamBitmapDecoder = streamBitmapDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options)
throws IOException
{
Log.w(TAG, "Checking item for encrypted Bitmap cache decoder: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Nullable
@Override
public Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
throws IOException
{
Log.w(TAG, "Encrypted Bitmap cache decoder running: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.decode(inputStream, width, height, options);
}
}
}

Wyświetl plik

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements ResourceEncoder<Bitmap> {
private static final String TAG = EncryptedBitmapResourceEncoder.class.getSimpleName();
private final byte[] secret;
public EncryptedBitmapResourceEncoder(@NonNull byte[] secret) {
this.secret = secret;
}
@Override
public EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.TRANSFORMED;
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
Log.w(TAG, "Encrypted resource encoder running: " + file.toString());
Bitmap bitmap = data.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);
try (OutputStream os = createEncryptedOutputStream(secret, file)) {
bitmap.compress(format, quality, os);
os.close();
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
private Bitmap.CompressFormat getFormat(Bitmap bitmap, Options options) {
Bitmap.CompressFormat format = options.get(BitmapEncoder.COMPRESSION_FORMAT);
if (format != null) {
return format;
} else if (bitmap.hasAlpha()) {
return Bitmap.CompressFormat.PNG;
} else {
return Bitmap.CompressFormat.JPEG;
}
}
}

Wyświetl plik

@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.glide.cache;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.load.Encoder;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<InputStream> {
private static final String TAG = EncryptedCacheEncoder.class.getSimpleName();
private final byte[] secret;
private final ArrayPool byteArrayPool;
public EncryptedCacheEncoder(@NonNull byte[] secret, @NonNull ArrayPool byteArrayPool) {
this.secret = secret;
this.byteArrayPool = byteArrayPool;
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
Log.w(TAG, "Encrypted cache encoder running: " + file.toString());
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
int read;
while ((read = data.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
} finally {
byteArrayPool.put(buffer);
}
}
}

Wyświetl plik

@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.glide.cache;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
class EncryptedCoder {
private static byte[] MAGIC_BYTES = {(byte)0x91, (byte)0x5e, (byte)0x6d, (byte)0xb4,
(byte)0x09, (byte)0xa6, (byte)0x68, (byte)0xbe,
(byte)0xe5, (byte)0xb1, (byte)0x1b, (byte)0xd7,
(byte)0x29, (byte)0xe5, (byte)0x04, (byte)0xcc};
OutputStream createEncryptedOutputStream(@NonNull byte[] masterKey, @NonNull File file)
throws IOException
{
try {
byte[] random = Util.getSecretBytes(32);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] iv = new byte[16];
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
fileOutputStream.write(MAGIC_BYTES);
fileOutputStream.write(random);
CipherOutputStream outputStream = new CipherOutputStream(fileOutputStream, cipher);
outputStream.write(MAGIC_BYTES);
return outputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
InputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
FileInputStream fileInputStream = new FileInputStream(file);
byte[] theirMagic = new byte[MAGIC_BYTES.length];
byte[] theirRandom = new byte[32];
byte[] theirEncryptedMagic = new byte[MAGIC_BYTES.length];
Util.readFully(fileInputStream, theirMagic);
Util.readFully(fileInputStream, theirRandom);
if (!MessageDigest.isEqual(theirMagic, MAGIC_BYTES)) {
throw new IOException("Not an encrypted cache file!");
}
byte[] iv = new byte[16];
byte[] key = mac.doFinal(theirRandom);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
CipherInputStream inputStream = new CipherInputStream(fileInputStream, cipher);
Util.readFully(inputStream, theirEncryptedMagic);
if (!MessageDigest.isEqual(theirEncryptedMagic, MAGIC_BYTES)) {
throw new IOException("Key change on encrypted cache file!");
}
return inputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
}

Wyświetl plik

@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.glide.cache;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedGifCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, GifDrawable> {
private static final String TAG = EncryptedGifCacheDecoder.class.getSimpleName();
private final byte[] secret;
private final StreamGifDecoder gifDecoder;
public EncryptedGifCacheDecoder(@NonNull byte[] secret, @NonNull StreamGifDecoder gifDecoder) {
this.secret = secret;
this.gifDecoder = gifDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options) {
Log.w(TAG, "Checking item for encrypted GIF cache decoder: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Nullable
@Override
public Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
Log.w(TAG, "Encrypted GIF cache decoder running...");
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.decode(inputStream, width, height, options);
}
}
}

Wyświetl plik

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.glide.cache;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.util.ByteBufferUtil;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class EncryptedGifDrawableResourceEncoder extends EncryptedCoder implements ResourceEncoder<GifDrawable> {
private static final String TAG = EncryptedGifDrawableResourceEncoder.class.getSimpleName();
private final byte[] secret;
public EncryptedGifDrawableResourceEncoder(@NonNull byte[] secret) {
this.secret = secret;
}
@Override
public EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.TRANSFORMED;
}
@Override
public boolean encode(@NonNull Resource<GifDrawable> data, @NonNull File file, @NonNull Options options) {
GifDrawable drawable = data.get();
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
ByteBufferUtil.toStream(drawable.getBuffer(), outputStream);
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
}

Wyświetl plik

@ -208,7 +208,7 @@ public class AttachmentManager {
{
inflateStub();
new AsyncTask<Void, Void, Slide>() {
new AsyncTask<Void, Void, Slide>() {
@Override
protected void onPreExecute() {
thumbnail.clear(glideRequests);

Wyświetl plik

@ -47,10 +47,7 @@ public class ImageSlide extends Slide {
@Override
public @Nullable Uri getThumbnailUri() {
Uri thumbnailUri = super.getThumbnailUri();
if (thumbnailUri == null) return getUri();
else return thumbnailUri;
return getUri();
}
@Override

Wyświetl plik

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.Glide;
@ -10,16 +12,29 @@ import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.engine.cache.DiskCache;
import com.bumptech.glide.load.engine.cache.DiskCacheAdapter;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.resource.bitmap.Downsampler;
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import com.bumptech.glide.module.AppGlideModule;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl;
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
import org.thoughtcrime.securesms.glide.GiphyPaddedUrlLoader;
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import java.io.File;
import java.io.InputStream;
@GlideModule
@ -37,7 +52,17 @@ public class SignalGlideModule extends AppGlideModule {
}
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
byte[] secret = attachmentSecret.getModernKey();
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret));
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());

Wyświetl plik

@ -14,6 +14,7 @@ import java.util.Map;
public class SingleUseBlobProvider {
@SuppressWarnings("unused")
private static final String TAG = SingleUseBlobProvider.class.getSimpleName();
public static final String AUTHORITY = "org.thoughtcrime.securesms";