Add box for self-signed

pull/653/merge
Varun Patil 2023-10-13 23:12:46 -07:00
parent 49f97e2895
commit b0c5927d9b
8 changed files with 136 additions and 53 deletions

View File

@ -93,5 +93,15 @@ input.m-input:focus {
.login-button { .login-button {
margin: 10px; 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;
}

View File

@ -18,9 +18,20 @@
type="url" type="url"
id="server-url" id="server-url"
class="m-input" class="m-input"
placeholder="https://nextcloud.example.com" placeholder="nextcloud.example.com"
/> />
<div class="trust">
<label for="trust-all">
<input
type="checkbox"
id="trust-all"
class="m-checkbox"
/>
Disable certificate verification (unsafe)
</label>
</div>
<button class="m-button login-button" id="login"> <button class="m-button login-button" id="login">
Continue to Login Continue to Login
</button> </button>
@ -93,7 +104,11 @@
// Login signal // Login signal
const encUrl = encodeURIComponent(encodeURIComponent(getMemoriesUrl().toString())); 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", method: "GET",
signal: controller.signal, signal: controller.signal,
}); });

View File

@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() {
// WebView.setWebContentsDebuggingEnabled(true); // WebView.setWebContentsDebuggingEnabled(true);
// Welcome page or actual app // Welcome page or actual app
nativex.account.refreshAuthHeader() nativex.account.refreshCredentials()
val isApp = loadDefaultUrl() val isApp = loadDefaultUrl()
// Start version check if loaded account // Start version check if loaded account

View File

@ -224,7 +224,12 @@ class NativeX(private val mCtx: MainActivity) {
val parts = path.split("/").toTypedArray() val parts = path.split("/").toTypedArray()
return if (path.matches(API.LOGIN)) { 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)) { } else if (path.matches(API.DAYS)) {
makeResponse(query.getDays()) makeResponse(query.getDays())
} else if (path.matches(API.DAY)) { } else if (path.matches(API.DAY)) {

View File

@ -17,17 +17,19 @@ class AccountService(private val mCtx: MainActivity, private val mHttp: HttpServ
} }
private val store = SecureStorage(mCtx) private val store = SecureStorage(mCtx)
private var mTrustAll = false
/** /**
* Make the first request to log in * Make the first request to log in
* @param url The URL of the Nextcloud server * @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 { try {
Log.v(TAG, "login: Connecting to ${url}api/describe") mTrustAll = trustAll
mHttp.setBaseUrl(url) mHttp.build(url, trustAll)
val res = mHttp.getApiDescription()
val res = mHttp.getApiDescription()
if (res.code != 200) { if (res.code != 200) {
throw Exception("${url}api/describe (status ${res.code})") 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 * @param password The password to store
*/ */
fun storeCredentials(url: String, user: String, password: String) { fun storeCredentials(url: String, user: String, password: String) {
store.saveCredentials(url, user, password) store.saveCredentials(Credential(
mHttp.setBaseUrl(url) url = url,
mHttp.setAuthHeader(Pair(user, password)) trustAll = mTrustAll,
} username = user,
token = password,
/** ))
* Get the stored credentials refreshCredentials()
* @return The stored credentials
*/
fun getCredentials(): Pair<String, String>? {
val saved = store.getCredentials()
if (saved == null) return null
mHttp.setBaseUrl(saved.first)
return Pair(saved.second, saved.third)
} }
/** /**
* Delete the stored credentials * Delete the stored credentials
*/ */
fun deleteCredentials() { fun deleteCredentials() {
mHttp.setAuthHeader(null)
mHttp.setBaseUrl(null)
store.deleteCredentials() store.deleteCredentials()
mHttp.setAuthHeader(null)
mHttp.build(null, false)
} }
/** /**
* Refresh the authorization header * Refresh the authorization header
*/ */
fun refreshAuthHeader() { fun refreshCredentials() {
mHttp.setAuthHeader(getCredentials()) val cred = store.getCredentials() ?: return
mHttp.build(cred.url, cred.trustAll)
mHttp.setAuthHeader(Pair(cred.username, cred.token))
} }
/** /**

View File

@ -0,0 +1,8 @@
package gallery.memories.service
data class Credential(
var url: String,
var trustAll: Boolean,
var username: String,
var token: String,
)

View File

@ -10,15 +10,22 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject 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 { class HttpService {
companion object { companion object {
val TAG = HttpService::class.java.simpleName val TAG = HttpService::class.java.simpleName
} }
private val client = OkHttpClient() private var client = OkHttpClient()
private var authHeader: String? = null private var authHeader: String? = null
private var memoriesUrl: String? = null private var baseUrl: String? = null
/** /**
* Check if the HTTP service is logged in * 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 url The URL to use
* @param trustAll Whether to trust all certificates
*/ */
fun setBaseUrl(url: String?) { fun build(url: String?, trustAll: Boolean) {
memoriesUrl = url baseUrl = url
client = if (trustAll) {
val trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
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? { fun loadWebView(webView: WebView, subpath: String? = null): String? {
// Load app interface if authenticated // Load app interface if authenticated
if (authHeader != null && memoriesUrl != null) { if (authHeader != null && baseUrl != null) {
var url = memoriesUrl var url = baseUrl
if (subpath != null) url += subpath if (subpath != null) url += subpath
// Get host name // Get host name
@ -104,20 +145,24 @@ class HttpService {
/** Make login flow request */ /** Make login flow request */
@Throws(Exception::class) @Throws(Exception::class)
fun postLoginFlow(loginFlowUrl: String): Response { fun postLoginFlow(loginFlowUrl: String): Response {
return runRequest(Request.Builder() return runRequest(
.url(loginFlowUrl) Request.Builder()
.header("User-Agent", "Memories") .url(loginFlowUrl)
.post("".toRequestBody("application/json".toMediaTypeOrNull())) .header("User-Agent", "Memories")
.build()) .post("".toRequestBody("application/json".toMediaTypeOrNull()))
.build()
)
} }
/** Make login polling request */ /** Make login polling request */
@Throws(Exception::class) @Throws(Exception::class)
fun getPollLogin(pollUrl: String, pollToken: String): Response { fun getPollLogin(pollUrl: String, pollToken: String): Response {
return runRequest(Request.Builder() return runRequest(
.url(pollUrl) Request.Builder()
.post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull())) .url(pollUrl)
.build()) .post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull()))
.build()
)
} }
/** Run a request and get a JSON object */ /** Run a request and get a JSON object */
@ -129,7 +174,7 @@ class HttpService {
/** Build a GET request */ /** Build a GET request */
private fun buildGet(path: String, auth: Boolean = true): Request { private fun buildGet(path: String, auth: Boolean = true): Request {
val builder = Request.Builder() val builder = Request.Builder()
.url(memoriesUrl + path) .url(baseUrl + path)
.header("User-Agent", "Memories") .header("User-Agent", "Memories")
.get() .get()

View File

@ -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_DECRYPT
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
import android.util.Base64 import android.util.Base64
import gallery.memories.service.Credential
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.KeyGenerator 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() val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) 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() context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit()
.putString("url", url) .putString("url", cred.url)
.putString("username", username) .putBoolean("trustAll", cred.trustAll)
.putString("username", cred.username)
.putString("encryptedToken", Base64.encodeToString(encryptedToken, Base64.DEFAULT)) .putString("encryptedToken", Base64.encodeToString(encryptedToken, Base64.DEFAULT))
.putString("iv", Base64.encodeToString(cipher.iv, Base64.DEFAULT)) .putString("iv", Base64.encodeToString(cipher.iv, Base64.DEFAULT))
.apply() .apply()
} }
fun getCredentials(): Triple<String, String, String>? { fun getCredentials(): Credential? {
val sharedPreferences = context.getSharedPreferences("credentials", Context.MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences("credentials", Context.MODE_PRIVATE)
val url = sharedPreferences.getString("url", null) val url = sharedPreferences.getString("url", null)
val trustAll = sharedPreferences.getBoolean("trustAll", false)
val username = sharedPreferences.getString("username", null) val username = sharedPreferences.getString("username", null)
val encryptedToken = sharedPreferences.getString("encryptedToken", null) val encryptedToken = sharedPreferences.getString("encryptedToken", null)
val ivStr = sharedPreferences.getString("iv", null) val ivStr = sharedPreferences.getString("iv", null)
if (url != null && username != null && encryptedToken != null && ivStr != null) { if (url != null && username != null && encryptedToken != null && ivStr != null) {
val iv = Base64.decode(ivStr, Base64.DEFAULT)
val cipher = getCipher() val cipher = getCipher()
val iv = Base64.decode(ivStr, Base64.DEFAULT)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IvParameterSpec(iv))
val token = String(cipher.doFinal(Base64.decode(encryptedToken, Base64.DEFAULT))) val token = String(cipher.doFinal(Base64.decode(encryptedToken, Base64.DEFAULT)))
return Triple(url, username, token) return Credential(url, trustAll, username, token)
} }
return null return null
@ -59,6 +61,7 @@ class SecureStorage(private val context: Context) {
fun deleteCredentials() { fun deleteCredentials() {
context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit() context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit()
.remove("url") .remove("url")
.remove("trustAll")
.remove("encryptedUsername") .remove("encryptedUsername")
.remove("encryptedToken") .remove("encryptedToken")
.remove("iv") .remove("iv")