diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java b/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java index f3a4662746..98a1e31e12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java +++ b/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java @@ -52,6 +52,7 @@ public final class AppInitialization { Log.i(TAG, "onPostBackupRestore()"); ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch(); + SignalStore.onPostBackupRestore(); SignalStore.onFirstEverAppLaunch(); SignalStore.onboarding().clearAll(); TextSecurePreferences.onPostBackupRestore(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index b97d6088ca..8ec210d066 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -24,6 +24,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.qr.ScanListener; import org.thoughtcrime.securesms.util.Base64; @@ -187,7 +188,7 @@ public class DeviceActivity extends PassphraseRequiredActivity } ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context); + IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); Optional profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext())); TextSecurePreferences.setMultiDevice(DeviceActivity.this, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java index 18c3887b7d..44ee991406 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java @@ -22,6 +22,7 @@ import android.os.Bundle; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.VersionTracker; /** @@ -61,7 +62,7 @@ public class PassphraseCreateActivity extends PassphraseActivity { passphrase); MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret); - IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this); + SignalStore.account().generateAciIdentityKey(); VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index a5408aae6b..b50b68d58a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -182,12 +182,6 @@ public class FullBackupExporter extends FullBackupBase { stopwatch.split("table::" + table); } - for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { - throwIfCanceled(cancellationSignal); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); - outputStream.write(preference); - } - for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) { throwIfCanceled(cancellationSignal); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index ca490da328..0818e1691a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BackupUtil; @@ -250,6 +251,17 @@ public class FullBackupImporter extends FullBackupBase { private static void processPreference(@NonNull Context context, SharedPreference preference) { SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0); + // Identity keys were moved from shared prefs into SignalStore. Need to handle importing backups made before the migration. + if ("SecureSMS-Preferences".equals(preference.getFile())) { + if ("pref_identity_public_v3".equals(preference.getKey()) && preference.hasValue()) { + SignalStore.account().restoreLegacyIdentityPublicKeyFromBackup(preference.getValue()); + } else if ("pref_identity_private_v3".equals(preference.getKey()) && preference.hasValue()) { + SignalStore.account().restoreLegacyIdentityPrivateKeyFromBackup(preference.getValue()); + } + + return; + } + if (preference.hasValue()) { preferences.edit().putString(preference.getKey(), preference.getValue()).commit(); } else if (preference.hasBooleanValue()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java b/app/src/main/java/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java index f7759f5442..4427236914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java @@ -9,7 +9,7 @@ import androidx.appcompat.app.AlertDialog; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; -import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; +import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; @@ -40,7 +40,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn @Override public void onClick(DialogInterface dialog, int which) { - final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); + final SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); SimpleTask.run(() -> { try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java index 14b46776a6..3a4a7e37f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java @@ -14,7 +14,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.SessionUtil; import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; -import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; +import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.IdentityRecord; @@ -96,7 +96,7 @@ final class SafetyNumberChangeRepository { @WorkerThread private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List changedRecipients) { - TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); + SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) { for (ChangedRecipient changedRecipient : changedRecipients) { @@ -129,10 +129,10 @@ final class SafetyNumberChangeRepository { SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), SignalServiceAddress.DEFAULT_DEVICE_ID); Log.d(TAG, "Saving identity for: " + changedRecipient.getRecipient().getId() + " " + changedRecipient.getIdentityRecord().getIdentityKey().hashCode()); - TextSecureIdentityKeyStore.SaveResult result = ApplicationDependencies.getProtocolStore().aci().identities().saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true); + SignalIdentityKeyStore.SaveResult result = ApplicationDependencies.getProtocolStore().aci().identities().saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true); Log.d(TAG, "Saving identity result: " + result); - if (result == TextSecureIdentityKeyStore.SaveResult.NO_CHANGE) { + if (result == SignalIdentityKeyStore.SaveResult.NO_CHANGE) { Log.i(TAG, "Archiving sessions explicitly as they appear to be out of sync."); SessionUtil.archiveSession(changedRecipient.getRecipient().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID); SessionUtil.archiveSiblingSessions(mismatchAddress); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index 46baa095b6..7e82474c53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -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 getBackupRecord(@NonNull Context context) { - SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0); - - return new LinkedList() {{ - 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(); - } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java rename to app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java index 7f26ec93a3..793a2c5b4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java @@ -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 - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalIdentityKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalIdentityKeyStore.java new file mode 100644 index 0000000000..16e678e79d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalIdentityKeyStore.java @@ -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 identitySupplier; + + public SignalIdentityKeyStore(@NonNull SignalBaseIdentityKeyStore baseStore, @NonNull Supplier 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 getIdentityRecord(@NonNull RecipientId recipientId) { + return baseStore.getIdentityRecord(recipientId); + } + + public @NonNull IdentityRecordList getIdentityRecords(@NonNull List 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java index 4cb0dcce8d..317c9c03ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceDataStoreImpl.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceDataStoreImpl.java index e92c7df330..53d02ec295 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceDataStoreImpl.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceDataStoreImpl.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java index 4009109bb4..a2e21387a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java @@ -11,6 +11,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.devicelist.Device; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.AsyncLoader; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.IdentityKeyPair; @@ -77,7 +78,7 @@ public class DeviceListLoader extends AsyncLoader> { throw new IOException("Got a DeviceName that wasn't properly populated."); } - return new Device(deviceInfo.getId(), new String(decryptName(deviceName, IdentityKeyUtil.getIdentityKeyPair(getContext()))), deviceInfo.getCreated(), deviceInfo.getLastSeen()); + return new Device(deviceInfo.getId(), new String(decryptName(deviceName, SignalStore.account().getAciIdentityKey())), deviceInfo.getCreated(), deviceInfo.getLastSeen()); } catch (IOException e) { Log.w(TAG, "Failed while reading the protobuf.", e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 432ab8d08f..07f4959de8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -13,10 +13,11 @@ import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; +import org.thoughtcrime.securesms.crypto.storage.SignalBaseIdentityKeyStore; import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl; import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl; import org.thoughtcrime.securesms.crypto.storage.SignalSenderKeyStore; -import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; +import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore; import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseObserver; @@ -75,6 +76,7 @@ import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; @@ -276,12 +278,20 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalServiceDataStoreImpl provideProtocolStore() { - SignalServiceAccountDataStoreImpl aci = new SignalServiceAccountDataStoreImpl(context, - new TextSecurePreKeyStore(context), - new TextSecureIdentityKeyStore(context), - new TextSecureSessionStore(context), - new SignalSenderKeyStore(context)); - return new SignalServiceDataStoreImpl(context, aci, aci); + SignalBaseIdentityKeyStore baseIdentityStore = new SignalBaseIdentityKeyStore(context); + + SignalServiceAccountDataStoreImpl aciStore = new SignalServiceAccountDataStoreImpl(context, + new TextSecurePreKeyStore(context), + new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getAciIdentityKey()), + new TextSecureSessionStore(context), + new SignalSenderKeyStore(context)); + + SignalServiceAccountDataStoreImpl pniStore = new SignalServiceAccountDataStoreImpl(context, + new TextSecurePreKeyStore(context), + new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getPniIdentityKey()), + new TextSecureSessionStore(context), + new SignalSenderKeyStore(context)); + return new SignalServiceDataStoreImpl(context, aciStore, pniStore); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java index 3ddd6124d4..9f82487958 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java @@ -61,7 +61,7 @@ public class CreateSignedPreKeyJob extends BaseJob { } SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context); + IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair, true); accountManager.setSignedPreKey(signedPreKeyRecord); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index 788c22819d..3c02837fe4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -129,7 +129,7 @@ public final class PushDecryptMessageJob extends BaseJob { } private boolean needsMigration() { - return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context); + return TextSecurePreferences.getNeedsSqlCipherMigration(context); } private void postMigrationNotification() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 9a0033d7c7..5e1443e7c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -98,7 +98,7 @@ public class RefreshAttributesJob extends BaseJob { boolean phoneNumberDiscoverable = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable(); String deviceName = SignalStore.account().getDeviceName(); - byte[] encryptedDeviceName = (deviceName == null) ? null : DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), IdentityKeyUtil.getIdentityKeyPair(context)); + byte[] encryptedDeviceName = (deviceName == null) ? null : DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), SignalStore.account().getAciIdentityKey()); AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut()); Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() + diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java index 8e0a814695..728c154ddf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java @@ -85,7 +85,7 @@ public class RefreshPreKeysJob extends BaseJob { } List preKeyRecords = PreKeyUtil.generatePreKeys(context); - IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); + IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); Log.i(TAG, "Registering new prekeys..."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java index 164249134f..f1b64539c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -53,7 +54,7 @@ public class RotateSignedPreKeyJob extends BaseJob { Log.i(TAG, "Rotating signed prekey..."); SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); + IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); accountManager.setSignedPreKey(signedPreKeyRecord); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index c7f1845e75..466b16d41e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -1,14 +1,23 @@ package org.thoughtcrime.securesms.keyvalue import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager import androidx.annotation.VisibleForTesting import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MasterCipher import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.service.KeyCachingService +import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util +import org.whispersystems.libsignal.IdentityKey +import org.whispersystems.libsignal.IdentityKeyPair +import org.whispersystems.libsignal.ecc.Curve import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.SignalServiceAddress @@ -26,6 +35,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time" private const val KEY_DEVICE_NAME = "account.device_name" private const val KEY_DEVICE_ID = "account.device_id" + private const val KEY_ACI_IDENTITY_PUBLIC_KEY = "account.aci_identity_public_key" + private const val KEY_ACI_IDENTITY_PRIVATE_KEY = "account.aci_identity_private_key" + private const val KEY_PNI_IDENTITY_PUBLIC_KEY = "account.pni_identity_public_key" + private const val KEY_PNI_IDENTITY_PRIVATE_KEY = "account.pni_identity_private_key" @VisibleForTesting const val KEY_E164 = "account.e164" @@ -37,14 +50,23 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal init { if (!store.containsKey(KEY_ACI)) { - migrateFromSharedPrefs(ApplicationDependencies.getApplication()) + migrateFromSharedPrefsV1(ApplicationDependencies.getApplication()) + } + + if (!store.containsKey(KEY_ACI_IDENTITY_PUBLIC_KEY)) { + migrateFromSharedPrefsV2(ApplicationDependencies.getApplication()) } } public override fun onFirstEverAppLaunch() = Unit public override fun getKeysToIncludeInBackup(): List { - return emptyList() + return listOf( + KEY_ACI_IDENTITY_PUBLIC_KEY, + KEY_ACI_IDENTITY_PRIVATE_KEY, + KEY_PNI_IDENTITY_PUBLIC_KEY, + KEY_PNI_IDENTITY_PRIVATE_KEY, + ) } /** The local user's [ACI]. */ @@ -84,6 +106,74 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal get() = getInteger(KEY_REGISTRATION_ID, 0) set(value) = putInteger(KEY_REGISTRATION_ID, value) + /** The identity key pair for the ACI identity. */ + val aciIdentityKey: IdentityKeyPair + get() { + require(store.containsKey(KEY_ACI_IDENTITY_PUBLIC_KEY)) { "Not yet set!" } + return IdentityKeyPair( + IdentityKey(getBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, null)), + Curve.decodePrivatePoint(getBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, null)) + ) + } + + /** The identity key pair for the PNI identity. */ + val pniIdentityKey: IdentityKeyPair + get() { + require(store.containsKey(KEY_PNI_IDENTITY_PUBLIC_KEY)) { "Not yet set!" } + return IdentityKeyPair( + IdentityKey(getBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, null)), + Curve.decodePrivatePoint(getBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, null)) + ) + } + + /** Generates and saves an identity key pair for the ACI identity. Should only be done once. */ + fun generateAciIdentityKey() { + Log.i(TAG, "Generating a new ACI identity key pair.") + require(!store.containsKey(KEY_ACI_IDENTITY_PUBLIC_KEY)) { "Already generated!" } + + val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() + store + .beginWrite() + .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) + .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) + .commit() + } + + /** Generates and saves an identity key pair for the PNI identity. Should only be done once. */ + fun generatePniIdentityKey() { + Log.i(TAG, "Generating a new PNI identity key pair.") + require(!store.containsKey(KEY_PNI_IDENTITY_PUBLIC_KEY)) { "Already generated!" } + + val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() + store + .beginWrite() + .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) + .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) + .commit() + } + + /** When acting as a linked device, this method lets you store the identity keys sent from the primary device */ + fun setIdentityKeysFromPrimaryDevice(aciKeys: IdentityKeyPair) { + require(isLinkedDevice) { "Must be a linked device!" } + store + .beginWrite() + .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, aciKeys.publicKey.serialize()) + .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, aciKeys.privateKey.serialize()) + .commit() + } + + /** Only to be used when restoring an identity public key from an old backup */ + fun restoreLegacyIdentityPublicKeyFromBackup(base64: String) { + Log.w(TAG, "Restoring legacy identity public key from backup.") + putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, Base64.decode(base64)) + } + + /** Only to be used when restoring an identity private key from an old backup */ + fun restoreLegacyIdentityPrivateKeyFromBackup(base64: String) { + Log.w(TAG, "Restoring legacy identity private key from backup.") + putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, Base64.decode(base64)) + } + /** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */ var fcmEnabled: Boolean @JvmName("isFcmEnabled") @@ -159,8 +249,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal ApplicationDependencies.getGroupsV2Authorization().clear() } - private fun migrateFromSharedPrefs(context: Context) { - Log.i(TAG, "Migrating account values from shared prefs.") + private fun migrateFromSharedPrefsV1(context: Context) { + Log.i(TAG, "[V1] Migrating account values from shared prefs.") putString(KEY_ACI, TextSecurePreferences.getStringPreference(context, "pref_local_uuid", null)) putString(KEY_E164, TextSecurePreferences.getStringPreference(context, "pref_local_number", null)) @@ -172,4 +262,63 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal putInteger(KEY_FCM_TOKEN_VERSION, TextSecurePreferences.getIntegerPreference(context, "pref_gcm_registration_id_version", 0)) putLong(KEY_FCM_TOKEN_LAST_SET_TIME, TextSecurePreferences.getLongPreference(context, "pref_gcm_registration_id_last_set_time", 0)) } + + private fun migrateFromSharedPrefsV2(context: Context) { + Log.i(TAG, "[V2] Migrating account values from shared prefs.") + + val masterSecretPrefs: SharedPreferences = context.getSharedPreferences("SecureSMS-Preferences", 0) + val defaultPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + if (masterSecretPrefs.hasStringData("pref_identity_public_v3")) { + Log.i(TAG, "Migrating modern identity key.") + + val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_v3", null)!!) + val identityPrivate = Base64.decode(masterSecretPrefs.getString("pref_identity_private_v3", null)!!) + + store + .beginWrite() + .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) + .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) + .commit() + } else if (masterSecretPrefs.hasStringData("pref_identity_public_curve25519")) { + Log.i(TAG, "Migrating legacy identity key.") + + val masterCipher = MasterCipher(KeyCachingService.getMasterSecret(context)) + val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_curve25519", null)!!) + val identityPrivate = masterCipher.decryptKey(Base64.decode(masterSecretPrefs.getString("pref_identity_private_curve25519", null)!!)).serialize() + + store + .beginWrite() + .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) + .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) + .commit() + } else { + Log.w(TAG, "No pre-existing identity key! No migration.") + } + + masterSecretPrefs + .edit() + .remove("pref_identity_public_v3") + .remove("pref_identity_private_v3") + .remove("pref_identity_public_curve25519") + .remove("pref_identity_private_curve25519") + .commit() + + defaultPrefs + .edit() + .remove("pref_local_uuid") + .remove("pref_identity_public_v3") + .remove("pref_gcm_password") + .remove("pref_gcm_registered") + .remove("pref_local_registration_id") + .remove("pref_gcm_disabled") + .remove("pref_gcm_registration_id") + .remove("pref_gcm_registration_id_version") + .remove("pref_gcm_registration_id_last_set_time") + .commit() + } + + private fun SharedPreferences.hasStringData(key: String): Boolean { + return this.getString(key, null) != null + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java index cde316d84e..d5bda974c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java @@ -131,9 +131,7 @@ public final class KeyValueStore implements KeyValueReader { /** * Forces the store to re-fetch all of it's data from the database. - * Should only be used for testing! */ - @VisibleForTesting synchronized void resetCache() { dataSet = null; initializeIfNecessary(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index 374eb9236b..7e0b45c3e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -150,6 +150,13 @@ public final class SignalStore { getInstance().store.resetCache(); } + /** + * Restoring a backup changes the underlying disk values, so the cache needs to be reset. + */ + public static void onPostBackupRestore() { + getInstance().store.resetCache(); + } + public static @NonNull AccountValues account() { return getInstance().accountValues; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java index 7546df0591..3e5151255b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java @@ -168,7 +168,6 @@ public class IncomingMessageProcessor { private boolean needsToEnqueueDecryption() { return !jobManager.areQueuesEmpty(SetUtil.newHashSet(Job.Parameters.MIGRATION_QUEUE_KEY, PushDecryptMessageJob.QUEUE)) || - !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index f0e274fdd2..7eadc5b1da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -113,10 +113,6 @@ public class LegacyMigrationJob extends MigrationJob { throw new RetryLaterException(); } - if (lastSeenVersion < CURVE25519_VERSION) { - IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); - } - if (lastSeenVersion < NO_V1_VERSION) { File v1sessions = new File(context.getFilesDir(), "sessions"); @@ -149,7 +145,6 @@ public class LegacyMigrationJob extends MigrationJob { // new TextSecureSessionStore(context, masterSecret).migrateSessions(); // new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); - IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); scheduleMessagesInPushDatabase(context);; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index 0a8201bd7e..a1f0e0916b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -134,7 +134,7 @@ public final class RegistrationRepository { PNI pni = PNI.parseOrThrow(response.getPni()); boolean hasPin = response.isStorageCapable(); - IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); + IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); List records = PreKeyUtil.generatePreKeys(context); SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java index cf082ec730..aa344285c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.WebRtcViewModel; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.RemotePeer; @@ -149,7 +150,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor { try { byte[] remoteIdentityKey = WebRtcUtil.getPublicKeyBytes(receivedAnswerMetadata.getRemoteIdentityKey()); - byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(context).serialize()); + byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(SignalStore.account().getAciIdentityKey().getPublicKey().serialize()); webRtcInteractor.getCallManager().receivedAnswer(callMetadata.getCallId(), callMetadata.getRemoteDevice(), answerMetadata.getOpaque(), remoteIdentityKey, localIdentityKey); } catch (CallException | InvalidKeyException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 2f0cc55623..2bbdd3cad5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -215,7 +215,7 @@ public abstract class WebRtcActionProcessor { try { byte[] remoteIdentityKey = WebRtcUtil.getPublicKeyBytes(receivedOfferMetadata.getRemoteIdentityKey()); - byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(context).serialize()); + byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(SignalStore.account().getAciIdentityKey().getPublicKey().serialize()); webRtcInteractor.getCallManager().receivedOffer(callMetadata.getCallId(), callMetadata.getRemotePeer(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java index caf4d17f02..21c3db8d9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -10,7 +10,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; -import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; +import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.MessageDatabase; @@ -160,9 +160,9 @@ public final class IdentityUtil { public static void processVerifiedMessage(Context context, VerifiedMessage verifiedMessage) { try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) { - TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); - Recipient recipient = Recipient.externalPush(context, verifiedMessage.getDestination()); - Optional identityRecord = identityStore.getIdentityRecord(recipient.getId()); + SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); + Recipient recipient = Recipient.externalPush(context, verifiedMessage.getDestination()); + Optional identityRecord = identityStore.getIdentityRecord(recipient.getId()); if (!identityRecord.isPresent() && verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT) { Log.w(TAG, "No existing record for default status"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index c376338dcb..c33eeab77a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -300,7 +300,7 @@ public final class ProfileUtil { if (!SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) { return null; } else { - IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(ApplicationDependencies.getApplication()); + IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); MobileCoinPublicAddress publicAddress = ApplicationDependencies.getPayments() .getWallet() .getMobileCoinPublicAddress(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt index daa1920c5b..1b3f9b38c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment import org.signal.core.util.ThreadUtil import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.qr.ScanListener import org.thoughtcrime.securesms.recipients.Recipient @@ -45,7 +45,7 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen VerifyDisplayFragment.create( recipientId, remoteIdentity, - IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(requireContext())), + IdentityKeyParcelable(SignalStore.account().aciIdentityKey.publicKey), Recipient.self().requireE164(), isVerified ) diff --git a/app/src/test/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStoreTest.kt b/app/src/test/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStoreTest.kt similarity index 93% rename from app/src/test/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStoreTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStoreTest.kt index 794d3669c6..2e833a14ba 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStoreTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStoreTest.kt @@ -15,7 +15,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress import org.whispersystems.libsignal.ecc.ECPublicKey import org.whispersystems.signalservice.test.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS -class TextSecureIdentityKeyStoreTest { +class SignalBaseIdentityKeyStoreTest { companion object { private const val ADDRESS = "address1" @@ -29,7 +29,7 @@ class TextSecureIdentityKeyStoreTest { @Test fun `getIdentity() hits disk on first retrieve but not the second`() { val mockDb = mock(IdentityDatabase::class.java) - val subject = TextSecureIdentityKeyStore(mock(Context::class.java), mockDb) + val subject = SignalBaseIdentityKeyStore(mock(Context::class.java), mockDb) val identityKey = IdentityKey(ECPublicKey.fromPublicKeyBytes(ByteArray(32))) val record = mockRecord(ADDRESS, identityKey) @@ -45,7 +45,7 @@ class TextSecureIdentityKeyStoreTest { @Test fun `invalidate() evicts cache entry`() { val mockDb = mock(IdentityDatabase::class.java) - val subject = TextSecureIdentityKeyStore(mock(Context::class.java), mockDb) + val subject = SignalBaseIdentityKeyStore(mock(Context::class.java), mockDb) val identityKey = IdentityKey(ECPublicKey.fromPublicKeyBytes(ByteArray(32))) val record = mockRecord(ADDRESS, identityKey)