Switch to URL-based Git config and refactor Git settings (#1008)

* Make Git config URL-based and refactor

* Use Kotlin style null handling for string prefs

* Also show an error if generated URL can't be parsed

* Add some testcases for migration strategy

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Fabian Henneke 2020-08-11 18:11:39 +02:00 committed by GitHub
parent 8f957ca994
commit 15aa929802
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 395 additions and 585 deletions

View file

@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
### Changed
- The Git repository URL can now be specified directly
- Slightly reduce APK size
- Always show the parent path in entries
- Passwords will no longer be copied to the clipboard by default

View file

@ -0,0 +1,81 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
@file:Suppress("DEPRECATION")
package com.zeapo.pwdstore
import android.content.Context
import androidx.core.content.edit
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import org.junit.Test
import org.junit.Assert.*
class MigrationsTest {
private fun checkOldKeysAreRemoved(context: Context) = with(context.sharedPrefs) {
assertNull(getString(PreferenceKeys.GIT_REMOTE_PORT))
assertNull(getString(PreferenceKeys.GIT_REMOTE_USERNAME))
assertNull(getString(PreferenceKeys.GIT_REMOTE_SERVER))
assertNull(getString(PreferenceKeys.GIT_REMOTE_LOCATION))
}
@Test
fun verifySshWithCustomPortMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_PORT, "2200")
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"ssh://msfjarvis@192.168.0.102:2200/mnt/disk3/pass-repo"
)
}
@Test
fun verifySshWithDefaultPortMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"msfjarvis@192.168.0.102:/mnt/disk3/pass-repo"
)
}
@Test
fun verifyHttpsWithGitHubMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "Android-Password-Store/pass-test")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "github.com")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "https://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"https://github.com/Android-Password-Store/pass-test"
)
}
}

View file

@ -1,126 +0,0 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git
import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.android.material.button.MaterialButtonToggleGroup
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.BaseGitActivity.GitUpdateUrlResult
import kotlin.test.assertEquals
import org.hamcrest.Matcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GitServerConfigActivityTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(GitServerConfigActivity::class.java, true)
private val activity get() = activityRule.activity
@Test
fun invalidValuesFailPredictably() {
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.CustomPortRequiresAbsoluteUrlError)
setDefaultSshValues()
onView(withId(R.id.server_url)).perform(replaceText(""))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.EmptyHostnameError)
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("xyz_is_not_a_port"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.NonNumericPortError)
setDefaultHttpsValues()
onView(withId(R.id.server_port)).perform(replaceText("xyz_is_not_a_port"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.NonNumericPortError)
setDefaultHttpsValues()
onView(withId(R.id.server_url)).perform(replaceText(""))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.EmptyHostnameError)
}
@Test
fun urlIsConstructedCorrectly() {
setDefaultSshValues()
activity.updateUrl()
assertEquals("john_doe@github.com:john_doe/my_secret_repository", activity.url)
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
onView(withId(R.id.server_url)).perform(replaceText("192.168.0.102"))
onView(withId(R.id.server_path)).perform(replaceText("/home/john_doe/my_secret_repository"))
activity.updateUrl()
assertEquals("ssh://john_doe@192.168.0.102:69420/home/john_doe/my_secret_repository", activity.url)
setDefaultHttpsValues()
activity.updateUrl()
assertEquals("https://github.com/john_doe/my_secret_repository", activity.url)
setDefaultHttpsValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
onView(withId(R.id.server_url)).perform(replaceText("192.168.0.102"))
onView(withId(R.id.server_path)).perform(replaceText("/home/john_doe/my_secret_repository"))
activity.updateUrl()
assertEquals("https://192.168.0.102:69420/home/john_doe/my_secret_repository", activity.url)
}
private fun <T> callMethod(message: String = "", viewMethod: (view: T) -> Unit): ViewAction {
return object : ViewAction {
override fun getDescription(): String {
return if (message.isBlank()) viewMethod.toString() else message
}
override fun getConstraints(): Matcher<View> {
return isEnabled()
}
@Suppress("UNCHECKED_CAST")
override fun perform(uiController: UiController?, view: View?) {
viewMethod(view!! as T)
}
}
}
private fun setDefaultHttpsValues() {
onView(withId(R.id.clone_protocol_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.clone_protocol_https)
})
onView(withId(R.id.connection_mode_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.connection_mode_password)
})
onView(withId(R.id.server_path)).perform(replaceText("john_doe/my_secret_repository"))
onView(withId(R.id.server_port)).perform(replaceText(""))
onView(withId(R.id.server_url)).perform(replaceText("github.com"))
onView(withId(R.id.server_user)).perform(replaceText("john_doe"))
}
private fun setDefaultSshValues() {
onView(withId(R.id.clone_protocol_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.clone_protocol_ssh)
})
onView(withId(R.id.connection_mode_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.connection_mode_ssh_key)
})
onView(withId(R.id.server_path)).perform(replaceText("john_doe/my_secret_repository"))
onView(withId(R.id.server_port)).perform(replaceText(""))
onView(withId(R.id.server_url)).perform(replaceText("github.com"))
onView(withId(R.id.server_user)).perform(replaceText("john_doe"))
}
}

View file

@ -10,12 +10,12 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.DebugTree
import com.github.ajalt.timberkt.Timber.plant
import com.zeapo.pwdstore.git.config.setUpBouncyCastleForSshj
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.getString
@Suppress("Unused")
class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener {
@ -30,6 +30,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
sharedPrefs.registerOnSharedPreferenceChangeListener(this)
setNightMode()
setUpBouncyCastleForSshj()
runMigrations(applicationContext)
}
override fun onTerminate() {
@ -44,7 +45,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
}
private fun setNightMode() {
AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME, getString(R.string.app_theme_def))) {
AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) {
"light" -> MODE_NIGHT_NO
"dark" -> MODE_NIGHT_YES
"follow_system" -> MODE_NIGHT_FOLLOW_SYSTEM

View file

@ -15,10 +15,10 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.clipboard
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -46,7 +46,7 @@ class ClipboardService : Service() {
ACTION_START -> {
val time = try {
Integer.parseInt(settings.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45") as String)
Integer.parseInt(settings.getString(PreferenceKeys.GENERAL_SHOW_TIME) ?: "45")
} catch (e: NumberFormatException) {
45
}

View file

@ -9,7 +9,6 @@ import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.PreferenceKeys

View file

@ -0,0 +1,83 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
@file:Suppress("DEPRECATION")
package com.zeapo.pwdstore
import android.content.Context
import androidx.core.content.edit
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.net.URI
fun runMigrations(context: Context) {
migrateToGitUrlBasedConfig(context)
}
private fun migrateToGitUrlBasedConfig(context: Context) {
val serverHostname = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_SERVER)
?: return
i { "Migrating to URL-based Git config" }
val serverPort = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PORT) ?: ""
val serverUser = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_USERNAME) ?: ""
val serverPath = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_LOCATION) ?: ""
val protocol = Protocol.fromString(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
// Whether we need the leading ssh:// depends on the use of a custom port.
val hostnamePart = serverHostname.removePrefix("ssh://")
val url = when (protocol) {
Protocol.Ssh -> {
val userPart = if (serverUser.isEmpty()) "" else "${serverUser.trimEnd('@')}@"
val portPart =
if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
if (portPart.isEmpty()) {
"$userPart$hostnamePart:$serverPath"
} else {
// Only absolute paths are supported with custom ports.
if (!serverPath.startsWith('/'))
null
else
// We have to specify the ssh scheme as this is the only way to pass a custom
// port.
"ssh://$userPart$hostnamePart$portPart$serverPath"
}
}
Protocol.Https -> {
val portPart =
if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
val pathPart = serverPath.trimStart('/', ':')
val urlWithFreeEntryScheme = "$hostnamePart$portPart/$pathPart"
val url = when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme"
}
try {
if (URI(url).rawAuthority != null)
url
else
null
} catch (_: Exception) {
null
}
}
}
context.sharedPrefs.edit {
remove(PreferenceKeys.GIT_REMOTE_LOCATION)
remove(PreferenceKeys.GIT_REMOTE_PORT)
remove(PreferenceKeys.GIT_REMOTE_SERVER)
remove(PreferenceKeys.GIT_REMOTE_USERNAME)
}
if (url == null || !GitSettings.updateUrlIfValid(url)) {
e { "Failed to migrate to URL-based Git config, generated URL is invalid" }
}
}

View file

@ -26,12 +26,12 @@ import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.OnOffItemAnimator
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
import com.zeapo.pwdstore.ui.dialogs.ItemCreationBottomSheet
import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding
import java.io.File
@ -91,8 +91,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
} else {
// When authentication is set to ConnectionMode.None then the only git operation we
// can run is a pull, so automatically fallback to that.
val operationId = when (ConnectionMode.fromString(settings.getString
(PreferenceKeys.GIT_REMOTE_AUTH, null))) {
val operationId = when (GitSettings.connectionMode) {
ConnectionMode.None -> BaseGitActivity.REQUEST_PULL
else -> BaseGitActivity.REQUEST_SYNC
}

View file

@ -7,7 +7,6 @@ package com.zeapo.pwdstore
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo.Builder
import android.content.pm.ShortcutManager
@ -34,7 +33,6 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
@ -52,6 +50,7 @@ import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment
import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository
@ -66,6 +65,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.g
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.contains
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.listFilesRecursively
import com.zeapo.pwdstore.utils.requestInputFocusOnView
@ -114,8 +114,8 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private val directoryChangeAction = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) &&
settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) != null) {
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null)
settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null) {
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
val dir = externalRepoPath?.let { File(it) }
if (dir != null &&
dir.exists() &&
@ -252,8 +252,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val menuRes = when {
ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH, null))
== ConnectionMode.None -> R.menu.main_menu_no_auth
GitSettings.connectionMode == ConnectionMode.None -> R.menu.main_menu_no_auth
PasswordRepository.isGitRepo() -> R.menu.main_menu_git
else -> R.menu.main_menu_non_git
}
@ -403,7 +402,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private fun initializeRepositoryInfo() {
val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null)
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo && !hasRequiredStoragePermissions()) {
return
}
@ -849,7 +848,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
}
.setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ ->
settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) }
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null)
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo == null) {
val intent = Intent(activity, UserPreference::class.java)
intent.putExtra("operation", "git_external")

View file

@ -15,7 +15,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.preference.PreferenceManager
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.Selection

View file

@ -34,7 +34,6 @@ import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.d
@ -54,6 +53,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.autofillManager
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.io.IOException
@ -141,11 +141,11 @@ class UserPreference : AppCompatActivity() {
// Misc preferences
val appVersionPreference = findPreference<Preference>(PreferenceKeys.APP_VERSION)
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO, getString(R.string.no_repo_selected))
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) ?: getString(R.string.no_repo_selected)
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean(PreferenceKeys.USE_GENERATED_KEY, false)
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
clearClipboard20xPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45")?.toInt() != 0
openkeystoreIdPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null)?.isNotEmpty()
clearClipboard20xPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toInt() != 0
openkeystoreIdPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty()
?: false
updateAutofillSettings()
@ -171,9 +171,9 @@ class UserPreference : AppCompatActivity() {
clearSavedPassPreference?.onPreferenceClickListener = ClickListener {
encryptedPreferences.edit {
if (encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD, null) != null)
if (encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) != null)
remove(PreferenceKeys.HTTPS_PASSWORD)
else if (encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE, null) != null)
else if (encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) != null)
remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
}
updateClearSavedPassphrasePrefs()
@ -226,7 +226,7 @@ class UserPreference : AppCompatActivity() {
}
selectExternalGitRepositoryPreference?.summary =
sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO, context.getString(R.string.no_repo_selected))
sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) ?: context.getString(R.string.no_repo_selected)
selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener {
prefsActivity.selectExternalGitRepository()
true
@ -321,7 +321,7 @@ class UserPreference : AppCompatActivity() {
prefsActivity.storeCustomDictionaryPath()
true
}
val dictUri = sharedPreferences.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, "")
val dictUri = sharedPreferences.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) ?: ""
if (!TextUtils.isEmpty(dictUri)) {
setCustomDictSummary(prefCustomXkpwdDictionary, Uri.parse(dictUri))
@ -377,8 +377,8 @@ class UserPreference : AppCompatActivity() {
private fun updateClearSavedPassphrasePrefs() {
clearSavedPassPreference?.apply {
val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE, null)
val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD, null)
val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD)
if (sshPass == null && httpsPass == null) {
isVisible = false
return@apply

View file

@ -23,6 +23,8 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.security.MessageDigest
@ -37,11 +39,7 @@ private fun ByteArray.base64(): String {
return Base64.encodeToString(this, Base64.NO_WRAP)
}
private fun Context.getDefaultUsername(): String? {
return PreferenceManager
.getDefaultSharedPreferences(this)
.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME, null)
}
private fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
private fun stableHash(array: Collection<ByteArray>): String {
val hashes = array.map { it.sha256().base64() }

View file

@ -5,10 +5,8 @@
package com.zeapo.pwdstore.autofill.oreo
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.nio.file.Paths

View file

@ -6,8 +6,8 @@ package com.zeapo.pwdstore.autofill.oreo
import android.content.Context
import android.util.Patterns
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.runBlocking
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
@ -69,9 +69,10 @@ fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
}
fun getCustomSuffixes(context: Context): Sequence<String> {
return context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES, "")!!
.splitToSequence('\n')
.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
return context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
?.splitToSequence('\n')
?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
?: emptySequence()
}
suspend fun getCanonicalSuffix(context: Context, domain: String): String {

View file

@ -30,6 +30,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.clipboard
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.snackbar
import java.io.File
@ -254,8 +255,7 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
var clearAfter = 45
try {
clearAfter = (settings.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45")
?: "45").toInt()
clearAfter = (settings.getString(PreferenceKeys.GENERAL_SHOW_TIME) ?: "45").toInt()
} catch (_: NumberFormatException) {
}

View file

@ -31,6 +31,7 @@ import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.snackbar
import com.zeapo.pwdstore.utils.viewBinding
@ -220,7 +221,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
private fun generatePassword() {
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, KEY_PWGEN_TYPE_CLASSIC)) {
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
.show(supportFragmentManager, "generator")
KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment()

View file

@ -5,20 +5,15 @@
package com.zeapo.pwdstore.git
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.MenuItem
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.text.isDigitsOnly
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation
@ -28,11 +23,6 @@ import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.net.URI
import kotlinx.coroutines.launch
/**
@ -41,39 +31,8 @@ import kotlinx.coroutines.launch
*/
abstract class BaseGitActivity : AppCompatActivity() {
lateinit var protocol: Protocol
lateinit var connectionMode: ConnectionMode
var url: String? = null
lateinit var serverHostname: String
lateinit var serverPort: String
lateinit var serverUser: String
lateinit var serverPath: String
lateinit var username: String
lateinit var email: String
lateinit var branch: String
private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
private var identity: SshApiSessionFactory.ApiIdentity? = null
lateinit var settings: SharedPreferences
private set
private lateinit var encryptedSettings: SharedPreferences
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settings = sharedPrefs
encryptedSettings = getEncryptedPrefs("git_operation")
protocol = Protocol.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL, null))
connectionMode = ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH, null))
serverHostname = settings.getString(PreferenceKeys.GIT_REMOTE_SERVER, null) ?: ""
serverPort = settings.getString(PreferenceKeys.GIT_REMOTE_PORT, null) ?: ""
serverUser = settings.getString(PreferenceKeys.GIT_REMOTE_USERNAME, null) ?: ""
serverPath = settings.getString(PreferenceKeys.GIT_REMOTE_LOCATION, null) ?: ""
username = settings.getString(PreferenceKeys.GIT_CONFIG_USER_NAME, null) ?: ""
email = settings.getString(PreferenceKeys.GIT_CONFIG_USER_EMAIL, null) ?: ""
branch = settings.getString(PreferenceKeys.GIT_BRANCH_NAME, null) ?: "master"
updateUrl()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
@ -95,77 +54,6 @@ abstract class BaseGitActivity : AppCompatActivity() {
super.onDestroy()
}
enum class GitUpdateUrlResult(val textRes: Int) {
Ok(0),
CustomPortRequiresAbsoluteUrlError(R.string.git_config_error_custom_port_absolute),
EmptyHostnameError(R.string.git_config_error_hostname_empty),
GenericError(R.string.git_config_error_generic),
NonNumericPortError(R.string.git_config_error_nonnumeric_port)
}
/**
* Update the [url] field with the values that build it up. This function returns a
* [GitUpdateUrlResult] indicating whether the values could be used to build a URL and only adds
* the `origin` remote when they were. This check is not perfect, it is mostly meant to catch
* syntax-related typos.
*/
fun updateUrl(): GitUpdateUrlResult {
if (serverHostname.isEmpty())
return GitUpdateUrlResult.EmptyHostnameError
if (!serverPort.isDigitsOnly())
return GitUpdateUrlResult.NonNumericPortError
val previousUrl = url ?: ""
// Whether we need the leading ssh:// depends on the use of a custom port.
val hostnamePart = serverHostname.removePrefix("ssh://")
val newUrl = when (protocol) {
Protocol.Ssh -> {
val userPart = if (serverUser.isEmpty()) "" else "${serverUser.trimEnd('@')}@"
val portPart =
if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
if (portPart.isEmpty()) {
"$userPart$hostnamePart:$serverPath"
} else {
// Only absolute paths are supported with custom ports.
if (!serverPath.startsWith('/'))
return GitUpdateUrlResult.CustomPortRequiresAbsoluteUrlError
val pathPart = serverPath
// We have to specify the ssh scheme as this is the only way to pass a custom
// port.
"ssh://$userPart$hostnamePart$portPart$pathPart"
}
}
Protocol.Https -> {
val portPart =
if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
val pathPart = serverPath.trimStart('/', ':')
val urlWithFreeEntryScheme = "$hostnamePart$portPart/$pathPart"
val url = when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme"
}
try {
if (URI(url).rawAuthority != null)
url
else
return GitUpdateUrlResult.GenericError
} catch (_: Exception) {
return GitUpdateUrlResult.GenericError
}
}
}
if (PasswordRepository.isInitialized)
PasswordRepository.addRemote("origin", newUrl, true)
// When the server changes, remote password and host key file should be deleted.
if (previousUrl.isNotEmpty() && newUrl != previousUrl) {
encryptedSettings.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
File("$filesDir/.host_key").delete()
}
url = newUrl
return GitUpdateUrlResult.Ok
}
/**
* Attempt to launch the requested Git operation. Depending on the configured auth, it may not
* be possible to launch the operation immediately. In that case, this function may launch an
@ -176,7 +64,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
* @param operation The type of git operation to launch
*/
suspend fun launchGitOperation(operation: Int) {
if (url == null) {
if (GitSettings.url == null) {
setResult(RESULT_CANCELED)
finish()
return
@ -185,7 +73,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
// Before launching the operation with OpenKeychain auth, we need to issue several requests
// to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
// we just need to keep calling it until it returns a completed ApiIdentity.
if (connectionMode == ConnectionMode.OpenKeychain && identity == null) {
if (GitSettings.connectionMode == ConnectionMode.OpenKeychain && identity == null) {
// Lazy initialization of the IdentityBuilder
if (identityBuilder == null) {
identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
@ -199,7 +87,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, url!!, this)
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this)
REQUEST_PULL -> PullOperation(localDir, this)
REQUEST_PUSH -> PushOperation(localDir, this)
REQUEST_SYNC -> SyncOperation(localDir, this)
@ -213,7 +101,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
return
}
}
op.executeAfterAuthentication(connectionMode, serverUser, identity)
op.executeAfterAuthentication(GitSettings.connectionMode, identity)
} catch (e: Exception) {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()

View file

@ -7,15 +7,14 @@ package com.zeapo.pwdstore.git
import android.os.Bundle
import android.os.Handler
import android.util.Patterns
import androidx.core.content.edit
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Constants
@ -29,16 +28,16 @@ class GitConfigActivity : BaseGitActivity() {
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (username.isEmpty())
if (GitSettings.authorName.isEmpty())
binding.gitUserName.requestFocus()
else
binding.gitUserName.setText(username)
binding.gitUserEmail.setText(email)
binding.gitUserName.setText(GitSettings.authorName)
binding.gitUserEmail.setText(GitSettings.authorEmail)
val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory())
if (repo != null) {
try {
val objectId = repo.resolve(Constants.HEAD)
val ref = repo.getRef("refs/heads/$branch")
val ref = repo.getRef("refs/heads/${GitSettings.branch}")
val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED"
binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head)
@ -60,12 +59,10 @@ class GitConfigActivity : BaseGitActivity() {
.setPositiveButton(getString(R.string.dialog_ok), null)
.show()
} else {
settings.edit {
putString(PreferenceKeys.GIT_CONFIG_USER_EMAIL, email)
putString(PreferenceKeys.GIT_CONFIG_USER_NAME, name)
}
PasswordRepository.setUserName(name)
PasswordRepository.setUserEmail(email)
GitSettings.authorEmail = email
GitSettings.authorName = name
PasswordRepository.setGitAuthorEmail(email)
PasswordRepository.setGitAuthorName(name)
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
Handler().postDelayed(500) { finish() }
}

View file

@ -12,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.utils.PasswordRepository
import kotlinx.coroutines.launch
@ -57,7 +58,7 @@ open class GitOperationActivity : BaseGitActivity() {
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/
private suspend fun syncRepository(operation: Int) {
if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isNullOrEmpty())
if (GitSettings.url.isNullOrEmpty())
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.set_information_dialog_text))
.setPositiveButton(getString(R.string.dialog_positive)) { _, _ ->
@ -72,7 +73,7 @@ open class GitOperationActivity : BaseGitActivity() {
.show()
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", url!!, true)
PasswordRepository.addRemote("origin", GitSettings.url!!, true)
launchGitOperation(operation)
}
}

View file

@ -7,18 +7,16 @@ package com.zeapo.pwdstore.git
import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.core.content.edit
import androidx.core.os.postDelayed
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding
import java.io.IOException
import kotlinx.coroutines.launch
@ -40,22 +38,22 @@ class GitServerConfigActivity : BaseGitActivity() {
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.cloneProtocolGroup.check(when (protocol) {
binding.cloneProtocolGroup.check(when (GitSettings.protocol) {
Protocol.Ssh -> R.id.clone_protocol_ssh
Protocol.Https -> R.id.clone_protocol_https
})
binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked ->
if (checked) {
when (checkedId) {
R.id.clone_protocol_https -> protocol = Protocol.Https
R.id.clone_protocol_ssh -> protocol = Protocol.Ssh
R.id.clone_protocol_https -> GitSettings.protocol = Protocol.Https
R.id.clone_protocol_ssh -> GitSettings.protocol = Protocol.Ssh
}
updateConnectionModeToggleGroup()
}
}
binding.connectionModeGroup.apply {
when (connectionMode) {
when (GitSettings.connectionMode) {
ConnectionMode.SshKey -> check(R.id.connection_mode_ssh_key)
ConnectionMode.Password -> check(R.id.connection_mode_password)
ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain)
@ -63,82 +61,37 @@ class GitServerConfigActivity : BaseGitActivity() {
}
addOnButtonCheckedListener { _, _, _ ->
when (checkedButtonId) {
R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey
R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain
R.id.connection_mode_password -> connectionMode = ConnectionMode.Password
View.NO_ID -> connectionMode = ConnectionMode.None
R.id.connection_mode_ssh_key -> GitSettings.connectionMode = ConnectionMode.SshKey
R.id.connection_mode_open_keychain -> GitSettings.connectionMode = ConnectionMode.OpenKeychain
R.id.connection_mode_password -> GitSettings.connectionMode = ConnectionMode.Password
View.NO_ID -> GitSettings.connectionMode = ConnectionMode.None
}
}
}
updateConnectionModeToggleGroup()
binding.serverUrl.apply {
setText(serverHostname)
doOnTextChanged { text, _, _, _ ->
serverHostname = text.toString().trim()
}
}
binding.serverPort.apply {
setText(serverPort)
doOnTextChanged { text, _, _, _ ->
serverPort = text.toString().trim()
}
}
binding.serverUser.apply {
if (serverUser.isEmpty()) {
requestFocus()
} else {
setText(serverUser)
}
doOnTextChanged { text, _, _, _ ->
serverUser = text.toString().trim()
}
}
binding.serverPath.apply {
setText(serverPath)
doOnTextChanged { text, _, _, _ ->
serverPath = text.toString().trim()
}
}
binding.serverBranch.apply {
setText(branch)
doOnTextChanged { text, _, _, _ ->
branch = text.toString().trim()
}
}
binding.serverUrl.setText(GitSettings.url)
binding.serverBranch.setText(GitSettings.branch)
binding.saveButton.setOnClickListener {
if (isClone && PasswordRepository.getRepository(null) == null)
PasswordRepository.initialize()
when (val result = updateUrl()) {
GitUpdateUrlResult.Ok -> {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, protocol.pref)
putString(PreferenceKeys.GIT_REMOTE_AUTH, connectionMode.pref)
putString(PreferenceKeys.GIT_REMOTE_SERVER, serverHostname)
putString(PreferenceKeys.GIT_REMOTE_PORT, serverPort)
putString(PreferenceKeys.GIT_REMOTE_USERNAME, serverUser)
putString(PreferenceKeys.GIT_REMOTE_LOCATION, serverPath)
putString(PreferenceKeys.GIT_BRANCH_NAME, branch)
}
GitSettings.branch = binding.serverBranch.text.toString().trim()
if (GitSettings.updateUrlIfValid(binding.serverUrl.text.toString().trim())) {
if (!isClone) {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
Handler().postDelayed(500) { finish() }
} else {
cloneRepository()
}
}
else -> Snackbar.make(binding.root, getString(R.string.git_server_config_save_error_prefix, getString(result.textRes)), Snackbar.LENGTH_LONG).show()
} else {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show()
}
}
}
private fun updateConnectionModeToggleGroup() {
if (protocol == Protocol.Ssh) {
if (GitSettings.protocol == Protocol.Ssh) {
// Reset connection mode to SSH key if the current value (none) is not valid for SSH
if (binding.connectionModeGroup.checkedButtonIds.isEmpty())
binding.connectionModeGroup.check(R.id.connection_mode_ssh_key)
@ -150,7 +103,7 @@ class GitServerConfigActivity : BaseGitActivity() {
// Reset connection mode to password if the current value is not valid for HTTPS
// Important note: This has to happen before disabling the other toggle buttons or they
// won't uncheck.
if (connectionMode !in listOf(ConnectionMode.None, ConnectionMode.Password))
if (GitSettings.connectionMode !in listOf(ConnectionMode.None, ConnectionMode.Password))
binding.connectionModeGroup.check(R.id.connection_mode_password)
binding.connectionModeSshKey.isEnabled = false
binding.connectionModeOpenKeychain.isEnabled = false

View file

@ -1,22 +0,0 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
enum class ConnectionMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
None("None"),
;
companion object {
private val map = values().associateBy(ConnectionMode::pref)
fun fromString(type: String?): ConnectionMode {
return map[type ?: return SshKey]
?: throw IllegalArgumentException("$type is not a valid ConnectionMode")
}
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
import androidx.core.content.edit
import com.zeapo.pwdstore.Application
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import org.eclipse.jgit.transport.URIish
enum class Protocol(val pref: String) {
Ssh("ssh://"),
Https("https://"),
;
companion object {
private val map = values().associateBy(Protocol::pref)
fun fromString(type: String?): Protocol {
return map[type ?: return Ssh]
?: throw IllegalArgumentException("$type is not a valid Protocol")
}
}
}
enum class ConnectionMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
None("None"),
;
companion object {
private val map = values().associateBy(ConnectionMode::pref)
fun fromString(type: String?): ConnectionMode {
return map[type ?: return SshKey]
?: throw IllegalArgumentException("$type is not a valid ConnectionMode")
}
}
}
object GitSettings {
private const val DEFAULT_BRANCH = "master"
private val settings by lazy { Application.instance.sharedPrefs }
private val encryptedSettings by lazy { Application.instance.getEncryptedPrefs("git_operation") }
var protocol
get() = Protocol.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, value.pref)
}
}
var connectionMode
get() = ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_AUTH, value.pref)
}
}
var url
get() = settings.getString(PreferenceKeys.GIT_REMOTE_URL)
private set(value) {
require(value != null)
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_URL, value)
}
if (PasswordRepository.isInitialized)
PasswordRepository.addRemote("origin", value, true)
// When the server changes, remote password and host key file should be deleted.
encryptedSettings.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
File("${Application.instance.filesDir}/.host_key").delete()
}
var authorName
get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME) ?: ""
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME, value)
}
}
var authorEmail
get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL) ?: ""
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL, value)
}
}
var branch
get() = settings.getString(PreferenceKeys.GIT_BRANCH_NAME) ?: DEFAULT_BRANCH
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_BRANCH_NAME, value)
}
}
fun updateUrlIfValid(newUrl: String): Boolean {
try {
URIish(newUrl)
} catch (_: Exception) {
return false
}
if (newUrl != url)
url = newUrl
return true
}
}

View file

@ -1,20 +0,0 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
enum class Protocol(val pref: String) {
Ssh("ssh://"),
Https("https://"),
;
companion object {
private val map = values().associateBy(Protocol::pref)
fun fromString(type: String?): Protocol {
return map[type ?: return Ssh]
?: throw IllegalArgumentException("$type is not a valid Protocol")
}
}
}

View file

@ -18,18 +18,12 @@ import com.jcraft.jsch.Identity;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.git.BaseGitActivity;
import com.zeapo.pwdstore.utils.PreferenceKeys;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.CredentialsProviderUserInfo;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.FS;
import org.openintents.ssh.authentication.ISshAuthenticationService;
@ -52,11 +46,9 @@ public class SshApiSessionFactory extends JschConfigSessionFactory {
*/
public static final int POST_SIGNATURE = 301;
private final String username;
private final Identity identity;
public SshApiSessionFactory(String username, Identity identity) {
this.username = username;
public SshApiSessionFactory(Identity identity) {
this.identity = identity;
}
@ -74,32 +66,6 @@ public class SshApiSessionFactory extends JschConfigSessionFactory {
protected void configure(@NonNull OpenSshConfig.Host hc, Session session) {
session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("PreferredAuthentications", "publickey");
CredentialsProvider provider =
new CredentialsProvider() {
@Override
public boolean isInteractive() {
return false;
}
@Override
public boolean supports(CredentialItem... items) {
return true;
}
@Override
public boolean get(URIish uri, CredentialItem... items)
throws UnsupportedCredentialItem {
for (CredentialItem item : items) {
if (item instanceof CredentialItem.Username) {
((CredentialItem.Username) item).setValue(username);
}
}
return true;
}
};
UserInfo userInfo = new CredentialsProviderUserInfo(session, provider);
session.setUserInfo(userInfo);
}
/**

View file

@ -95,10 +95,10 @@ abstract class InteractivePasswordFinder : PasswordFinder {
final override fun shouldRetry(resource: Resource<*>?) = true
}
class SshjSessionFactory(private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
return SshjSession(uri, username, authData, hostKeyFile).connect()
return SshjSession(uri, uri.user, authData, hostKeyFile).connect()
}
fun clearCredentials() {

View file

@ -15,6 +15,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshAuthData
@ -47,18 +48,15 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
protected val repository = PasswordRepository.getRepository(gitDir)
protected val git = Git(repository)
protected val remoteBranch = PreferenceManager
.getDefaultSharedPreferences(callingActivity.applicationContext)
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
protected val remoteBranch = GitSettings.branch
private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() {
private class PasswordFinderCredentialsProvider(private val passwordFinder: PasswordFinder) : CredentialsProvider() {
override fun isInteractive() = true
override fun get(uri: URIish?, vararg items: CredentialItem): Boolean {
for (item in items) {
when (item) {
is CredentialItem.Username -> item.value = username
is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null)
else -> UnsupportedCredentialItem(uri, item.javaClass.name)
}
@ -67,26 +65,26 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}
override fun supports(vararg items: CredentialItem) = items.all {
it is CredentialItem.Username || it is CredentialItem.Password
it is CredentialItem.Password
}
}
private fun withPasswordAuthentication(username: String, passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.Password(passwordFinder), hostKeyFile)
private fun withPasswordAuthentication(passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(SshAuthData.Password(passwordFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = PasswordFinderCredentialsProvider(username, passwordFinder)
this.provider = PasswordFinderCredentialsProvider(passwordFinder)
return this
}
private fun withPublicKeyAuthentication(username: String, passphraseFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile)
private fun withPublicKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = null
return this
}
private fun withOpenKeychainAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
SshSessionFactory.setInstance(SshApiSessionFactory(username, identity))
private fun withOpenKeychainAuthentication(identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
SshSessionFactory.setInstance(SshApiSessionFactory(identity))
this.provider = null
return this
}
@ -117,7 +115,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
suspend fun executeAfterAuthentication(
connectionMode: ConnectionMode,
username: String,
identity: SshApiSessionFactory.ApiIdentity?
) {
when (connectionMode) {
@ -136,12 +133,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
callingActivity.finish()
}.show()
} else {
withPublicKeyAuthentication(username, CredentialFinder(callingActivity,
connectionMode)).execute()
withPublicKeyAuthentication(
CredentialFinder(callingActivity, connectionMode)).execute()
}
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(identity).execute()
ConnectionMode.Password -> withPasswordAuthentication(
username, CredentialFinder(callingActivity, connectionMode)).execute()
CredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute()
}
}

View file

@ -5,9 +5,9 @@
package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
@ -17,7 +17,7 @@ class XkpwdDictionary(context: Context) {
init {
val prefs = context.sharedPrefs
val uri = prefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, "")!!
val uri = prefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) ?: ""
val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
val lines = if (prefs.getBoolean(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT, false) &&

View file

@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jcraft.jsch.JSch
import com.jcraft.jsch.KeyPair

View file

@ -10,7 +10,6 @@ import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.preference.PreferenceManager
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.Selection
import androidx.recyclerview.widget.RecyclerView

View file

@ -101,6 +101,8 @@ fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val Context.sharedPrefs: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(applicationContext)
fun SharedPreferences.getString(key: String): String? = getString(key, null)
suspend fun FragmentActivity.commitChange(
message: String,
finishWithResultOnEnd: Intent? = null,

View file

@ -39,8 +39,7 @@ open class PasswordRepository protected constructor() {
@JvmStatic
fun getSortOrder(settings: SharedPreferences): PasswordSortOrder {
return valueOf(settings.getString(PreferenceKeys.SORT_ORDER, null)
?: FOLDER_FIRST.name)
return valueOf(settings.getString(PreferenceKeys.SORT_ORDER) ?: FOLDER_FIRST.name)
}
}
}
@ -155,7 +154,7 @@ open class PasswordRepository protected constructor() {
@JvmStatic
fun getRepositoryDirectory(): File {
return if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null)
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo != null)
File(externalRepo)
else
@ -239,7 +238,7 @@ open class PasswordRepository protected constructor() {
* @param username username
*/
@JvmStatic
fun setUserName(username: String) {
fun setGitAuthorName(username: String) {
setStringConfig("user", null, "name", username)
}
@ -249,7 +248,7 @@ open class PasswordRepository protected constructor() {
* @param email email
*/
@JvmStatic
fun setUserEmail(email: String) {
fun setGitAuthorEmail(email: String) {
setStringConfig("user", null, "email", email)
}

View file

@ -23,16 +23,21 @@ object PreferenceKeys {
const val FILTER_RECURSIVELY = "filter_recursively"
const val GENERAL_SHOW_TIME = "general_show_time"
const val GIT_CONFIG = "git_config"
const val GIT_CONFIG_USER_EMAIL = "git_config_user_email"
const val GIT_CONFIG_USER_NAME = "git_config_user_name"
const val GIT_CONFIG_AUTHOR_EMAIL = "git_config_user_email"
const val GIT_CONFIG_AUTHOR_NAME = "git_config_user_name"
const val GIT_EXTERNAL = "git_external"
const val GIT_EXTERNAL_REPO = "git_external_repo"
const val GIT_REMOTE_AUTH = "git_remote_auth"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_LOCATION = "git_remote_location"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_PORT = "git_remote_port"
const val GIT_REMOTE_PROTOCOL = "git_remote_protocol"
const val GIT_DELETE_REPO = "git_delete_repo"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_SERVER = "git_remote_server"
const val GIT_REMOTE_URL = "git_remote_url"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_USERNAME = "git_remote_username"
const val GIT_SERVER_INFO = "git_server_info"
const val GIT_BRANCH_NAME = "git_branch"

View file

@ -65,81 +65,21 @@
android:text="@string/clone_protocol_https" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/server_user_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_user"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:nextFocusForward="@id/server_url" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_url"
app:layout_constraintEnd_toStartOf="@id/label_server_port"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/server_user_layout">
app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:nextFocusForward="@id/server_port" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_port_hint"
app:layout_constraintDimensionRatio="1:0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/label_server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:nextFocusForward="@id/server_path" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_path"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:nextFocusForward="@id/server_branch"
android:inputType="textWebEmailAddress" />
@ -150,10 +90,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Branch"
android:hint="@string/server_branch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_path">
app:layout_constraintTop_toBottomOf="@id/label_server_url">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_branch"

View file

@ -29,10 +29,6 @@
<string name="initialize">إستخدام مجلد محلي</string>
<string name="server_name">الخادوم</string>
<string name="server_protocol">البروتوكول</string>
<string name="server_url">عنوان الخادوم</string>
<string name="server_port_hint">22</string>
<string name="server_path">مسار المستودع</string>
<string name="server_user">إسم المستخدم</string>
<string name="connection_mode">نوع المصادقة</string>

View file

@ -57,10 +57,6 @@
<string name="external_repository_dialog_title">Vyberte kam ukládat hesla</string>
<string name="server_name">Server</string>
<string name="server_protocol">Protokol</string>
<string name="server_url">URL serveru</string>
<string name="server_port_hint">22</string>
<string name="server_path">Cesta k repozitáři</string>
<string name="server_user">Jméno</string>
<string name="connection_mode">Mód ověření</string>

View file

@ -40,10 +40,6 @@
<string name="initialize">Nutze lokalen Ordner</string>
<string name="server_name">Server</string>
<string name="server_protocol">Protokoll</string>
<string name="server_url">Server URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo-Pfad</string>
<string name="server_user">Nutzername</string>
<string name="connection_mode">Authentifizierungsmethode</string>

View file

@ -54,10 +54,6 @@
<string name="server_name">Servidor</string>
<string name="server_protocol">Protocolo</string>
<string name="server_url">URL de servidor</string>
<string name="server_port_hint">22</string>
<string name="server_path">Ruta del repositorio</string>
<string name="server_user">Nombre de usuario</string>
<string name="connection_mode">Modo de autenticación</string>

View file

@ -60,10 +60,6 @@
<string name="server_name">Serveur</string>
<string name="server_protocol">Protocole</string>
<string name="server_url">URL du serveur</string>
<string name="server_port_hint">22</string>
<string name="server_path">Chemin du dépôt</string>
<string name="server_user">Nom d\'utilisateur</string>
<string name="connection_mode">Méthode d\'authentification</string>

View file

@ -41,10 +41,6 @@
<string name="initialize">ローカルディレクトリーを使用する</string>
<string name="server_name">サーバー</string>
<string name="server_protocol">プロトコル</string>
<string name="server_url">サーバー URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">リポジトリのパス</string>
<string name="server_user">ユーザー名</string>
<string name="connection_mode">認証モード</string>

View file

@ -68,10 +68,6 @@
<string name="external_repository_dialog_text">Você deve selecionar um diretório onde armazenar suas senhas. Se você deseja armazenar suas senhas dentro do armazenamento oculto do aplicativo, cancele esta caixa de diálogo e desative a opção \"Repositório Externo\".</string>
<string name="server_name">Servidor</string>
<string name="server_protocol">Protocolo</string>
<string name="server_url">URL do servidor</string>
<string name="server_port_hint">Porta</string>
<string name="server_path">Caminho do repositório</string>
<string name="server_user">Usuário</string>
<string name="connection_mode">Modo de autenticação</string>
<!-- Git Config fragment -->
<string name="git_user_name_hint">Usuário</string>
@ -295,7 +291,6 @@
<string name="connection_mode_ssh_key">Chave SSH</string>
<string name="connection_mode_basic_authentication">Senha</string>
<string name="git_server_config_save_success">Configuração salva com sucesso</string>
<string name="git_server_config_save_error_prefix">Erro de configuração: %s</string>
<string name="git_config_error_hostname_empty">hostname vazio</string>
<string name="git_config_error_generic">por favor, verifique suas configurações e tente novamente</string>
<string name="git_config_error_nonnumeric_port">porta deve ser numérica</string>

View file

@ -62,10 +62,6 @@
<string name="server_name">Сервер</string>
<string name="server_protocol">Протокол</string>
<string name="server_url">URL сервера</string>
<string name="server_port_hint">22</string>
<string name="server_path">Путь к репозиторию</string>
<string name="server_user">Имя пользователя</string>
<string name="connection_mode">Тип авторизации</string>

View file

@ -41,10 +41,6 @@
<string name="initialize">使用本地目录</string>
<string name="server_name">服务器</string>
<string name="server_protocol">接口</string>
<string name="server_url">服务器 URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo 路径</string>
<string name="server_user">用户名</string>
<string name="connection_mode">认证模式</string>

View file

@ -38,10 +38,6 @@
<string name="initialize">使用本機目錄</string>
<string name="server_name">伺服器</string>
<string name="server_protocol">port</string>
<string name="server_url">伺服器 URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo 路徑</string>
<string name="server_user">使用者名稱</string>
<string name="connection_mode">認證模式</string>

View file

@ -83,10 +83,8 @@
<string name="server_name">Server</string>
<string name="server_protocol">Protocol</string>
<string name="server_url">Server URL</string>
<string name="server_port_hint">Port</string>
<string name="server_path">Repo path</string>
<string name="server_user">Username</string>
<string name="server_url">Repository URL</string>
<string name="server_branch">Branch</string>
<string name="connection_mode">Authentication Mode</string>
@ -326,7 +324,7 @@
<string name="connection_mode_basic_authentication">Password</string>
<string name="connection_mode_openkeychain" translatable="false">OpenKeychain</string>
<string name="git_server_config_save_success">Successfully saved configuration</string>
<string name="git_server_config_save_error_prefix">Configuration error: %s</string>
<string name="git_server_config_save_error">The provided repository URL is not valid</string>
<string name="git_config_error_hostname_empty">empty hostname</string>
<string name="git_config_error_generic">please verify your settings and try again</string>
<string name="git_config_error_nonnumeric_port">port must be numeric</string>