diff --git a/res/layout/activity_display_name_v2.xml b/res/layout/activity_display_name_v2.xml index 47dba739d0..af21fa9e15 100644 --- a/res/layout/activity_display_name_v2.xml +++ b/res/layout/activity_display_name_v2.xml @@ -48,6 +48,7 @@ Device unlinked This device has been successfully unlinked + + + + Copied to clipboard + + diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 0bed4c6adb..b15d58ec1f 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -37,6 +37,7 @@ import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.AlertDialog; import android.support.v7.preference.Preference; +import android.util.Log; import android.widget.Toast; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; @@ -365,7 +366,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setNeutralButton(R.string.activity_settings_seed_dialog_ok_button_title, null) .show(); } catch (Exception e) { - // Do nothing + Log.d("Loki", e.getMessage()); } break; default: diff --git a/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index cf5e3e6c9b..f18fde6ac2 100644 --- a/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -49,8 +49,8 @@ public class IdentityKeyUtil { 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 final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3"; + public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3"; public static final String lokiSeedKey = "loki_seed"; diff --git a/src/org/thoughtcrime/securesms/loki/redesign/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/DisplayNameActivity.kt index 01724b652b..b053472282 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/DisplayNameActivity.kt @@ -1,10 +1,18 @@ package org.thoughtcrime.securesms.loki.redesign +import android.content.Intent import android.os.Bundle +import android.view.inputmethod.InputMethodManager +import android.widget.Toast import kotlinx.android.synthetic.main.activity_display_name_v2.* import network.loki.messenger.R +import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.ConversationListActivity +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.crypto.ProfileCipher class DisplayNameActivity : BaseActionBarActivity() { @@ -12,10 +20,40 @@ class DisplayNameActivity : BaseActionBarActivity() { super.onCreate(savedInstanceState) setUpActionBarSessionLogo() setContentView(R.layout.activity_display_name_v2) + registerButton.setOnClickListener { register() } } override fun onResume() { super.onResume() displayNameEditText.requestFocus() } + + private fun register() { + val displayName = displayNameEditText.text.toString().trim() + if (displayName.isEmpty()) { + return Toast.makeText(this, "Please pick a display name", Toast.LENGTH_SHORT).show() + } + if (!displayName.matches(Regex("[a-zA-Z0-9_]+"))) { + return Toast.makeText(this, "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", Toast.LENGTH_SHORT).show() + } + if (displayName.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) { + return Toast.makeText(this, "Please pick a shorter display name", Toast.LENGTH_SHORT).show() + } + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0) + TextSecurePreferences.setProfileName(this, displayName) + TextSecurePreferences.setHasSeenWelcomeScreen(this, true) + TextSecurePreferences.setPromptedPushRegistration(this, true) + val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI + if (publicChatAPI != null) { + // TODO: This won't be necessary anymore when we don't auto-join the Loki Public Chat anymore + val application = ApplicationContext.getInstance(this) + application.setUpStorageAPIIfNeeded() + application.createDefaultPublicChatsIfNeeded() + val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() + servers.forEach { publicChatAPI.setDisplayName(displayName, it) } + } + startActivity(Intent(this, ConversationListActivity::class.java)) + finish() + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/redesign/RegisterActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/RegisterActivity.kt index c1a5242205..acec052b43 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/RegisterActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/RegisterActivity.kt @@ -1,32 +1,125 @@ package org.thoughtcrime.securesms.loki.redesign +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.content.Intent import android.graphics.Typeface +import android.net.Uri import android.os.Bundle import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.StyleSpan +import android.widget.Toast import kotlinx.android.synthetic.main.activity_register.* import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity +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.redesign.utilities.setUpActionBarSessionLogo - +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.Hex +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.curve25519.Curve25519 +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.ecc.ECKeyPair +import org.whispersystems.libsignal.util.KeyHelper +import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey +import java.io.File +import java.io.FileOutputStream class RegisterActivity : BaseActionBarActivity() { + private var seed: ByteArray? = null + private var keyPair: ECKeyPair? = null + set(value) { field = value; updatePublicKeyTextView() } + // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_register) + setUpLanguageFileDirectory() setUpActionBarSessionLogo() registerButton.setOnClickListener { register() } + copyButton.setOnClickListener { copyPublicKey() } val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms and Conditions and Privacy Statement") termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 60, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 65, 82, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - legalButton.text = termsExplanation + termsButton.text = termsExplanation + termsButton.setOnClickListener { showTerms() } + updateKeyPair() + } + // endregion + + // region General + private fun setUpLanguageFileDirectory() { + val languages = listOf( "english", "japanese", "portuguese", "spanish" ) + val directory = File(applicationInfo.dataDir) + for (language in languages) { + val fileName = "$language.txt" + if (directory.list().contains(fileName)) { continue } + val inputStream = assets.open("mnemonic/$fileName") + val file = File(directory, fileName) + val outputStream = FileOutputStream(file) + val buffer = ByteArray(1024) + while (true) { + val count = inputStream.read(buffer) + if (count < 0) { break } + outputStream.write(buffer, 0, count) + } + inputStream.close() + outputStream.close() + } + } + // endregion + + // region Updating + private fun updateKeyPair() { + val seedCandidate = Curve25519.getInstance(Curve25519.BEST).generateSeed(16) + try { + this.keyPair = Curve.generateKeyPair(seedCandidate + seedCandidate) // Validate the seed + } catch (exception: Exception) { + return updateKeyPair() + } + seed = seedCandidate } + private fun updatePublicKeyTextView() { + publicKeyTextView.text = keyPair!!.hexEncodedPublicKey + } + // endregion + + // region Interaction private fun register() { + IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, Hex.toStringCondensed(seed)) + IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(keyPair!!.publicKey.serialize())) + IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(keyPair!!.privateKey.serialize())) + val userHexEncodedPublicKey = keyPair!!.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + TextSecurePreferences.setLocalRegistrationId(this, registrationID) + DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey), + IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED, + true, System.currentTimeMillis(), true) + TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) val intent = Intent(this, DisplayNameActivity::class.java) startActivity(intent) } + + private fun copyPublicKey() { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Session ID", keyPair!!.hexEncodedPublicKey) + clipboard.primaryClip = clip + Toast.makeText(this, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show() + } + + private fun showTerms() { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/loki-project/loki-messenger-android/blob/master/privacy-policy.md")) + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Couldn't open link", Toast.LENGTH_SHORT).show() + } + } + // endregion } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/redesign/RestoreActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/RestoreActivity.kt index 78e551996d..82b3fb3e92 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/RestoreActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/RestoreActivity.kt @@ -1,29 +1,111 @@ package org.thoughtcrime.securesms.loki.redesign +import android.content.Intent import android.graphics.Typeface +import android.net.Uri import android.os.Bundle import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.StyleSpan +import android.widget.Toast import kotlinx.android.synthetic.main.activity_restore.* import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity +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.redesign.utilities.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.Hex +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.util.KeyHelper +import org.whispersystems.signalservice.loki.crypto.MnemonicCodec +import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey +import java.io.File +import java.io.FileOutputStream class RestoreActivity : BaseActionBarActivity() { + private lateinit var languageFileDirectory: File + // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setUpLanguageFileDirectory() setUpActionBarSessionLogo() setContentView(R.layout.activity_restore) + mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard + restoreButton.setOnClickListener { restore() } val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms and Conditions and Privacy Statement") termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 60, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 65, 82, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - legalButton.text = termsExplanation + termsButton.text = termsExplanation + termsButton.setOnClickListener { showTerms() } } override fun onResume() { super.onResume() mnemonicEditText.requestFocus() } + // endregion + + // region General + private fun setUpLanguageFileDirectory() { + val languages = listOf( "english", "japanese", "portuguese", "spanish" ) + val directory = File(applicationInfo.dataDir) + for (language in languages) { + val fileName = "$language.txt" + if (directory.list().contains(fileName)) { continue } + val inputStream = assets.open("mnemonic/$fileName") + val file = File(directory, fileName) + val outputStream = FileOutputStream(file) + val buffer = ByteArray(1024) + while (true) { + val count = inputStream.read(buffer) + if (count < 0) { break } + outputStream.write(buffer, 0, count) + } + inputStream.close() + outputStream.close() + } + languageFileDirectory = directory + } + // endregion + + // region Interaction + private fun restore() { + val mnemonic = mnemonicEditText.text.toString() + try { + val hexEncodedSeed = MnemonicCodec(languageFileDirectory).decode(mnemonic) + var seed = Hex.fromStringCondensed(hexEncodedSeed) + IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, Hex.toStringCondensed(seed)) + if (seed.size == 16) { seed = seed + seed } + val keyPair = Curve.generateKeyPair(seed) + IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(keyPair.publicKey.serialize())) + IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(keyPair.privateKey.serialize())) + val userHexEncodedPublicKey = keyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + TextSecurePreferences.setLocalRegistrationId(this, registrationID) + DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey), + IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED, + true, System.currentTimeMillis(), true) + TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) + val intent = Intent(this, DisplayNameActivity::class.java) + startActivity(intent) + } catch (e: Exception) { + val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description + return Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + } + + private fun showTerms() { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/loki-project/loki-messenger-android/blob/master/privacy-policy.md")) + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Couldn't open link", Toast.LENGTH_SHORT).show() + } + } + // endregion } \ No newline at end of file