diff --git a/android/app/build.gradle b/android/app/build.gradle
index cd31b4e..7c04bd0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -48,6 +48,7 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
+ multiDexEnabled true
}
buildTypes {
@@ -65,4 +66,10 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "androidx.multidex:multidex:2.0.1"
+
+ implementation "com.google.crypto.tink:tink-android:1.6.1"
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 766561f..a8abd2e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,37 +1,36 @@
+ package="com.hoc.node_auth">
-
+
+ android:name=".MyApp"
+ android:icon="@mipmap/ic_launcher"
+ android:label="node_auth">
+ android:name=".MainActivity"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:exported="true"
+ android:hardwareAccelerated="true"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:windowSoftInputMode="adjustResize">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
+ android:name="flutterEmbedding"
+ android:value="2" />
diff --git a/android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt b/android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt
index 7436f1a..f7ceb60 100644
--- a/android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt
@@ -1,6 +1,107 @@
package com.hoc.node_auth
+import android.util.Log
+import com.google.crypto.tink.subtle.Base64
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import kotlinx.coroutines.*
-class MainActivity: FlutterActivity() {
+class MainActivity : FlutterActivity() {
+ private lateinit var cryptoChannel: MethodChannel
+ private lateinit var mainScope: CoroutineScope
+
+ //region Lifecycle
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ Log.d("Flutter", "configureFlutterEngine flutterEngine=$flutterEngine $this")
+
+ mainScope = MainScope()
+ cryptoChannel = MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ CRYPTO_CHANNEL,
+ ).apply { setMethodCallHandler(MethodCallHandlerImpl()) }
+ }
+
+ override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
+ super.cleanUpFlutterEngine(flutterEngine)
+ Log.d("Flutter", "cleanUpFlutterEngine flutterEngine=$flutterEngine $this")
+
+ cryptoChannel.setMethodCallHandler(null)
+ mainScope.cancel()
+ }
+ //endregion
+
+ private inner class MethodCallHandlerImpl : MethodChannel.MethodCallHandler {
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ ENCRYPT_METHOD -> encrypt(call, result)
+ DECRYPT_METHOD -> decrypt(call, result)
+ else -> result.notImplemented()
+ }
+ }
+ }
+
+ //region Handlers
+ private fun encrypt(
+ call: MethodCall,
+ result: MethodChannel.Result
+ ) {
+ val plaintext = checkNotNull(call.arguments()) { "plaintext must be not null" }
+
+ mainScope.launch {
+ runCatching {
+ withContext(Dispatchers.IO) {
+ plaintext
+ .encodeToByteArray()
+ .let { myApp.aead.encrypt(it, null) }
+ .let { Base64.encode(it) }
+ }
+ }
+ .onSuccess { result.success(it) }
+ .onFailureExceptCancellationException {
+ Log.e("Flutter", "encrypt", it)
+ result.error(CRYPTO_ERROR_CODE, it.message, null)
+ }
+ }
+ }
+
+ private fun decrypt(
+ call: MethodCall,
+ result: MethodChannel.Result
+ ) {
+ val ciphertext = checkNotNull(call.arguments()) { "ciphertext must be not null" }
+
+ mainScope.launch {
+ runCatching {
+ withContext(Dispatchers.IO) {
+ Base64
+ .decode(ciphertext, Base64.DEFAULT)
+ .let { myApp.aead.decrypt(it, null) }
+ .decodeToString()
+ }
+ }
+ .onSuccess { result.success(it) }
+ .onFailureExceptCancellationException {
+ Log.e("Flutter", "decrypt", it)
+ result.error(CRYPTO_ERROR_CODE, it.message, null)
+ }
+ }
+ }
+ //endregion
+
+ private companion object {
+ const val CRYPTO_CHANNEL = "com.hoc.node_auth/crypto"
+ const val CRYPTO_ERROR_CODE = "com.hoc.node_auth/crypto_error"
+ const val ENCRYPT_METHOD = "encrypt"
+ const val DECRYPT_METHOD = "decrypt"
+ }
}
+
+private inline fun Result.onFailureExceptCancellationException(action: (throwable: Throwable) -> Unit): Result {
+ return onFailure {
+ if (it is CancellationException) throw it
+ action(it)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/hoc/node_auth/MyApp.kt b/android/app/src/main/kotlin/com/hoc/node_auth/MyApp.kt
new file mode 100644
index 0000000..f1682b0
--- /dev/null
+++ b/android/app/src/main/kotlin/com/hoc/node_auth/MyApp.kt
@@ -0,0 +1,37 @@
+package com.hoc.node_auth
+
+import android.app.Activity
+import androidx.multidex.MultiDex
+import com.google.crypto.tink.Aead
+import com.google.crypto.tink.KeyTemplates
+import com.google.crypto.tink.aead.AeadConfig
+import com.google.crypto.tink.integration.android.AndroidKeysetManager
+import com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient
+import io.flutter.app.FlutterApplication
+
+class MyApp : FlutterApplication() {
+ val aead: Aead by lazy {
+ AndroidKeysetManager
+ .Builder()
+ .withSharedPref(this, KEYSET_NAME, PREF_FILE_NAME)
+ .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
+ .withMasterKeyUri(MASTER_KEY_URI)
+ .build()
+ .keysetHandle
+ .getPrimitive(Aead::class.java)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ MultiDex.install(this)
+ AeadConfig.register()
+ }
+
+ private companion object {
+ private const val KEYSET_NAME = "nodeauth_keyset"
+ private const val PREF_FILE_NAME = "nodeauth_pref"
+ private const val MASTER_KEY_URI = "${AndroidKeystoreKmsClient.PREFIX}nodeauth_master_key"
+ }
+}
+
+val Activity.myApp: MyApp get() = application as MyApp
\ No newline at end of file
diff --git a/lib/data/local/local_data_source.dart b/lib/data/local/local_data_source.dart
index c1af87d..e8970c1 100644
--- a/lib/data/local/local_data_source.dart
+++ b/lib/data/local/local_data_source.dart
@@ -15,3 +15,9 @@ abstract class LocalDataSource {
/// Throws [LocalDataSourceException] if removing is failed
Future removeUserAndToken();
}
+
+abstract class Crypto {
+ Future encrypt(String plaintext);
+
+ Future decrypt(String ciphertext);
+}
diff --git a/lib/data/local/method_channel_crypto_impl.dart b/lib/data/local/method_channel_crypto_impl.dart
new file mode 100644
index 0000000..32f4cbc
--- /dev/null
+++ b/lib/data/local/method_channel_crypto_impl.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/services.dart';
+import 'package:node_auth/data/exception/local_data_source_exception.dart';
+import 'package:node_auth/data/local/local_data_source.dart';
+
+class MethodChannelCryptoImpl implements Crypto {
+ static const cryptoChannel = 'com.hoc.node_auth/crypto';
+ static const cryptoErrorCode = 'com.hoc.node_auth/crypto_error';
+ static const encryptMethod = 'encrypt';
+ static const decryptMethod = 'decrypt';
+ static const MethodChannel channel = MethodChannel(cryptoChannel);
+
+ @override
+ Future encrypt(String plaintext) => channel
+ .invokeMethod(encryptMethod, plaintext)
+ .then((v) => v!)
+ .onError((e, s) => plaintext)
+ .onError