simplify PersistentBlobProvider

This commit is contained in:
B. Petersen
2019-05-05 18:02:50 +02:00
parent a15b482296
commit 310c616a63
8 changed files with 14 additions and 773 deletions
@@ -1,115 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import android.util.Base64;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.IOException;
/**
* Encapsulates the key material used to encrypt attachments on disk.
*
* There are two logical pieces of material, a deprecated set of keys used to encrypt
* legacy attachments, and a key that is used to encrypt attachments going forward.
*/
public class AttachmentSecret {
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] classicCipherKey;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] classicMacKey;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] modernKey;
public AttachmentSecret(byte[] classicCipherKey, byte[] classicMacKey, byte[] modernKey)
{
this.classicCipherKey = classicCipherKey;
this.classicMacKey = classicMacKey;
this.modernKey = modernKey;
}
@SuppressWarnings("unused")
public AttachmentSecret() {
}
@JsonIgnore
byte[] getClassicCipherKey() {
return classicCipherKey;
}
@JsonIgnore
byte[] getClassicMacKey() {
return classicMacKey;
}
@JsonIgnore
public byte[] getModernKey() {
return modernKey;
}
@JsonIgnore
void setClassicCipherKey(byte[] classicCipherKey) {
this.classicCipherKey = classicCipherKey;
}
@JsonIgnore
void setClassicMacKey(byte[] classicMacKey) {
this.classicMacKey = classicMacKey;
}
public String serialize() {
try {
return JsonUtils.toJson(this);
} catch (IOException e) {
throw new AssertionError(e);
}
}
static AttachmentSecret fromString(@NonNull String value) {
try {
return JsonUtils.fromJson(value, AttachmentSecret.class);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
}
}
}
@@ -1,93 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Prefs;
import java.security.SecureRandom;
/**
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
*
* On modern Android, the serialized secrets are themselves encrypted using a key that lives
* in the system KeyStore, for whatever that is worth.
*/
public class AttachmentSecretProvider {
private static AttachmentSecretProvider provider;
public static synchronized AttachmentSecretProvider getInstance(@NonNull Context context) {
if (provider == null) provider = new AttachmentSecretProvider(context.getApplicationContext());
return provider;
}
private final Context context;
private AttachmentSecret attachmentSecret;
private AttachmentSecretProvider(@NonNull Context context) {
this.context = context.getApplicationContext();
}
public synchronized AttachmentSecret getOrCreateAttachmentSecret() {
if (attachmentSecret != null) return attachmentSecret;
String unencryptedSecret = Prefs.getAttachmentUnencryptedSecret(context);
String encryptedSecret = Prefs.getAttachmentEncryptedSecret(context);
if (unencryptedSecret != null) attachmentSecret = getUnencryptedAttachmentSecret(context, unencryptedSecret);
else if (encryptedSecret != null) attachmentSecret = getEncryptedAttachmentSecret(encryptedSecret);
else attachmentSecret = createAndStoreAttachmentSecret(context);
return attachmentSecret;
}
private AttachmentSecret getUnencryptedAttachmentSecret(@NonNull Context context, @NonNull String unencryptedSecret)
{
AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return attachmentSecret;
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
Prefs.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
Prefs.setAttachmentUnencryptedSecret(context, null);
return attachmentSecret;
}
}
private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret)));
}
}
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
storeAttachmentSecret(context, attachmentSecret);
return attachmentSecret;
}
private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
Prefs.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
} else {
Prefs.setAttachmentUnencryptedSecret(context, attachmentSecret.serialize());
}
}
}
@@ -1,170 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.util.LimitedInputStream;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
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.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ClassicDecryptingPartInputStream {
private static final String TAG = ClassicDecryptingPartInputStream.class.getSimpleName();
private static final int IV_LENGTH = 16;
private static final int MAC_LENGTH = 20;
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file)
throws IOException
{
try {
if (file.length() <= IV_LENGTH + MAC_LENGTH) {
throw new IOException("File too short");
}
verifyMac(attachmentSecret, file);
FileInputStream fileStream = new FileInputStream(file);
byte[] ivBytes = new byte[IV_LENGTH];
readFully(fileStream, ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(attachmentSecret.getClassicCipherKey(), "AES"), iv);
return new CipherInputStreamWrapper(new LimitedInputStream(fileStream, file.length() - MAC_LENGTH - IV_LENGTH), cipher);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private static void verifyMac(AttachmentSecret attachmentSecret, File file) throws IOException {
Mac mac = initializeMac(new SecretKeySpec(attachmentSecret.getClassicMacKey(), "HmacSHA1"));
FileInputStream macStream = new FileInputStream(file);
InputStream dataStream = new LimitedInputStream(new FileInputStream(file), file.length() - MAC_LENGTH);
byte[] theirMac = new byte[MAC_LENGTH];
if (macStream.skip(file.length() - MAC_LENGTH) != file.length() - MAC_LENGTH) {
throw new IOException("Unable to seek");
}
readFully(macStream, theirMac);
byte[] buffer = new byte[4096];
int read;
while ((read = dataStream.read(buffer)) != -1) {
mac.update(buffer, 0, read);
}
byte[] ourMac = mac.doFinal();
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new IOException("Bad MAC");
}
macStream.close();
dataStream.close();
}
private static Mac initializeMac(SecretKeySpec key) {
try {
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length-offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
// Note (4/3/17) -- Older versions of Android have a busted OpenSSL provider that
// throws a RuntimeException on a BadPaddingException, so we have to catch
// that here in case someone calls close() before reaching the end of the
// stream (since close() implicitly calls doFinal())
//
// See Signal-Android Issue #6477
// Android: https://android-review.googlesource.com/#/c/65321/
private static class CipherInputStreamWrapper extends CipherInputStream {
CipherInputStreamWrapper(InputStream is, Cipher c) {
super(is, c);
}
@Override
public void close() throws IOException {
try {
super.close();
} catch (Throwable t) {
Log.w(TAG, t);
}
}
@Override
public long skip(long skipAmount)
throws IOException
{
long remaining = skipAmount;
if (skipAmount <= 0) {
return 0;
}
byte[] skipBuffer = new byte[4092];
while (remaining > 0) {
int read = super.read(skipBuffer, 0, Util.toIntExact(Math.min(skipBuffer.length, remaining)));
if (read < 0) {
break;
}
remaining -= read;
}
return skipAmount - remaining;
}
}
}
@@ -1,32 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Hex;
import java.io.IOException;
public class DatabaseSecret {
private final byte[] key;
private final String encoded;
public DatabaseSecret(@NonNull byte[] key) {
this.key = key;
this.encoded = Hex.toStringCondensed(key);
}
public DatabaseSecret(@NonNull String encoded) throws IOException {
this.key = Hex.fromStringCondensed(encoded);
this.encoded = encoded;
}
public String asString() {
return encoded;
}
public byte[] asBytes() {
return key;
}
}
@@ -1,180 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Base64;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class KeyStoreHelper {
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "SignalSecret";
@RequiresApi(Build.VERSION_CODES.M)
public static SealedData seal(@NonNull byte[] input) {
SecretKey secretKey = getOrCreateKeyStoreEntry();
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV();
byte[] data = cipher.doFinal(input);
return new SealedData(iv, data);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
public static byte[] unseal(@NonNull SealedData sealedData) {
SecretKey secretKey = getKeyStoreEntry();
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
return cipher.doFinal(sealedData.data);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey getOrCreateKeyStoreEntry() {
if (hasKeyStoreEntry()) return getKeyStoreEntry();
else return createKeyStoreEntry();
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey createKeyStoreEntry() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build();
keyGenerator.init(keyGenParameterSpec);
return keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static SecretKey getKeyStoreEntry() {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableEntryException e) {
throw new AssertionError(e);
}
}
@RequiresApi(Build.VERSION_CODES.M)
private static boolean hasKeyStoreEntry() {
try {
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
ks.load(null);
return ks.containsAlias(KEY_ALIAS) && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
throw new AssertionError(e);
}
}
public static class SealedData {
@SuppressWarnings("unused")
private static final String TAG = SealedData.class.getSimpleName();
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] iv;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] data;
SealedData(@NonNull byte[] iv, @NonNull byte[] data) {
this.iv = iv;
this.data = data;
}
@SuppressWarnings("unused")
public SealedData() {}
public String serialize() {
try {
return JsonUtils.toJson(this);
} catch (IOException e) {
throw new AssertionError(e);
}
}
static SealedData fromString(@NonNull String value) {
try {
return JsonUtils.fromJson(value, SealedData.class);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
}
}
}
}
@@ -1,77 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.Conversions;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ModernDecryptingPartInputStream {
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, long offset)
throws IOException
{
FileInputStream inputStream = new FileInputStream(file);
byte[] random = new byte[32];
readFully(inputStream, random);
return createFor(attachmentSecret, random, inputStream, offset);
}
private static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull InputStream inputStream, long offset) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
byte[] iv = new byte[16];
int remainder = (int) (offset % 16);
Conversions.longTo4ByteArray(iv, 12, offset / 16);
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
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) {
throw new AssertionError(e);
}
}
private static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length-offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
}
@@ -1,57 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.support.annotation.NonNull;
import android.util.Pair;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Constructs an OutputStream that encrypts data written to it with the AttachmentSecret provided.
*
* The on-disk format is very simple, and intentionally no longer includes authentication.
*/
public class ModernEncryptingPartOutputStream {
public static Pair<byte[], OutputStream> createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, boolean inline)
throws IOException
{
byte[] random = new byte[32];
new SecureRandom().nextBytes(random);
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "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));
if (inline) {
fileOutputStream.write(random);
}
return new Pair<>(random, new CipherOutputStream(fileOutputStream, cipher));
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}
@@ -8,25 +8,18 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import android.webkit.MimeTypeMap;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
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.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -67,13 +60,9 @@ public class PersistentBlobProvider {
}
@SuppressLint("UseSparseArrays")
private final Map<Long, byte[]> cache = Collections.synchronizedMap(new HashMap<Long, byte[]>());
private final ExecutorService executor = Executors.newCachedThreadPool();
private final AttachmentSecret attachmentSecret;
private PersistentBlobProvider(@NonNull Context context) {
this.attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
}
public Uri create(@NonNull Context context,
@@ -82,8 +71,7 @@ public class PersistentBlobProvider {
@Nullable String fileName)
{
final long id = System.currentTimeMillis();
cache.put(id, blobBytes);
return create(context, attachmentSecret, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length);
return create(context, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length);
}
public Uri create(@NonNull Context context,
@@ -92,18 +80,17 @@ public class PersistentBlobProvider {
@Nullable String fileName,
@Nullable Long fileSize)
{
return create(context, attachmentSecret, input, System.currentTimeMillis(), mimeType, fileName, fileSize);
return create(context, input, System.currentTimeMillis(), mimeType, fileName, fileSize);
}
private Uri create(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull InputStream input,
long id,
@NonNull String mimeType,
@Nullable String fileName,
@Nullable Long fileSize)
{
persistToDisk(context, attachmentSecret, id, input);
persistToDisk(context, id, input);
final Uri uniqueUri = CONTENT_URI.buildUpon()
.appendPath(mimeType)
.appendPath(fileName)
@@ -114,18 +101,15 @@ public class PersistentBlobProvider {
}
private void persistToDisk(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
final long id, final InputStream input)
{
executor.submit(() -> {
try {
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, getFile(context, id).file, true);
Util.copy(input, output.second);
OutputStream output = new FileOutputStream(getFile(context, id));
Util.copy(input, output);
} catch (IOException e) {
Log.w(TAG, e);
}
cache.remove(id);
});
}
@@ -139,8 +123,7 @@ public class PersistentBlobProvider {
case MATCH_OLD:
case MATCH_NEW:
long id = ContentUris.parseId(uri);
cache.remove(id);
return getFile(context, ContentUris.parseId(uri)).file.delete();
return getFile(context, ContentUris.parseId(uri)).delete();
}
//noinspection SimplifiableIfStatement
@@ -152,26 +135,18 @@ public class PersistentBlobProvider {
}
public @NonNull InputStream getStream(@NonNull Context context, long id) throws IOException {
final byte[] cached = cache.get(id);
if (cached != null) {
return new ByteArrayInputStream(cached);
}
FileData fileData = getFile(context, id);
if (fileData.modern) return ModernDecryptingPartInputStream.createFor(attachmentSecret, fileData.file, 0);
else return ClassicDecryptingPartInputStream.createFor(attachmentSecret, fileData.file);
File file = getFile(context, id);
return new FileInputStream(file);
}
private FileData getFile(@NonNull Context context, long id) {
private File getFile(@NonNull Context context, long id) {
File legacy = getLegacyFile(context, id);
File cache = getCacheFile(context, id);
File modernCache = getModernCacheFile(context, id);
if (legacy.exists()) return new FileData(legacy, false);
else if (cache.exists()) return new FileData(cache, false);
else return new FileData(modernCache, true);
if (legacy.exists()) return legacy;
else if (cache.exists()) return cache;
else return modernCache;
}
private File getLegacyFile(@NonNull Context context, long id) {
@@ -243,14 +218,4 @@ public class PersistentBlobProvider {
return false;
}
}
private static class FileData {
private final File file;
private final boolean modern;
private FileData(File file, boolean modern) {
this.file = file;
this.modern = modern;
}
}
}