Skip to content

Commit 9bb3a97

Browse files
authored
tink android side (#24)
* tink android side * wip * wip * tink android side * tink android side
1 parent fbd7b63 commit 9bb3a97

File tree

10 files changed

+236
-35
lines changed

10 files changed

+236
-35
lines changed

android/app/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ android {
4848
targetSdkVersion flutter.targetSdkVersion
4949
versionCode flutterVersionCode.toInteger()
5050
versionName flutterVersionName
51+
multiDexEnabled true
5152
}
5253

5354
buildTypes {
@@ -65,4 +66,10 @@ flutter {
6566

6667
dependencies {
6768
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
69+
implementation "androidx.multidex:multidex:2.0.1"
70+
71+
implementation "com.google.crypto.tink:tink-android:1.6.1"
72+
73+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
74+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
6875
}
Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
package="com.hoc.node_auth">
2+
package="com.hoc.node_auth">
33

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

66
<application
7-
android:label="node_auth"
8-
android:name="${applicationName}"
9-
android:icon="@mipmap/ic_launcher">
7+
android:name=".MyApp"
8+
android:icon="@mipmap/ic_launcher"
9+
android:label="node_auth">
1010
<activity
11-
android:name=".MainActivity"
12-
android:exported="true"
13-
android:launchMode="singleTop"
14-
android:theme="@style/LaunchTheme"
15-
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
16-
android:hardwareAccelerated="true"
17-
android:windowSoftInputMode="adjustResize">
11+
android:name=".MainActivity"
12+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
13+
android:exported="true"
14+
android:hardwareAccelerated="true"
15+
android:launchMode="singleTop"
16+
android:theme="@style/LaunchTheme"
17+
android:windowSoftInputMode="adjustResize">
1818
<!-- Specifies an Android theme to apply to this Activity as soon as
1919
the Android process has started. This theme is visible to the user
2020
while the Flutter UI initializes. After that, this theme continues
2121
to determine the Window background behind the Flutter UI. -->
2222
<meta-data
23-
android:name="io.flutter.embedding.android.NormalTheme"
24-
android:resource="@style/NormalTheme"
25-
/>
23+
android:name="io.flutter.embedding.android.NormalTheme"
24+
android:resource="@style/NormalTheme" />
2625
<intent-filter>
27-
<action android:name="android.intent.action.MAIN"/>
28-
<category android:name="android.intent.category.LAUNCHER"/>
26+
<action android:name="android.intent.action.MAIN" />
27+
<category android:name="android.intent.category.LAUNCHER" />
2928
</intent-filter>
3029
</activity>
3130
<!-- Don't delete the meta-data below.
3231
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
3332
<meta-data
34-
android:name="flutterEmbedding"
35-
android:value="2"/>
33+
android:name="flutterEmbedding"
34+
android:value="2" />
3635
</application>
3736
</manifest>
Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,107 @@
11
package com.hoc.node_auth
22

3+
import android.util.Log
4+
import com.google.crypto.tink.subtle.Base64
35
import io.flutter.embedding.android.FlutterActivity
6+
import io.flutter.embedding.engine.FlutterEngine
7+
import io.flutter.plugin.common.MethodCall
8+
import io.flutter.plugin.common.MethodChannel
9+
import kotlinx.coroutines.*
410

5-
class MainActivity: FlutterActivity() {
11+
class MainActivity : FlutterActivity() {
12+
private lateinit var cryptoChannel: MethodChannel
13+
private lateinit var mainScope: CoroutineScope
14+
15+
//region Lifecycle
16+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
17+
super.configureFlutterEngine(flutterEngine)
18+
Log.d("Flutter", "configureFlutterEngine flutterEngine=$flutterEngine $this")
19+
20+
mainScope = MainScope()
21+
cryptoChannel = MethodChannel(
22+
flutterEngine.dartExecutor.binaryMessenger,
23+
CRYPTO_CHANNEL,
24+
).apply { setMethodCallHandler(MethodCallHandlerImpl()) }
25+
}
26+
27+
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
28+
super.cleanUpFlutterEngine(flutterEngine)
29+
Log.d("Flutter", "cleanUpFlutterEngine flutterEngine=$flutterEngine $this")
30+
31+
cryptoChannel.setMethodCallHandler(null)
32+
mainScope.cancel()
33+
}
34+
//endregion
35+
36+
private inner class MethodCallHandlerImpl : MethodChannel.MethodCallHandler {
37+
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
38+
when (call.method) {
39+
ENCRYPT_METHOD -> encrypt(call, result)
40+
DECRYPT_METHOD -> decrypt(call, result)
41+
else -> result.notImplemented()
42+
}
43+
}
44+
}
45+
46+
//region Handlers
47+
private fun encrypt(
48+
call: MethodCall,
49+
result: MethodChannel.Result
50+
) {
51+
val plaintext = checkNotNull(call.arguments<String?>()) { "plaintext must be not null" }
52+
53+
mainScope.launch {
54+
runCatching {
55+
withContext(Dispatchers.IO) {
56+
plaintext
57+
.encodeToByteArray()
58+
.let { myApp.aead.encrypt(it, null) }
59+
.let { Base64.encode(it) }
60+
}
61+
}
62+
.onSuccess { result.success(it) }
63+
.onFailureExceptCancellationException {
64+
Log.e("Flutter", "encrypt", it)
65+
result.error(CRYPTO_ERROR_CODE, it.message, null)
66+
}
67+
}
68+
}
69+
70+
private fun decrypt(
71+
call: MethodCall,
72+
result: MethodChannel.Result
73+
) {
74+
val ciphertext = checkNotNull(call.arguments<String?>()) { "ciphertext must be not null" }
75+
76+
mainScope.launch {
77+
runCatching {
78+
withContext(Dispatchers.IO) {
79+
Base64
80+
.decode(ciphertext, Base64.DEFAULT)
81+
.let { myApp.aead.decrypt(it, null) }
82+
.decodeToString()
83+
}
84+
}
85+
.onSuccess { result.success(it) }
86+
.onFailureExceptCancellationException {
87+
Log.e("Flutter", "decrypt", it)
88+
result.error(CRYPTO_ERROR_CODE, it.message, null)
89+
}
90+
}
91+
}
92+
//endregion
93+
94+
private companion object {
95+
const val CRYPTO_CHANNEL = "com.hoc.node_auth/crypto"
96+
const val CRYPTO_ERROR_CODE = "com.hoc.node_auth/crypto_error"
97+
const val ENCRYPT_METHOD = "encrypt"
98+
const val DECRYPT_METHOD = "decrypt"
99+
}
6100
}
101+
102+
private inline fun <T> Result<T>.onFailureExceptCancellationException(action: (throwable: Throwable) -> Unit): Result<T> {
103+
return onFailure {
104+
if (it is CancellationException) throw it
105+
action(it)
106+
}
107+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.hoc.node_auth
2+
3+
import android.app.Activity
4+
import androidx.multidex.MultiDex
5+
import com.google.crypto.tink.Aead
6+
import com.google.crypto.tink.KeyTemplates
7+
import com.google.crypto.tink.aead.AeadConfig
8+
import com.google.crypto.tink.integration.android.AndroidKeysetManager
9+
import com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient
10+
import io.flutter.app.FlutterApplication
11+
12+
class MyApp : FlutterApplication() {
13+
val aead: Aead by lazy {
14+
AndroidKeysetManager
15+
.Builder()
16+
.withSharedPref(this, KEYSET_NAME, PREF_FILE_NAME)
17+
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
18+
.withMasterKeyUri(MASTER_KEY_URI)
19+
.build()
20+
.keysetHandle
21+
.getPrimitive(Aead::class.java)
22+
}
23+
24+
override fun onCreate() {
25+
super.onCreate()
26+
MultiDex.install(this)
27+
AeadConfig.register()
28+
}
29+
30+
private companion object {
31+
private const val KEYSET_NAME = "nodeauth_keyset"
32+
private const val PREF_FILE_NAME = "nodeauth_pref"
33+
private const val MASTER_KEY_URI = "${AndroidKeystoreKmsClient.PREFIX}nodeauth_master_key"
34+
}
35+
}
36+
37+
val Activity.myApp: MyApp get() = application as MyApp

lib/data/local/local_data_source.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ abstract class LocalDataSource {
1515
/// Throws [LocalDataSourceException] if removing is failed
1616
Future<void> removeUserAndToken();
1717
}
18+
19+
abstract class Crypto {
20+
Future<String> encrypt(String plaintext);
21+
22+
Future<String> decrypt(String ciphertext);
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:node_auth/data/exception/local_data_source_exception.dart';
3+
import 'package:node_auth/data/local/local_data_source.dart';
4+
5+
class MethodChannelCryptoImpl implements Crypto {
6+
static const cryptoChannel = 'com.hoc.node_auth/crypto';
7+
static const cryptoErrorCode = 'com.hoc.node_auth/crypto_error';
8+
static const encryptMethod = 'encrypt';
9+
static const decryptMethod = 'decrypt';
10+
static const MethodChannel channel = MethodChannel(cryptoChannel);
11+
12+
@override
13+
Future<String> encrypt(String plaintext) => channel
14+
.invokeMethod<String>(encryptMethod, plaintext)
15+
.then((v) => v!)
16+
.onError<MissingPluginException>((e, s) => plaintext)
17+
.onError<Object>((e, s) =>
18+
throw LocalDataSourceException('Cannot encrypt the plaintext', e, s));
19+
20+
@override
21+
Future<String> decrypt(String ciphertext) => channel
22+
.invokeMethod<String>(decryptMethod, ciphertext)
23+
.then((v) => v!)
24+
.onError<MissingPluginException>((e, s) => ciphertext)
25+
.onError<Object>((e, s) => throw LocalDataSourceException(
26+
'Cannot decrypt the ciphertext', e, s));
27+
}

lib/data/local/shared_pref_util.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:convert';
23

34
import 'package:node_auth/data/exception/local_data_source_exception.dart';
@@ -9,8 +10,9 @@ import 'package:rxdart/rxdart.dart';
910
class SharedPrefUtil implements LocalDataSource {
1011
static const _kUserTokenKey = 'com.hoc.node_auth_flutter.user_and_token';
1112
final RxSharedPreferences _rxPrefs;
13+
final Crypto _crypto;
1214

13-
const SharedPrefUtil(this._rxPrefs);
15+
const SharedPrefUtil(this._rxPrefs, this._crypto);
1416

1517
@override
1618
Future<void> removeUserAndToken() =>
@@ -37,10 +39,17 @@ class SharedPrefUtil implements LocalDataSource {
3739
.onErrorReturnWith((e, s) =>
3840
throw LocalDataSourceException('Cannot read user and token', e, s));
3941

40-
static UserAndTokenEntity? _toEntity(dynamic jsonString) => jsonString == null
41-
? null
42-
: UserAndTokenEntity.fromJson(json.decode(jsonString));
42+
//
43+
// Encoder and Decoder
44+
//
4345

44-
static String? _toString(UserAndTokenEntity? entity) =>
45-
entity == null ? null : jsonEncode(entity);
46+
FutureOr<UserAndTokenEntity?> _toEntity(dynamic jsonString) =>
47+
jsonString == null
48+
? null
49+
: _crypto
50+
.decrypt(jsonString as String)
51+
.then((s) => UserAndTokenEntity.fromJson(jsonDecode(s)));
52+
53+
FutureOr<String?> _toString(UserAndTokenEntity? entity) =>
54+
entity == null ? null : _crypto.encrypt(jsonEncode(entity));
4655
}

lib/main.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart';
77
import 'package:flutter_provider/flutter_provider.dart';
88
import 'package:node_auth/app.dart';
99
import 'package:node_auth/data/local/local_data_source.dart';
10+
import 'package:node_auth/data/local/method_channel_crypto_impl.dart';
1011
import 'package:node_auth/data/local/shared_pref_util.dart';
1112
import 'package:node_auth/data/remote/api_service.dart';
1213
import 'package:node_auth/data/remote/remote_data_source.dart';
@@ -25,7 +26,8 @@ void main() async {
2526

2627
// construct LocalDataSource
2728
final rxPrefs = RxSharedPreferences.getInstance();
28-
final LocalDataSource localDataSource = SharedPrefUtil(rxPrefs);
29+
final crypto = MethodChannelCryptoImpl();
30+
final LocalDataSource localDataSource = SharedPrefUtil(rxPrefs, crypto);
2931

3032
// construct UserRepository
3133
final UserRepository userRepository = UserRepositoryImpl(

pubspec.lock

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -495,16 +495,20 @@ packages:
495495
rx_shared_preferences:
496496
dependency: "direct main"
497497
description:
498-
name: rx_shared_preferences
499-
url: "https://pub.dartlang.org"
500-
source: hosted
501-
version: "2.3.0"
498+
path: "."
499+
ref: "78db985d06f275ebf3f12aa21511cfc7333c220b"
500+
resolved-ref: "78db985d06f275ebf3f12aa21511cfc7333c220b"
501+
url: "https://github.com/hoc081098/rx_shared_preferences.git"
502+
source: git
503+
version: "3.0.0-dev.0"
502504
rx_storage:
503-
dependency: transitive
505+
dependency: "direct overridden"
504506
description:
505-
name: rx_storage
506-
url: "https://pub.dartlang.org"
507-
source: hosted
507+
path: "."
508+
ref: "50c711605b6aa9a185e7f3ca9b2c87f0d2b35187"
509+
resolved-ref: "50c711605b6aa9a185e7f3ca9b2c87f0d2b35187"
510+
url: "https://github.com/Flutter-Dart-Open-Source/rx_storage.git"
511+
source: git
508512
version: "1.2.0"
509513
rxdart:
510514
dependency: "direct main"

pubspec.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ flutter:
4646
- assets/bg.jpg
4747
- assets/user.png
4848

49+
dependency_overrides:
50+
rx_shared_preferences:
51+
git:
52+
url: https://github.com/hoc081098/rx_shared_preferences.git
53+
ref: 78db985d06f275ebf3f12aa21511cfc7333c220b
54+
rx_storage:
55+
git:
56+
url: https://github.com/Flutter-Dart-Open-Source/rx_storage.git
57+
ref: 50c711605b6aa9a185e7f3ca9b2c87f0d2b35187

0 commit comments

Comments
 (0)