mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-08 05:32:18 +00:00
restructure and unite service android/java to libsignal
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.net.ChunkedDataFetcher;
|
||||
import org.thoughtcrime.securesms.net.RequestController;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
class ChunkedImageUrlFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = ChunkedImageUrlFetcher.class.getSimpleName();
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final ChunkedImageUrl url;
|
||||
|
||||
private RequestController requestController;
|
||||
|
||||
ChunkedImageUrlFetcher(@NonNull OkHttpClient client, @NonNull ChunkedImageUrl url) {
|
||||
this.client = client;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(client);
|
||||
requestController = fetcher.fetch(url.getUrl(), url.getSize(), new ChunkedDataFetcher.Callback() {
|
||||
@Override
|
||||
public void onSuccess(InputStream stream) {
|
||||
callback.onDataReady(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (requestController != null) {
|
||||
requestController.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Log.d(TAG, "Canceled.");
|
||||
if (requestController != null) {
|
||||
requestController.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class ChunkedImageUrlLoader implements ModelLoader<ChunkedImageUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private ChunkedImageUrlLoader(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull ChunkedImageUrl url, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(url, new ChunkedImageUrlFetcher(client, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ChunkedImageUrl url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<ChunkedImageUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
public Factory() {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.cache(null)
|
||||
.addNetworkInterceptor(new ContentProxySafetyInterceptor())
|
||||
.addNetworkInterceptor(new PaddedHeadersInterceptor())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<ChunkedImageUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new ChunkedImageUrlLoader(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class ContactPhotoFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private final Context context;
|
||||
private final ContactPhoto contactPhoto;
|
||||
|
||||
private InputStream inputStream;
|
||||
|
||||
ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.contactPhoto = contactPhoto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
try {
|
||||
inputStream = contactPhoto.openInputStream(context);
|
||||
callback.onDataReady(inputStream);
|
||||
} catch (IOException e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource getDataSource() {
|
||||
return DataSource.LOCAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ContactPhotoLoader implements ModelLoader<ContactPhoto, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private ContactPhotoLoader(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(contactPhoto, new ContactPhotoFetcher(context, contactPhoto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ContactPhoto contactPhoto) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<ContactPhoto, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public Factory(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<ContactPhoto, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new ContactPhotoLoader(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.util.ContentLengthInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* Fetches an {@link InputStream} using the okhttp library.
|
||||
*/
|
||||
class OkHttpStreamFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = OkHttpStreamFetcher.class.getSimpleName();
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final GlideUrl url;
|
||||
private InputStream stream;
|
||||
private ResponseBody responseBody;
|
||||
|
||||
OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
|
||||
this.client = client;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
try {
|
||||
Request.Builder requestBuilder = new Request.Builder()
|
||||
.url(url.toStringUrl());
|
||||
|
||||
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
|
||||
String key = headerEntry.getKey();
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
Response response = client.newCall(request).execute();
|
||||
|
||||
responseBody = response.body();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Request failed with code: " + response.code());
|
||||
}
|
||||
|
||||
long contentLength = responseBody.contentLength();
|
||||
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
|
||||
|
||||
callback.onDataReady(stream);
|
||||
} catch (IOException e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
if (responseBody != null) {
|
||||
responseBody.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// TODO: call cancel on the client when this method is called on a background thread. See #257
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* A simple model loader for fetching media over http/https using OkHttp.
|
||||
*/
|
||||
public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private OkHttpUrlLoader(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull GlideUrl glideUrl, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(glideUrl, new OkHttpStreamFetcher(client, glideUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull GlideUrl glideUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
|
||||
private static volatile OkHttpClient internalClient;
|
||||
private OkHttpClient client;
|
||||
|
||||
private static OkHttpClient getInternalClient() {
|
||||
if (internalClient == null) {
|
||||
synchronized (Factory.class) {
|
||||
if (internalClient == null) {
|
||||
internalClient = new OkHttpClient.Builder()
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return internalClient;
|
||||
}
|
||||
|
||||
public Factory() {
|
||||
this(getInternalClient());
|
||||
}
|
||||
|
||||
private Factory(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<GlideUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new OkHttpUrlLoader(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// Do nothing, this instance doesn't own the client.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* An interceptor that adds a header with a random amount of bytes to disguise header length.
|
||||
*/
|
||||
public class PaddedHeadersInterceptor implements Interceptor {
|
||||
|
||||
private static final String PADDING_HEADER = "X-SignalPadding";
|
||||
private static final int MIN_RANDOM_BYTES = 1;
|
||||
private static final int MAX_RANDOM_BYTES = 64;
|
||||
|
||||
@Override
|
||||
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
|
||||
Request padded = chain.request().newBuilder()
|
||||
.headers(getPaddedHeaders(chain.request().headers()))
|
||||
.build();
|
||||
|
||||
return chain.proceed(padded);
|
||||
}
|
||||
|
||||
private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) {
|
||||
return headers.newBuilder()
|
||||
.add(PADDING_HEADER, getRandomString(new SecureRandom(), MIN_RANDOM_BYTES, MAX_RANDOM_BYTES))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull String getRandomString(@NonNull SecureRandom secureRandom, int minLength, int maxLength) {
|
||||
char[] buffer = new char[secureRandom.nextInt(maxLength - minLength) + minLength];
|
||||
|
||||
for (int i = 0 ; i < buffer.length; i++) {
|
||||
buffer[i] = (char) (secureRandom.nextInt(74) + 48); // Random char from 0-Z
|
||||
}
|
||||
|
||||
return new String(buffer);
|
||||
}
|
||||
}
|
||||
54
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java
vendored
Normal file
54
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.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.i(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.i(TAG, "Encrypted Bitmap cache decoder running: " + source.toString());
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java
vendored
Normal file
65
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.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.i(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
53
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java
vendored
Normal file
53
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Encoder;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
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.i(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
98
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java
vendored
Normal file
98
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCoder.java
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import androidx.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedGifCacheDecoder.java
vendored
Normal file
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedGifCacheDecoder.java
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.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.i(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.i(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 androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user