diff --git a/app/src/main/assets/styles.css b/app/src/main/assets/styles.css index 51a98e0f..00082c06 100644 --- a/app/src/main/assets/styles.css +++ b/app/src/main/assets/styles.css @@ -93,5 +93,15 @@ input.m-input:focus { .login-button { margin: 10px; - margin-top: 20px; + margin-top: 12px; } + +div.trust { + margin-top: 10px; +} + +input[type="checkbox"] { + width: 1.5em; + height: 1.5em; + vertical-align: -3px; +} \ No newline at end of file diff --git a/app/src/main/assets/welcome.html b/app/src/main/assets/welcome.html index 7c0e770e..c1ba1a3a 100644 --- a/app/src/main/assets/welcome.html +++ b/app/src/main/assets/welcome.html @@ -18,9 +18,20 @@ type="url" id="server-url" class="m-input" - placeholder="https://nextcloud.example.com" + placeholder="nextcloud.example.com" /> +
+ +
+ @@ -93,7 +104,11 @@ // Login signal const encUrl = encodeURIComponent(encodeURIComponent(getMemoriesUrl().toString())); - await fetch(`http://127.0.0.1/api/login/${encUrl}`, { + + // Trust all certificates + const trustAll = document.getElementById("trust-all").checked ? "1" : "0"; + + await fetch(`http://127.0.0.1/api/login/${encUrl}?trustAll=${trustAll}`, { method: "GET", signal: controller.signal, }); diff --git a/app/src/main/java/gallery/memories/MainActivity.kt b/app/src/main/java/gallery/memories/MainActivity.kt index 780f54b9..25279f61 100644 --- a/app/src/main/java/gallery/memories/MainActivity.kt +++ b/app/src/main/java/gallery/memories/MainActivity.kt @@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() { // WebView.setWebContentsDebuggingEnabled(true); // Welcome page or actual app - nativex.account.refreshAuthHeader() + nativex.account.refreshCredentials() val isApp = loadDefaultUrl() // Start version check if loaded account diff --git a/app/src/main/java/gallery/memories/NativeX.kt b/app/src/main/java/gallery/memories/NativeX.kt index 222d632a..51366b6f 100644 --- a/app/src/main/java/gallery/memories/NativeX.kt +++ b/app/src/main/java/gallery/memories/NativeX.kt @@ -224,7 +224,12 @@ class NativeX(private val mCtx: MainActivity) { val parts = path.split("/").toTypedArray() return if (path.matches(API.LOGIN)) { - makeResponse(account.login(URLDecoder.decode(parts[3], "UTF-8"))) + makeResponse( + account.login( + URLDecoder.decode(parts[3], "UTF-8"), + request.url.getBooleanQueryParameter("trustAll", false) + ) + ) } else if (path.matches(API.DAYS)) { makeResponse(query.getDays()) } else if (path.matches(API.DAY)) { diff --git a/app/src/main/java/gallery/memories/service/AccountService.kt b/app/src/main/java/gallery/memories/service/AccountService.kt index d5f26a40..09caf426 100644 --- a/app/src/main/java/gallery/memories/service/AccountService.kt +++ b/app/src/main/java/gallery/memories/service/AccountService.kt @@ -17,17 +17,19 @@ class AccountService(private val mCtx: MainActivity, private val mHttp: HttpServ } private val store = SecureStorage(mCtx) + private var mTrustAll = false /** * Make the first request to log in * @param url The URL of the Nextcloud server + * @param trustAll Whether to trust all certificates */ - fun login(url: String) { + fun login(url: String, trustAll: Boolean) { try { - Log.v(TAG, "login: Connecting to ${url}api/describe") - mHttp.setBaseUrl(url) - val res = mHttp.getApiDescription() + mTrustAll = trustAll + mHttp.build(url, trustAll) + val res = mHttp.getApiDescription() if (res.code != 200) { throw Exception("${url}api/describe (status ${res.code})") } @@ -185,36 +187,31 @@ class AccountService(private val mCtx: MainActivity, private val mHttp: HttpServ * @param password The password to store */ fun storeCredentials(url: String, user: String, password: String) { - store.saveCredentials(url, user, password) - mHttp.setBaseUrl(url) - mHttp.setAuthHeader(Pair(user, password)) - } - - /** - * Get the stored credentials - * @return The stored credentials - */ - fun getCredentials(): Pair? { - val saved = store.getCredentials() - if (saved == null) return null - mHttp.setBaseUrl(saved.first) - return Pair(saved.second, saved.third) + store.saveCredentials(Credential( + url = url, + trustAll = mTrustAll, + username = user, + token = password, + )) + refreshCredentials() } /** * Delete the stored credentials */ fun deleteCredentials() { - mHttp.setAuthHeader(null) - mHttp.setBaseUrl(null) store.deleteCredentials() + mHttp.setAuthHeader(null) + mHttp.build(null, false) } /** * Refresh the authorization header */ - fun refreshAuthHeader() { - mHttp.setAuthHeader(getCredentials()) + fun refreshCredentials() { + val cred = store.getCredentials() ?: return + mHttp.build(cred.url, cred.trustAll) + mHttp.setAuthHeader(Pair(cred.username, cred.token)) } /** diff --git a/app/src/main/java/gallery/memories/service/Credential.kt b/app/src/main/java/gallery/memories/service/Credential.kt new file mode 100644 index 00000000..2c4be08e --- /dev/null +++ b/app/src/main/java/gallery/memories/service/Credential.kt @@ -0,0 +1,8 @@ +package gallery.memories.service + +data class Credential( + var url: String, + var trustAll: Boolean, + var username: String, + var token: String, +) diff --git a/app/src/main/java/gallery/memories/service/HttpService.kt b/app/src/main/java/gallery/memories/service/HttpService.kt index 2fb12594..7a7599f1 100644 --- a/app/src/main/java/gallery/memories/service/HttpService.kt +++ b/app/src/main/java/gallery/memories/service/HttpService.kt @@ -10,15 +10,22 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import org.json.JSONArray import org.json.JSONObject +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + class HttpService { companion object { val TAG = HttpService::class.java.simpleName } - private val client = OkHttpClient() + private var client = OkHttpClient() private var authHeader: String? = null - private var memoriesUrl: String? = null + private var baseUrl: String? = null /** * Check if the HTTP service is logged in @@ -28,11 +35,45 @@ class HttpService { } /** - * Set the Memories URL + * Build the HTTP client * @param url The URL to use + * @param trustAll Whether to trust all certificates */ - fun setBaseUrl(url: String?) { - memoriesUrl = url + fun build(url: String?, trustAll: Boolean) { + baseUrl = url + client = if (trustAll) { + val trustAllCerts = arrayOf( + object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted( + chain: Array, + authType: String + ) { + } + + @Throws(CertificateException::class) + override fun checkServerTrusted( + chain: Array, + authType: String + ) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + } + ) + + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, trustAllCerts, SecureRandom()) + + OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier({ hostname, session -> true }) + .build() + } else { + OkHttpClient() + } } /** @@ -56,8 +97,8 @@ class HttpService { */ fun loadWebView(webView: WebView, subpath: String? = null): String? { // Load app interface if authenticated - if (authHeader != null && memoriesUrl != null) { - var url = memoriesUrl + if (authHeader != null && baseUrl != null) { + var url = baseUrl if (subpath != null) url += subpath // Get host name @@ -104,20 +145,24 @@ class HttpService { /** Make login flow request */ @Throws(Exception::class) fun postLoginFlow(loginFlowUrl: String): Response { - return runRequest(Request.Builder() - .url(loginFlowUrl) - .header("User-Agent", "Memories") - .post("".toRequestBody("application/json".toMediaTypeOrNull())) - .build()) + return runRequest( + Request.Builder() + .url(loginFlowUrl) + .header("User-Agent", "Memories") + .post("".toRequestBody("application/json".toMediaTypeOrNull())) + .build() + ) } /** Make login polling request */ @Throws(Exception::class) fun getPollLogin(pollUrl: String, pollToken: String): Response { - return runRequest(Request.Builder() - .url(pollUrl) - .post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull())) - .build()) + return runRequest( + Request.Builder() + .url(pollUrl) + .post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull())) + .build() + ) } /** Run a request and get a JSON object */ @@ -129,7 +174,7 @@ class HttpService { /** Build a GET request */ private fun buildGet(path: String, auth: Boolean = true): Request { val builder = Request.Builder() - .url(memoriesUrl + path) + .url(baseUrl + path) .header("User-Agent", "Memories") .get() diff --git a/app/src/main/java/gallery/memories/service/SecureStorage.kt b/app/src/main/java/gallery/memories/service/SecureStorage.kt index f2d08fbc..b5c3bdc9 100644 --- a/app/src/main/java/gallery/memories/service/SecureStorage.kt +++ b/app/src/main/java/gallery/memories/service/SecureStorage.kt @@ -5,6 +5,7 @@ import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES import android.security.keystore.KeyProperties.PURPOSE_DECRYPT import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT import android.util.Base64 +import gallery.memories.service.Credential import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator @@ -23,34 +24,35 @@ class SecureStorage(private val context: Context) { } } - fun saveCredentials(url: String, username: String, token: String) { + fun saveCredentials(cred: Credential) { val cipher = getCipher() cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) - val encryptedToken = cipher.doFinal(token.toByteArray()) + val encryptedToken = cipher.doFinal(cred.token.toByteArray()) context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit() - .putString("url", url) - .putString("username", username) + .putString("url", cred.url) + .putBoolean("trustAll", cred.trustAll) + .putString("username", cred.username) .putString("encryptedToken", Base64.encodeToString(encryptedToken, Base64.DEFAULT)) .putString("iv", Base64.encodeToString(cipher.iv, Base64.DEFAULT)) .apply() } - fun getCredentials(): Triple? { + fun getCredentials(): Credential? { val sharedPreferences = context.getSharedPreferences("credentials", Context.MODE_PRIVATE) + val url = sharedPreferences.getString("url", null) + val trustAll = sharedPreferences.getBoolean("trustAll", false) val username = sharedPreferences.getString("username", null) val encryptedToken = sharedPreferences.getString("encryptedToken", null) val ivStr = sharedPreferences.getString("iv", null) if (url != null && username != null && encryptedToken != null && ivStr != null) { - val iv = Base64.decode(ivStr, Base64.DEFAULT) - val cipher = getCipher() + val iv = Base64.decode(ivStr, Base64.DEFAULT) cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IvParameterSpec(iv)) - val token = String(cipher.doFinal(Base64.decode(encryptedToken, Base64.DEFAULT))) - return Triple(url, username, token) + return Credential(url, trustAll, username, token) } return null @@ -59,6 +61,7 @@ class SecureStorage(private val context: Context) { fun deleteCredentials() { context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit() .remove("url") + .remove("trustAll") .remove("encryptedUsername") .remove("encryptedToken") .remove("iv")