Add box for self-signed
parent
49f97e2895
commit
b0c5927d9b
|
@ -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;
|
||||||
}
|
}
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package gallery.memories.service
|
||||||
|
|
||||||
|
data class Credential(
|
||||||
|
var url: String,
|
||||||
|
var trustAll: Boolean,
|
||||||
|
var username: String,
|
||||||
|
var token: String,
|
||||||
|
)
|
|
@ -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(
|
||||||
|
Request.Builder()
|
||||||
.url(loginFlowUrl)
|
.url(loginFlowUrl)
|
||||||
.header("User-Agent", "Memories")
|
.header("User-Agent", "Memories")
|
||||||
.post("".toRequestBody("application/json".toMediaTypeOrNull()))
|
.post("".toRequestBody("application/json".toMediaTypeOrNull()))
|
||||||
.build())
|
.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(
|
||||||
|
Request.Builder()
|
||||||
.url(pollUrl)
|
.url(pollUrl)
|
||||||
.post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull()))
|
.post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull()))
|
||||||
.build())
|
.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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue