Simple encrypted glide disk cache

This commit is contained in:
Moxie Marlinspike
2018-03-18 14:52:49 -07:00
parent 7e1e666172
commit 95d76638dc
15 changed files with 420 additions and 18 deletions

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}