Simple encrypted glide disk cache
parent
7e1e666172
commit
95d76638dc
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue