Implement auto-migration

This commit is contained in:
Niels Andriesse
2020-12-16 15:41:37 +11:00
parent 3cd1f0acff
commit 0ab429872c
11 changed files with 198 additions and 17 deletions

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="@dimen/large_spacing"
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_shield"
android:tint="?android:textColorPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
android:text="Upgrade Successful!"
android:textStyle="bold"
android:textSize="@dimen/large_font_size" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
android:text="Your new and improved Session ID is:"
android:textSize="@dimen/medium_font_size"
android:textAlignment="center" />
<TextView
style="@style/SessionIDTextView"
android:id="@+id/sessionIDTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
android:layout_marginTop="@dimen/medium_spacing"
android:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
<Button
style="@style/Widget.Session.Button.Common.UnimportantOutline"
android:id="@+id/copyButton"
android:layout_width="240dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginTop="@dimen/very_large_spacing"
android:text="Copy" />
<Button
style="@style/Widget.Session.Button.Common.UnimportantOutline"
android:id="@+id/okButton"
android:layout_width="240dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginTop="@dimen/medium_spacing"
android:text="@string/ok" />
</LinearLayout>

View File

@@ -588,14 +588,19 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}); });
} }
public void clearAllData() { public void clearAllData(boolean isMigratingToV2KeyPair) {
String token = TextSecurePreferences.getFCMToken(this); String token = TextSecurePreferences.getFCMToken(this);
if (token != null && !token.isEmpty()) { if (token != null && !token.isEmpty()) {
LokiPushNotificationManager.unregister(token, this); LokiPushNotificationManager.unregister(token, this);
} }
boolean wasUnlinked = TextSecurePreferences.getWasUnlinked(this); String displayName = TextSecurePreferences.getProfileName(this);
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
TextSecurePreferences.clearAll(this); TextSecurePreferences.clearAll(this);
TextSecurePreferences.setWasUnlinked(this, wasUnlinked); if (isMigratingToV2KeyPair) {
TextSecurePreferences.setIsMigratingKeyPair(this, true);
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
TextSecurePreferences.setProfileName(this, displayName);
}
MasterSecretUtil.clear(this); MasterSecretUtil.clear(this);
if (!deleteDatabase("signal.db")) { if (!deleteDatabase("signal.db")) {
Log.d("Loki", "Failed to delete database."); Log.d("Loki", "Failed to delete database.");

View File

@@ -169,11 +169,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
} }
this.broadcastReceiver = broadcastReceiver this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged")) LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
// Clear all data if this is a secondary device
if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) {
TextSecurePreferences.setWasUnlinked(this, true)
ApplicationContext.getInstance(this).clearAllData()
}
} }
override fun onResume() { override fun onResume() {
@@ -185,7 +180,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
if (hasViewedSeed || !isMasterDevice) { if (hasViewedSeed || !isMasterDevice) {
seedReminderView.visibility = View.GONE seedReminderView.visibility = View.GONE
} }
// Show key pair migration sheet if needed showKeyPairMigrationSheetIfNeeded()
showKeyPairMigrationSuccessSheetIfNeeded()
}
private fun showKeyPairMigrationSheetIfNeeded() {
if (KeyPairUtilities.hasV2KeyPair(this)) { return } if (KeyPairUtilities.hasV2KeyPair(this)) { return }
val lastNudge = TextSecurePreferences.getLastKeyPairMigrationNudge(this) val lastNudge = TextSecurePreferences.getLastKeyPairMigrationNudge(this)
val nudgeInterval: Long = 3 * 24 * 60 * 60 * 1000 // 3 days val nudgeInterval: Long = 3 * 24 * 60 * 60 * 1000 // 3 days
@@ -196,6 +195,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
TextSecurePreferences.setLastKeyPairMigrationNudge(this, Date().time) TextSecurePreferences.setLastKeyPairMigrationNudge(this, Date().time)
} }
private fun showKeyPairMigrationSuccessSheetIfNeeded() {
if (!KeyPairUtilities.hasV2KeyPair(this) || !TextSecurePreferences.getIsMigratingKeyPair(this)) { return }
val keyPairMigrationSuccessSheet = KeyPairMigrationSuccessBottomSheet()
keyPairMigrationSuccessSheet.show(supportFragmentManager, keyPairMigrationSuccessSheet.tag)
TextSecurePreferences.setIsMigratingKeyPair(this, false)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == CreateClosedGroupActivity.closedGroupCreatedResultCode) { if (resultCode == CreateClosedGroupActivity.closedGroupCreatedResultCode) {

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialog
import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialogDelegate import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialogDelegate
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.utilities.show import org.thoughtcrime.securesms.loki.utilities.show
@@ -44,9 +45,11 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
fakeChatView.startAnimating() fakeChatView.startAnimating()
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }
// linkButton.setOnClickListener { linkDevice() } if (TextSecurePreferences.getIsMigratingKeyPair(this)) {
if (TextSecurePreferences.getWasUnlinked(this)) { val displayName = TextSecurePreferences.getProfileName(this)
Toast.makeText(this, R.string.activity_landing_device_unlinked_dialog_title, Toast.LENGTH_LONG).show() if (displayName != null) {
KeyPairUtilities.migrateToV2KeyPair(this)
}
} }
} }

View File

@@ -305,7 +305,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
dialog.setMessage("Youre upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?") dialog.setMessage("Youre upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?")
dialog.setPositiveButton(R.string.yes) { _, _ -> dialog.setPositiveButton(R.string.yes) { _, _ ->
applicationContext.clearAllData() applicationContext.clearAllData(true)
} }
dialog.setNegativeButton(R.string.cancel) { _, _ -> dialog.setNegativeButton(R.string.cancel) { _, _ ->
// Do nothing // Do nothing

View File

@@ -27,13 +27,13 @@ class ClearAllDataDialog : DialogFragment() {
private fun clearAllData() { private fun clearAllData() {
if (KeyPairUtilities.hasV2KeyPair(requireContext())) { if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
ApplicationContext.getInstance(context).clearAllData() ApplicationContext.getInstance(context).clearAllData(false)
} else { } else {
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext())
val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID."
dialog.setMessage(message) dialog.setMessage(message)
dialog.setPositiveButton("Yes") { _, _ -> dialog.setPositiveButton("Yes") { _, _ ->
ApplicationContext.getInstance(context).clearAllData() ApplicationContext.getInstance(context).clearAllData(false)
} }
dialog.setNegativeButton("Cancel") { _, _ -> dialog.setNegativeButton("Cancel") { _, _ ->
// Do nothing // Do nothing

View File

@@ -43,7 +43,7 @@ class KeyPairMigrationBottomSheet : BottomSheetDialogFragment() {
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext())
dialog.setMessage("Youre upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?") dialog.setMessage("Youre upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?")
dialog.setPositiveButton(R.string.yes) { _, _ -> dialog.setPositiveButton(R.string.yes) { _, _ ->
applicationContext.clearAllData() applicationContext.clearAllData(true)
} }
dialog.setNegativeButton(R.string.cancel) { _, _ -> dialog.setNegativeButton(R.string.cancel) { _, _ ->
// Do nothing // Do nothing

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.loki.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_key_pair_migration_bottom_sheet.*
import kotlinx.android.synthetic.main.fragment_key_pair_migration_success_bottom_sheet.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
class KeyPairMigrationSuccessBottomSheet : BottomSheetDialogFragment() {
private val sessionID by lazy {
TextSecurePreferences.getLocalNumber(requireContext())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_key_pair_migration_success_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionIDTextView.text = sessionID
copyButton.setOnClickListener { copySessionID() }
okButton.setOnClickListener { dismiss() }
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
// Expand the bottom sheet by default
dialog.setOnShowListener {
val d = dialog as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
BottomSheetBehavior.from(bottomSheet!!).setState(BottomSheetBehavior.STATE_EXPANDED);
}
return dialog
}
private fun copySessionID() {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Session ID", sessionID)
clipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -197,7 +197,7 @@ object MultiDeviceProtocol {
FileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server FileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server
} }
TextSecurePreferences.setWasUnlinked(context, true) TextSecurePreferences.setWasUnlinked(context, true)
ApplicationContext.getInstance(context).clearAllData() ApplicationContext.getInstance(context).clearAllData(false)
} }
} }
} }

View File

@@ -1,18 +1,30 @@
package org.thoughtcrime.securesms.loki.utilities package org.thoughtcrime.securesms.loki.utilities
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.utils.Key import com.goterl.lazycode.lazysodium.utils.Key
import com.goterl.lazycode.lazysodium.utils.KeyPair import com.goterl.lazycode.lazysodium.utils.KeyPair
import kotlinx.android.synthetic.main.activity_pn_mode.*
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.loki.activities.HomeActivity
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.ecc.DjbECPrivateKey import org.whispersystems.libsignal.ecc.DjbECPrivateKey
import org.whispersystems.libsignal.ecc.DjbECPublicKey import org.whispersystems.libsignal.ecc.DjbECPublicKey
import org.whispersystems.libsignal.ecc.ECKeyPair import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
import org.whispersystems.signalservice.loki.utilities.toHexString import org.whispersystems.signalservice.loki.utilities.toHexString
object KeyPairUtilities { object KeyPairUtilities {
@@ -60,4 +72,32 @@ object KeyPairUtilities {
val ed25519SecretKey = Key.fromBase64String(hexEncodedED25519SecretKey) val ed25519SecretKey = Key.fromBase64String(hexEncodedED25519SecretKey)
return KeyPair(ed25519PublicKey, ed25519SecretKey) return KeyPair(ed25519PublicKey, ed25519SecretKey)
} }
fun migrateToV2KeyPair(context: AppCompatActivity) {
val keyPairGenerationResult = generate()
val seed = keyPairGenerationResult.seed
val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
store(context, seed, ed25519KeyPair, x25519KeyPair)
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
DatabaseFactory.getIdentityDatabase(context).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
IdentityKeyUtil.getIdentityKeyPair(context).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true)
TextSecurePreferences.setLocalNumber(context, userHexEncodedPublicKey)
TextSecurePreferences.setRestorationTime(context, 0)
TextSecurePreferences.setHasViewedSeed(context, false)
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
TextSecurePreferences.setPromptedPushRegistration(context, true)
TextSecurePreferences.setIsUsingFCM(context, TextSecurePreferences.isUsingFCM(context))
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(context)
TextSecurePreferences.setHasSeenLightThemeIntroSheet(context)
val application = ApplicationContext.getInstance(context)
application.setUpStorageAPIIfNeeded()
application.setUpP2PAPIIfNeeded()
val intent = Intent(context, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.show(intent)
}
} }

View File

@@ -1350,5 +1350,13 @@ public class TextSecurePreferences {
public static void setLastKeyPairMigrationNudge(Context context, long newValue) { public static void setLastKeyPairMigrationNudge(Context context, long newValue) {
setLongPreference(context, "last_key_pair_migration_nudge", newValue); setLongPreference(context, "last_key_pair_migration_nudge", newValue);
} }
public static boolean getIsMigratingKeyPair(Context context) {
return getBooleanPreference(context, "is_migrating_key_pair", false);
}
public static void setIsMigratingKeyPair(Context context, boolean newValue) {
setBooleanPreference(context, "is_migrating_key_pair", newValue);
}
// endregion // endregion
} }