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((e, s) => + throw LocalDataSourceException('Cannot encrypt the plaintext', e, s)); + + @override + Future decrypt(String ciphertext) => channel + .invokeMethod(decryptMethod, ciphertext) + .then((v) => v!) + .onError((e, s) => ciphertext) + .onError((e, s) => throw LocalDataSourceException( + 'Cannot decrypt the ciphertext', e, s)); +} diff --git a/lib/data/local/shared_pref_util.dart b/lib/data/local/shared_pref_util.dart index 27bd861..5e56814 100644 --- a/lib/data/local/shared_pref_util.dart +++ b/lib/data/local/shared_pref_util.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:node_auth/data/exception/local_data_source_exception.dart'; @@ -9,8 +10,9 @@ import 'package:rxdart/rxdart.dart'; class SharedPrefUtil implements LocalDataSource { static const _kUserTokenKey = 'com.hoc.node_auth_flutter.user_and_token'; final RxSharedPreferences _rxPrefs; + final Crypto _crypto; - const SharedPrefUtil(this._rxPrefs); + const SharedPrefUtil(this._rxPrefs, this._crypto); @override Future removeUserAndToken() => @@ -37,10 +39,17 @@ class SharedPrefUtil implements LocalDataSource { .onErrorReturnWith((e, s) => throw LocalDataSourceException('Cannot read user and token', e, s)); - static UserAndTokenEntity? _toEntity(dynamic jsonString) => jsonString == null - ? null - : UserAndTokenEntity.fromJson(json.decode(jsonString)); + // + // Encoder and Decoder + // - static String? _toString(UserAndTokenEntity? entity) => - entity == null ? null : jsonEncode(entity); + FutureOr _toEntity(dynamic jsonString) => + jsonString == null + ? null + : _crypto + .decrypt(jsonString as String) + .then((s) => UserAndTokenEntity.fromJson(jsonDecode(s))); + + FutureOr _toString(UserAndTokenEntity? entity) => + entity == null ? null : _crypto.encrypt(jsonEncode(entity)); } diff --git a/lib/main.dart b/lib/main.dart index 52352a0..5e1d87c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; import 'package:flutter_provider/flutter_provider.dart'; import 'package:node_auth/app.dart'; import 'package:node_auth/data/local/local_data_source.dart'; +import 'package:node_auth/data/local/method_channel_crypto_impl.dart'; import 'package:node_auth/data/local/shared_pref_util.dart'; import 'package:node_auth/data/remote/api_service.dart'; import 'package:node_auth/data/remote/remote_data_source.dart'; @@ -25,7 +26,8 @@ void main() async { // construct LocalDataSource final rxPrefs = RxSharedPreferences.getInstance(); - final LocalDataSource localDataSource = SharedPrefUtil(rxPrefs); + final crypto = MethodChannelCryptoImpl(); + final LocalDataSource localDataSource = SharedPrefUtil(rxPrefs, crypto); // construct UserRepository final UserRepository userRepository = UserRepositoryImpl( diff --git a/pubspec.lock b/pubspec.lock index f6659bb..1d3050a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -495,16 +495,20 @@ packages: rx_shared_preferences: dependency: "direct main" description: - name: rx_shared_preferences - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" + path: "." + ref: "78db985d06f275ebf3f12aa21511cfc7333c220b" + resolved-ref: "78db985d06f275ebf3f12aa21511cfc7333c220b" + url: "https://github.com/hoc081098/rx_shared_preferences.git" + source: git + version: "3.0.0-dev.0" rx_storage: - dependency: transitive + dependency: "direct overridden" description: - name: rx_storage - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "50c711605b6aa9a185e7f3ca9b2c87f0d2b35187" + resolved-ref: "50c711605b6aa9a185e7f3ca9b2c87f0d2b35187" + url: "https://github.com/Flutter-Dart-Open-Source/rx_storage.git" + source: git version: "1.2.0" rxdart: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 909c9ea..bd184c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,3 +46,12 @@ flutter: - assets/bg.jpg - assets/user.png +dependency_overrides: + rx_shared_preferences: + git: + url: https://github.com/hoc081098/rx_shared_preferences.git + ref: 78db985d06f275ebf3f12aa21511cfc7333c220b + rx_storage: + git: + url: https://github.com/Flutter-Dart-Open-Source/rx_storage.git + ref: 50c711605b6aa9a185e7f3ca9b2c87f0d2b35187 \ No newline at end of file