Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation "androidx.media3:media3-exoplayer:$media_version"
implementation "androidx.media3:media3-ui:$media_version"
implementation "androidx.media3:media3-exoplayer-hls:$media_version"
implementation "androidx.media3:media3-datasource-okhttp:$media_version"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
</intent-filter>
</activity>

<activity android:name=".SelectClientCertificateHelperActivity" />

<receiver
android:name=".service.DownloadBroadcastReceiver"
android:exported="true">
Expand Down
28 changes: 26 additions & 2 deletions android/app/src/main/assets/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
</label>
</div>

<button class="m-button" id="client-cert">
Select client certificate
</button>

<button class="m-button login-button" id="login">
Continue to Login
</button>
Expand All @@ -45,6 +49,8 @@
<script>
const urlBox = document.getElementById("server-url");
const loginButton = document.getElementById("login");
const clientCertButton = document.getElementById("client-cert");
const trustAllCheckbox = document.getElementById("trust-all");

function validateUrl(url) {
try {
Expand All @@ -66,7 +72,10 @@
}

function updateLoginEnabled() {
loginButton.disabled = !validateUrl(getUrl());
const validUrl = validateUrl(getUrl());
loginButton.disabled = !validUrl;
const trustAll = trustAllCheckbox.checked;
clientCertButton.disabled = !validUrl || trustAll;
}

function getMemoriesUrl() {
Expand Down Expand Up @@ -106,7 +115,7 @@
const encUrl = encodeURIComponent(encodeURIComponent(getMemoriesUrl().toString()));

// Trust all certificates
const trustAll = document.getElementById("trust-all").checked ? "1" : "0";
const trustAll = trustAllCheckbox.checked ? "1" : "0";

await fetch(`http://127.0.0.1/api/login/${encUrl}?trustAll=${trustAll}`, {
method: "GET",
Expand All @@ -123,6 +132,21 @@
}
});

// ClientCert button click handler
clientCertButton.addEventListener("click", async () => {
try {
const url = getUrl();
// go to URL to have WebView and AdvancedX509KeyManager
// handle certificate selection
globalThis.nativex?.requestClientCertFor(url)
} catch (e) {
}
});

trustAllCheckbox.addEventListener("click", async() => {
updateLoginEnabled();
});

// Set action bar color
const themeColor = getComputedStyle(
document.documentElement
Expand Down
40 changes: 37 additions & 3 deletions android/app/src/main/java/gallery/memories/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.webkit.ClientCertRequest
import android.webkit.CookieManager
import android.webkit.PermissionRequest
import android.webkit.SslErrorHandler
Expand All @@ -34,11 +35,12 @@ import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import gallery.memories.databinding.ActivityMainBinding
import gallery.memories.network.AdvancedX509KeyManager
import java.util.concurrent.Executors


Expand Down Expand Up @@ -214,6 +216,38 @@ class MainActivity : AppCompatActivity() {
super.onReceivedSslError(view, handler, error)
}
}

/**
* Handle request for a TLS client certificate.
*/
override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest?) {
if (view == null || request == null) {
return
}
AdvancedX509KeyManager(view.context).handleWebViewClientCertRequest(request)
}

/**
* Handle HTTP errors.
*
* We might receive an HTTP status code 400 (bad request), which probably tells us that our certificate
* is not valid (anymore), e.g. because it expired. In that case we forget the selected client certificate,
* so it can be re-selected.
*/
override fun onReceivedHttpError(
view: WebView?,
request: WebResourceRequest?,
errorResponse: WebResourceResponse?
) {
val errorCode = errorResponse?.statusCode ?: return
if (errorCode == 400) {
Log.w(TAG, "WebView failed with error code $errorCode; remove key chain aliases")
// chosen client certificate alias does not seem to work -> discard it
val failingUrl = request?.url ?: return
val context = view?.context ?: return
AdvancedX509KeyManager(context).removeKeys(failingUrl)
}
}
}

// Use the web chrome client to handle file uploads
Expand Down Expand Up @@ -331,9 +365,9 @@ class MainActivity : AppCompatActivity() {
// Add cookies from webview to data source
val cookies = CookieManager.getInstance().getCookie(uri.toString())
val httpDataSourceFactory =
DefaultHttpDataSource.Factory()
OkHttpDataSource.Factory(nativex.http.client)
.setDefaultRequestProperties(mapOf("cookie" to cookies))
.setAllowCrossProtocolRedirects(true)
// .setAllowCrossProtocolRedirects(true)
val dataSourceFactory =
DefaultDataSource.Factory(this, httpDataSourceFactory)

Expand Down
14 changes: 14 additions & 0 deletions android/app/src/main/java/gallery/memories/NativeX.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
import gallery.memories.network.AdvancedX509KeyManager
import gallery.memories.service.AccountService
import gallery.memories.service.DownloadService
import gallery.memories.service.HttpService
Expand Down Expand Up @@ -183,6 +184,19 @@ class NativeX(private val mCtx: MainActivity) {
}
}

@JavascriptInterface
fun requestClientCertFor(url: String) {
Log.v(TAG, "requestClientCertFor: url=$url")
// TODO: URL sanity check! (no script, etc.)
// drop old certificate
AdvancedX509KeyManager(mCtx).removeKeys(url)
// navigate WebView to given URL to handle requests for
// TLS client certificate
mCtx.runOnUiThread {
mCtx.binding.webview.loadUrl(url)
}
}

fun handleRequest(request: WebResourceRequest): WebResourceResponse {
val path = request.url.path ?: return makeErrorResponse()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Nextcloud Android Library
*
* SPDX-FileCopyrightText: 2023 Elv1zz <elv1zz.git@gmail.com>
* SPDX-License-Identifier: MIT
*/
package gallery.memories;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Build;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.util.Log;
import androidx.annotation.Nullable;
import gallery.memories.network.AdvancedX509KeyManager;

public class SelectClientCertificateHelperActivity extends Activity implements KeyChainAliasCallback {

private static final String TAG = SelectClientCertificateHelperActivity.class.getName();

private static final int REQ_CODE_INSTALL_CERTS = 1;

private int decisionId;
private String hostname;
private int port;

private Dialog installCertsDialog = null;

@Override
public void onResume() {
super.onResume();
// Load data from intent
Intent i = getIntent();
decisionId = i.getIntExtra(AdvancedX509KeyManager.DECISION_INTENT_ID, AdvancedX509KeyManager.AKMDecision.DECISION_INVALID);
hostname = i.getStringExtra(AdvancedX509KeyManager.DECISION_INTENT_HOSTNAME);
port = i.getIntExtra(AdvancedX509KeyManager.DECISION_INTENT_PORT, -1);
Log.d(TAG, "onResume() with " + i.getExtras() + " decId=" + decisionId + " data=" + i.getData());
if (installCertsDialog == null) {
KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
}
}

/**
* Called with the alias of the certificate chosen by the user, or null if no value was chosen.
*
* @param alias The alias of the certificate chosen by the user, or null if no value was chosen.
*/
@Override
public void alias(@Nullable String alias) {
// Show a dialog to add a certificate if no certificate was found
// API Versions < 29 still handle this automatically
if (alias == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
runOnUiThread(() -> {
installCertsDialog = new AlertDialog.Builder(this)
.setTitle(R.string.title_no_client_cert)
.setMessage(R.string.message_install_client_cert)
.setPositiveButton(
android.R.string.yes,
(dialog, which) -> startActivityForResult(KeyChain.createInstallIntent(), REQ_CODE_INSTALL_CERTS)
)
.setNegativeButton(android.R.string.no, (dialog, which) -> {
dialog.dismiss();
sendDecision(AdvancedX509KeyManager.AKMDecision.DECISION_ABORT, null);
})
.create();
installCertsDialog.show();
});
} else {
sendDecision(AdvancedX509KeyManager.AKMDecision.DECISION_KEYCHAIN, alias);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_CODE_INSTALL_CERTS) {
installCertsDialog = null;
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}

/**
* Stop the user interaction and send result to invoking AdvancedX509KeyManager.
*
* @param state type of the result as defined in AKMDecision
* @param param keychain alias respectively keystore filename
*/
void sendDecision(int state, String param) {
Log.d(TAG, "sendDecision(" + state + ", " + param + ", " + hostname + ", " + port + ")");
AdvancedX509KeyManager.interactResult(decisionId, state, param, hostname, port);
finish();
}
}
Loading