Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,138 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.thoughtcrime.securesms.util.Conversions;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class is used to asymmetrically encrypt local data. This is used in the case
* where TextSecure receives an SMS, but the user's local encryption passphrase is
* not cached (either because of a timeout, or because it hasn't yet been entered).
*
* In this case, we have access to the public key of a local keypair. We encrypt
* the message with this, and put it into the DB. When the user enters their passphrase,
* we can get access to the private key of the local keypair, decrypt the message, and
* replace it into the DB with symmetric encryption.
*
* The encryption protocol is as follows:
*
* 1) Generate an ephemeral keypair.
* 2) Do ECDH with the public key of the local durable keypair.
* 3) Do KMF with the ECDH result to obtain a master secret.
* 4) Encrypt the message with that master secret.
*
* @author Moxie Marlinspike
*
*/
public class AsymmetricMasterCipher {
private final AsymmetricMasterSecret asymmetricMasterSecret;
public AsymmetricMasterCipher(AsymmetricMasterSecret asymmetricMasterSecret) {
this.asymmetricMasterSecret = asymmetricMasterSecret;
}
public byte[] encryptBytes(byte[] body) {
try {
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
ECKeyPair ourKeyPair = Curve.generateKeyPair();
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
MasterCipher masterCipher = getMasterCipherForSecret(secret);
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body);
PublicKey ourPublicKey = new PublicKey(31337, ourKeyPair.getPublicKey());
byte[] publicKeyBytes = ourPublicKey.serialize();
return Util.combine(publicKeyBytes, encryptedBodyBytes);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decryptBytes(byte[] combined) throws IOException, InvalidMessageException {
try {
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey();
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
MasterCipher masterCipher = getMasterCipherForSecret(secret);
return masterCipher.decryptBytes(parts[1]);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public String decryptBody(String body) throws IOException, InvalidMessageException {
byte[] combined = Base64.decode(body);
return new String(decryptBytes(combined));
}
public String encryptBody(String body) {
return Base64.encodeBytes(encryptBytes(body.getBytes()));
}
private MasterCipher getMasterCipherForSecret(byte[] secretBytes) {
SecretKeySpec cipherKey = deriveCipherKey(secretBytes);
SecretKeySpec macKey = deriveMacKey(secretBytes);
MasterSecret masterSecret = new MasterSecret(cipherKey, macKey);
return new MasterCipher(masterSecret);
}
private SecretKeySpec deriveMacKey(byte[] secretBytes) {
byte[] digestedBytes = getDigestedBytes(secretBytes, 1);
byte[] macKeyBytes = new byte[20];
System.arraycopy(digestedBytes, 0, macKeyBytes, 0, macKeyBytes.length);
return new SecretKeySpec(macKeyBytes, "HmacSHA1");
}
private SecretKeySpec deriveCipherKey(byte[] secretBytes) {
byte[] digestedBytes = getDigestedBytes(secretBytes, 0);
byte[] cipherKeyBytes = new byte[16];
System.arraycopy(digestedBytes, 0, cipherKeyBytes, 0, cipherKeyBytes.length);
return new SecretKeySpec(cipherKeyBytes, "AES");
}
private byte[] getDigestedBytes(byte[] secretBytes, int iteration) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretBytes, "HmacSHA256"));
return mac.doFinal(Conversions.intToByteArray(iteration));
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
/**
* When a user first initializes TextSecure, a few secrets
* are generated. These are:
*
* 1) A 128bit symmetric encryption key.
* 2) A 160bit symmetric MAC key.
* 3) An ECC keypair.
*
* The first two, along with the ECC keypair's private key, are
* then encrypted on disk using PBE.
*
* This class represents the ECC keypair.
*
* @author Moxie Marlinspike
*
*/
public class AsymmetricMasterSecret {
private final ECPublicKey djbPublicKey;
private final ECPrivateKey djbPrivateKey;
public AsymmetricMasterSecret(ECPublicKey djbPublicKey, ECPrivateKey djbPrivateKey)
{
this.djbPublicKey = djbPublicKey;
this.djbPrivateKey = djbPrivateKey;
}
public ECPublicKey getDjbPublicKey() {
return djbPublicKey;
}
public ECPrivateKey getPrivateKey() {
return djbPrivateKey;
}
}

View File

@@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.crypto;
import androidx.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);
}
}
}

View File

@@ -0,0 +1,103 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.os.Build;
import androidx.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());
}
}
}

View File

@@ -0,0 +1,170 @@
/**
* 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 androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.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;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.crypto;
import androidx.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;
}
}

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.os.Build;
import androidx.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;
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (C) 2014 Open 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.os.Parcel;
import android.os.Parcelable;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
public class IdentityKeyParcelable implements Parcelable {
public static final Parcelable.Creator<IdentityKeyParcelable> CREATOR = new Parcelable.Creator<IdentityKeyParcelable>() {
public IdentityKeyParcelable createFromParcel(Parcel in) {
try {
return new IdentityKeyParcelable(in);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public IdentityKeyParcelable[] newArray(int size) {
return new IdentityKeyParcelable[size];
}
};
private final IdentityKey identityKey;
public IdentityKeyParcelable(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public IdentityKeyParcelable(Parcel in) throws InvalidKeyException {
int serializedLength = in.readInt();
byte[] serialized = new byte[serializedLength];
in.readByteArray(serialized);
this.identityKey = new IdentityKey(serialized, 0);
}
public IdentityKey get() {
return identityKey;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(identityKey.serialize().length);
dest.writeByteArray(identityKey.serialize());
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.backup.BackupProtos;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* Utility class for working with identity keys.
*
* @author Moxie Marlinspike
*/
public class IdentityKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
private static final String IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_private_curve25519";
private static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
private static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeys(Context context) {
ECKeyPair djbKeyPair = Curve.generateKeyPair();
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
ECPrivateKey djbPrivateKey = djbKeyPair.getPrivateKey();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(djbPrivateKey.serialize()));
}
public static void migrateIdentityKeys(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
if (!hasIdentityKey(context)) {
if (hasLegacyIdentityKeys(context)) {
IdentityKeyPair legacyPair = getLegacyIdentityKeyPair(context, masterSecret);
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(legacyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(legacyPair.getPrivateKey().serialize()));
delete(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF);
delete(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF);
} else {
generateIdentityKeys(context);
}
}
}
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return new LinkedList<BackupProtos.SharedPreference>() {{
add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PUBLIC_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
.build());
add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PRIVATE_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
.build());
}};
}
private static boolean hasLegacyIdentityKeys(Context context) {
return
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF) != null;
}
private static IdentityKeyPair getLegacyIdentityKeyPair(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF));
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF)));
return new IdentityKeyPair(identityKey, privateKey);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
private static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
private static void delete(Context context, String key) {
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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;
public class InvalidPassphraseException extends Exception {
public InvalidPassphraseException() {
super();
// TODO Auto-generated constructor stub
}
public InvalidPassphraseException(String detailMessage) {
super(detailMessage);
// TODO Auto-generated constructor stub
}
public InvalidPassphraseException(Throwable throwable) {
super(throwable);
// TODO Auto-generated constructor stub
}
public InvalidPassphraseException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
// TODO Auto-generated constructor stub
}
}

View 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 androidx.annotation.NonNull;
import androidx.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);
}
}
public 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);
}
}
}
}

View File

@@ -0,0 +1,225 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Hex;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Class that handles encryption for local storage.
*
* The protocol format is roughly:
*
* 1) 16 byte random IV.
* 2) AES-CBC(plaintext)
* 3) HMAC-SHA1 of 1 and 2
*
* @author Moxie Marlinspike
*/
public class MasterCipher {
private static final String TAG = MasterCipher.class.getSimpleName();
private final MasterSecret masterSecret;
private final Cipher encryptingCipher;
private final Cipher decryptingCipher;
private final Mac hmac;
public MasterCipher(MasterSecret masterSecret) {
try {
this.masterSecret = masterSecret;
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.hmac = Mac.getInstance("HmacSHA1");
} catch (NoSuchPaddingException | NoSuchAlgorithmException nspe) {
throw new AssertionError(nspe);
}
}
public byte[] encryptKey(ECPrivateKey privateKey) {
return encryptBytes(privateKey.serialize());
}
public String encryptBody(@NonNull String body) {
return encryptAndEncodeBytes(body.getBytes());
}
public String decryptBody(String body) throws InvalidMessageException {
return new String(decodeAndDecryptBytes(body));
}
public ECPrivateKey decryptKey(byte[] key)
throws org.whispersystems.libsignal.InvalidKeyException
{
try {
return Curve.decodePrivatePoint(decryptBytes(key));
} catch (InvalidMessageException ime) {
throw new org.whispersystems.libsignal.InvalidKeyException(ime);
}
}
public byte[] decryptBytes(@NonNull byte[] decodedBody) throws InvalidMessageException {
try {
Mac mac = getMac(masterSecret.getMacKey());
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
return encrypted;
} catch (GeneralSecurityException ge) {
throw new InvalidMessageException(ge);
}
}
public byte[] encryptBytes(byte[] body) {
try {
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
Mac mac = getMac(masterSecret.getMacKey());
byte[] encryptedBody = getEncryptedBody(cipher, body);
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
return encryptedAndMacBody;
} catch (GeneralSecurityException ge) {
Log.w("bodycipher", ge);
return null;
}
}
public boolean verifyMacFor(String content, byte[] theirMac) {
byte[] ourMac = getMacFor(content);
Log.i(TAG, "Our Mac: " + Hex.toString(ourMac));
Log.i(TAG, "Thr Mac: " + Hex.toString(theirMac));
return Arrays.equals(ourMac, theirMac);
}
public byte[] getMacFor(String content) {
Log.w(TAG, "Macing: " + content);
try {
Mac mac = getMac(masterSecret.getMacKey());
return mac.doFinal(content.getBytes());
} catch (GeneralSecurityException ike) {
throw new AssertionError(ike);
}
}
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
try {
byte[] decodedBody = Base64.decode(body);
return decryptBytes(decodedBody);
} catch (IOException e) {
throw new InvalidMessageException("Bad Base64 Encoding...", e);
}
}
private String encryptAndEncodeBytes(@NonNull byte[] bytes) {
byte[] encryptedAndMacBody = encryptBytes(bytes);
return Base64.encodeBytes(encryptedAndMacBody);
}
private byte[] verifyMacBody(@NonNull Mac hmac, @NonNull byte[] encryptedAndMac) throws InvalidMessageException {
if (encryptedAndMac.length < hmac.getMacLength()) {
throw new InvalidMessageException("length(encrypted body + MAC) < length(MAC)");
}
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
byte[] remoteMac = new byte[hmac.getMacLength()];
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
byte[] localMac = hmac.doFinal(encrypted);
if (!Arrays.equals(remoteMac, localMac))
throw new InvalidMessageException("MAC doesen't match.");
return encrypted;
}
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
}
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
byte[] encrypted = cipher.doFinal(body);
byte[] iv = cipher.getIV();
byte[] ivAndBody = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
return ivAndBody;
}
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
// Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
}
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
byte[] mac = hmac.doFinal(encryptedBody);
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
return encryptedAndMac;
}
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
return decryptingCipher;
}
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
return encryptingCipher;
}
}

View File

@@ -0,0 +1,119 @@
/**
* 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.os.Parcel;
import android.os.Parcelable;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
/**
* When a user first initializes TextSecure, a few secrets
* are generated. These are:
*
* 1) A 128bit symmetric encryption key.
* 2) A 160bit symmetric MAC key.
* 3) An ECC keypair.
*
* The first two, along with the ECC keypair's private key, are
* then encrypted on disk using PBE.
*
* This class represents 1 and 2.
*
* @author Moxie Marlinspike
*/
public class MasterSecret implements Parcelable {
private final SecretKeySpec encryptionKey;
private final SecretKeySpec macKey;
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
@Override
public MasterSecret createFromParcel(Parcel in) {
return new MasterSecret(in);
}
@Override
public MasterSecret[] newArray(int size) {
return new MasterSecret[size];
}
};
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
this.encryptionKey = encryptionKey;
this.macKey = macKey;
}
private MasterSecret(Parcel in) {
byte[] encryptionKeyBytes = new byte[in.readInt()];
in.readByteArray(encryptionKeyBytes);
byte[] macKeyBytes = new byte[in.readInt()];
in.readByteArray(macKeyBytes);
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
// SecretKeySpec does an internal copy in its constructor.
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
Arrays.fill(macKeyBytes, (byte)0x00);
}
public SecretKeySpec getEncryptionKey() {
return this.encryptionKey;
}
public SecretKeySpec getMacKey() {
return this.macKey;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(encryptionKey.getEncoded().length);
out.writeByteArray(encryptionKey.getEncoded());
out.writeInt(macKey.getEncoded().length);
out.writeByteArray(macKey.getEncoded());
}
@Override
public int describeContents() {
return 0;
}
public MasterSecret parcelClone() {
Parcel thisParcel = Parcel.obtain();
Parcel thatParcel = Parcel.obtain();
byte[] bytes = null;
thisParcel.writeValue(this);
bytes = thisParcel.marshall();
thatParcel.unmarshall(bytes, 0, bytes.length);
thatParcel.setDataPosition(0);
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
thisParcel.recycle();
thatParcel.recycle();
return that;
}
}

View File

@@ -0,0 +1,370 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Helper class for generating and securely storing a MasterSecret.
*
* @author Moxie Marlinspike
*/
public class MasterSecretUtil {
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
public static MasterSecret changeMasterSecretPassphrase(Context context,
MasterSecret masterSecret,
String newPassphrase)
{
try {
byte[] combinedSecrets = Util.combine(masterSecret.getEncryptionKey().getEncoded(),
masterSecret.getMacKey().getEncoded());
byte[] encryptionSalt = generateSalt();
int iterations = generateIterationCount(newPassphrase, encryptionSalt);
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
byte[] macSalt = generateSalt();
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);
save(context, "encryption_salt", encryptionSalt);
save(context, "mac_salt", macSalt);
save(context, "passphrase_iterations", iterations);
save(context, "master_secret", encryptedAndMacdMasterSecret);
save(context, "passphrase_initialized", true);
return masterSecret;
} catch (GeneralSecurityException gse) {
throw new AssertionError(gse);
}
}
public static MasterSecret changeMasterSecretPassphrase(Context context,
String originalPassphrase,
String newPassphrase)
throws InvalidPassphraseException
{
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
changeMasterSecretPassphrase(context, masterSecret, newPassphrase);
return masterSecret;
}
public static MasterSecret getMasterSecret(Context context, String passphrase)
throws InvalidPassphraseException
{
try {
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
byte[] macSalt = retrieve(context, "mac_salt");
int iterations = retrieve(context, "passphrase_iterations", 100);
byte[] encryptedMasterSecret = verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
byte[] encryptionSalt = retrieve(context, "encryption_salt");
byte[] combinedSecrets = decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
new SecretKeySpec(macSecret, "HmacSHA1"));
} catch (GeneralSecurityException e) {
Log.w("keyutil", e);
return null; //XXX
} catch (IOException e) {
Log.w("keyutil", e);
return null; //XXX
}
}
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
@Nullable MasterSecret masterSecret)
{
try {
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
ECPublicKey djbPublicKey = null;
ECPrivateKey djbPrivateKey = null;
if (djbPublicBytes != null) {
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
}
if (masterSecret != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
if (djbPrivateBytes != null) {
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
}
}
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
} catch (InvalidKeyException | IOException ike) {
throw new AssertionError(ike);
}
}
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context,
MasterSecret masterSecret)
{
MasterCipher masterCipher = new MasterCipher(masterSecret);
ECKeyPair keyPair = Curve.generateKeyPair();
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
}
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
try {
byte[] encryptionSecret = generateEncryptionSecret();
byte[] macSecret = generateMacSecret();
byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
byte[] encryptionSalt = generateSalt();
int iterations = generateIterationCount(passphrase, encryptionSalt);
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
byte[] macSalt = generateSalt();
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);
save(context, "encryption_salt", encryptionSalt);
save(context, "mac_salt", macSalt);
save(context, "passphrase_iterations", iterations);
save(context, "master_secret", encryptedAndMacdMasterSecret);
save(context, "passphrase_initialized", true);
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
new SecretKeySpec(macSecret, "HmacSHA1"));
} catch (GeneralSecurityException e) {
Log.w("keyutil", e);
return null;
}
}
public static boolean hasAsymmericMasterSecret(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
}
public static boolean isPassphraseInitialized(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
return preferences.getBoolean("passphrase_initialized", false);
}
private static void save(Context context, String key, int value) {
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
.edit()
.putInt(key, value)
.commit())
{
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
}
}
private static void save(Context context, String key, byte[] value) {
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
.edit()
.putString(key, Base64.encodeBytes(value))
.commit())
{
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
}
}
private static void save(Context context, String key, boolean value) {
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
.edit()
.putBoolean(key, value)
.commit())
{
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
}
}
private static byte[] retrieve(Context context, String key) throws IOException {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
String encodedValue = settings.getString(key, "");
if (TextUtils.isEmpty(encodedValue)) return null;
else return Base64.decode(encodedValue);
}
private static int retrieve(Context context, String key, int defaultValue) throws IOException {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
return settings.getInt(key, defaultValue);
}
private static byte[] generateEncryptionSecret() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128);
SecretKey key = generator.generateKey();
return key.getEncoded();
} catch (NoSuchAlgorithmException ex) {
Log.w("keyutil", ex);
return null;
}
}
private static byte[] generateMacSecret() {
try {
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
return generator.generateKey().getEncoded();
} catch (NoSuchAlgorithmException e) {
Log.w("keyutil", e);
return null;
}
}
private static byte[] generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return salt;
}
private static int generateIterationCount(String passphrase, byte[] salt) {
int TARGET_ITERATION_TIME = 50; //ms
int MINIMUM_ITERATION_COUNT = 100; //default for low-end devices
int BENCHMARK_ITERATION_COUNT = 10000; //baseline starting iteration count
try {
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, BENCHMARK_ITERATION_COUNT);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
long startTime = System.currentTimeMillis();
skf.generateSecret(keyspec);
long finishTime = System.currentTimeMillis();
int scaledIterationTarget = (int) (((double)BENCHMARK_ITERATION_COUNT / (double)(finishTime - startTime)) * TARGET_ITERATION_TIME);
if (scaledIterationTarget < MINIMUM_ITERATION_COUNT) return MINIMUM_ITERATION_COUNT;
else return scaledIterationTarget;
} catch (NoSuchAlgorithmException e) {
Log.w("MasterSecretUtil", e);
return MINIMUM_ITERATION_COUNT;
} catch (InvalidKeySpecException e) {
Log.w("MasterSecretUtil", e);
return MINIMUM_ITERATION_COUNT;
}
}
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt, int iterations)
throws GeneralSecurityException
{
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
return skf.generateSecret(keyspec);
}
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int iterations, int opMode)
throws GeneralSecurityException
{
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(opMode, key, new PBEParameterSpec(salt, iterations));
return cipher;
}
private static byte[] encryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
throws GeneralSecurityException
{
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.ENCRYPT_MODE);
return cipher.doFinal(data);
}
private static byte[] decryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
throws GeneralSecurityException, IOException
{
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.DECRYPT_MODE);
return cipher.doFinal(data);
}
private static Mac getMacForPassphrase(String passphrase, byte[] salt, int iterations)
throws GeneralSecurityException
{
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
byte[] pbkdf2 = key.getEncoded();
SecretKeySpec hmacKey = new SecretKeySpec(pbkdf2, "HmacSHA1");
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(hmacKey);
return hmac;
}
private static byte[] verifyMac(byte[] macSalt, int iterations, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
byte[] givenMac = new byte[hmac.getMacLength()];
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
byte[] localMac = hmac.doFinal(encryptedData);
if (Arrays.equals(givenMac, localMac)) return encryptedData;
else throw new InvalidPassphraseException("MAC Error");
}
private static byte[] macWithPassphrase(byte[] macSalt, int iterations, byte[] data, String passphrase) throws GeneralSecurityException {
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
byte[] mac = hmac.doFinal(data);
byte[] result = new byte[data.length + mac.length];
System.arraycopy(data, 0, result, 0, data.length);
System.arraycopy(mac, 0, result, data.length, mac.length);
return result;
}
}

View File

@@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.crypto;
import androidx.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 byte[] random, @NonNull File file, long offset)
throws IOException
{
return createFor(attachmentSecret, random, new FileInputStream(file), offset);
}
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 == -1) throw new IOException("Prematurely reached end of stream!");
else if (read + offset < buffer.length) offset += read;
else return;
}
}
}

View File

@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.crypto;
import androidx.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);
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2013-2018 Open 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.content.Context;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.util.Medium;
import java.util.LinkedList;
import java.util.List;
public class PreKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = PreKeyUtil.class.getSimpleName();
private static final int BATCH_SIZE = 100;
public synchronized static List<PreKeyRecord> generatePreKeys(Context context) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context);
List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context);
for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
preKeyStore.storePreKey(preKeyId, record);
records.add(record);
}
TextSecurePreferences.setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records;
}
public synchronized static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) {
try {
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context);
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
if (active) {
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId);
}
return record;
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static synchronized void setActiveSignedPreKeyId(Context context, int id) {
TextSecurePreferences.setActiveSignedPreKeyId(context, id);
}
public static synchronized int getActiveSignedPreKeyId(Context context) {
return TextSecurePreferences.getActiveSignedPreKeyId(context);
}
}

View File

@@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
public class ProfileKeyUtil {
public static synchronized boolean hasProfileKey(@NonNull Context context) {
return TextSecurePreferences.getProfileKey(context) != null;
}
public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
try {
String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
if (encodedProfileKey == null) {
encodedProfileKey = Util.getSecret(32);
TextSecurePreferences.setProfileKey(context, encodedProfileKey);
}
return Base64.decode(encodedProfileKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
TextSecurePreferences.setProfileKey(context, null);
return getProfileKey(context);
}
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open 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 org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.thoughtcrime.securesms.util.Conversions;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class PublicKey {
private static final String TAG = PublicKey.class.getSimpleName();
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
private final ECPublicKey publicKey;
private int id;
public PublicKey(PublicKey publicKey) {
this.id = publicKey.id;
// FIXME :: This not strictly an accurate copy constructor.
this.publicKey = publicKey.publicKey;
}
public PublicKey(int id, ECPublicKey publicKey) {
this.publicKey = publicKey;
this.id = id;
}
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
Log.i(TAG, "PublicKey Length: " + (bytes.length - offset));
if ((bytes.length - offset) < KEY_SIZE)
throw new InvalidKeyException("Provided bytes are too short.");
this.id = Conversions.byteArrayToMedium(bytes, offset);
this.publicKey = Curve.decodePoint(bytes, offset + 3);
}
public PublicKey(byte[] bytes) throws InvalidKeyException {
this(bytes, 0);
}
public int getType() {
return publicKey.getType();
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public ECPublicKey getKey() {
return publicKey;
}
public String getFingerprint() {
return Hex.toString(getFingerprintBytes());
}
public byte[] getFingerprintBytes() {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(serialize());
} catch (NoSuchAlgorithmException nsae) {
Log.w("LocalKeyPair", nsae);
throw new IllegalArgumentException("SHA-1 isn't supported!");
}
}
public byte[] serialize() {
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
byte[] serializedPoint = publicKey.serialize();
Log.i(TAG, "Serializing public key point: " + Hex.toString(serializedPoint));
return Util.combine(keyIdBytes, serializedPoint);
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
/**
* This class processes key exchange interactions.
*
* @author Moxie Marlinspike
*/
public class SecurityEvent {
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
public static void broadcastSecurityUpdateEvent(Context context) {
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
}
}

View File

@@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SessionUtil {
public static boolean hasSession(@NonNull Context context, @NonNull RecipientId id) {
SessionStore sessionStore = new TextSecureSessionStore(context);
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(Recipient.resolved(id).requireServiceId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
return sessionStore.containsSession(axolotlAddress);
}
public static void archiveSiblingSessions(Context context, SignalProtocolAddress address) {
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
sessionStore.archiveSiblingSessions(address);
}
public static void archiveAllSessions(Context context) {
new TextSecureSessionStore(context).archiveAllSessions();
}
}

View File

@@ -0,0 +1,122 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException;
public class UnidentifiedAccessUtil {
private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName();
public static CertificateValidator getCertificateValidator() {
try {
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BuildConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
return new CertificateValidator(unidentifiedSenderTrustRoot);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
}
@WorkerThread
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context,
@NonNull Recipient recipient)
{
try {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
}
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
" | UUID certificate supported? " + recipient.isUuidSupported());
if (theirUnidentifiedAccessKey != null &&
ourUnidentifiedAccessKey != null &&
ourUnidentifiedAccessCertificate != null)
{
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate),
new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate)));
}
return Optional.absent();
} catch (InvalidCertificateException e) {
Log.w(TAG, e);
return Optional.absent();
}
}
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
try {
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessCertificate = Recipient.self().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
}
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate),
new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate)));
}
return Optional.absent();
} catch (InvalidCertificateException e) {
Log.w(TAG, e);
return Optional.absent();
}
}
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
byte[] theirProfileKey = recipient.resolve().getProfileKey();
switch (recipient.resolve().getUnidentifiedAccessMode()) {
case UNKNOWN:
if (theirProfileKey == null) return Util.getSecretBytes(16);
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
case DISABLED:
return null;
case ENABLED:
if (theirProfileKey == null) return null;
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
case UNRESTRICTED:
return Util.getSecretBytes(16);
default:
throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode());
}
}
}

View File

@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import java.util.List;
public class SignalProtocolStoreImpl implements SignalProtocolStore {
private final PreKeyStore preKeyStore;
private final SignedPreKeyStore signedPreKeyStore;
private final IdentityKeyStore identityKeyStore;
private final SessionStore sessionStore;
public SignalProtocolStoreImpl(Context context) {
this.preKeyStore = new TextSecurePreKeyStore(context);
this.signedPreKeyStore = new TextSecurePreKeyStore(context);
this.identityKeyStore = new TextSecureIdentityKeyStore(context);
this.sessionStore = new TextSecureSessionStore(context);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
}
@Override
public int getLocalRegistrationId() {
return identityKeyStore.getLocalRegistrationId();
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
return identityKeyStore.getIdentity(address);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
preKeyStore.storePreKey(preKeyId, record);
}
@Override
public boolean containsPreKey(int preKeyId) {
return preKeyStore.containsPreKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
preKeyStore.removePreKey(preKeyId);
}
@Override
public SessionRecord loadSession(SignalProtocolAddress axolotlAddress) {
return sessionStore.loadSession(axolotlAddress);
}
@Override
public List<Integer> getSubDeviceSessions(String number) {
return sessionStore.getSubDeviceSessions(number);
}
@Override
public void storeSession(SignalProtocolAddress axolotlAddress, SessionRecord record) {
sessionStore.storeSession(axolotlAddress, record);
}
@Override
public boolean containsSession(SignalProtocolAddress axolotlAddress) {
return sessionStore.containsSession(axolotlAddress);
}
@Override
public void deleteSession(SignalProtocolAddress axolotlAddress) {
sessionStore.deleteSession(axolotlAddress);
}
@Override
public void deleteAllSessions(String number) {
sessionStore.deleteAllSessions(number);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return signedPreKeyStore.loadSignedPreKeys();
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
}
}

View File

@@ -0,0 +1,166 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
private static final String TAG = TextSecureIdentityKeyStore.class.getSimpleName();
private static final Object LOCK = new Object();
private final Context context;
public TextSecureIdentityKeyStore(Context context) {
this.context = context;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return IdentityKeyUtil.getIdentityKeyPair(context);
}
@Override
public int getLocalRegistrationId() {
return TextSecurePreferences.getLocalRegistrationId(context);
}
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipient recipient = Recipient.external(context, address.getName());
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipient.getId());
if (!identityRecord.isPresent()) {
Log.i(TAG, "Saving new identity...");
identityDatabase.saveIdentity(recipient.getId(), identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
return false;
}
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
Log.i(TAG, "Replacing existing identity...");
VerifiedStatus verifiedStatus;
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED ||
identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED)
{
verifiedStatus = VerifiedStatus.UNVERIFIED;
} else {
verifiedStatus = VerifiedStatus.DEFAULT;
}
identityDatabase.saveIdentity(recipient.getId(), identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
IdentityUtil.markIdentityUpdate(context, recipient);
SessionUtil.archiveSiblingSessions(context, address);
return true;
}
if (isNonBlockingApprovalRequired(identityRecord.get())) {
Log.i(TAG, "Setting approval status...");
identityDatabase.setApproval(recipient.getId(), nonBlockingApproval);
return false;
}
return false;
}
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, false);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
synchronized (LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
RecipientId ourRecipientId = Recipient.self().getId();
RecipientId theirRecipientId = Recipient.external(context, address.getName()).getId();
if (ourRecipientId.equals(theirRecipientId)) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
}
switch (direction) {
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirRecipientId));
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
} else {
Log.w(TAG, "Tried to check if identity is trusted for " + address.getName() + ", but no matching recipient existed!");
switch (direction) {
case SENDING: return false;
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
}
}
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
Optional<IdentityRecord> record = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipientId);
if (record.isPresent()) {
return record.get().getIdentityKey();
} else {
return null;
}
} else {
Log.w(TAG, "Tried to get identity for " + address.getName() + ", but no matching recipient existed!");
return null;
}
}
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
if (!identityRecord.isPresent()) {
Log.w(TAG, "Nothing here, returning true...");
return true;
}
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
Log.w(TAG, "Identity keys don't match...");
return false;
}
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
Log.w(TAG, "Needs unverified approval!");
return false;
}
if (isNonBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Needs non-blocking approval!");
return false;
}
return true;
}
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
!identityRecord.isApprovedNonBlocking();
}
}

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import java.util.List;
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
@SuppressWarnings("unused")
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
private static final Object FILE_LOCK = new Object();
@NonNull
private final Context context;
public TextSecurePreKeyStore(@NonNull Context context) {
this.context = context;
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
PreKeyRecord preKeyRecord = DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId);
if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId);
else return preKeyRecord;
}
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
SignedPreKeyRecord signedPreKeyRecord = DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId);
if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId);
else return signedPreKeyRecord;
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (FILE_LOCK) {
return DatabaseFactory.getSignedPreKeyDatabase(context).getAllSignedPreKeys();
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (FILE_LOCK) {
DatabaseFactory.getPreKeyDatabase(context).insertPreKey(preKeyId, record);
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (FILE_LOCK) {
DatabaseFactory.getSignedPreKeyDatabase(context).insertSignedPreKey(signedPreKeyId, record);
}
}
@Override
public boolean containsPreKey(int preKeyId) {
return DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId) != null;
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId) != null;
}
@Override
public void removePreKey(int preKeyId) {
DatabaseFactory.getPreKeyDatabase(context).removePreKey(preKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
DatabaseFactory.getSignedPreKeyDatabase(context).removeSignedPreKey(signedPreKeyId);
}
}

View File

@@ -0,0 +1,137 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
public class TextSecureSessionStore implements SessionStore {
private static final String TAG = TextSecureSessionStore.class.getSimpleName();
private static final Object FILE_LOCK = new Object();
@NonNull private final Context context;
public TextSecureSessionStore(@NonNull Context context) {
this.context = context;
}
@Override
public SessionRecord loadSession(@NonNull SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
if (sessionRecord == null) {
Log.w(TAG, "No existing session information found.");
return new SessionRecord();
}
return sessionRecord;
}
}
@Override
public void storeSession(@NonNull SignalProtocolAddress address, @NonNull SessionRecord record) {
synchronized (FILE_LOCK) {
RecipientId id = Recipient.external(context, address.getName()).getId();
DatabaseFactory.getSessionDatabase(context).store(id, address.getDeviceId(), record);
}
}
@Override
public boolean containsSession(SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
return sessionRecord != null &&
sessionRecord.getSessionState().hasSenderChain() &&
sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
} else {
return false;
}
}
}
@Override
public void deleteSession(SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
DatabaseFactory.getSessionDatabase(context).delete(recipientId, address.getDeviceId());
} else {
Log.w(TAG, "Tried to delete session for " + address.toString() + ", but none existed!");
}
}
}
@Override
public void deleteAllSessions(String name) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(name)) {
RecipientId recipientId = Recipient.external(context, name).getId();
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipientId);
}
}
}
@Override
public List<Integer> getSubDeviceSessions(String name) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(name)) {
RecipientId recipientId = Recipient.external(context, name).getId();
return DatabaseFactory.getSessionDatabase(context).getSubDevices(recipientId);
} else {
Log.w(TAG, "Tried to get sub device sessions for " + name + ", but none existed!");
return Collections.emptyList();
}
}
}
public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAllFor(recipientId);
for (SessionDatabase.SessionRow row : sessions) {
if (row.getDeviceId() != address.getDeviceId()) {
row.getRecord().archiveCurrentState();
storeSession(new SignalProtocolAddress(Recipient.resolved(row.getRecipientId()).requireServiceId(), row.getDeviceId()), row.getRecord());
}
}
} else {
Log.w(TAG, "Tried to archive sibling sessions for " + address.toString() + ", but none existed!");
}
}
}
public void archiveAllSessions() {
synchronized (FILE_LOCK) {
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAll();
for (SessionDatabase.SessionRow row : sessions) {
row.getRecord().archiveCurrentState();
storeSession(new SignalProtocolAddress(Recipient.resolved(row.getRecipientId()).requireServiceId(), row.getDeviceId()), row.getRecord());
}
}
}
}