From c55ee23b5cb5000ed5c95b2362dae1d78667408f Mon Sep 17 00:00:00 2001 From: hh Date: Tue, 17 May 2022 14:32:18 +0430 Subject: [PATCH 1/6] add web3 sign in --- app/build.gradle | 17 +- app/src/main/AndroidManifest.xml | 7 +- .../ai/elimu/crowdsource/BaseApplication.java | 148 ++++++++++- .../ai/elimu/crowdsource/MainActivity.java | 4 +- .../elimu/crowdsource/server/BridgeServer.kt | 100 ++++++++ ...oogleActivity.java => SignInActivity.java} | 229 +++++++++++++++++- .../elimu/crowdsource/util/EthersUtils.java | 63 +++++ .../util/SharedPreferencesHelper.java | 18 ++ .../layout/activity_sign_in_with_google.xml | 14 +- app/src/main/res/xml/network_config.xml | 8 + .../ai/elimu/crowdsource/ExampleUnitTest.java | 13 +- build.gradle | 6 +- 12 files changed, 610 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt rename app/src/main/java/ai/elimu/crowdsource/ui/authentication/{SignInWithGoogleActivity.java => SignInActivity.java} (51%) create mode 100644 app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java create mode 100644 app/src/main/res/xml/network_config.xml diff --git a/app/build.gradle b/app/build.gradle index 698a8b6..20e1321 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 32 @@ -44,7 +45,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:20.1.0' implementation 'com.google.android.material:material:1.5.0' - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation 'com.jakewharton.timber:timber:4.7.1' // AndroidX implementation 'androidx.appcompat:appcompat:1.2.0' @@ -58,9 +59,23 @@ dependencies { // Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.moshi:moshi-kotlin:1.13.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // Web3 + implementation 'com.github.mobilekosmos:kotlin-walletconnect-lib:0.9.9.8' + implementation 'org.java-websocket:Java-WebSocket:1.5.3' + implementation ('org.web3j:core:4.8.7-android'){ + exclude group: 'org.bouncycastle', module: '*' + } + + //1.5.0 is currently the latest stable version of AndroidX Core for Kotlin. + //If you already have "androidx.core:core" implemented, remove it. + implementation 'androidx.core:core-ktx:1.5.+' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.+' } + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1219675..9641450 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -11,8 +12,10 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" + android:networkSecurityConfig="@xml/network_config" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:targetApi="n"> @@ -24,7 +27,7 @@ - + diff --git a/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java b/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java index 1e7b4a3..b8bda2c 100644 --- a/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java +++ b/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java @@ -3,22 +3,57 @@ import android.app.Application; import android.util.Log; +import androidx.annotation.NonNull; + +import com.squareup.moshi.Moshi; +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory; + +import org.jetbrains.annotations.NotNull; +import org.walletconnect.Session; +import org.walletconnect.impls.FileWCSessionStore; +import org.walletconnect.impls.MoshiPayloadAdapter; +import org.walletconnect.impls.OkHttpTransport; +import org.walletconnect.impls.WCSession; +import org.walletconnect.impls.WCSessionStore; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Random; +import java.util.UUID; + +import ai.elimu.crowdsource.server.BridgeServer; import ai.elimu.crowdsource.util.SharedPreferencesHelper; import ai.elimu.crowdsource.util.VersionHelper; import ai.elimu.model.v2.enums.Language; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.internal.Intrinsics; +import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import timber.log.Timber; public class BaseApplication extends Application { + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + public static Session.FullyQualifiedConfig config; + public static Session session; + private static OkHttpClient client; + private static Moshi moshi; + private static BridgeServer bridge; + private static WCSessionStore storage; @Override public void onCreate() { Log.i(getClass().getName(), "onCreate"); super.onCreate(); - - // Log config - Timber.plant(new Timber.DebugTree()); + this.initMoshi(); + this.initClient(); + this.initBridge(); + this.initSessionStorage(); + if(BuildConfig.DEBUG){ + Timber.plant(new Timber.DebugTree()); + } Timber.i("onCreate"); VersionHelper.updateAppVersion(getApplicationContext()); @@ -48,4 +83,111 @@ public String getBaseUrl() { public String getRestUrl() { return getBaseUrl() + "/rest/v2"; } + + + private void initClient() { + OkHttpClient var10000 = (new OkHttpClient.Builder()).build(); + Intrinsics.checkNotNullExpressionValue(var10000, "OkHttpClient.Builder().build()"); + client = var10000; + } + + private void initMoshi() { + Moshi var10000 = (new com.squareup.moshi.Moshi.Builder()).add(new KotlinJsonAdapterFactory()).build(); + Intrinsics.checkNotNullExpressionValue(var10000, "Moshi.Builder().build()"); + moshi = var10000; + } + + private void initBridge() { + bridge = new BridgeServer(moshi); + bridge.start(); + } + + private void initSessionStorage() { + File tmp = new File(this.getCacheDir(), "session_store.json"); + try { + tmp.createNewFile(); + storage = new FileWCSessionStore(tmp, moshi); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + @NotNull + public static String bytesToHex(byte[] bytes) { + Intrinsics.checkNotNullParameter(bytes, "bytes"); + char[] hexChars = new char[bytes.length * 2]; + int j = 0; + + for (int var4 = bytes.length; j < var4; ++j) { + int v = bytes[j] & 255; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 15]; + } + + return new String(hexChars); + } + + public static void resetSession() throws Exception { + if (session != null) { + session.clearCallbacks(); + } + byte[] randomBytes = new byte[32]; + Random r = new Random(); + r.nextBytes(randomBytes); + String key = bytesToHex(randomBytes); + String uuid = UUID.randomUUID().toString(); + config = new Session.FullyQualifiedConfig(uuid, "http://localhost:" + BridgeServer.Companion.getPORT(), key, "wc", 1); +// config = new Session.FullyQualifiedConfig(uuid, "https://bridge.walletconnect.org", "f70af2060965927b7e709503e71b3cbf", "wc", 1); + session = new WCSession( + config, + new WrappedMoshiPayloadAdapter(new MoshiPayloadAdapter(moshi)), + storage, + new WrappedOkHttpTransportBuilder(new OkHttpTransport.Builder(client, moshi)), + new Session.PeerMeta( + "elimu.ai", + "Elimu Crowdsource App", + "Elimu Crowdsource App", + Collections.emptyList() + ), + null, + null + ); + session.offer(); + } + + static class WrappedMoshiPayloadAdapter implements Session.PayloadAdapter { + private final MoshiPayloadAdapter wrapped; + + public WrappedMoshiPayloadAdapter(MoshiPayloadAdapter wrapped) { + this.wrapped = wrapped; + } + + @NonNull + @Override + public Session.MethodCall parse(@NonNull String s, @NonNull String s1) { + return this.wrapped.parse(s, s1); + } + + @NonNull + @Override + public String prepare(@NonNull Session.MethodCall methodCall, @NonNull String s) { + return this.wrapped.prepare(methodCall, s); + } + } + + static class WrappedOkHttpTransportBuilder implements Session.Transport.Builder { + private final OkHttpTransport.Builder wrapped; + + public WrappedOkHttpTransportBuilder(OkHttpTransport.Builder wrapped) { + this.wrapped = wrapped; + } + + @NonNull + @Override + public Session.Transport build(@NonNull String s, @NonNull Function1 function1, @NonNull Function1 function11) { + return this.wrapped.build(s, function1, function11); + } + } } diff --git a/app/src/main/java/ai/elimu/crowdsource/MainActivity.java b/app/src/main/java/ai/elimu/crowdsource/MainActivity.java index 67bd8a6..fb329a1 100644 --- a/app/src/main/java/ai/elimu/crowdsource/MainActivity.java +++ b/app/src/main/java/ai/elimu/crowdsource/MainActivity.java @@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity; -import ai.elimu.crowdsource.ui.authentication.SignInWithGoogleActivity; +import ai.elimu.crowdsource.ui.authentication.SignInActivity; import ai.elimu.crowdsource.ui.language.SelectLanguageActivity; import ai.elimu.crowdsource.ui.BottomNavigationActivity; import ai.elimu.crowdsource.util.SharedPreferencesHelper; @@ -42,7 +42,7 @@ protected void onStart() { Timber.i("providerIdGoogle: " + providerIdGoogle); if (TextUtils.isEmpty(providerIdGoogle)) { // Redirect to sign-in with Google - Intent signInWithGoogleIntent = new Intent(getApplicationContext(), SignInWithGoogleActivity.class); + Intent signInWithGoogleIntent = new Intent(getApplicationContext(), SignInActivity.class); startActivity(signInWithGoogleIntent); finish(); } else { diff --git a/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt b/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt new file mode 100644 index 0000000..fdeaea1 --- /dev/null +++ b/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt @@ -0,0 +1,100 @@ +package ai.elimu.crowdsource.server + +import android.util.Log +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.java_websocket.WebSocket +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.lang.Exception +import java.lang.ref.WeakReference +import java.net.InetSocketAddress +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class BridgeServer(moshi: Moshi) : WebSocketServer(InetSocketAddress(PORT)) { + + private val adapter = moshi.adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) + + private val pubs: MutableMap>> = ConcurrentHashMap() + private val pubsLock = Any() + private val pubsCache: MutableMap = ConcurrentHashMap() + + override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) { + Log.d("#####", "onOpen: ${conn?.remoteSocketAddress?.address?.hostAddress}") + } + + override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) { + Log.d("#####", "onClose: ${conn?.remoteSocketAddress?.address?.hostAddress}") + conn?.let { cleanUpSocket(it) } + } + + override fun onMessage(conn: WebSocket?, message: String?) { + Log.d("#####", "Message: $message") + try { + conn ?: error("Unknown socket") + message?.also { + val msg = adapter.fromJson(it) ?: error("Invalid message") + val type: String = msg["type"] as String? ?: error("Type not found") + val topic: String = msg["topic"] as String? ?: error("Topic not found") + when (type) { + "pub" -> { + var sendMessage = false + pubs[topic]?.forEach { r -> + r.get()?.apply { + send(message) + sendMessage = true + } + } + if (!sendMessage) { + Log.d("#####", "Cache message: $message") + pubsCache[topic] = message + } + } + "sub" -> { + pubs.getOrPut(topic, { mutableListOf() }).add(WeakReference(conn)) + pubsCache[topic]?.let { cached -> + Log.d("#####", "Send cached: $cached") + conn.send(cached) + } + } + "ack" -> { + + } + else -> error("Unknown type") + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onStart() { + Log.d("#####", "Server started") + connectionLostTimeout = 0 + } + + override fun onError(conn: WebSocket?, ex: Exception?) { + Log.d("#####", "onError") + ex?.printStackTrace() + conn?.let { cleanUpSocket(it) } + } + + private fun cleanUpSocket(conn: WebSocket) { + synchronized(pubsLock) { + pubs.forEach { + it.value.removeAll { r -> r.get().let { v -> v == null || v == conn } } + } + } + } + + companion object { + val PORT = 5000 + Random().nextInt(60000) + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithGoogleActivity.java b/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInActivity.java similarity index 51% rename from app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithGoogleActivity.java rename to app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInActivity.java index e5ed0c8..58ad1e0 100644 --- a/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithGoogleActivity.java +++ b/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInActivity.java @@ -1,11 +1,14 @@ package ai.elimu.crowdsource.ui.authentication; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.View; +import android.widget.Button; import android.widget.ProgressBar; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.google.android.gms.auth.api.signin.GoogleSignIn; @@ -19,8 +22,11 @@ import org.json.JSONException; import org.json.JSONObject; +import org.walletconnect.Session; import java.io.IOException; +import java.security.SignatureException; +import java.util.HashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -29,6 +35,7 @@ import ai.elimu.crowdsource.MainActivity; import ai.elimu.crowdsource.R; import ai.elimu.crowdsource.rest.ContributorService; +import ai.elimu.crowdsource.util.EthersUtils; import ai.elimu.crowdsource.util.SharedPreferencesHelper; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -41,19 +48,25 @@ /** * Prompts the Contributor for access to her Google account. Then stores the account details in the * webapp's database. - * + *

* See https://developers.google.com/identity/sign-in/android/sign-in */ -public class SignInWithGoogleActivity extends AppCompatActivity implements View.OnClickListener { +public class SignInActivity extends AppCompatActivity implements View.OnClickListener, Session.Callback { private static final int RC_SIGN_IN = 0; - + private static String w3Account = ""; + public static final String W3_SIGN_MESSAGE = "elimu.ai"; private SignInButton signInButton; + private Button connectW3Button; + private ProgressBar signInProgressBar; private GoogleSignInClient googleSignInClient; + private long txRequest; + private Button signInW3Button; + @Override protected void onCreate(Bundle savedInstanceState) { Timber.i("onCreate"); @@ -61,10 +74,25 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_sign_in_with_google); - signInButton = findViewById(R.id.sign_in_button); + signInButton = findViewById(R.id.g_sign_in_button); signInButton.setSize(SignInButton.SIZE_WIDE); signInProgressBar = findViewById(R.id.sign_in_progressbar); + + signInW3Button = findViewById(R.id.sign_in_web3_button); + connectW3Button = findViewById(R.id.connect_web3_button); + connectW3Button.setOnClickListener(view -> { + try { + BaseApplication.resetSession(); + } catch (Exception e) { + Toast.makeText(this, "WalletConnect session is null!", Toast.LENGTH_LONG).show(); + return; + } + BaseApplication.session.addCallback(SignInActivity.this); + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(BaseApplication.config.toWCUri())); + startActivity(i); + }); } @Override @@ -86,6 +114,49 @@ protected void onStart() { signInButton.setOnClickListener(this); } + + // Web3 Sign In button. + initialSetup(); + signInW3Button.setOnClickListener(view -> { + if (BaseApplication.session.approvedAccounts() == null || BaseApplication.session.approvedAccounts().size() == 0) { + Toast.makeText(this, "No approved account found", Toast.LENGTH_LONG).show(); + return; + } + String from = BaseApplication.session.approvedAccounts().get(0); + long txRequest = System.currentTimeMillis(); + Timber.tag("web3.wallet").i("from: %s", from); + BaseApplication.session.performMethodCall( + new Session.MethodCall.SignMessage( + txRequest, + from, + W3_SIGN_MESSAGE + ), + response -> { + if (response.id() == SignInActivity.this.txRequest) { + SignInActivity.this.txRequest = -1; + if (response.getResult() != null) { + + Timber.tag("web3.wallet").i("signed message: %s", response); + boolean recovered = false; + recovered = EthersUtils.verifyMessage(from, W3_SIGN_MESSAGE, response.getResult().toString()); + Timber.tag("web3.wallet").i("recovered address: %s", recovered); + if (recovered) { + w3Account = from; + } + } + } + return null; + } + ); + navigateToWallet(); + this.txRequest = txRequest; + }); + + if (!w3Account.equals("")){ + updateUIW3(w3Account); + } + + } @Override @@ -134,6 +205,97 @@ private void handleSignInResult(Task completedTask) { } } + + private void updateUIW3(String web3Account) { + Timber.i("updateUI"); + + + // Get the details from the Contributor's Google account + String providerIdGoogle = ""; + String email = ""; + String firstName = web3Account; + String lastName = ""; + String imageUrl = ""; + + // Display the progressbar while connecting to the webapp + signInButton.setVisibility(View.GONE); + signInW3Button.setVisibility(View.GONE); + connectW3Button.setVisibility(View.GONE); + signInProgressBar.setVisibility(View.VISIBLE); + + // Prepare JSON object to be sent to the webapp's REST API + JSONObject contributorJSONObject = new JSONObject(); + try { + contributorJSONObject.put("providerIdWeb3", web3Account); + contributorJSONObject.put("providerIdGoogle", web3Account); + contributorJSONObject.put("email", web3Account); + } catch (JSONException e) { + Timber.e(e); + } + Timber.i("contributorJSONObject: " + contributorJSONObject); + + // Register the Contributor in the webapp's database + BaseApplication baseApplication = (BaseApplication) getApplication(); + Retrofit retrofit = baseApplication.getRetrofit(); + ContributorService contributorService = retrofit.create(ContributorService.class); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), contributorJSONObject.toString()); + Call call = contributorService.createContributor(requestBody); + Timber.i("call.request(): " + call.request()); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(new Runnable() { + @Override + public void run() { + Timber.i("run"); + + try { + Response response = call.execute(); + Timber.i("response: " + response); + Timber.i("response.isSuccessful(): " + response.isSuccessful()); + if (response.isSuccessful()) { + String bodyString = response.body().string(); + Timber.i("bodyString: " + bodyString); + + // Persist the Contributor's account details in SharedPreferences + SharedPreferencesHelper.storeWeb3Account(getApplicationContext(), web3Account); + + // Redirect to the MainActivity + Intent mainActivityIntent = new Intent(getApplicationContext(), MainActivity.class); + startActivity(mainActivityIntent); + finish(); + } else { + String errorBodyString = response.errorBody().string(); + Timber.e("errorBodyString: " + errorBodyString); + // TODO: Handle error + + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), "Error " + response.code() + ": \"" + response.message() + "\"", Toast.LENGTH_LONG).show(); + + // Hide the progressbar + signInButton.setVisibility(View.VISIBLE); + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); + signInProgressBar.setVisibility(View.GONE); + }); + } + } catch (IOException e) { + Timber.e(e, null); + // TODO: Handle error + + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), "Error: " + e.getClass().getSimpleName(), Toast.LENGTH_LONG).show(); + + // Hide the progressbar + signInButton.setVisibility(View.VISIBLE); + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); + signInProgressBar.setVisibility(View.GONE); + }); + } + } + }); + + } + private void updateUI(GoogleSignInAccount googleSignInAccount) { Timber.i("updateUI"); @@ -205,6 +367,8 @@ public void run() { // Hide the progressbar signInButton.setVisibility(View.VISIBLE); + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); signInProgressBar.setVisibility(View.GONE); }); } @@ -217,6 +381,8 @@ public void run() { // Hide the progressbar signInButton.setVisibility(View.VISIBLE); + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); signInProgressBar.setVisibility(View.GONE); }); } @@ -224,4 +390,59 @@ public void run() { }); } } + + @Override + public void onMethodCall(@NonNull Session.MethodCall methodCall) { + + } + + private void sessionApproved() { + signInW3Button.setVisibility(View.VISIBLE); + } + + private void sessionClosed() { + signInW3Button.setVisibility(View.INVISIBLE); + } + + @Override + public void onStatus(@NonNull Session.Status status) { + if (status instanceof Session.Status.Error) { + + } else if (status instanceof Session.Status.Approved) { + sessionApproved(); + } else if (status instanceof Session.Status.Closed) { + sessionClosed(); + } else if (status instanceof Session.Status.Connected) { + requestConnectionToWallet(); + } else if (status instanceof Session.Status.Disconnected) { + + } + } + + private void requestConnectionToWallet() { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(BaseApplication.config.toWCUri())); + startActivity(i); + } + + private void navigateToWallet() { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("wc:")); + startActivity(i); + } + + private void initialSetup() { + if (BaseApplication.session != null) { + BaseApplication.session.addCallback(this); + sessionApproved(); + } + + } + + @Override + protected void onDestroy() { + BaseApplication.session.removeCallback(this); + super.onDestroy(); + } + } diff --git a/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java b/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java new file mode 100644 index 0000000..e8b34ab --- /dev/null +++ b/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java @@ -0,0 +1,63 @@ +package ai.elimu.crowdsource.util; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.web3j.crypto.ECDSASignature; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; +import org.web3j.crypto.Sign.SignatureData; +import org.web3j.crypto.WalletFile; +import org.web3j.utils.Numeric; + +import timber.log.Timber; + +public class EthersUtils { + private static final String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"; + + public static boolean verifyMessage(String address,String message, String signature) { + String recovered = EthersUtils.recoverAddress(EthersUtils.hashMessage(message), signature); + Timber.tag("ethers-utils").i("recovered address: %s", recovered); + return address.equalsIgnoreCase(recovered); + } + + public static String hashMessage(String message) { + return Hash.sha3( + Numeric.toHexStringNoPrefix( + (EthersUtils.MESSAGE_PREFIX + message.length() + message).getBytes(StandardCharsets.UTF_8))); + } + + public static String recoverAddress(String digest, String signature) { + SignatureData signatureData = EthersUtils.getSignatureData(signature); + int header = 0; + for (byte b : signatureData.getV()) { + header = (header << 8) + (b & 0xFF); + } + if (header < 27 || header > 34) { + return null; + } + int recId = header - 27; + BigInteger key = Sign.recoverFromSignature( + recId, + new ECDSASignature( + new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())), + Numeric.hexStringToByteArray(digest)); + if (key == null) { + return null; + } + return ("0x" + Keys.getAddress(key)).trim(); + } + + private static SignatureData getSignatureData(String signature) { + byte[] signatureBytes = Numeric.hexStringToByteArray(signature); + byte v = signatureBytes[64]; + if (v < 27) { + v += 27; + } + byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32); + byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64); + return new SignatureData(v, r, s); + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java b/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java index b3a0d22..80dc6cc 100644 --- a/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java +++ b/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java @@ -65,6 +65,7 @@ public static void storeProviderIdGoogle(Context context, String providerIdGoogl sharedPreferences.edit().putString(PREF_PROVIDER_ID_GOOGLE, providerIdGoogle).apply(); } + public static String getProviderIdGoogle(Context context) { Timber.i("getProviderIdGoogle"); SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE); @@ -76,6 +77,23 @@ public static String getProviderIdGoogle(Context context) { } } + public static void storeWeb3Account(Context context, String web3Account) { + Timber.i("storeWeb3Account"); + SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE); + sharedPreferences.edit().putString(PREF_WEB3_ACCOUNT, web3Account).apply(); + } + + public static String getWeb3Account(Context context) { + Timber.i("getWeb3Account"); + SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE); + String web3Account = sharedPreferences.getString(PREF_WEB3_ACCOUNT, null); + if (TextUtils.isEmpty(web3Account)) { + return null; + } else { + return web3Account; + } + } + public static void storeProviderIdWeb3(Context context, String providerIdWeb3) { Timber.i("storeProviderIdWeb3"); diff --git a/app/src/main/res/layout/activity_sign_in_with_google.xml b/app/src/main/res/layout/activity_sign_in_with_google.xml index 1a18b2d..dfe4316 100644 --- a/app/src/main/res/layout/activity_sign_in_with_google.xml +++ b/app/src/main/res/layout/activity_sign_in_with_google.xml @@ -23,10 +23,22 @@ android:src="@drawable/ic_elimu" /> +