mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-26 09:59:28 +00:00
Migrate from SQLite and ciphertext blobs to SQLCipher + KeyStore
This commit is contained in:
115
src/org/thoughtcrime/securesms/crypto/AttachmentSecret.java
Normal file
115
src/org/thoughtcrime/securesms/crypto/AttachmentSecret.java
Normal file
@@ -0,0 +1,115 @@
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
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 = TextSecurePreferences.getAttachmentUnencryptedSecret(context);
|
||||
String encryptedSecret = TextSecurePreferences.getAttachmentEncryptedSecret(context);
|
||||
|
||||
if (unencryptedSecret != null) attachmentSecret = getUnencryptedAttachmentSecret(context, unencryptedSecret);
|
||||
else if (encryptedSecret != null) attachmentSecret = getEncryptedAttachmentSecret(encryptedSecret);
|
||||
else attachmentSecret = createAndStoreAttachmentSecret(context);
|
||||
|
||||
return attachmentSecret;
|
||||
}
|
||||
|
||||
public synchronized AttachmentSecret setClassicKey(@NonNull Context context, @NonNull byte[] classicCipherKey, @NonNull byte[] classicMacKey) {
|
||||
AttachmentSecret currentSecret = getOrCreateAttachmentSecret();
|
||||
currentSecret.setClassicCipherKey(classicCipherKey);
|
||||
currentSecret.setClassicMacKey(classicMacKey);
|
||||
|
||||
storeAttachmentSecret(context, attachmentSecret);
|
||||
|
||||
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());
|
||||
|
||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||
TextSecurePreferences.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());
|
||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||
} else {
|
||||
TextSecurePreferences.setAttachmentUnencryptedSecret(context, attachmentSecret.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LimitedInputStream;
|
||||
@@ -37,14 +38,14 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class DecryptingPartInputStream {
|
||||
public class ClassicDecryptingPartInputStream {
|
||||
|
||||
private static final String TAG = DecryptingPartInputStream.class.getSimpleName();
|
||||
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(MasterSecret masterSecret, File file)
|
||||
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
@@ -52,7 +53,7 @@ public class DecryptingPartInputStream {
|
||||
throw new IOException("File too short");
|
||||
}
|
||||
|
||||
verifyMac(masterSecret, file);
|
||||
verifyMac(attachmentSecret, file);
|
||||
|
||||
FileInputStream fileStream = new FileInputStream(file);
|
||||
byte[] ivBytes = new byte[IV_LENGTH];
|
||||
@@ -60,7 +61,7 @@ public class DecryptingPartInputStream {
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, masterSecret.getEncryptionKey(), iv);
|
||||
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) {
|
||||
@@ -68,8 +69,8 @@ public class DecryptingPartInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyMac(MasterSecret masterSecret, File file) throws IOException {
|
||||
Mac mac = initializeMac(masterSecret.getMacKey());
|
||||
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];
|
||||
32
src/org/thoughtcrime/securesms/crypto/DatabaseSecret.java
Normal file
32
src/org/thoughtcrime/securesms/crypto/DatabaseSecret.java
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DatabaseSecretProvider {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DatabaseSecretProvider.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DatabaseSecretProvider(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public DatabaseSecret getOrCreateDatabaseSecret() {
|
||||
String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context);
|
||||
String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context);
|
||||
|
||||
if (unencryptedSecret != null) return getUnencryptedDatabaseSecret(context, unencryptedSecret);
|
||||
else if (encryptedSecret != null) return getEncryptedDatabaseSecret(encryptedSecret);
|
||||
else return createAndStoreDatabaseSecret(context);
|
||||
}
|
||||
|
||||
private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
|
||||
{
|
||||
try {
|
||||
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return databaseSecret;
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||
|
||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, null);
|
||||
|
||||
return databaseSecret;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseSecret getEncryptedDatabaseSecret(@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 new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret));
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] secret = new byte[32];
|
||||
random.nextBytes(secret);
|
||||
|
||||
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||
} else {
|
||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, databaseSecret.asString());
|
||||
}
|
||||
|
||||
return databaseSecret;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +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 java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class for streaming an encrypted MMS "part" to disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class EncryptingPartOutputStream extends FileOutputStream {
|
||||
|
||||
private Cipher cipher;
|
||||
private Mac mac;
|
||||
private boolean closed;
|
||||
|
||||
public EncryptingPartOutputStream(File file, MasterSecret masterSecret) throws FileNotFoundException {
|
||||
super(file);
|
||||
|
||||
try {
|
||||
mac = initializeMac(masterSecret.getMacKey());
|
||||
cipher = initializeCipher(mac, masterSecret.getEncryptionKey());
|
||||
closed = false;
|
||||
} catch (IOException ioe) {
|
||||
Log.w("EncryptingPartOutputStream", ioe);
|
||||
throw new FileNotFoundException("Couldn't write IV");
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
this.write(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||
byte[] encryptedBuffer = cipher.update(buffer, offset, length);
|
||||
|
||||
if (encryptedBuffer != null) {
|
||||
mac.update(encryptedBuffer);
|
||||
super.write(encryptedBuffer, 0, encryptedBuffer.length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (!closed) {
|
||||
byte[] encryptedRemainder = cipher.doFinal();
|
||||
mac.update(encryptedRemainder);
|
||||
|
||||
byte[] macBytes = mac.doFinal();
|
||||
|
||||
super.write(encryptedRemainder, 0, encryptedRemainder.length);
|
||||
super.write(macBytes, 0, macBytes.length);
|
||||
|
||||
closed = true;
|
||||
}
|
||||
|
||||
super.close();
|
||||
} catch (BadPaddingException bpe) {
|
||||
throw new AssertionError(bpe);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Mac initializeMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private Cipher initializeCipher(Mac mac, SecretKeySpec key) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
byte[] ivBytes = cipher.getIV();
|
||||
mac.update(ivBytes);
|
||||
super.write(ivBytes, 0, ivBytes.length);
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
}
|
||||
180
src/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java
Normal file
180
src/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java
Normal file
@@ -0,0 +1,180 @@
|
||||
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,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MediaKey {
|
||||
|
||||
public static String getEncrypted(@NonNull MasterSecretUnion masterSecret, @NonNull byte[] key) {
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
return Base64.encodeBytes(new MasterCipher(masterSecret.getMasterSecret().get()).encryptBytes(key));
|
||||
} else {
|
||||
return "?ASYNC-" + Base64.encodeBytes(new AsymmetricMasterCipher(masterSecret.getAsymmetricMasterSecret().get()).encryptBytes(key));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getDecrypted(@NonNull MasterSecret masterSecret,
|
||||
@NonNull AsymmetricMasterSecret asymmetricMasterSecret,
|
||||
@NonNull String encodedKey)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
if (encodedKey.startsWith("?ASYNC-")) {
|
||||
return new AsymmetricMasterCipher(asymmetricMasterSecret).decryptBytes(Base64.decode(encodedKey.substring("?ASYNC-".length())));
|
||||
} else {
|
||||
return new MasterCipher(masterSecret).decryptBytes(Base64.decode(encodedKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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 byte[] random, @NonNull File file)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
|
||||
|
||||
FileInputStream fileInputStream = new FileInputStream(file);
|
||||
byte[] iv = new byte[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));
|
||||
|
||||
return new CipherInputStream(fileInputStream, cipher);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
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 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 OutputStream createFor(@NonNull AttachmentSecret attachmentSecret, byte[] random, File file)
|
||||
throws IOException
|
||||
{
|
||||
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));
|
||||
|
||||
return new CipherOutputStream(fileOutputStream, cipher);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,12 +15,12 @@ import java.util.List;
|
||||
|
||||
public class SessionUtil {
|
||||
|
||||
public static boolean hasSession(Context context, MasterSecret masterSecret, Recipient recipient) {
|
||||
return hasSession(context, masterSecret, recipient.getAddress());
|
||||
public static boolean hasSession(Context context, Recipient recipient) {
|
||||
return hasSession(context, recipient.getAddress());
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, MasterSecret masterSecret, @NonNull Address address) {
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||
public static boolean hasSession(Context context, @NonNull Address address) {
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, null);
|
||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(address.serialize(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
|
||||
return sessionStore.containsSession(axolotlAddress);
|
||||
|
||||
Reference in New Issue
Block a user