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
7 changes: 7 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}

buildTypes {
Expand All @@ -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'
}
37 changes: 18 additions & 19 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hoc.node_auth">
package="com.hoc.node_auth">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />

<application
android:label="node_auth"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:name=".MyApp"
android:icon="@mipmap/ic_launcher"
android:label="node_auth">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
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">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
103 changes: 102 additions & 1 deletion android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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<String?>()) { "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<String?>()) { "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 <T> Result<T>.onFailureExceptCancellationException(action: (throwable: Throwable) -> Unit): Result<T> {
return onFailure {
if (it is CancellationException) throw it
action(it)
}
}
37 changes: 37 additions & 0 deletions android/app/src/main/kotlin/com/hoc/node_auth/MyApp.kt
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions lib/data/local/local_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ abstract class LocalDataSource {
/// Throws [LocalDataSourceException] if removing is failed
Future<void> removeUserAndToken();
}

abstract class Crypto {
Future<String> encrypt(String plaintext);

Future<String> decrypt(String ciphertext);
}
27 changes: 27 additions & 0 deletions lib/data/local/method_channel_crypto_impl.dart
Original file line number Diff line number Diff line change
@@ -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<String> encrypt(String plaintext) => channel
.invokeMethod<String>(encryptMethod, plaintext)
.then((v) => v!)
.onError<MissingPluginException>((e, s) => plaintext)
.onError<Object>((e, s) =>
throw LocalDataSourceException('Cannot encrypt the plaintext', e, s));

@override
Future<String> decrypt(String ciphertext) => channel
.invokeMethod<String>(decryptMethod, ciphertext)
.then((v) => v!)
.onError<MissingPluginException>((e, s) => ciphertext)
.onError<Object>((e, s) => throw LocalDataSourceException(
'Cannot decrypt the ciphertext', e, s));
}
21 changes: 15 additions & 6 deletions lib/data/local/shared_pref_util.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';

import 'package:node_auth/data/exception/local_data_source_exception.dart';
Expand All @@ -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<void> removeUserAndToken() =>
Expand All @@ -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<UserAndTokenEntity?> _toEntity(dynamic jsonString) =>
jsonString == null
? null
: _crypto
.decrypt(jsonString as String)
.then((s) => UserAndTokenEntity.fromJson(jsonDecode(s)));

FutureOr<String?> _toString(UserAndTokenEntity? entity) =>
entity == null ? null : _crypto.encrypt(jsonEncode(entity));
}
4 changes: 3 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand Down
20 changes: 12 additions & 8 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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