Switch password authentication over to SSHJ (#811)

* Switch password authentication over to SSHJ

* Address review comments and refactor further
This commit is contained in:
Fabian Henneke
2020-05-30 19:39:17 +02:00
committed by GitHub
parent 72ede314ef
commit 2428d4c0de
9 changed files with 169 additions and 183 deletions

View File

@@ -8,6 +8,7 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.checkbox.MaterialCheckBox
@@ -16,7 +17,6 @@ import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitConfigSessionFactory
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshAuthData import com.zeapo.pwdstore.git.config.SshAuthData
@@ -24,37 +24,129 @@ import com.zeapo.pwdstore.git.config.SshjSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.getEncryptedPrefs import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView import com.zeapo.pwdstore.utils.requestInputFocusOnView
import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider import org.eclipse.jgit.transport.URIish
import java.io.File import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
private class GitOperationCredentialFinder(val callingActivity: Activity, val connectionMode: ConnectionMode) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
require(connectionMode == ConnectionMode.Password)
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
val credentialPref: String
@StringRes val messageRes: Int
@StringRes val hintRes: Int
@StringRes val rememberRes: Int
@StringRes val errorRes: Int
when (connectionMode) {
ConnectionMode.SshKey -> {
credentialPref = "ssh_key_local_passphrase"
messageRes = R.string.passphrase_dialog_text
hintRes = R.string.ssh_keygen_passphrase
rememberRes = R.string.git_operation_remember_passphrase
errorRes = R.string.git_operation_wrong_passphrase
}
ConnectionMode.Password -> {
// Could be either an SSH or an HTTPS password
credentialPref = "https_password"
messageRes = R.string.password_dialog_text
hintRes = R.string.git_operation_hint_password
rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password
}
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
}
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (isRetry)
gitOperationPrefs.edit { remove(credentialPref) }
if (storedCredential.isNullOrEmpty()) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes)
if (isRetry)
editCredential.error = callingActivity.resources.getString(errorRes)
MaterialAlertDialogBuilder(callingActivity).run {
setTitle(R.string.passphrase_dialog_title)
setMessage(messageRes)
setView(dialogView)
setPositiveButton(R.string.dialog_ok) { _, _ ->
val credential = editCredential.text.toString()
if (rememberCredential.isChecked) {
gitOperationPrefs.edit {
putString(credentialPref, credential)
}
}
cont.resume(credential)
}
setNegativeButton(R.string.dialog_cancel) { _, _ ->
cont.resume(null)
}
setOnCancelListener {
cont.resume(null)
}
create()
}.run {
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
show()
}
} else {
cont.resume(storedCredential)
}
}
}
/** /**
* Creates a new git operation * Creates a new git operation
* *
* @param fileDir the git working tree directory * @param gitDir the git working tree directory
* @param callingActivity the calling activity * @param callingActivity the calling activity
*/ */
abstract class GitOperation(fileDir: File, internal val callingActivity: Activity) { abstract class GitOperation(gitDir: File, internal val callingActivity: Activity) {
protected val repository: Repository? = PasswordRepository.getRepository(fileDir) protected val repository: Repository? = PasswordRepository.getRepository(gitDir)
internal var provider: UsernamePasswordCredentialsProvider? = null internal var provider: CredentialsProvider? = null
internal var command: GitCommand<*>? = null internal var command: GitCommand<*>? = null
private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key") private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key")
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
/** private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() {
* Sets the authentication using user/pwd scheme
* override fun isInteractive() = true
* @param username the username
* @param password the password override fun get(uri: URIish?, vararg items: CredentialItem): Boolean {
* @return the current object for (item in items) {
*/ when (item) {
internal open fun setAuthentication(username: String, password: String): GitOperation { is CredentialItem.Username -> item.value = username
SshSessionFactory.setInstance(GitConfigSessionFactory()) is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null)
this.provider = UsernamePasswordCredentialsProvider(username, password) else -> UnsupportedCredentialItem(uri, item.javaClass.name)
}
}
return true
}
override fun supports(vararg items: CredentialItem) = items.all {
it is CredentialItem.Username || it is CredentialItem.Password
}
}
private fun withPasswordAuthentication(username: String, passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.Password(passwordFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = PasswordFinderCredentialsProvider(username, passwordFinder)
return this return this
} }
@@ -65,146 +157,58 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
return this return this
} }
/** private fun withOpenKeychainAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
* Sets the authentication using OpenKeystore scheme
*
* @param identity The identiy to use
* @return the current object
*/
private fun setAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
SshSessionFactory.setInstance(SshApiSessionFactory(username, identity)) SshSessionFactory.setInstance(SshApiSessionFactory(username, identity))
this.provider = null this.provider = null
return this return this
} }
private fun getSshKey(make: Boolean) {
try {
// Ask the UserPreference to provide us with the ssh-key
// onResult has to be handled by the callingActivity
val intent = Intent(callingActivity.applicationContext, UserPreference::class.java)
intent.putExtra("operation", if (make) "make_ssh_key" else "get_ssh_key")
callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE)
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
}
}
/** /**
* Executes the GitCommand in an async task * Executes the GitCommand in an async task
*/ */
abstract fun execute() abstract fun execute()
/**
* Executes the GitCommand in an async task after creating the authentication
*
* @param connectionMode the server-connection mode
* @param username the username
* @param identity the api identity to use for auth in OpenKeychain connection mode
*/
fun executeAfterAuthentication( fun executeAfterAuthentication(
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
username: String, username: String,
identity: SshApiSessionFactory.ApiIdentity? identity: SshApiSessionFactory.ApiIdentity?
) { ) {
val encryptedSettings = callingActivity.applicationContext.getEncryptedPrefs("git_operation")
when (connectionMode) { when (connectionMode) {
ConnectionMode.SshKey -> { ConnectionMode.SshKey -> if (!sshKeyFile.exists()) {
if (!sshKeyFile.exists()) { MaterialAlertDialogBuilder(callingActivity)
MaterialAlertDialogBuilder(callingActivity) .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) .setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> getSshKey(false)
try { }
// Ask the UserPreference to provide us with the ssh-key .setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ ->
// onResult has to be handled by the callingActivity getSshKey(true)
val intent = Intent(callingActivity.applicationContext, UserPreference::class.java) }
intent.putExtra("operation", "get_ssh_key") .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE) // Finish the blank GitActivity so user doesn't have to press back
} catch (e: Exception) { callingActivity.finish()
println("Exception caught :(") }.show()
e.printStackTrace() } else {
} withPublicKeyAuthentication(username, GitOperationCredentialFinder(callingActivity,
} connectionMode)).execute()
.setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ ->
try {
// Duplicated code
val intent = Intent(callingActivity.applicationContext, UserPreference::class.java)
intent.putExtra("operation", "make_ssh_key")
callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE)
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
}
}
.setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
// Finish the blank GitActivity so user doesn't have to press back
callingActivity.finish()
}.show()
} else {
withPublicKeyAuthentication(username, InteractivePasswordFinder { cont, isRetry ->
val storedPassphrase = encryptedSettings.getString("ssh_key_local_passphrase", null)
if (isRetry)
encryptedSettings.edit { putString("ssh_key_local_passphrase", null) }
if (storedPassphrase.isNullOrEmpty()) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null)
val editPassphrase = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase)
val rememberPassphrase = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase)
if (isRetry)
editPassphrase.error = callingActivity.resources.getString(R.string.git_operation_wrong_passphrase)
MaterialAlertDialogBuilder(callingActivity).run {
setTitle(R.string.passphrase_dialog_title)
setMessage(R.string.passphrase_dialog_text)
setView(dialogView)
setPositiveButton(R.string.dialog_ok) { _, _ ->
val passphrase = editPassphrase.text.toString()
if (rememberPassphrase.isChecked) {
encryptedSettings.edit {
putString("ssh_key_local_passphrase", passphrase)
}
}
cont.resume(passphrase)
}
setNegativeButton(R.string.dialog_cancel) { _, _ ->
cont.resume(null)
}
setOnCancelListener {
cont.resume(null)
}
create()
}.run {
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase)
show()
}
} else {
cont.resume(storedPassphrase)
}
}).execute()
}
}
ConnectionMode.OpenKeychain -> {
setAuthentication(username, identity).execute()
}
ConnectionMode.Password -> {
@SuppressLint("InflateParams") val dialogView = callingActivity.layoutInflater.inflate(R.layout.git_passphrase_layout, null)
val passwordView = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase)
val password = encryptedSettings.getString("https_password", null)
if (password != null && password.isNotEmpty()) {
setAuthentication(username, password).execute()
} else {
val dialog = MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
.setMessage(callingActivity.resources.getString(R.string.password_dialog_text))
.setView(dialogView)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
if (dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase).isChecked) {
encryptedSettings.edit { putString("https_password", passwordView.text.toString()) }
}
// authenticate using the user/pwd and then execute the command
setAuthentication(username, passwordView.text.toString()).execute()
}
.setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
callingActivity.finish()
}
.setOnCancelListener { callingActivity.finish() }
.create()
dialog.requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase)
dialog.show()
}
}
ConnectionMode.None -> {
execute()
} }
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
ConnectionMode.Password -> withPasswordAuthentication(
username, GitOperationCredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute()
} }
} }
@@ -216,17 +220,15 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
when (SshSessionFactory.getInstance()) { when (SshSessionFactory.getInstance()) {
is SshApiSessionFactory -> { is SshApiSessionFactory -> {
PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext)
.edit { putString("ssh_openkeystore_keyid", null) } .edit { remove("ssh_openkeystore_keyid") }
} }
is SshjSessionFactory -> { is SshjSessionFactory -> {
callingActivity.applicationContext callingActivity.applicationContext
.getEncryptedPrefs("git_operation") .getEncryptedPrefs("git_operation")
.edit { remove("ssh_key_local_passphrase") } .edit {
} remove("ssh_key_local_passphrase")
is GitConfigSessionFactory -> { remove("https_password")
callingActivity.applicationContext }
.getEncryptedPrefs("git_operation")
.edit { remove("https_password") }
} }
} }
} }

View File

@@ -1,26 +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
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.jcraft.jsch.Session
import org.eclipse.jgit.transport.JschConfigSessionFactory
import org.eclipse.jgit.transport.OpenSshConfig
import org.eclipse.jgit.util.FS
open class GitConfigSessionFactory : JschConfigSessionFactory() {
override fun configure(hc: OpenSshConfig.Host, session: Session) {
session.setConfig("StrictHostKeyChecking", "no")
}
@Throws(JSchException::class)
override fun getJSch(hc: OpenSshConfig.Host, fs: FS): JSch {
val jsch = super.getJSch(hc, fs)
jsch.removeAllIdentity()
return jsch
}
}

View File

@@ -26,6 +26,7 @@ import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.CredentialsProviderUserInfo; import org.eclipse.jgit.transport.CredentialsProviderUserInfo;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig; import org.eclipse.jgit.transport.OpenSshConfig;
import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.Base64; import org.eclipse.jgit.util.Base64;
@@ -43,7 +44,7 @@ import org.openintents.ssh.authentication.util.SshAuthenticationApiUtils;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
public class SshApiSessionFactory extends GitConfigSessionFactory { public class SshApiSessionFactory extends JschConfigSessionFactory {
/** /**
* Intent request code indicating a completed signature that should be posted to an outstanding * Intent request code indicating a completed signature that should be posted to an outstanding
* ApiIdentity * ApiIdentity

View File

@@ -37,12 +37,14 @@ sealed class SshAuthData {
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData() class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
} }
class InteractivePasswordFinder(val askForPassword: (cont: Continuation<String?>, isRetry: Boolean) -> Unit) : PasswordFinder { abstract class InteractivePasswordFinder : PasswordFinder {
private var isRetry = false private var isRetry = false
private var shouldRetry = true private var shouldRetry = true
override fun reqPassword(resource: Resource<*>?): CharArray { abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
final override fun reqPassword(resource: Resource<*>?): CharArray {
val password = runBlocking(Dispatchers.Main) { val password = runBlocking(Dispatchers.Main) {
suspendCoroutine<String?> { cont -> suspendCoroutine<String?> { cont ->
askForPassword(cont, isRetry) askForPassword(cont, isRetry)
@@ -57,7 +59,7 @@ class InteractivePasswordFinder(val askForPassword: (cont: Continuation<String?>
} }
} }
override fun shouldRetry(resource: Resource<*>?) = shouldRetry final override fun shouldRetry(resource: Resource<*>?) = shouldRetry
} }
class SshjSessionFactory(private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() { class SshjSessionFactory(private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {

View File

@@ -19,7 +19,7 @@
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/git_auth_passphrase" android:id="@+id/git_auth_credential"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/ssh_keygen_passphrase" android:hint="@string/ssh_keygen_passphrase"
@@ -27,10 +27,10 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/git_auth_remember_passphrase" android:id="@+id/git_auth_remember_credential"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/remember_the_passphrase" android:text="@string/git_operation_remember_passphrase"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/git_auth_passphrase_layout" /> app:layout_constraintTop_toBottomOf="@+id/git_auth_passphrase_layout" />

View File

@@ -208,7 +208,8 @@
<string name="git_push_nff_error">La subida fue rechazada por el servidor, Ejecuta \'Descargar desde servidor\' antes de subir o pulsa \'Sincronizar con servidor\' para realizar ambas acciones.</string> <string name="git_push_nff_error">La subida fue rechazada por el servidor, Ejecuta \'Descargar desde servidor\' antes de subir o pulsa \'Sincronizar con servidor\' para realizar ambas acciones.</string>
<string name="git_push_generic_error">El envío fue rechazado por el servidor, la razón:</string> <string name="git_push_generic_error">El envío fue rechazado por el servidor, la razón:</string>
<string name="jgit_error_push_dialog_text">Ocurrió un error durante el envío:</string> <string name="jgit_error_push_dialog_text">Ocurrió un error durante el envío:</string>
<string name="remember_the_passphrase">Recordar contraseñagit (inseguro)</string> <string name="hotp_remember_clear_choice">Limpiar preferencia para incremento HOTP</string>
<string name="git_operation_remember_passphrase">Recordar contraseñagit (inseguro)</string>
<string name="hackish_tools">Hackish tools</string> <string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abortar rebase</string> <string name="abort_rebase">Abortar rebase</string>
<string name="commit_hash">Hash del commit</string> <string name="commit_hash">Hash del commit</string>

View File

@@ -209,7 +209,8 @@
<string name="git_push_generic_error">Poussée rejetée par le dépôt distant, raison:</string> <string name="git_push_generic_error">Poussée rejetée par le dépôt distant, raison:</string>
<string name="git_push_other_error">Pousser au dépôt distant sans avance rapide rejetée. Vérifiez la variable receive.denyNonFastForwards dans le fichier de configuration du répertoire de destination.</string> <string name="git_push_other_error">Pousser au dépôt distant sans avance rapide rejetée. Vérifiez la variable receive.denyNonFastForwards dans le fichier de configuration du répertoire de destination.</string>
<string name="jgit_error_push_dialog_text">Une erreur s\'est produite lors de l\'opération de poussée:</string> <string name="jgit_error_push_dialog_text">Une erreur s\'est produite lors de l\'opération de poussée:</string>
<string name="remember_the_passphrase">Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr)</string> <string name="hotp_remember_clear_choice">Effacer les préférences enregistrées pour lincrémentation HOTP</string>
<string name="git_operation_remember_passphrase">Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr)</string>
<string name="hackish_tools">Outils de hack</string> <string name="hackish_tools">Outils de hack</string>
<string name="commit_hash">Commettre la clé</string> <string name="commit_hash">Commettre la clé</string>
<string name="crypto_extra_edit_hint">nom d\'utilisateur: quelque chose d\'autre contenu supplémentaire</string> <string name="crypto_extra_edit_hint">nom d\'utilisateur: quelque chose d\'autre contenu supplémentaire</string>

View File

@@ -270,7 +270,8 @@
<string name="git_push_generic_error">Запись изменений была отклонена удаленным репозиторием, причина:</string> <string name="git_push_generic_error">Запись изменений была отклонена удаленным репозиторием, причина:</string>
<string name="git_push_other_error">Удаленный репозиторий отклонил запись изменений без быстрой перемотки вперед. Проверьте переменную receive.denyNonFastForwards в файле конфигурации репозитория назначения.</string> <string name="git_push_other_error">Удаленный репозиторий отклонил запись изменений без быстрой перемотки вперед. Проверьте переменную receive.denyNonFastForwards в файле конфигурации репозитория назначения.</string>
<string name="jgit_error_push_dialog_text">В хоте операции записи изменений возникла ошибка:</string> <string name="jgit_error_push_dialog_text">В хоте операции записи изменений возникла ошибка:</string>
<string name="remember_the_passphrase">Заполнить парольную фразу в конфигурации приложнеия (небезопасно)</string> <string name="hotp_remember_clear_choice">Очистить сохраненные настройки для увеличения HOTP</string>
<string name="git_operation_remember_passphrase">Заполнить парольную фразу в конфигурации приложнеия (небезопасно)</string>
<string name="hackish_tools">Костыльные инструменты</string> <string name="hackish_tools">Костыльные инструменты</string>
<string name="abort_rebase">Прервать перебазирование и записать изменения в новую ветку</string> <string name="abort_rebase">Прервать перебазирование и записать изменения в новую ветку</string>
<string name="reset_to_remote">Полный сброс до состояния удаленной ветки</string> <string name="reset_to_remote">Полный сброс до состояния удаленной ветки</string>

View File

@@ -307,7 +307,8 @@
<string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string> <string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
<string name="clear_saved_passphrase_ssh">Clear saved passphrase for local SSH key</string> <string name="clear_saved_passphrase_ssh">Clear saved passphrase for local SSH key</string>
<string name="clear_saved_passphrase_https">Clear saved HTTPS password</string> <string name="clear_saved_passphrase_https">Clear saved HTTPS password</string>
<string name="remember_the_passphrase">Remember key passphrase</string> <string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string>
<string name="git_operation_remember_passphrase">Remember key passphrase</string>
<string name="hackish_tools">Hackish tools</string> <string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abort rebase and push new branch</string> <string name="abort_rebase">Abort rebase and push new branch</string>
<string name="reset_to_remote">Hard reset to remote branch</string> <string name="reset_to_remote">Hard reset to remote branch</string>
@@ -360,6 +361,7 @@
<string name="git_operation_unable_to_open_ssh_key_title">Unable to open the ssh-key</string> <string name="git_operation_unable_to_open_ssh_key_title">Unable to open the ssh-key</string>
<string name="git_operation_unable_to_open_ssh_key_message">Please check that it was imported.</string> <string name="git_operation_unable_to_open_ssh_key_message">Please check that it was imported.</string>
<string name="git_operation_wrong_passphrase">Wrong passphrase</string> <string name="git_operation_wrong_passphrase">Wrong passphrase</string>
<string name="git_operation_wrong_password">Wrong password</string>
<string name="bottom_sheet_create_new_folder">Create new folder</string> <string name="bottom_sheet_create_new_folder">Create new folder</string>
<string name="bottom_sheet_create_new_password">Create new password</string> <string name="bottom_sheet_create_new_password">Create new password</string>
<string name="autofill_onboarding_dialog_title">New, revamped Autofill!</string> <string name="autofill_onboarding_dialog_title">New, revamped Autofill!</string>
@@ -369,4 +371,6 @@
<string name="pref_debug_logging_title">Debug logging</string> <string name="pref_debug_logging_title">Debug logging</string>
<string name="preference_default_username_summary">If Autofill is unable to determine a username from your password file or directory structure, it will use the value specified here</string> <string name="preference_default_username_summary">If Autofill is unable to determine a username from your password file or directory structure, it will use the value specified here</string>
<string name="preference_default_username_title">Default username</string> <string name="preference_default_username_title">Default username</string>
<string name="git_operation_remember_password">Remember password</string>
<string name="git_operation_hint_password">Password</string>
</resources> </resources>