Skip to content

Commit

Permalink
[Feature] Add private key password setting for SFTP server.
Browse files Browse the repository at this point in the history
  • Loading branch information
zhanghai committed Apr 18, 2022
1 parent 0669915 commit 01359a0
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private const val VERSION_CODE_1_1_0 = 18
private const val VERSION_CODE_1_2_0 = 22
private const val VERSION_CODE_1_3_0 = 24
private const val VERSION_CODE_1_4_0 = 26
private const val VERSION_CODE_1_5_0 = 29
private const val VERSION_CODE_LATEST = BuildConfig.VERSION_CODE

private var lastVersionCode: Int
Expand Down Expand Up @@ -48,5 +49,8 @@ private fun upgradeAppFrom(lastVersionCode: Int) {
if (lastVersionCode < VERSION_CODE_1_4_0) {
upgradeAppTo1_4_0()
}
if (lastVersionCode < VERSION_CODE_1_5_0) {
upgradeAppTo1_5_0()
}
// Continue with new `if`s on lastVersionCode instead of `else if`.
}
61 changes: 61 additions & 0 deletions app/src/main/java/me/zhanghai/android/files/app/AppUpgraders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,64 @@ private fun migrateRootStrategySetting1_4_0() {
}.ordinal.toString()
defaultSharedPreferences.edit { putString(key, newValue) }
}

internal fun upgradeAppTo1_5_0() {
migrateSftpServersSetting1_5_0()
}

private fun migrateSftpServersSetting1_5_0() {
val key = application.getString(R.string.pref_key_storages)
val oldBytes = defaultSharedPreferences.getString(key, null)?.asBase64()?.toByteArray()
?: return
val newBytes = try {
Parcel.obtain().use { newParcel ->
Parcel.obtain().use { oldParcel ->
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
oldParcel.setDataPosition(0)
newParcel.writeInt(oldParcel.readInt())
val size = oldParcel.readInt()
newParcel.writeInt(size)
repeat(size) {
val oldPosition = oldParcel.dataPosition()
oldParcel.readInt()
when (oldParcel.readString()) {
"me.zhanghai.android.files.storage.SftpServer" -> {
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
newParcel.writeString("me.zhanghai.android.files.storage.SftpServer")
val id = oldParcel.readLong()
newParcel.writeLong(id)
val customName = oldParcel.readString()
newParcel.writeString(customName)
val authorityHost = oldParcel.readString()
newParcel.writeString(authorityHost)
val authorityPort = oldParcel.readInt()
newParcel.writeInt(authorityPort)
val authorityUsername = oldParcel.readString()
newParcel.writeString(authorityUsername)
val authenticationClassName = oldParcel.readString()
newParcel.writeString(authenticationClassName)
val authenticationPasswordOrPrivateKey = oldParcel.readString()
newParcel.writeString(authenticationPasswordOrPrivateKey)
if (authenticationClassName == "me.zhanghai.android.files.provider.sftp"
+ ".client.PublicKeyAuthentication") {
newParcel.writeString(null)
}
val relativePath = oldParcel.readString()
newParcel.writeString(relativePath)
}
else -> {
oldParcel.setDataPosition(oldPosition)
val storage = oldParcel.readValue(appClassLoader)
newParcel.writeValue(storage)
}
}
}
}
newParcel.marshall()
}
} catch (e: Exception) {
e.printStackTrace()
null
}
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,38 @@ data class PasswordAuthentication(

@Parcelize
data class PublicKeyAuthentication(
val privateKey: String
val privateKey: String,
val privateKeyPassword: String?
) : Authentication() {
override fun toAuthMethod(): AuthMethod =
AuthPublickey(privateKey.toKeyProvider())
AuthPublickey(createKeyProvider(privateKey, privateKeyPassword))

companion object {
private val KEY_PROVIDER_FACTORIES = DefaultConfig().fileKeyProviderFactories

fun validatePrivateKey(privateKey: String): Boolean =
fun validate(privateKey: String, privateKeyPassword: String?): IOException? =
try {
privateKey.toKeyProvider().private
true
createKeyProvider(privateKey, privateKeyPassword).private
null
} catch (e: IOException) {
e.printStackTrace()
false
e
}

/**
* @see net.schmizz.sshj.SSHClient.loadKeys
*/
@Throws(IOException::class)
private fun String.toKeyProvider(): KeyProvider {
val format = KeyProviderUtil.detectKeyFileFormat(this, false)
private fun createKeyProvider(
privateKey: String,
privateKeyPassword: String?
): KeyProvider {
val format = KeyProviderUtil.detectKeyFileFormat(privateKey, false)
val keyProvider = Factory.Named.Util.create(KEY_PROVIDER_FACTORIES, format.toString())
?: throw IOException("No key provider factory found for $format")
keyProvider.init(this, null)
keyProvider.init(
privateKey, null,
privateKeyPassword?.let { PasswordUtils.createOneOff(it.toCharArray()) }
)
return keyProvider
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputEditText
import com.hierynomus.sshj.common.KeyDecryptionFailedException
import java8.nio.file.Path
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
Expand Down Expand Up @@ -106,7 +107,12 @@ class EditSftpServerFragment : Fragment() {
binding.usernameEdit.hideTextInputLayoutErrorOnTextChange(binding.usernameLayout)
binding.usernameEdit.doAfterTextChanged { updateNamePlaceholder() }
binding.privateKeyLayout.setEndIconOnClickListener { onOpenPrivateKeyFile() }
binding.privateKeyEdit.hideTextInputLayoutErrorOnTextChange(binding.privateKeyLayout)
binding.privateKeyEdit.hideTextInputLayoutErrorOnTextChange(
binding.privateKeyLayout, binding.privateKeyPasswordLayout
)
binding.privateKeyPasswordEdit.hideTextInputLayoutErrorOnTextChange(
binding.privateKeyLayout, binding.privateKeyPasswordLayout
)
binding.saveOrConnectAndAddButton.setText(
if (args.server != null) {
R.string.save
Expand Down Expand Up @@ -151,6 +157,7 @@ class EditSftpServerFragment : Fragment() {
is PublicKeyAuthentication -> {
authenticationType = AuthenticationType.PUBLIC_KEY
binding.privateKeyEdit.setText(authentication.privateKey)
binding.privateKeyPasswordEdit.setText(authentication.privateKeyPassword)
}
}
binding.pathEdit.setText(server.relativePath)
Expand Down Expand Up @@ -190,7 +197,8 @@ class EditSftpServerFragment : Fragment() {

private fun onAuthenticationTypeChanged(authenticationType: AuthenticationType) {
binding.passwordLayout.isVisible = authenticationType == AuthenticationType.PASSWORD
binding.privateKeyLayout.isVisible = authenticationType == AuthenticationType.PUBLIC_KEY
binding.publicKeyAuthenticationLayout.isVisible =
authenticationType == AuthenticationType.PUBLIC_KEY
}

private fun onOpenPrivateKeyFile() {
Expand Down Expand Up @@ -310,20 +318,40 @@ class EditSftpServerFragment : Fragment() {
}
AuthenticationType.PUBLIC_KEY -> {
val privateKey = binding.privateKeyEdit.text.toString().takeIfNotEmpty()
val privateKeyPassword =
binding.privateKeyPasswordEdit.text.toString().takeIfNotEmpty()
if (privateKey == null) {
binding.privateKeyLayout.error =
getString(R.string.storage_edit_sftp_server_private_key_error_empty)
if (errorEdit == null) {
errorEdit = binding.privateKeyEdit
}
} else if (!PublicKeyAuthentication.validatePrivateKey(privateKey)) {
binding.privateKeyLayout.error =
getString(R.string.storage_edit_sftp_server_private_key_error_invalid)
if (errorEdit == null) {
errorEdit = binding.privateKeyEdit
} else {
val exception = PublicKeyAuthentication.validate(privateKey, privateKeyPassword)
if (exception != null) {
exception.printStackTrace()
if (exception is KeyDecryptionFailedException) {
binding.privateKeyPasswordLayout.error = getString(
R.string.storage_edit_sftp_server_private_key_password_error_invalid
)
if (errorEdit == null) {
errorEdit = binding.privateKeyPasswordEdit
}
} else {
binding.privateKeyLayout.error = getString(
R.string.storage_edit_sftp_server_private_key_error_invalid
)
if (errorEdit == null) {
errorEdit = binding.privateKeyEdit
}
}
}
}
if (errorEdit == null) PublicKeyAuthentication(privateKey!!) else null
if (errorEdit == null) {
PublicKeyAuthentication(privateKey!!, privateKeyPassword)
} else {
null
}
}
}
if (errorEdit != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.textfield.TextInputLayout

fun TextView.hideTextInputLayoutErrorOnTextChange(textInputLayout: TextInputLayout) {
doAfterTextChanged { textInputLayout.error = null }
fun TextView.hideTextInputLayoutErrorOnTextChange(vararg textInputLayouts: TextInputLayout) {
doAfterTextChanged { textInputLayouts.forEach { it.error = null } }
}

var TextView.isBold: Boolean
Expand Down
46 changes: 35 additions & 11 deletions app/src/main/res/layout/edit_sftp_server_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,22 +169,46 @@
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/privateKeyLayout"
<LinearLayout
android:id="@+id/publicKeyAuthenticationLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storage_edit_sftp_server_private_key"
app:endIconContentDescription="@string/storage_edit_sftp_server_private_key_open_file"
app:endIconDrawable="@drawable/file_icon_white_24dp"
app:endIconMode="custom"
app:errorEnabled="true">
android:orientation="vertical">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/privateKeyEdit"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/privateKeyLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
android:hint="@string/storage_edit_sftp_server_private_key"
app:endIconContentDescription="@string/storage_edit_sftp_server_private_key_open_file"
app:endIconDrawable="@drawable/file_icon_white_24dp"
app:endIconMode="custom"
app:errorEnabled="true">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/privateKeyEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/privateKeyPasswordLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storage_edit_sftp_server_private_key_password"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:expandedHintEnabled="false"
app:placeholderText="@string/storage_edit_sftp_server_private_key_password_placeholder">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/privateKeyPasswordEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@
<string name="storage_edit_sftp_server_private_key_open_file">打开文件</string>
<string name="storage_edit_sftp_server_private_key_error_empty">输入私钥</string>
<string name="storage_edit_sftp_server_private_key_error_invalid">私钥无效</string>
<string name="storage_edit_sftp_server_private_key_password">私钥密码</string>
<string name="storage_edit_sftp_server_private_key_password_placeholder">可留空</string>
<string name="storage_edit_sftp_server_private_key_password_error_invalid">私钥密码无效</string>
<string name="storage_edit_sftp_server_connect_and_add">连接并添加</string>
<string name="storage_edit_sftp_server_add">添加</string>
<string name="storage_add_lan_smb_server_loading">正在搜索 SMB 服务器…</string>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@
<string name="storage_edit_sftp_server_private_key_open_file">開啟檔案</string>
<string name="storage_edit_sftp_server_private_key_error_empty">輸入私密金鑰</string>
<string name="storage_edit_sftp_server_private_key_error_invalid">無效的私密金鑰</string>
<string name="storage_edit_sftp_server_private_key_password">私密金鑰密碼</string>
<string name="storage_edit_sftp_server_private_key_password_placeholder">可留空</string>
<string name="storage_edit_sftp_server_private_key_password_error_invalid">無效的私密金鑰密碼</string>
<string name="storage_edit_sftp_server_connect_and_add">連線並新增</string>
<string name="storage_edit_sftp_server_add">新增</string>
<string name="storage_add_lan_smb_server_loading">正在搜尋 SMB 伺服器…</string>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,9 @@
<string name="storage_edit_sftp_server_private_key_open_file">Open file</string>
<string name="storage_edit_sftp_server_private_key_error_empty">Enter a private key</string>
<string name="storage_edit_sftp_server_private_key_error_invalid">Invalid private key</string>
<string name="storage_edit_sftp_server_private_key_password">Private key password</string>
<string name="storage_edit_sftp_server_private_key_password_placeholder">Can be left empty</string>
<string name="storage_edit_sftp_server_private_key_password_error_invalid">Invalid private key password</string>
<string name="storage_edit_sftp_server_connect_and_add">Connect and add</string>
<string name="storage_edit_sftp_server_add">Add</string>
<string name="storage_add_lan_smb_server_title" translatable="false">@string/storage_edit_smb_server_title_add</string>
Expand Down

0 comments on commit 01359a0

Please sign in to comment.