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")