mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
simplify PersistentBlobProvider
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user