srctree

Robin Linden parent 9d229446 835c487b
Add support for encrypted Tox saves

inlinesplit
atox/src/main/kotlin/settings/Settings.kt added: 144, removed: 25, total 119
@@ -24,6 +24,10 @@ enum class BootstrapNodeSource {
class Settings @Inject constructor(private val ctx: Context) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
 
companion object {
var password: String? = null
}
 
var theme: Int
get() = preferences.getInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
set(theme) {
 
atox/src/main/kotlin/tox/ToxStarter.kt added: 144, removed: 25, total 119
@@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Build
import android.util.Log
import im.tox.tox4j.core.exceptions.ToxNewException
import im.tox.tox4j.crypto.exceptions.ToxDecryptionException
import javax.inject.Inject
import ltd.evilcorp.atox.ToxService
import ltd.evilcorp.atox.settings.Settings
@@ -39,10 +40,13 @@ class ToxStarter @Inject constructor(
SaveOptions(save, settings.udpEnabled, settings.proxyType, settings.proxyAddress, settings.proxyPort)
try {
tox.isBootstrapNeeded = true
tox.start(options, eventListener, avEventListener)
tox.start(options, Settings.password, eventListener, avEventListener)
} catch (e: ToxNewException) {
Log.e(TAG, e.message)
return testToxSave(options)
return testToxSave(options, Settings.password)
} catch (e: ToxDecryptionException) {
Log.e(TAG, e.message)
return ToxSaveStatus.Encrypted
}
 
// This can stay alive across core restarts and it doesn't work well when toxcore resets its numbers
 
atox/src/main/kotlin/ui/contactlist/ContactListFragment.kt added: 144, removed: 25, total 119
@@ -1,13 +1,16 @@
package ltd.evilcorp.atox.ui.contactlist
 
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.view.ContextMenu
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.EditText
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
@@ -27,6 +30,7 @@ import ltd.evilcorp.atox.databinding.ContactListViewItemBinding
import ltd.evilcorp.atox.databinding.FragmentContactListBinding
import ltd.evilcorp.atox.databinding.FriendRequestItemBinding
import ltd.evilcorp.atox.databinding.NavHeaderContactListBinding
import ltd.evilcorp.atox.settings.Settings
import ltd.evilcorp.atox.truncated
import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.ui.ReceiveShareDialog
@@ -282,6 +286,7 @@ class ContactListFragment :
.setTitle(R.string.quit_confirm)
.setPositiveButton(R.string.quit) { _, _ ->
viewModel.quitTox()
Settings.password = null
activity?.finishAffinity()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
@@ -308,6 +313,37 @@ class ContactListFragment :
}
ToxSaveStatus.SaveNotFound ->
findNavController().navigate(R.id.action_contactListFragment_to_profileFragment)
ToxSaveStatus.Encrypted -> {
view?.visibility = View.INVISIBLE
val passwordEdit = EditText(requireContext()).apply {
hint = getString(R.string.password)
inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
setSingleLine()
transformationMethod = PasswordTransformationMethod()
}
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.unlock_profile))
.setView(passwordEdit)
.setPositiveButton(android.R.string.ok) { _, _ ->
Settings.password = passwordEdit.text.toString()
if (viewModel.tryLoadTox() == ToxSaveStatus.Ok) {
// Hack to reload fragment.
parentFragmentManager.beginTransaction().detach(this).commitAllowingStateLoss()
parentFragmentManager.beginTransaction().attach(this).commitAllowingStateLoss()
} else {
Settings.password = null
Toast.makeText(
requireContext(),
getString(R.string.incorrect_password),
Toast.LENGTH_LONG,
).show()
activity?.finishAffinity()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> activity?.finishAffinity() }
.setOnDismissListener { activity?.finishAffinity() }
.show()
}
ToxSaveStatus.Ok -> {
}
else -> throw Exception("Unhandled tox save error $status")
 
atox/src/main/kotlin/ui/contactlist/ContactListViewModel.kt added: 144, removed: 25, total 119
@@ -16,6 +16,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.settings.Settings
import ltd.evilcorp.atox.tox.ToxStarter
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.FriendRequest
@@ -76,7 +77,7 @@ class ContactListViewModel @Inject constructor(
FileInputStream(fd.fileDescriptor).use { ios ->
val saveData = ios.readBytes()
val save = SaveOptions(saveData, true, ProxyType.None, "", 0)
val toast = when (val status = testToxSave(save)) {
val toast = when (val status = testToxSave(save, Settings.password)) {
ToxSaveStatus.Ok -> context.getText(R.string.tox_save_exported)
else -> context.getString(R.string.tox_save_export_failure, status.name)
}
 
atox/src/main/kotlin/ui/create_profile/CreateProfileFragment.kt added: 144, removed: 25, total 119
@@ -1,11 +1,15 @@
package ltd.evilcorp.atox.ui.create_profile
 
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@@ -13,6 +17,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentProfileBinding
import ltd.evilcorp.atox.settings.Settings
import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.core.vo.User
@@ -26,12 +31,39 @@ class CreateProfileFragment : BaseFragment<FragmentProfileBinding>(FragmentProfi
 
Log.i("ProfileFragment", "Importing file $uri")
viewModel.tryImportToxSave(uri)?.also { save ->
val startStatus = viewModel.startTox(save)
if (startStatus == ToxSaveStatus.Ok) {
viewModel.verifyUserExists(viewModel.publicKey)
findNavController().popBackStack()
} else {
Toast.makeText(
when (val startStatus = viewModel.startTox(save)) {
ToxSaveStatus.Ok -> {
viewModel.verifyUserExists(viewModel.publicKey)
findNavController().popBackStack()
}
ToxSaveStatus.Encrypted -> {
val passwordEdit = EditText(requireContext()).apply {
hint = getString(R.string.password)
inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
setSingleLine()
transformationMethod = PasswordTransformationMethod()
}
AlertDialog.Builder(requireContext())
.setTitle(R.string.unlock_profile)
.setView(passwordEdit)
.setPositiveButton(android.R.string.ok) { _, _ ->
Settings.password = passwordEdit.text.toString()
if (viewModel.startTox(save) == ToxSaveStatus.Ok) {
viewModel.verifyUserExists(viewModel.publicKey)
findNavController().popBackStack()
} else {
Settings.password = null
Toast.makeText(
requireContext(),
getString(R.string.incorrect_password),
Toast.LENGTH_LONG
).show()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.show()
}
else -> Toast.makeText(
requireContext(),
resources.getString(R.string.import_tox_save_failed, startStatus.name),
Toast.LENGTH_LONG
 
atox/src/main/kotlin/ui/settings/SettingsViewModel.kt added: 144, removed: 25, total 119
@@ -120,7 +120,7 @@ class SettingsViewModel @Inject constructor(
checkProxyJob?.cancel(null)
checkProxyJob = viewModelScope.launch(Dispatchers.IO) {
val saveStatus = testToxSave(
SaveOptions(saveData = null, getUdpEnabled(), getProxyType(), getProxyAddress(), getProxyPort())
SaveOptions(saveData = null, getUdpEnabled(), getProxyType(), getProxyAddress(), getProxyPort()), null
)
 
val proxyStatus = when (saveStatus) {
 
atox/src/main/res/values/strings.xml added: 144, removed: 25, total 119
@@ -164,4 +164,7 @@
<string name="incoming_call">Incoming call</string>
<string name="incoming_call_from">Incoming call from %1$s</string>
<string name="call_mic_permission_needed">Microphone permission required to enable sending audio</string>
<string name="password">Password</string>
<string name="incorrect_password">Incorrect password</string>
<string name="unlock_profile">Unlock profile</string>
</resources>
No newline at end of file
 
domain/src/main/kotlin/tox/Tox.kt added: 144, removed: 25, total 119
@@ -2,6 +2,7 @@ package ltd.evilcorp.domain.tox
 
import android.util.Log
import im.tox.tox4j.core.exceptions.ToxBootstrapException
import im.tox.tox4j.impl.jni.ToxCryptoImpl
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
@@ -39,10 +40,24 @@ class Tox @Inject constructor(
private var running = false
private var toxAvRunning = false
 
private var passkey: ByteArray? = null
 
private lateinit var tox: ToxWrapper
 
fun start(saveOption: SaveOptions, listener: ToxEventListener, avListener: ToxAvEventListener) {
tox = ToxWrapper(listener, avListener, saveOption)
fun start(saveOption: SaveOptions, password: String?, listener: ToxEventListener, avListener: ToxAvEventListener) {
tox = if (password == null) {
passkey = null
ToxWrapper(listener, avListener, saveOption)
} else {
val salt = ToxCryptoImpl.getSalt(saveOption.saveData)
passkey = ToxCryptoImpl.passKeyDeriveWithSalt(password.toByteArray(), salt)
ToxWrapper(
listener,
avListener,
saveOption.copy(saveData = ToxCryptoImpl.decrypt(saveOption.saveData, passkey)),
)
}
 
started = true
 
fun loadContacts() = launch {
@@ -93,12 +108,21 @@ class Tox @Inject constructor(
while (started) delay(10)
save().join()
tox.stop()
passkey = null
}
 
private val saveMutex = Mutex()
private fun save() = launch {
saveMutex.withLock {
saveManager.save(publicKey, tox.getSaveData())
val passkey = passkey
saveManager.save(
publicKey,
if (passkey == null) {
tox.getSaveData()
} else {
ToxCryptoImpl.encrypt(tox.getSaveData(), passkey)
}
)
}
}
 
@@ -148,7 +172,14 @@ class Tox @Inject constructor(
fun sendMessage(publicKey: PublicKey, message: String, type: MessageType) =
tox.sendMessage(publicKey, message, type)
 
fun getSaveData() = tox.getSaveData()
fun getSaveData(): ByteArray {
val passkey = passkey
return if (passkey == null) {
tox.getSaveData()
} else {
ToxCryptoImpl.encrypt(tox.getSaveData(), passkey)
}
}
 
private fun bootstrap() {
nodeRegistry.get(4).forEach { node ->
 
domain/src/main/kotlin/tox/ToxSaveTester.kt added: 144, removed: 25, total 119
@@ -2,6 +2,7 @@ package ltd.evilcorp.domain.tox
 
import im.tox.tox4j.core.exceptions.ToxNewException
import im.tox.tox4j.impl.jni.ToxCoreImpl
import im.tox.tox4j.impl.jni.ToxCryptoImpl
 
enum class ToxSaveStatus {
Ok,
@@ -17,8 +18,15 @@ enum class ToxSaveStatus {
SaveNotFound,
}
 
fun testToxSave(options: SaveOptions): ToxSaveStatus = try {
val t = ToxCoreImpl(options.toToxOptions())
fun testToxSave(options: SaveOptions, password: String?): ToxSaveStatus = try {
val toxOptions = if (password == null) {
options.toToxOptions()
} else {
val salt = ToxCryptoImpl.getSalt(options.saveData)
val passkey = ToxCryptoImpl.passKeyDeriveWithSalt(password.toByteArray(), salt)
options.copy(saveData = ToxCryptoImpl.decrypt(options.saveData, passkey)).toToxOptions()
}
val t = ToxCoreImpl(toxOptions)
t.close()
ToxSaveStatus.Ok
} catch (e: ToxNewException) {