summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpConnection.kt29
-rw-r--r--libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpHostsStorage.kt35
-rw-r--r--libs/sftp/src/main/java/org/the_jk/cleversync/sftp/SftpTreeFactory.kt12
-rw-r--r--libs/sftp/src/test/java/org/the_jk/cleversync/sftp/SftpTreeTest.kt60
4 files changed, 110 insertions, 26 deletions
diff --git a/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpConnection.kt b/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpConnection.kt
index 43eb88a..ed5889a 100644
--- a/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpConnection.kt
+++ b/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpConnection.kt
@@ -3,19 +3,24 @@ package org.the_jk.cleversync.io.sftp
import android.net.Uri
import org.the_jk.cleversync.PathUtils
-internal class SftpConnection(uri: Uri, credentials: SftpCredentials) {
+internal class SftpConnection(uri: Uri, credentials: SftpCredentials, hostsStorage: SftpHostsStorage) {
val description = uri.toString()
private val baseDir = uri.path ?: ""
private val sshSession = NativeSftp.newSshSession()
private var sftpSession: NativeSftp.SftpSession? = null
private var destroyed = false
+ private var fingerprintMismatch = false
- val connected = if (!destroyed) { login(uri, credentials) } else false
+ val connected = if (!destroyed) { login(uri, credentials, hostsStorage) } else false
val error: String
get() = if (destroyed) "[destroyed]" else {
- val err = sftpSession?.lastError()
- if (err.isNullOrEmpty()) sshSession.lastError() else err
+ if (fingerprintMismatch) {
+ "[fingerprint mismatch]"
+ } else {
+ val err = sftpSession?.lastError()
+ if (err.isNullOrEmpty()) sshSession.lastError() else err
+ }
}
protected fun finalize() {
@@ -66,12 +71,20 @@ internal class SftpConnection(uri: Uri, credentials: SftpCredentials) {
override fun toString() = description
- private fun login(uri: Uri, credentials: SftpCredentials): Boolean {
- if (!sshSession.connect(uri.host ?: "", if (uri.port == -1) DEFAULT_PORT else uri.port)) {
+ private fun login(uri: Uri, credentials: SftpCredentials, hostsStorage: SftpHostsStorage): Boolean {
+ val host = uri.host ?: ""
+ val port = if (uri.port == -1) DEFAULT_PORT else uri.port
+ if (!sshSession.connect(host, port)) {
+ return false
+ }
+ val expectedFingerprint = hostsStorage.get(host, port)
+ val fingerprint = sshSession.handshake() ?: return false
+ if (expectedFingerprint == null) {
+ hostsStorage.put(host, port, fingerprint)
+ } else if (expectedFingerprint != fingerprint) {
+ fingerprintMismatch = true
return false
}
- // TODO: Check fingerprint against last one
- if (sshSession.handshake() == null) return false
when (credentials) {
is SftpCredentials.SftpPasswordCredentials ->
if (!sshSession.authenticate(
diff --git a/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpHostsStorage.kt b/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpHostsStorage.kt
new file mode 100644
index 0000000..2fde819
--- /dev/null
+++ b/libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpHostsStorage.kt
@@ -0,0 +1,35 @@
+package org.the_jk.cleversync.io.sftp
+
+import android.content.Context
+import android.util.Base64
+
+class SftpHostsStorage(context: Context) {
+ private val prefs by lazy {
+ context.getSharedPreferences("sftp_hosts", Context.MODE_PRIVATE)
+ }
+
+ internal fun size(): Int {
+ return prefs.all.size
+ }
+
+ internal fun get(host: String, port: Int): NativeSftp.Fingerprint? {
+ val key = createKey(host, port)
+ val data = prefs.getString(key, null) ?: return null
+ return try {
+ NativeSftp.Fingerprint(Base64.decode(data, Base64.DEFAULT))
+ } catch (_: IllegalArgumentException) {
+ null
+ }
+ }
+
+ internal fun put(host: String, port: Int, fingerprint: NativeSftp.Fingerprint) {
+ val key = createKey(host, port)
+ val editor = prefs.edit()
+ editor.putString(key, Base64.encodeToString(fingerprint.data, Base64.NO_WRAP))
+ editor.apply()
+ }
+
+ private companion object {
+ fun createKey(host: String, port: Int) = "$host:$port"
+ }
+}
diff --git a/libs/sftp/src/main/java/org/the_jk/cleversync/sftp/SftpTreeFactory.kt b/libs/sftp/src/main/java/org/the_jk/cleversync/sftp/SftpTreeFactory.kt
index 7a45829..08894b9 100644
--- a/libs/sftp/src/main/java/org/the_jk/cleversync/sftp/SftpTreeFactory.kt
+++ b/libs/sftp/src/main/java/org/the_jk/cleversync/sftp/SftpTreeFactory.kt
@@ -5,15 +5,21 @@ import org.the_jk.cleversync.io.ModifiableTree
import org.the_jk.cleversync.io.Tree
import org.the_jk.cleversync.io.sftp.SftpConnection
import org.the_jk.cleversync.io.sftp.SftpCredentials
+import org.the_jk.cleversync.io.sftp.SftpHostsStorage
import org.the_jk.cleversync.io.sftp.SftpTree
object SftpTreeFactory {
- fun tree(uri: String, credentials: SftpCredentials): Result<Tree> = modifiableTree(uri, credentials)
+ fun tree(uri: String, credentials: SftpCredentials, hostsStorage: SftpHostsStorage): Result<Tree>
+ = modifiableTree(uri, credentials, hostsStorage)
- fun modifiableTree(uri: String, credentials: SftpCredentials): Result<ModifiableTree> {
+ fun modifiableTree(
+ uri: String,
+ credentials: SftpCredentials,
+ hostsStorage: SftpHostsStorage,
+ ): Result<ModifiableTree> {
val url = Uri.parse(uri)
if (url.scheme != "ssh") return Result.failure(IllegalArgumentException("Invalid url: $uri"))
- val connection = SftpConnection(url, credentials)
+ val connection = SftpConnection(url, credentials, hostsStorage)
if (!connection.connected) {
val e = Exception(connection.error)
connection.destroy()
diff --git a/libs/sftp/src/test/java/org/the_jk/cleversync/sftp/SftpTreeTest.kt b/libs/sftp/src/test/java/org/the_jk/cleversync/sftp/SftpTreeTest.kt
index da654a1..b254c4e 100644
--- a/libs/sftp/src/test/java/org/the_jk/cleversync/sftp/SftpTreeTest.kt
+++ b/libs/sftp/src/test/java/org/the_jk/cleversync/sftp/SftpTreeTest.kt
@@ -1,6 +1,7 @@
package org.the_jk.cleversync.sftp
import android.content.Context
+import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.After
@@ -16,7 +17,9 @@ import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper
import org.the_jk.cleversync.TreeAbstractTest
import org.the_jk.cleversync.io.Link
+import org.the_jk.cleversync.io.sftp.NativeSftp
import org.the_jk.cleversync.io.sftp.SftpCredentials
+import org.the_jk.cleversync.io.sftp.SftpHostsStorage
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
@@ -28,26 +31,17 @@ class SftpTreeTest : TreeAbstractTest() {
@get:Rule
val testName = TestName()
+ private lateinit var hostsStorage: SftpHostsStorage
+
@Before
fun setUpTest() {
assertThat(shareDir.listFiles()).isEmpty()
- val credentials: SftpCredentials
- // Test both password and key authentication
- // "Stable" as it depends on the hashCode of the test method name
- if (testName.methodName.hashCode() % 2 == 0) {
- credentials =
- SftpCredentials.SftpPasswordCredentials("user", "notverysecret")
- } else {
- val private = File(dockerDir, "user_private.pem")
- credentials = SftpCredentials.SftpKeyCredentials(
- "user",
- private.readBytes(),
- "notsecret",
- )
- }
+ val credentials = getCredentials()
- tree = SftpTreeFactory.modifiableTree(uri, credentials).getOrThrow()
+ hostsStorage = SftpHostsStorage(ApplicationProvider.getApplicationContext())
+
+ tree = SftpTreeFactory.modifiableTree(uri, credentials, hostsStorage).getOrThrow()
}
@After
@@ -143,12 +137,48 @@ class SftpTreeTest : TreeAbstractTest() {
assertThat(File(shareDir, "foo").isDirectory).isTrue()
}
+ @Test
+ fun matchFingerprint() {
+ assertThat(hostsStorage.size()).isEqualTo(1)
+
+ val credentials = getCredentials()
+ // Connect again, this time with a cached fingerprint
+ SftpTreeFactory.tree(uri, credentials, hostsStorage).getOrThrow()
+
+ assertThat(hostsStorage.size()).isEqualTo(1)
+ }
+
+ @Test
+ fun wrongFingerprint() {
+ val actualUri = Uri.parse(uri)
+ hostsStorage.put(actualUri.host!!, actualUri.port, NativeSftp.Fingerprint(ByteArray(0)))
+
+ val credentials = getCredentials()
+ assertThat(SftpTreeFactory.tree(uri, credentials, hostsStorage)
+ .exceptionOrNull()?.message).isEqualTo("[fingerprint mismatch]")
+ }
+
override fun supportSymlinks() = true
override fun idle() {
ShadowLooper.idleMainLooper(10, TimeUnit.SECONDS)
}
+ private fun getCredentials(): SftpCredentials {
+ // Test both password and key authentication
+ // "Stable" as it depends on the hashCode of the test method name
+ return if (testName.methodName.hashCode() % 2 == 0) {
+ SftpCredentials.SftpPasswordCredentials("user", "notverysecret")
+ } else {
+ val private = File(dockerDir, "user_private.pem")
+ SftpCredentials.SftpKeyCredentials(
+ "user",
+ private.readBytes(),
+ "notsecret",
+ )
+ }
+ }
+
companion object {
private lateinit var uri: String
private lateinit var dockerDir: File