From 105ad0b2dd52579d16dc7c04b31acfeea1b33ab4 Mon Sep 17 00:00:00 2001 From: hewigovens <360470+hewigovens@users.noreply.github.com> Date: Sun, 12 Sep 2021 16:14:21 +0800 Subject: [PATCH] Implement message signing for android demo (#177) * add android small demo * update for comment suggestion * consolidate configuration info * Update android/app/build.gradle * implement personal / typed sign * Add unknown method Co-authored-by: Dougie Co-authored-by: Viktor Radchenko <1641795+vikmeup@users.noreply.github.com> --- README.md | 2 +- android/app/build.gradle | 9 +- android/app/src/main/AndroidManifest.xml | 1 + .../src/main/java/com/trust/web3/demo/App.kt | 9 ++ .../java/com/trust/web3/demo/DAppMethod.kt | 29 ++++ .../java/com/trust/web3/demo/MainActivity.kt | 46 ++++--- .../main/java/com/trust/web3/demo/Numeric.kt | 65 +++++++++ .../com/trust/web3/demo/WebAppInterface.kt | 127 ++++++++++++++++-- .../com/trust/web3/demo/WebViewExtension.kt | 29 ++++ android/build.gradle | 20 ++- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/lib/build.gradle | 2 +- 12 files changed, 300 insertions(+), 41 deletions(-) create mode 100644 android/app/src/main/java/com/trust/web3/demo/App.kt create mode 100644 android/app/src/main/java/com/trust/web3/demo/DAppMethod.kt create mode 100644 android/app/src/main/java/com/trust/web3/demo/Numeric.kt create mode 100644 android/app/src/main/java/com/trust/web3/demo/WebViewExtension.kt diff --git a/README.md b/README.md index 92539b12..b479e896 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Step 2. Add the dependency ```groovy dependencies { - implementation 'com.github.TrustWallet:trust-web3-provider:1.0.4' + implementation 'com.github.trustwallet:trust-web3-provider:TAG' } ``` diff --git a/android/app/build.gradle b/android/app/build.gradle index 59bdbd64..c0a364d4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.trust.web3.demo" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -33,13 +33,16 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation project(path: ':lib') + + implementation "com.louiscad.splitties:splitties-alertdialog-material:3.0.0" + implementation "com.trustwallet:wallet-core:2.6.23" + + implementation 'com.github.trustwallet:trust-web3-provider:1.0.5' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 837b9dcb..ba4431bd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:name=".App" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MyApplication"> diff --git a/android/app/src/main/java/com/trust/web3/demo/App.kt b/android/app/src/main/java/com/trust/web3/demo/App.kt new file mode 100644 index 00000000..4ed5f201 --- /dev/null +++ b/android/app/src/main/java/com/trust/web3/demo/App.kt @@ -0,0 +1,9 @@ +package com.trust.web3.demo + +import android.app.Application + +class App: Application() { + init { + System.loadLibrary("TrustWalletCore") + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/trust/web3/demo/DAppMethod.kt b/android/app/src/main/java/com/trust/web3/demo/DAppMethod.kt new file mode 100644 index 00000000..9a335695 --- /dev/null +++ b/android/app/src/main/java/com/trust/web3/demo/DAppMethod.kt @@ -0,0 +1,29 @@ +package com.trust.web3.demo + +enum class DAppMethod { + SIGNTRANSACTION, + SIGNPERSONALMESSAGE, + SIGNMESSAGE, + SIGNTYPEDMESSAGE, + ECRECOVER, + REQUESTACCOUNTS, + WATCHASSET, + ADDETHEREUMCHAIN, + UNKNOWN; + + companion object { + fun fromValue(value: String): DAppMethod { + return when (value) { + "signTransaction" -> SIGNTRANSACTION + "signPersonalMessage" -> SIGNPERSONALMESSAGE + "signMessage" -> SIGNMESSAGE + "signTypedMessage" -> SIGNTYPEDMESSAGE + "ecRecover" -> ECRECOVER + "requestAccounts" -> REQUESTACCOUNTS + "watchAsset" -> WATCHASSET + "addEthereumChain" -> ADDETHEREUMCHAIN + else -> UNKNOWN + } + } + } +} diff --git a/android/app/src/main/java/com/trust/web3/demo/MainActivity.kt b/android/app/src/main/java/com/trust/web3/demo/MainActivity.kt index 6045b06c..de7dc84a 100644 --- a/android/app/src/main/java/com/trust/web3/demo/MainActivity.kt +++ b/android/app/src/main/java/com/trust/web3/demo/MainActivity.kt @@ -1,43 +1,53 @@ package com.trust.web3.demo -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.webkit.WebView import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { + companion object { + private const val DAPP_URL = "https://js-eth-sign.surge.sh" + private const val CHAIN_ID = 56 + private const val RPC_URL = "https://bsc-dataseed2.binance.org" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) val provderJs = loadProviderJs() val initJs = loadInitJs( - 1, - "https://mainnet.infura.io/v3/6e822818ec644335be6f0ed231f48310" + CHAIN_ID, + RPC_URL ) - println("file lenght: ${provderJs.length}") WebView.setWebContentsDebuggingEnabled(true) val webview: WebView = findViewById(R.id.webview) - webview.settings.javaScriptEnabled = true - webview.addJavascriptInterface(WebAppInterface(webview), "_tw_") - - val webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - println("loaded: ${url}") - view?.evaluateJavascript(provderJs, null) - view?.evaluateJavascript(initJs, null) + webview.settings.run { + javaScriptEnabled = true + domStorageEnabled = true + } + WebAppInterface(this, webview, DAPP_URL).run { + webview.addJavascriptInterface(this, "_tw_") + + val webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.evaluateJavascript(provderJs, null) + view?.evaluateJavascript(initJs, null) + } } + webview.webViewClient = webViewClient + webview.loadUrl(DAPP_URL) } - webview.webViewClient = webViewClient - webview.loadUrl("https://js-eth-sign.surge.sh") } - fun loadProviderJs(): String { - return resources.openRawResource(R.raw.trust).bufferedReader().use { it.readText() } + private fun loadProviderJs(): String { + return resources.openRawResource(R.raw.trust_min).bufferedReader().use { it.readText() } } - fun loadInitJs(chainId: Int, rpcUrl: String): String { + private fun loadInitJs(chainId: Int, rpcUrl: String): String { val source = """ (function() { var config = { diff --git a/android/app/src/main/java/com/trust/web3/demo/Numeric.kt b/android/app/src/main/java/com/trust/web3/demo/Numeric.kt new file mode 100644 index 00000000..dfb25a98 --- /dev/null +++ b/android/app/src/main/java/com/trust/web3/demo/Numeric.kt @@ -0,0 +1,65 @@ +package com.trust.web3.demo + +import kotlin.experimental.and + +object Numeric { + fun containsHexPrefix(input: String): Boolean { + return input.length > 1 && input[0] == '0' && input[1] == 'x' + } + + fun cleanHexPrefix(input: String): String { + return if (containsHexPrefix(input)) { + input.substring(2) + } else { + input + } + } + + fun hexStringToByteArray(input: String): ByteArray { + val cleanInput = cleanHexPrefix(input) + + val len = cleanInput.length + + if (len == 0) { + return byteArrayOf() + } + + val data: ByteArray + val startIdx: Int + if (len % 2 != 0) { + data = ByteArray(len / 2 + 1) + data[0] = Character.digit(cleanInput[0], 16).toByte() + startIdx = 1 + } else { + data = ByteArray(len / 2) + startIdx = 0 + } + + var i = startIdx + while (i < len) { + data[(i + 1) / 2] = + ((Character.digit(cleanInput[i], 16) shl 4) + Character.digit( + cleanInput[i + 1], + 16 + )).toByte() + i += 2 + } + return data + } + + fun toHexString(input: ByteArray?, offset: Int, length: Int, withPrefix: Boolean): String { + val stringBuilder = StringBuilder() + if (withPrefix) { + stringBuilder.append("0x") + } + for (i in offset until offset + length) { + stringBuilder.append(String.format("%02x", input!![i] and 0xFF.toByte())) + } + + return stringBuilder.toString() + } + + fun toHexString(input: ByteArray?): String { + return toHexString(input, 0, input!!.size, true) + } +} diff --git a/android/app/src/main/java/com/trust/web3/demo/WebAppInterface.kt b/android/app/src/main/java/com/trust/web3/demo/WebAppInterface.kt index 84c2a859..9cc341d8 100644 --- a/android/app/src/main/java/com/trust/web3/demo/WebAppInterface.kt +++ b/android/app/src/main/java/com/trust/web3/demo/WebAppInterface.kt @@ -3,28 +3,127 @@ package com.trust.web3.demo import android.content.Context import android.webkit.JavascriptInterface import android.webkit.WebView -import android.widget.Toast import org.json.JSONObject +import splitties.alertdialog.appcompat.cancelButton +import splitties.alertdialog.appcompat.message +import splitties.alertdialog.appcompat.okButton +import splitties.alertdialog.appcompat.title +import splitties.alertdialog.material.materialAlertDialog +import wallet.core.jni.CoinType +import wallet.core.jni.Curve +import wallet.core.jni.PrivateKey + +class WebAppInterface( + private val context: Context, + private val webView: WebView, + private val dappUrl: String +) { + private val privateKey = + PrivateKey("0x4646464646464646464646464646464646464646464646464646464646464646".toHexByteArray()) + private val addr = CoinType.ETHEREUM.deriveAddress(privateKey).toLowerCase() -class WebAppInterface(private val context: WebView) { @JavascriptInterface fun postMessage(json: String) { val obj = JSONObject(json) println(obj) - val id = obj["id"] - val addr = "0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1" - - when(obj["name"]) { - "requestAccounts" -> { - val callback = "window.ethereum.sendResponse($id, [\"$addr\"])" - context.post { - context.evaluateJavascript(callback) { value -> - println(value) + val id = obj.getLong("id") + val method = DAppMethod.fromValue(obj.getString("name")) + when (method) { + DAppMethod.REQUESTACCOUNTS -> { + context.materialAlertDialog { + title = "Request Accounts" + message = "${dappUrl} requests your address" + okButton { + val setAddress = "window.ethereum.setAddress(\"$addr\");" + val callback = "window.ethereum.sendResponse($id, [\"$addr\"])" + webView.post { + webView.evaluateJavascript(setAddress) { + // ignore + } + webView.evaluateJavascript(callback) { value -> + println(value) + } + } + } + cancelButton() + }.show() + } + DAppMethod.SIGNMESSAGE -> { + val data = extractMessage(obj) + handleSignMessage(id, data, addPrefix = false) + } + DAppMethod.SIGNPERSONALMESSAGE -> { + val data = extractMessage(obj) + handleSignMessage(id, data, addPrefix = true) + } + DAppMethod.SIGNTYPEDMESSAGE -> { + val data = extractMessage(obj) + val raw = extractRaw(obj) + handleSignTypedMessage(id, data, raw) + } + else -> { + context.materialAlertDialog { + title = "Error" + message = "$method not implemented" + okButton { } - } + }.show() } - // handle other methods here - // signTransaction, signMessage, ecRecover, watchAsset, addEthereumChain } } + + private fun extractMessage(json: JSONObject): ByteArray { + val param = json.getJSONObject("object") + val data = param.getString("data") + return Numeric.hexStringToByteArray(data) + } + + private fun extractRaw(json: JSONObject): String { + val param = json.getJSONObject("object") + return param.getString("raw") + } + + private fun handleSignMessage(id: Long, data: ByteArray, addPrefix: Boolean) { + context.materialAlertDialog { + title = "Sign Message" + message = if (addPrefix) String(data, Charsets.UTF_8) else Numeric.toHexString(data) + cancelButton { + webView.sendError("Cancel", id) + } + okButton { + webView.sendResult(signEthereumMessage(data, addPrefix), id) + } + }.show() + } + + private fun handleSignTypedMessage(id: Long, data: ByteArray, raw: String) { + context.materialAlertDialog { + title = "Sign Typed Message" + message = raw + cancelButton { + webView.sendError("Cancel", id) + } + okButton { + webView.sendResult(signEthereumMessage(data, false), id) + } + }.show() + } + + private fun signEthereumMessage(message: ByteArray, addPrefix: Boolean): String { + var data = message + if (addPrefix) { + val messagePrefix = "\u0019Ethereum Signed Message:\n" + val prefix = (messagePrefix + message.size).toByteArray() + val result = ByteArray(prefix.size + message.size) + System.arraycopy(prefix, 0, result, 0, prefix.size) + System.arraycopy(message, 0, result, prefix.size, message.size) + data = wallet.core.jni.Hash.keccak256(result) + } + + val signatureData = privateKey.sign(data, Curve.SECP256K1) + .apply { + (this[this.size - 1]) = (this[this.size - 1] + 27).toByte() + } + return Numeric.toHexString(signatureData) + } } diff --git a/android/app/src/main/java/com/trust/web3/demo/WebViewExtension.kt b/android/app/src/main/java/com/trust/web3/demo/WebViewExtension.kt new file mode 100644 index 00000000..72df2195 --- /dev/null +++ b/android/app/src/main/java/com/trust/web3/demo/WebViewExtension.kt @@ -0,0 +1,29 @@ +package com.trust.web3.demo + +import android.webkit.WebView + +fun WebView.sendError(message: String, methodId: Long) { + val script = "window.ethereum.sendError($methodId, \"$message\")" + this.post { + this.evaluateJavascript(script) {} + } +} + +fun WebView.sendResult(message: String, methodId: Long) { + val script = "window.ethereum.sendResponse($methodId, \"$message\")" + this.post { + this.evaluateJavascript(script) {} + } +} + +fun WebView.sendResults(messages: List, methodId: Long) { + val message = messages.joinToString(separator = ",") + val script = "window.ethereum.sendResponse($methodId, \"$message\")" + this.post { + this.evaluateJavascript(script) {} + } +} + +fun String.toHexByteArray(): ByteArray { + return Numeric.hexStringToByteArray(this) +} diff --git a/android/build.gradle b/android/build.gradle index 691b4272..f0c758d3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,10 +3,12 @@ buildscript { ext.kotlin_version = "1.4.31" repositories { google() - jcenter() + mavenCentral() + + maven { url 'https://jitpack.io' } } dependencies { - classpath "com.android.tools.build:gradle:4.1.2" + classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } @@ -15,7 +17,19 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() + + maven { url 'https://jitpack.io' } + + maven { + url = uri("https://maven.pkg.github.com/trustwallet/wallet-core") + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + credentials { + username = properties.getProperty("gpr.user") + password = properties.getProperty("gpr.key") + } + } } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c1f772e..a4312440 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip diff --git a/android/lib/build.gradle b/android/lib/build.gradle index d5d0ab58..24834d95 100644 --- a/android/lib/build.gradle +++ b/android/lib/build.gradle @@ -15,7 +15,7 @@ android { compileSdkVersion 28 defaultConfig { - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 28 versionCode 1 project.archivesBaseName = getArtificatId()