Migrate identity keys to SignalStore.

This commit is contained in:
Greyson Parrelli
2022-01-28 15:16:33 -05:00
parent 9a1b8c9bb2
commit db534cd376
31 changed files with 346 additions and 232 deletions

View File

@@ -17,27 +17,11 @@
*/
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.backup.BackupProtos;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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 org.whispersystems.libsignal.util.guava.Preconditions;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* Utility class for working with identity keys.
@@ -47,63 +31,6 @@ import java.util.List;
public class IdentityKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(IdentityKeyUtil.class);
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) {
IdentityKeyPair identityKeyPair = generateIdentityKeyPair();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
}
/**
* Only call when configuring as a secondary linked device.
*/
public static void setIdentityKeys(Context context, IdentityKeyPair identityKeyPair) {
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Identity keys can only be set directly by a linked device");
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
}
public static IdentityKeyPair generateIdentityKeyPair() {
ECKeyPair djbKeyPair = Curve.generateKeyPair();
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
@@ -111,78 +38,4 @@ public class IdentityKeyUtil {
return new IdentityKeyPair(djbIdentityKey, djbPrivateKey);
}
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

@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore.SaveResult;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.SignalDatabase;
@@ -19,12 +20,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.LRUCache;
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 org.whispersystems.signalservice.api.push.AccountIdentifier;
import java.util.ArrayList;
import java.util.List;
@@ -32,9 +33,14 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
/**
* We technically need a separate ACI and PNI identity store, but we want them both to share the same underlying data, including the same cache.
* So this class represents the core store, and we can create multiple {@link SignalIdentityKeyStore} that use this same instance, changing only what each of
* those reports as their own identity key.
*/
public class SignalBaseIdentityKeyStore {
private static final String TAG = Log.tag(TextSecureIdentityKeyStore.class);
private static final String TAG = Log.tag(SignalBaseIdentityKeyStore.class);
private static final Object LOCK = new Object();
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
@@ -42,26 +48,19 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
private final Context context;
private final Cache cache;
public TextSecureIdentityKeyStore(Context context) {
public SignalBaseIdentityKeyStore(@NonNull Context context) {
this(context, SignalDatabase.identities());
}
TextSecureIdentityKeyStore(@NonNull Context context, @NonNull IdentityDatabase identityDatabase) {
SignalBaseIdentityKeyStore(@NonNull Context context, @NonNull IdentityDatabase identityDatabase) {
this.context = context;
this.cache = new Cache(identityDatabase);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return IdentityKeyUtil.getIdentityKeyPair(context);
}
@Override
public int getLocalRegistrationId() {
return SignalStore.account().getRegistrationId();
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, false) == SaveResult.UPDATE;
}
@@ -121,15 +120,14 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
}
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, IdentityKeyStore.Direction direction) {
Recipient self = Recipient.self();
boolean isSelf = address.getName().equals(self.requireAci().toString()) ||
address.getName().equals(self.requireE164());
if (isSelf) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
return identityKey.equals(SignalStore.account().getAciIdentityKey().getPublicKey());
}
IdentityStoreRecord record = cache.get(address.getName());
@@ -144,7 +142,6 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
}
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
IdentityStoreRecord record = cache.get(address.getName());
return record != null ? record.getIdentityKey() : null;
@@ -317,11 +314,4 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
cache.remove(addressName);
}
}
public enum SaveResult {
NEW,
UPDATE,
NON_BLOCKING_APPROVAL_REQUIRED,
NO_CHANGE
}
}

View File

@@ -0,0 +1,102 @@
package org.thoughtcrime.securesms.crypto.storage;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
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.List;
import java.util.function.Supplier;
/**
* A wrapper around an instance of {@link SignalBaseIdentityKeyStore} that lets us report different values for {@link #getIdentityKeyPair()}.
* This lets us have multiple instances (one for ACI, one for PNI) that share the same underlying data while also reporting the correct identity key.
*/
public class SignalIdentityKeyStore implements IdentityKeyStore {
private final SignalBaseIdentityKeyStore baseStore;
private final Supplier<IdentityKeyPair> identitySupplier;
public SignalIdentityKeyStore(@NonNull SignalBaseIdentityKeyStore baseStore, @NonNull Supplier<IdentityKeyPair> identitySupplier) {
this.baseStore = baseStore;
this.identitySupplier = identitySupplier;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identitySupplier.get();
}
@Override
public int getLocalRegistrationId() {
return baseStore.getLocalRegistrationId();
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return baseStore.saveIdentity(address, identityKey);
}
public @NonNull SaveResult saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
return baseStore.saveIdentity(address, identityKey, nonBlockingApproval);
}
public void saveIdentityWithoutSideEffects(@NonNull RecipientId recipientId,
IdentityKey identityKey,
VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonBlockingApproval)
{
baseStore.saveIdentityWithoutSideEffects(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return baseStore.isTrustedIdentity(address, identityKey, direction);
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
return baseStore.getIdentity(address);
}
public @NonNull Optional<IdentityRecord> getIdentityRecord(@NonNull RecipientId recipientId) {
return baseStore.getIdentityRecord(recipientId);
}
public @NonNull IdentityRecordList getIdentityRecords(@NonNull List<Recipient> recipients) {
return baseStore.getIdentityRecords(recipients);
}
public void setApproval(@NonNull RecipientId recipientId, boolean nonBlockingApproval) {
baseStore.setApproval(recipientId, nonBlockingApproval);
}
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
baseStore.setVerified(recipientId, identityKey, verifiedStatus);
}
public void delete(@NonNull String addressName) {
baseStore.delete(addressName);
}
public void invalidate(@NonNull String addressName) {
baseStore.invalidate(addressName);
}
public enum SaveResult {
NEW,
UPDATE,
NON_BLOCKING_APPROVAL_REQUIRED,
NO_CHANGE
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
@@ -26,16 +25,16 @@ import java.util.UUID;
public class SignalServiceAccountDataStoreImpl implements SignalServiceAccountDataStore {
private final Context context;
private final TextSecurePreKeyStore preKeyStore;
private final TextSecurePreKeyStore signedPreKeyStore;
private final TextSecureIdentityKeyStore identityKeyStore;
private final TextSecureSessionStore sessionStore;
private final SignalSenderKeyStore senderKeyStore;
private final Context context;
private final TextSecurePreKeyStore preKeyStore;
private final TextSecurePreKeyStore signedPreKeyStore;
private final SignalIdentityKeyStore identityKeyStore;
private final TextSecureSessionStore sessionStore;
private final SignalSenderKeyStore senderKeyStore;
public SignalServiceAccountDataStoreImpl(@NonNull Context context,
@NonNull TextSecurePreKeyStore preKeyStore,
@NonNull TextSecureIdentityKeyStore identityKeyStore,
@NonNull SignalIdentityKeyStore identityKeyStore,
@NonNull TextSecureSessionStore sessionStore,
@NonNull SignalSenderKeyStore senderKeyStore)
{
@@ -193,7 +192,7 @@ public class SignalServiceAccountDataStoreImpl implements SignalServiceAccountDa
senderKeyStore.clearSenderKeySharedWith(addresses);
}
public @NonNull TextSecureIdentityKeyStore identities() {
public @NonNull SignalIdentityKeyStore identities() {
return identityKeyStore;
}

View File

@@ -29,7 +29,7 @@ public final class SignalServiceDataStoreImpl implements SignalServiceDataStore
if (accountIdentifier.equals(SignalStore.account().getAci())) {
return aciStore;
} else if (accountIdentifier.equals(SignalStore.account().getPni())) {
return pniStore;
throw new AssertionError("Not to be used yet!");
} else {
throw new IllegalArgumentException("No matching store found for " + accountIdentifier);
}