Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
163 changes: 95 additions & 68 deletions wrapper/src/main/assets/injectjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}
}

Expand Down
18 changes: 18 additions & 0 deletions wrapper/src/main/java/org/siros/wwwallet/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()") {}
}
Expand Down