diff --git a/wrapper/src/main/assets/injectjs.js b/wrapper/src/main/assets/injectjs.js index 0f4b924..73d20bf 100644 --- a/wrapper/src/main/assets/injectjs.js +++ b/wrapper/src/main/assets/injectjs.js @@ -9,6 +9,10 @@ // check if replaced in Android: if not defined, throws an 'ReferenceError'. JAVASCRIPT_BRIDGE +// Inject CSS class as early as possible to indicate to the web app, it's +// running inside the wrapper. +document.getElementsByTagName("html")[0].classList.add("is-wrapper-app"); + // optionally replaced in android: if not defined no visualization visualize = (typeof JAVASCRIPT_VISUALIZE_INJECTION) !== 'undefined' ? JAVASCRIPT_VISUALIZE_INJECTION : false @@ -73,92 +77,115 @@ JAVASCRIPT_BRIDGE.overrideHints = function(newHints) { // override functions on navigator function overrideNavigatorCredentialsWithBridgeCall(method) { + + JAVASCRIPT_BRIDGE[method + "Wrapped"] = navigator.credentials[method].bind(navigator.credentials); + navigator.credentials[method] = (options) => { - var uuid = crypto.randomUUID() + console.log("Executing " + method + " with request: ", options); - var promise = new Promise((resolve, reject) => { - JAVASCRIPT_BRIDGE.__promise_cache__[uuid] = {'resolve':resolve, 'reject':reject, 'method': method} + if ( + !("publicKey" in options) + || !("hints" in options.publicKey) + || !Array.isArray(options.publicKey.hints) + || !options.publicKey.hints.includes("security-key") + ) { + console.log("Forward to OS, because no security-key hint contained.") - if (JAVASCRIPT_BRIDGE.__override_hints.length > 0) { - options.publicKey['hints'] = JAVASCRIPT_BRIDGE.__override_hints + return JAVASCRIPT_BRIDGE[method + "Wrapped"](options); } - if (options.publicKey.hasOwnProperty('challenge')) { - options.publicKey.challenge = __encode(options.publicKey.challenge) - } + var uuid = crypto.randomUUID() - if (options.publicKey.hasOwnProperty('user') && options.publicKey.user.hasOwnProperty('id')) { - options.publicKey.user.id = __encode(options.publicKey.user.id) - } + var promise = new Promise((resolve, reject) => { + JAVASCRIPT_BRIDGE.__promise_cache__[uuid] = {'resolve':resolve, 'reject':reject, 'method': method} - if (options.publicKey.hasOwnProperty('allowCredentials')) { - var allowed = options.publicKey.allowCredentials - for(var i = 0; i < allowed.length; ++i) { - allowed[i].id = __encode(allowed[i].id); + if (JAVASCRIPT_BRIDGE.__override_hints.length > 0) { + options.publicKey['hints'] = JAVASCRIPT_BRIDGE.__override_hints } - } - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('prf') && - options.publicKey.extensions.prf.hasOwnProperty('eval') && - options.publicKey.extensions.prf.eval.hasOwnProperty('first') ) { - options.publicKey.extensions.prf.eval.first = __encode(options.publicKey.extensions.prf.eval.first) - } + if (options.publicKey.hasOwnProperty('challenge')) { + options.publicKey.challenge = __encode(options.publicKey.challenge) + } - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('prf') && - options.publicKey.extensions.prf.hasOwnProperty('evalByCredential') ) { - for (const k of Object.keys(options.publicKey.extensions.prf.evalByCredential)) { - if (options.publicKey.extensions.prf.evalByCredential[k].hasOwnProperty('first')) { - options.publicKey.extensions.prf.evalByCredential[k].first = __encode( - options.publicKey.extensions.prf.evalByCredential[k].first - ) + if (options.publicKey.hasOwnProperty('user') && options.publicKey.user.hasOwnProperty('id')) { + options.publicKey.user.id = __encode(options.publicKey.user.id) + } + + if (options.publicKey.hasOwnProperty('allowCredentials')) { + var allowed = options.publicKey.allowCredentials + for(var i = 0; i < allowed.length; ++i) { + allowed[i].id = __encode(allowed[i].id); } - if (options.publicKey.extensions.prf.evalByCredential[k].hasOwnProperty('second')) { - options.publicKey.extensions.prf.evalByCredential[k].second = __encode( - options.publicKey.extensions.prf.evalByCredential[k].second - ) + } + + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('prf') && + options.publicKey.extensions.prf.hasOwnProperty('eval') && + options.publicKey.extensions.prf.eval.hasOwnProperty('first') ) + { + options.publicKey.extensions.prf.eval.first = __encode(options.publicKey.extensions.prf.eval.first) + } + + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('prf') && + options.publicKey.extensions.prf.hasOwnProperty('evalByCredential') ) + { + for (const k of Object.keys(options.publicKey.extensions.prf.evalByCredential)) { + if (options.publicKey.extensions.prf.evalByCredential[k].hasOwnProperty('first')) { + options.publicKey.extensions.prf.evalByCredential[k].first = __encode( + options.publicKey.extensions.prf.evalByCredential[k].first) + } + + if (options.publicKey.extensions.prf.evalByCredential[k].hasOwnProperty('second')) { + options.publicKey.extensions.prf.evalByCredential[k].second = __encode( + options.publicKey.extensions.prf.evalByCredential[k].second) + } } } - } - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('prf') && - options.publicKey.extensions.prf.hasOwnProperty('eval') && - options.publicKey.extensions.prf.eval.hasOwnProperty('second') ) { - options.publicKey.extensions.prf.eval.second = __encode(options.publicKey.extensions.prf.eval.second) - } + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('prf') && + options.publicKey.extensions.prf.hasOwnProperty('eval') && + options.publicKey.extensions.prf.eval.hasOwnProperty('second') ) + { + options.publicKey.extensions.prf.eval.second = __encode(options.publicKey.extensions.prf.eval.second) + } - // sign extension v3 https://yubicolabs.github.io/webauthn-sign-extension/3/#sctn-sign-extension - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('sign') && - options.publicKey.extensions.sign.hasOwnProperty('generateKey') && - options.publicKey.extensions.sign.generateKey.hasOwnProperty('tbs') ) { - options.publicKey.extensions.sign.generateKey.tbs = __encode(options.publicKey.extensions.sign.generateKey.tbs) - } - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('sign') && - options.publicKey.extensions.sign.hasOwnProperty('sign') && - options.publicKey.extensions.sign.sign.hasOwnProperty('tbs') ) { - options.publicKey.extensions.sign.sign.tbs = __encode(options.publicKey.extensions.sign.sign.tbs) - } - if (options.publicKey.hasOwnProperty('extensions') && - options.publicKey.extensions.hasOwnProperty('sign') && - options.publicKey.extensions.sign.hasOwnProperty('sign') && - options.publicKey.extensions.sign.sign.hasOwnProperty('keyHandleByCredential')) { - for (const k of Object.keys(options.publicKey.extensions.sign.sign.keyHandleByCredential)) { - options.publicKey.extensions.sign.sign.keyHandleByCredential[k] = __encode(options.publicKey.extensions.sign.sign.keyHandleByCredential[k]); - } - } + // sign extension v3 https://yubicolabs.github.io/webauthn-sign-extension/3/#sctn-sign-extension + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('sign') && + options.publicKey.extensions.sign.hasOwnProperty('generateKey') && + options.publicKey.extensions.sign.generateKey.hasOwnProperty('tbs') ) + { + options.publicKey.extensions.sign.generateKey.tbs = __encode(options.publicKey.extensions.sign.generateKey.tbs) + } + + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('sign') && + options.publicKey.extensions.sign.hasOwnProperty('sign') && + options.publicKey.extensions.sign.sign.hasOwnProperty('tbs') ) + { + options.publicKey.extensions.sign.sign.tbs = __encode(options.publicKey.extensions.sign.sign.tbs) + } + + if (options.publicKey.hasOwnProperty('extensions') && + options.publicKey.extensions.hasOwnProperty('sign') && + options.publicKey.extensions.sign.hasOwnProperty('sign') && + options.publicKey.extensions.sign.sign.hasOwnProperty('keyHandleByCredential')) + { + for (const k of Object.keys(options.publicKey.extensions.sign.sign.keyHandleByCredential)) { + options.publicKey.extensions.sign.sign.keyHandleByCredential[k] = __encode(options.publicKey.extensions.sign.sign.keyHandleByCredential[k]); + } + } - // call bridge, JAVASCRIPT_BRIDGE.__resolve__(uid, ..) or JAVASCRIPT_BRIDGE.__reject__(uid,..) will be called back from android. - var options_json = JSON.stringify(options, null, 4) - console.log('options:', options_json) - JAVASCRIPT_BRIDGE[method](uuid, options_json) - }) + // call bridge, JAVASCRIPT_BRIDGE.__resolve__(uid, ..) or JAVASCRIPT_BRIDGE.__reject__(uid,..) will be called back from android. + var options_json = JSON.stringify(options, null, 4) + console.log('options:', options_json) + JAVASCRIPT_BRIDGE[method](uuid, options_json) + }) - return promise + return promise } } diff --git a/wrapper/src/main/java/org/siros/wwwallet/MainActivity.kt b/wrapper/src/main/java/org/siros/wwwallet/MainActivity.kt index b7c5ac2..6da7df0 100644 --- a/wrapper/src/main/java/org/siros/wwwallet/MainActivity.kt +++ b/wrapper/src/main/java/org/siros/wwwallet/MainActivity.kt @@ -37,6 +37,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature import ch.qos.logback.classic.android.BasicLogcatConfigurator import org.siros.wwwallet.bluetooth.BleClientHandler import org.siros.wwwallet.bluetooth.BleServerHandler @@ -223,6 +225,22 @@ private fun createWebViewFactory( loadWithOverviewMode = true } + // This is needed in order to make WebView support navigator.credentials.get/create + // on its own. This way, we only need to intercept the calls with the `security-key` hint, not + // any others. + // See https://developer.android.com/identity/sign-in/credential-manager-webview + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_AUTHENTICATION)) { + WebSettingsCompat.setWebAuthenticationSupport( + webView.settings, + WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_FOR_APP) + + YOLOLogger.i(webView.tagForLog, + "Web authentication support enabled: ${WebSettingsCompat.getWebAuthenticationSupport(webView.settings)}") + } + else { + YOLOLogger.e(webView.tagForLog, "WebView does not support passkeys.") + } + webView.webViewClient = webViewClient webView.webChromeClient = webChromeClient diff --git a/wrapper/src/main/java/org/siros/wwwallet/bridging/WalletJsBridge.kt b/wrapper/src/main/java/org/siros/wwwallet/bridging/WalletJsBridge.kt index e097ab2..ba3f008 100644 --- a/wrapper/src/main/java/org/siros/wwwallet/bridging/WalletJsBridge.kt +++ b/wrapper/src/main/java/org/siros/wwwallet/bridging/WalletJsBridge.kt @@ -39,33 +39,19 @@ class WalletJsBridge( val jsonHints = publicKey.getJSONArray("hints") val hints = jsonHints.toList().mapNotNull { it as? String } - var selectedContainer: Container? = null - for (hint in hints) { - selectedContainer = - when (hint) { - "security-key" -> securityKeyCredentialsContainer - "client-device" -> clientDeviceCredentialsContainer - "hybrid" -> null // explicitly not supported - else -> { - // error case: unknown hint. - YOLOLogger.e(tagForLog, "Hint '$hint' not supported. Ignoring.") - null - } - } - - if (selectedContainer != null) { - break - } + if (hints.contains("security-key")) { + securityKeyCredentialsContainer + } + else { + clientDeviceCredentialsContainer } - - selectedContainer ?: securityKeyCredentialsContainer } catch (jsonException: JSONException) { Log.i( tagForLog, - "'hints' field in credential options not found, defaulting back to 'security-key'.", + "'hints' field in credential options not found, defaulting back to 'client-device'.", jsonException, ) - securityKeyCredentialsContainer + clientDeviceCredentialsContainer } /** diff --git a/wrapper/src/main/java/org/siros/wwwallet/webkit/WalletWebViewClient.kt b/wrapper/src/main/java/org/siros/wwwallet/webkit/WalletWebViewClient.kt index c03654a..1fc8391 100644 --- a/wrapper/src/main/java/org/siros/wwwallet/webkit/WalletWebViewClient.kt +++ b/wrapper/src/main/java/org/siros/wwwallet/webkit/WalletWebViewClient.kt @@ -46,11 +46,8 @@ class WalletWebViewClient( return super.shouldOverrideUrlLoading(view, request) } - override fun onPageFinished( - view: WebView, - url: String, - ) { - super.onPageFinished(view, url) + override fun onPageCommitVisible(view: WebView, url: String) { + super.onPageCommitVisible(view, url) view.evaluateJavascript("${WalletJsBridge.JAVASCRIPT_BRIDGE_NAME}.inject()") {} }