From 6d185a2e7fca1e15008ef4906c57be5f42c3c6b3 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Thu, 31 Oct 2024 23:14:01 +0100 Subject: sftp: Verify server fingerprint If no fingerprint is stored -> save whatever the server gives If a fingerprint is stored -> error if fingerprint does not match --- .../the_jk/cleversync/io/sftp/SftpConnection.kt | 29 +++++++++++++----- .../the_jk/cleversync/io/sftp/SftpHostsStorage.kt | 35 ++++++++++++++++++++++ .../org/the_jk/cleversync/sftp/SftpTreeFactory.kt | 12 ++++++-- 3 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 libs/sftp/src/main/java/org/the_jk/cleversync/io/sftp/SftpHostsStorage.kt (limited to 'libs/sftp/src/main') 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 = modifiableTree(uri, credentials) + fun tree(uri: String, credentials: SftpCredentials, hostsStorage: SftpHostsStorage): Result + = modifiableTree(uri, credentials, hostsStorage) - fun modifiableTree(uri: String, credentials: SftpCredentials): Result { + fun modifiableTree( + uri: String, + credentials: SftpCredentials, + hostsStorage: SftpHostsStorage, + ): Result { 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() -- cgit v1.2.3-70-g09d2