From b9fb2d4f364e93e0f1b04de99523363b998bbf71 Mon Sep 17 00:00:00 2001 From: Boehrsi Date: Tue, 28 Apr 2020 16:38:31 +0200 Subject: [PATCH] Moved key generation into the dart part #501 OT-779 Key handling includes generating, persisting, getting, converting Refactored the method channel handling on Android Refactored the method channel handling on Dart / Flutter side Cleaned up Android native part Renaming and wrapping to better understand sharing flows --- .../com/openxchange/oxcoi/MainActivity.java | 103 +++++++----------- .../com/openxchange/oxcoi/MethodChannels.java | 39 +++++++ .../com/openxchange/oxcoi/SecurityHelper.java | 82 ++------------ lib/src/chat/chat.dart | 4 +- lib/src/extensions/numbers_apis.dart | 5 +- lib/src/invite/invite_bloc.dart | 23 ++-- lib/src/message/message_attachment_bloc.dart | 15 ++- lib/src/platform/method_channels.dart | 30 +++++ lib/src/platform/preferences.dart | 6 +- lib/src/push/push_bloc.dart | 20 ++-- lib/src/push/push_manager.dart | 19 +++- .../security_generator.dart} | 32 ++---- lib/src/security/security_manager.dart | 45 ++++++++ ...ed_data.dart => incoming_shared_data.dart} | 17 ++- lib/src/share/outgoing_shared_data.dart | 56 ++++++++++ lib/src/share/share.dart | 4 +- lib/src/share/share_bloc.dart | 10 +- lib/src/share/share_event_state.dart | 6 +- lib/src/utils/constants.dart | 4 - test/push/push.dart | 10 +- 20 files changed, 303 insertions(+), 227 deletions(-) create mode 100644 android/app/src/main/java/com/openxchange/oxcoi/MethodChannels.java create mode 100644 lib/src/platform/method_channels.dart rename lib/src/{secure/generator.dart => security/security_generator.dart} (76%) create mode 100644 lib/src/security/security_manager.dart rename lib/src/share/{shared_data.dart => incoming_shared_data.dart} (77%) create mode 100644 lib/src/share/outgoing_shared_data.dart diff --git a/android/app/src/main/java/com/openxchange/oxcoi/MainActivity.java b/android/app/src/main/java/com/openxchange/oxcoi/MainActivity.java index aaecc453..03b92b10 100644 --- a/android/app/src/main/java/com/openxchange/oxcoi/MainActivity.java +++ b/android/app/src/main/java/com/openxchange/oxcoi/MainActivity.java @@ -48,15 +48,13 @@ import android.os.Bundle; import android.util.Base64; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - import java.io.File; -import java.security.KeyPair; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.dart.DartExecutor; @@ -65,19 +63,11 @@ public class MainActivity extends FlutterActivity { private Map sharedData = new HashMap<>(); private String startString = ""; - private static final String SHARED_MIME_TYPE = "shared_mime_type"; - private static final String SHARED_TEXT = "shared_text"; - private static final String SHARED_PATH = "shared_path"; - private static final String SHARED_FILE_NAME = "shared_file_name"; - // TODO create constants for channel methods - private static final String INTENT_CHANNEL_NAME = "oxcoi.intent"; - // TODO create constants for channel methods - private static final String SECURITY_CHANNEL_NAME = "oxcoi.security"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - handleIntent(getIntent()); + cacheDateFromPlatform(getIntent()); } @Override @@ -85,70 +75,53 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); DartExecutor dartExecutor = flutterEngine.getDartExecutor(); setupSharingMethodChannel(dartExecutor); - SecurityHelper securityHelper = new SecurityHelper(this); + SecurityHelper securityHelper = new SecurityHelper(); setupSecurityMethodChannel(dartExecutor, securityHelper); } + @Override + protected void onNewIntent(@NonNull Intent intent) { + super.onNewIntent(intent); + cacheDateFromPlatform(intent); + } + private void setupSharingMethodChannel(DartExecutor dartExecutor) { - new MethodChannel(dartExecutor, INTENT_CHANNEL_NAME).setMethodCallHandler( + new MethodChannel(dartExecutor, MethodChannels.Sharing.NAME).setMethodCallHandler( (call, result) -> { - if (call.method.contentEquals("getSharedData")) { + if (call.method.contentEquals(MethodChannels.Sharing.Methods.GET_SHARE_DATA)) { result.success(sharedData); sharedData.clear(); - } else if (call.method.contentEquals("getInitialLink")) { + } else if (call.method.contentEquals(MethodChannels.Sharing.Methods.GET_INITIAL_LINK)) { if (startString != null && !startString.isEmpty()) { result.success(startString); startString = ""; } else { result.success(null); } - } else if (call.method.contentEquals("sendSharedData")) { - shareFile(call.arguments); + } else if (call.method.contentEquals(MethodChannels.Sharing.Methods.SEND_SHARE_DATA)) { + shareDataWithPlatform(call.arguments); result.success(null); } }); } private void setupSecurityMethodChannel(DartExecutor dartExecutor, SecurityHelper securityHelper) { - new MethodChannel(dartExecutor, SECURITY_CHANNEL_NAME).setMethodCallHandler( + new MethodChannel(dartExecutor, MethodChannels.Security.NAME).setMethodCallHandler( (call, result) -> { - if (call.method.contentEquals("generateSecrets")) { - KeyPair keyPair = securityHelper.generateKey(); - if (keyPair != null) { - securityHelper.persistKeyPair(keyPair); - } else { - throw new IllegalStateException("Key pair is empty"); - } - String authSecret = securityHelper.generateAuthSecret(); - if (authSecret != null && !authSecret.isEmpty()) { - securityHelper.persisAuthSecret(authSecret); - } else { - throw new IllegalStateException("Auth secret is empty"); - } - result.success(null); - } else if (call.method.contentEquals("getKey")) { - String publicKeyBase64 = securityHelper.getPublicKeyBase64(); - result.success(publicKeyBase64); - } else if (call.method.contentEquals("getAuthSecret")) { - String authSecret = securityHelper.getAuthSecretFromPersistedData(); - result.success(authSecret); - } else if (call.method.contentEquals("decrypt")) { - String input = call.argument("input"); - byte[] inputBytes = Base64.decode(input, Base64.DEFAULT); - String decryptMessage = securityHelper.decryptMessage(inputBytes); + if (call.method.contentEquals(MethodChannels.Security.Methods.DECRYPT)) { + String encryptedBase64Content = call.argument(MethodChannels.Security.Arguments.CONTENT); + String privateKeyBase64 = call.argument(MethodChannels.Security.Arguments.PRIVATE_KEY); + String publicKeyBase64 = call.argument(MethodChannels.Security.Arguments.PUBLIC_KEY); + String authBase64 = call.argument(MethodChannels.Security.Arguments.AUTH); + byte[] inputBytes = Base64.decode(encryptedBase64Content, Base64.DEFAULT); + String decryptMessage = securityHelper.decryptMessage(inputBytes, privateKeyBase64, publicKeyBase64, authBase64); result.success(decryptMessage); } }); } - @Override - protected void onNewIntent(@NonNull Intent intent) { - super.onNewIntent(intent); - handleIntent(intent); - } - - private void handleIntent(Intent intent) { + private void cacheDateFromPlatform(Intent intent) { String action = intent.getAction(); String type = intent.getType(); Uri data = intent.getData(); @@ -156,25 +129,27 @@ private void handleIntent(Intent intent) { if (Intent.ACTION_SEND.equals(action) && type != null) { if (type.startsWith("text/")) { String text = intent.getStringExtra(Intent.EXTRA_TEXT); - sharedData.put(SHARED_MIME_TYPE, type); - sharedData.put(SHARED_TEXT, text); + sharedData.put(MethodChannels.Sharing.Arguments.MIME_TYPE, type); + sharedData.put(MethodChannels.Sharing.Arguments.TEXT, text); } else if (type.startsWith("application/") || type.startsWith("audio/") || type.startsWith("image/") || type.startsWith("video/")) { Uri uri = (Uri) Objects.requireNonNull(getIntent().getExtras()).get(Intent.EXTRA_STREAM); if (uri == null) { ClipData clipData = intent.getClipData(); - ClipData.Item item = clipData.getItemAt(0); - uri = item.getUri(); + if (clipData != null) { + ClipData.Item item = clipData.getItemAt(0); + uri = item.getUri(); + } } if (uri != null) { String text = intent.getStringExtra(Intent.EXTRA_TEXT); ShareHelper shareHelper = new ShareHelper(); String uriPath = shareHelper.getFilePathForUri(this, uri); if (text != null && !text.isEmpty()) { - sharedData.put(SHARED_TEXT, text); + sharedData.put(MethodChannels.Sharing.Arguments.TEXT, text); } - sharedData.put(SHARED_MIME_TYPE, type); - sharedData.put(SHARED_PATH, uriPath); - sharedData.put(SHARED_FILE_NAME, shareHelper.getFileName()); + sharedData.put(MethodChannels.Sharing.Arguments.MIME_TYPE, type); + sharedData.put(MethodChannels.Sharing.Arguments.PATH, uriPath); + sharedData.put(MethodChannels.Sharing.Arguments.NAME, shareHelper.getFileName()); } } } else if (Intent.ACTION_VIEW.equals(action) && data != null) { @@ -182,13 +157,13 @@ private void handleIntent(Intent intent) { } } - private void shareFile(Object arguments) { + private void shareDataWithPlatform(Object arguments) { @SuppressWarnings("unchecked") HashMap argsMap = (HashMap) arguments; - String title = argsMap.get("title"); - String path = argsMap.get("path"); - String mimeType = argsMap.get("mimeType"); - String text = argsMap.get("text"); + String title = argsMap.get(MethodChannels.Sharing.Arguments.TITLE); + String path = argsMap.get(MethodChannels.Sharing.Arguments.PATH); + String mimeType = argsMap.get(MethodChannels.Sharing.Arguments.MIME_TYPE); + String text = argsMap.get(MethodChannels.Sharing.Arguments.TEXT); Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType(mimeType); diff --git a/android/app/src/main/java/com/openxchange/oxcoi/MethodChannels.java b/android/app/src/main/java/com/openxchange/oxcoi/MethodChannels.java new file mode 100644 index 00000000..26a52bf0 --- /dev/null +++ b/android/app/src/main/java/com/openxchange/oxcoi/MethodChannels.java @@ -0,0 +1,39 @@ +package com.openxchange.oxcoi; + + +class MethodChannels { + + abstract static class Security { + static final String NAME = "oxcoi.security"; + + abstract static class Methods { + static final String DECRYPT = "'decrypt'"; + } + + abstract static class Arguments { + static final String CONTENT = "encryptedBase64Content"; + static final String PRIVATE_KEY = "privateKeyBase64"; + static final String PUBLIC_KEY = "publicKeyBase64"; + static final String AUTH = "authBase64"; + } + } + + abstract static class Sharing { + static final String NAME = "oxcoi.sharing"; + + abstract static class Methods { + static final String GET_SHARE_DATA = "getSharedData"; + static final String SEND_SHARE_DATA = "sendSharedData"; + static final String GET_INITIAL_LINK = "getInitialLink"; + } + + abstract static class Arguments { + static final String MIME_TYPE = "mimeType"; + static final String TEXT = "text"; + static final String PATH = "path"; + static final String NAME = "fileName"; + static final String TITLE = "title"; + } + } +} + diff --git a/android/app/src/main/java/com/openxchange/oxcoi/SecurityHelper.java b/android/app/src/main/java/com/openxchange/oxcoi/SecurityHelper.java index 1d93a77d..79d6b83e 100644 --- a/android/app/src/main/java/com/openxchange/oxcoi/SecurityHelper.java +++ b/android/app/src/main/java/com/openxchange/oxcoi/SecurityHelper.java @@ -43,9 +43,6 @@ package com.openxchange.oxcoi; -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; import android.util.Base64; import com.google.crypto.tink.HybridDecrypt; @@ -55,87 +52,29 @@ import org.bouncycastle.jce.interfaces.ECPrivateKey; import org.bouncycastle.jce.interfaces.ECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.provider.asymmetric.ec.KeyPairGenerator; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECPrivateKeySpec; import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.Security; -import java.security.spec.ECGenParameterSpec; import java.security.spec.InvalidKeySpecException; class SecurityHelper { - private static final String KEY_PUSH_PRIVATE = "KEY_PUSH_PRIVATE"; - private static final String KEY_PUSH_PUBLIC = "KEY_PUSH_PUBLIC"; - private static final String KEY_PUSH_AUTH = "KEY_PUSH_AUTH"; private static final String CURVE_NAME = "secp256r1"; private static final String KEY_ALGORITHM = "ECDH"; - private Activity activity; - - SecurityHelper(Activity activity) { + SecurityHelper() { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); Security.addProvider(new BouncyCastleProvider()); - this.activity = activity; - } - - String getPublicKeyBase64() { - ECPublicKey publicKey = getPublicKeyFromPersistedData(); - if (publicKey == null) { - return null; - } - return Base64.encodeToString(publicKey.getQ().getEncoded(), Base64.URL_SAFE); - } - - KeyPair generateKey() { - ECGenParameterSpec params = new ECGenParameterSpec(CURVE_NAME); - KeyPairGenerator generator = new KeyPairGenerator.ECDH(); - try { - generator.initialize(params, new SecureRandom()); - } catch (InvalidAlgorithmParameterException e) { - e.printStackTrace(); - } - return generator.generateKeyPair(); } - String generateAuthSecret() { - SecureRandom random = new SecureRandom(); - byte[] bytes = new byte[16]; - random.nextBytes(bytes); - return Base64.encodeToString(bytes, Base64.URL_SAFE); - } - - void persistKeyPair(KeyPair keyPair) { - ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic(); - ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate(); - String publicKeyBase64 = Base64.encodeToString(ecPublicKey.getQ().getEncoded(), Base64.URL_SAFE); - String privateKeyBase64 = Base64.encodeToString(ecPrivateKey.getD().toByteArray(), Base64.URL_SAFE); - SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putString(KEY_PUSH_PRIVATE, privateKeyBase64); - editor.putString(KEY_PUSH_PUBLIC, publicKeyBase64); - editor.apply(); - } - - void persisAuthSecret(String authSecret) { - SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putString(KEY_PUSH_AUTH, authSecret); - editor.apply(); - } - - private ECPrivateKey getPrivateKeyFromPersistedData() { - SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); - String privateKeyBase64 = sharedPref.getString(KEY_PUSH_PRIVATE, ""); + private ECPrivateKey getPrivateKeyFromPersistedData(String privateKeyBase64) { byte[] privateKeyBytes = Base64.decode(privateKeyBase64, Base64.URL_SAFE); BigInteger privateKeyD = new BigInteger(privateKeyBytes); @@ -152,9 +91,7 @@ private ECPrivateKey getPrivateKeyFromPersistedData() { return null; } - private ECPublicKey getPublicKeyFromPersistedData() { - SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); - String publicKeyBase64 = sharedPref.getString(KEY_PUSH_PUBLIC, ""); + private ECPublicKey getPublicKeyFromBase64String(String publicKeyBase64) { byte[] publicKeyBytes = Base64.decode(publicKeyBase64, Base64.URL_SAFE); ECParameterSpec ecParameterSpec = ECNamedCurveTable.getParameterSpec(CURVE_NAME); @@ -171,21 +108,16 @@ private ECPublicKey getPublicKeyFromPersistedData() { return null; } - String getAuthSecretFromPersistedData() { - SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); - return sharedPref.getString(KEY_PUSH_AUTH, ""); - } - - String decryptMessage(byte[] encryptedContent) { - ECPrivateKey recipientPrivateKey = getPrivateKeyFromPersistedData(); - ECPublicKey recipientPublicKey = getPublicKeyFromPersistedData(); + String decryptMessage(byte[] encryptedContent, String privateKeyBase64, String publicKeyBase64, String authBase64) { + ECPrivateKey recipientPrivateKey = getPrivateKeyFromPersistedData(privateKeyBase64); + ECPublicKey recipientPublicKey = getPublicKeyFromBase64String(publicKeyBase64); try { java.security.interfaces.ECPublicKey recipientPublicKeyAsJavaSecurity = (java.security.interfaces.ECPublicKey) recipientPublicKey; if (recipientPublicKey == null) { throw new IllegalStateException("Public key is null"); } java.security.interfaces.ECPrivateKey recipientPrivateAsJavaSecurity = (java.security.interfaces.ECPrivateKey) recipientPrivateKey; - byte[] authSecret = Base64.decode(getAuthSecretFromPersistedData(), Base64.URL_SAFE); + byte[] authSecret = Base64.decode(authBase64, Base64.URL_SAFE); HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder() .withAuthSecret(authSecret) .withRecipientPublicKey(recipientPublicKeyAsJavaSecurity) diff --git a/lib/src/chat/chat.dart b/lib/src/chat/chat.dart index 4cd22ffb..3d21237b 100644 --- a/lib/src/chat/chat.dart +++ b/lib/src/chat/chat.dart @@ -73,7 +73,7 @@ import 'package:ox_coi/src/message_list/message_list_bloc.dart'; import 'package:ox_coi/src/message_list/message_list_event_state.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; -import 'package:ox_coi/src/share/shared_data.dart'; +import 'package:ox_coi/src/share/incoming_shared_data.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; import 'package:ox_coi/src/utils/image.dart'; import 'package:ox_coi/src/utils/keyMapping.dart'; @@ -94,7 +94,7 @@ class Chat extends StatefulWidget { final String newMessage; final String newPath; final int newFileType; - final SharedData sharedData; + final IncomingSharedData sharedData; final bool headlessStart; Chat({@required this.chatId, this.messageId, this.newMessage, this.newPath, this.newFileType, this.sharedData, this.headlessStart = false}); diff --git a/lib/src/extensions/numbers_apis.dart b/lib/src/extensions/numbers_apis.dart index 22daabab..e1e019dc 100644 --- a/lib/src/extensions/numbers_apis.dart +++ b/lib/src/extensions/numbers_apis.dart @@ -2,9 +2,10 @@ import 'package:date_format/date_format.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; +const _kilobyte = 1024; +const _megabyte = 1024 * _kilobyte; + extension Convert on int { - static const _kilobyte = 1024; - static const _megabyte = 1024 * _kilobyte; String byteToPrintableSize() { String unit; diff --git a/lib/src/invite/invite_bloc.dart b/lib/src/invite/invite_bloc.dart index 467ece9f..6ca7aee4 100644 --- a/lib/src/invite/invite_bloc.dart +++ b/lib/src/invite/invite_bloc.dart @@ -46,7 +46,6 @@ import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:delta_chat_core/delta_chat_core.dart'; -import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:mime/mime.dart'; import 'package:ox_coi/src/data/config.dart'; @@ -58,8 +57,8 @@ import 'package:ox_coi/src/extensions/string_apis.dart'; import 'package:ox_coi/src/invite/invite_service.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; -import 'package:ox_coi/src/share/shared_data.dart'; -import 'package:ox_coi/src/utils/constants.dart'; +import 'package:ox_coi/src/platform/method_channels.dart'; +import 'package:ox_coi/src/share/outgoing_shared_data.dart'; import 'package:ox_coi/src/utils/http.dart'; import 'package:ox_coi/src/utils/image.dart'; import 'package:path_provider/path_provider.dart'; @@ -69,7 +68,6 @@ import 'invite_event_state.dart'; class InviteBloc extends Bloc { final Repository _contactRepository = RepositoryManager.get(RepositoryType.contact); final Repository _chatRepository = RepositoryManager.get(RepositoryType.chat); - static const sharingChannel = const MethodChannel(kMethodChannelSharing); InviteService inviteService = InviteService(); @override @@ -96,13 +94,11 @@ class InviteBloc extends Bloc { bool valid = isHttpResponseValid(response); if (valid) { InviteServiceResponse responseInviteService = _getInviteResponse(response); - Map argsMap = { - 'title': '', - 'path': '', - 'mimeType': 'text/*', - 'text': '${responseInviteService.endpoint} \n ${L10n.get(L.inviteShareText)}' - }; - sendSharedData(argsMap); + final shareData = OutgoingSharedData( + mimeType: 'text/*', + text: '${responseInviteService.endpoint} \n ${L10n.get(L.inviteShareText)}', + ); + sendSharedData(shareData.toMap()); yield InviteStateSuccess(); } else { yield InviteStateFailure(errorMessage: response.reasonPhrase); @@ -205,7 +201,8 @@ class InviteBloc extends Bloc { return inviteResponse; } - Future _getInitialLink() async => await sharingChannel.invokeMethod('getInitialLink'); + Future _getInitialLink() async => await SharingChannel.instance.invokeMethod(SharingChannel.kMethodGetInitialLink); - void sendSharedData(Map argsMap) async => await sharingChannel.invokeMethod('sendSharedData', argsMap); + Future sendSharedData(Map shareData) async => + await SharingChannel.instance.invokeMethod(SharingChannel.kMethodSendSharedData, shareData); } diff --git a/lib/src/message/message_attachment_bloc.dart b/lib/src/message/message_attachment_bloc.dart index 9d890eae..174f12d6 100644 --- a/lib/src/message/message_attachment_bloc.dart +++ b/lib/src/message/message_attachment_bloc.dart @@ -49,11 +49,14 @@ import 'package:crypto/crypto.dart'; import 'package:delta_chat_core/delta_chat_core.dart' as Core; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; +import 'package:delta_chat_core/delta_chat_core.dart'; import 'package:open_file/open_file.dart'; import 'package:ox_coi/src/data/repository.dart'; import 'package:ox_coi/src/data/repository_manager.dart'; import 'package:ox_coi/src/extensions/numbers_apis.dart'; import 'package:ox_coi/src/message/message_attachment_event_state.dart'; +import 'package:ox_coi/src/platform/method_channels.dart'; +import 'package:ox_coi/src/share/outgoing_shared_data.dart'; import 'package:ox_coi/src/utils/constants.dart'; import 'package:ox_coi/src/utils/video.dart'; import 'package:path/path.dart'; @@ -63,6 +66,7 @@ class MessageAttachmentBloc extends Bloc _messageListRepository; + Repository _messageListRepository; @override MessageAttachmentState get initialState => MessageAttachmentStateInitial(); @@ -106,10 +110,15 @@ class MessageAttachmentBloc extends Bloc{'title': '$text', 'path': '$filePath', 'mimeType': '$mime', 'text': '$text'}; - await sharingChannel.invokeMethod('sendSharedData', argsMap); + final shareData = OutgoingSharedData( + title: text, + path: filePath, + mimeType: mimeType, + text: text, + ); + await SharingChannel.instance.invokeMethod(SharingChannel.kMethodSendSharedData, shareData.toMap()); } Core.ChatMsg _getMessage(int messageId) { diff --git a/lib/src/platform/method_channels.dart b/lib/src/platform/method_channels.dart new file mode 100644 index 00000000..ee8e398a --- /dev/null +++ b/lib/src/platform/method_channels.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; + +class SecurityChannel { + static const _name = 'oxcoi.security'; + + static const kMethodDecrypt = 'decrypt'; + + static const kArgumentContent = 'encryptedBase64Content'; + static const kArgumentPrivateKey = 'privateKeyBase64'; + static const kArgumentPublicKey = 'publicKeyBase64'; + static const kArgumentAuth = 'authBase64'; + + static const instance = const MethodChannel(_name); +} + +class SharingChannel { + static const _name = 'oxcoi.sharing'; + + static const kMethodGetSharedData = 'getSharedData'; + static const kMethodSendSharedData = 'sendSharedData'; + static const kMethodGetInitialLink = 'getInitialLink'; + + static const kArgumentMimeType = 'mimeType'; + static const kArgumentText = 'text'; + static const kArgumentPath = 'path'; + static const kArgumentFileName = 'fileName'; + static const kArgumentTitle = 'title'; + + static const instance = const MethodChannel(_name); +} diff --git a/lib/src/platform/preferences.dart b/lib/src/platform/preferences.dart index 8688edfc..5326e212 100644 --- a/lib/src/platform/preferences.dart +++ b/lib/src/platform/preferences.dart @@ -59,9 +59,9 @@ const preferenceNeedsOnboarding = "preferenceNeedsOnboarding"; const preferenceNotificationHistory = "preferenceNotificationHistory"; const preferenceNotificationInviteHistory = "preferenceNotificationInviteHistory"; -const preferenceNotificationsAuth = "preferenceNotificationsAuth"; // Unused -const preferenceNotificationsP256dhPublic = "preferenceNotificationsP256dhPublic"; // Unused -const preferenceNotificationsP256dhPrivate = "preferenceNotificationsP256dhPrivate"; // Unused +const preferenceNotificationsAuth = "preferenceNotificationAuth"; +const preferenceNotificationKeyPublic = "preferenceNotificationKeyPublic"; +const preferenceNotificationKeyPrivate = "preferenceNotificationKeyPrivate"; Future getPreference(String key) async { SharedPreferences sharedPreferences = await getSharedPreferences(); diff --git a/lib/src/push/push_bloc.dart b/lib/src/push/push_bloc.dart index feeb7fa2..4eb71948 100644 --- a/lib/src/push/push_bloc.dart +++ b/lib/src/push/push_bloc.dart @@ -45,7 +45,6 @@ import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:delta_chat_core/delta_chat_core.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:logging/logging.dart'; import 'package:ox_coi/src/data/push_metadata.dart'; @@ -55,8 +54,8 @@ import 'package:ox_coi/src/platform/preferences.dart'; import 'package:ox_coi/src/platform/system_information.dart'; import 'package:ox_coi/src/push/push_event_state.dart'; import 'package:ox_coi/src/push/push_manager.dart'; -import 'package:ox_coi/src/secure/generator.dart'; -import 'package:ox_coi/src/utils/constants.dart'; +import 'package:ox_coi/src/security/security_generator.dart'; +import 'package:ox_coi/src/security/security_manager.dart'; import 'package:ox_coi/src/utils/http.dart'; import 'package:rxdart/rxdart.dart'; @@ -75,7 +74,6 @@ enum PushSetupState { class PushBloc extends Bloc { static const _subscribeListenerId = 1001; static const _validateListenerId = 1002; - static const _securityChannel = const MethodChannel(kMethodChannelSecurity); final _logger = Logger("push_bloc"); final _pushManager = PushManager(); @@ -256,9 +254,10 @@ class PushBloc extends Bloc { } Future _subscribeMetaDataAsync(ResponsePushResource responsePushResource) async { - await _generateSecretsAsync(); - final publicKey = await _getKeyAsync(); - final auth = await _getAuthSecretAsync(); + await generateAndPersistPushKeyPairAsync(); + final publicKey = await getPushPublicKeyAsync(); + await generateAndPersistPushAuthAsync(); + final auth = await getPushAuthAsync(); final clientEndpoint = generateUuid(); await setPreference(preferenceNotificationsEndpoint, clientEndpoint); @@ -310,16 +309,11 @@ class PushBloc extends Bloc { } _errorCallback(error) { - _logger.info("An error occured while listening: $error"); + _logger.warning("An error occured while listening: $error"); } Future _setNotificationPushStatusAsync(PushSetupState state) async { await setPreference(preferenceNotificationsPushStatus, describeEnum(state)); } - Future _generateSecretsAsync() async => await _securityChannel.invokeMethod('generateSecrets'); // TODO move to dart - - Future _getKeyAsync() async => await _securityChannel.invokeMethod('getKey'); // TODO move to dart - - Future _getAuthSecretAsync() async => await _securityChannel.invokeMethod('getAuthSecret'); // TODO move to dart } diff --git a/lib/src/push/push_manager.dart b/lib/src/push/push_manager.dart index a4249922..193f3654 100644 --- a/lib/src/push/push_manager.dart +++ b/lib/src/push/push_manager.dart @@ -44,7 +44,6 @@ import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:ox_coi/src/data/notification.dart'; @@ -52,14 +51,13 @@ import 'package:ox_coi/src/data/push_chat_message.dart'; import 'package:ox_coi/src/data/push_validation.dart'; import 'package:ox_coi/src/extensions/string_apis.dart'; import 'package:ox_coi/src/notifications/display_notification_manager.dart'; +import 'package:ox_coi/src/platform/method_channels.dart'; import 'package:ox_coi/src/platform/preferences.dart'; import 'package:ox_coi/src/push/push_bloc.dart'; import 'package:ox_coi/src/push/push_event_state.dart'; -import 'package:ox_coi/src/utils/constants.dart'; +import 'package:ox_coi/src/security/security_manager.dart'; class PushManager { - static const securityChannel = const MethodChannel(kMethodChannelSecurity); - final _firebaseMessaging = FirebaseMessaging(); final _notificationManager = DisplayNotificationManager(); final _logger = Logger("push_manager"); @@ -116,8 +114,17 @@ class PushManager { return await getPreference(preferenceNotificationsPush); } - Future decryptAsync(String base64content) async { - return await securityChannel.invokeMethod('decrypt', {"input": base64content}); + Future decryptAsync(String encryptedBase64Content) async { + final privateKey = await getPushPrivateKeyAsync(); + final publicKey = await getPushPublicKeyAsync(); + final auth = await getPushAuthAsync(); + + return await SecurityChannel.instance.invokeMethod(SecurityChannel.kMethodDecrypt, { + SecurityChannel.kArgumentContent: encryptedBase64Content, + SecurityChannel.kArgumentPrivateKey: privateKey, + SecurityChannel.kArgumentPublicKey: publicKey, + SecurityChannel.kArgumentAuth: auth, + }); } bool _isValidationPush(String decryptedContent) { diff --git a/lib/src/secure/generator.dart b/lib/src/security/security_generator.dart similarity index 76% rename from lib/src/secure/generator.dart rename to lib/src/security/security_generator.dart index 21211f00..46273387 100644 --- a/lib/src/secure/generator.dart +++ b/lib/src/security/security_generator.dart @@ -40,12 +40,10 @@ * for more details. */ -import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/api.dart'; -import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256r1.dart'; import 'package:pointycastle/key_generators/api.dart'; import 'package:pointycastle/key_generators/ec_key_generator.dart'; @@ -53,36 +51,30 @@ import 'package:pointycastle/random/fortuna_random.dart'; import 'package:uuid/uuid.dart'; String generateUuid() { - var uuid = new Uuid(); + final uuid = Uuid(); return uuid.v4(); } -String getPublicEcKey(AsymmetricKeyPair keyPair) { - ECPublicKey publicKey = keyPair.publicKey; - var encoded = publicKey.Q.getEncoded(false); - return base64UrlEncode(encoded); -} - -String getPrivateEcKey(AsymmetricKeyPair keyPair) { - ECPrivateKey privateKey = keyPair.privateKey; - return privateKey.d.toString(); +Uint8List generateRandomBytes([int length = 16]) { + final secureRandom = _getSecureRandom(); + return secureRandom.nextBytes(length); } AsymmetricKeyPair generateEcKeyPair() { - var domainParameters = ECCurve_secp256r1(); - var params = ECKeyGeneratorParameters(domainParameters); - var generator = ECKeyGenerator(); - generator.init(ParametersWithRandom(params, _getSecureRandom())); + final domainParameters = ECCurve_secp256r1(); + final generatorParameters = ECKeyGeneratorParameters(domainParameters); + final generator = ECKeyGenerator(); + generator.init(ParametersWithRandom(generatorParameters, _getSecureRandom())); return generator.generateKeyPair(); } SecureRandom _getSecureRandom() { - var secureRandom = FortunaRandom(); - var random = Random.secure(); + final secureRandom = FortunaRandom(); + final seedingRandom = Random.secure(); List seeds = []; for (int i = 0; i < 32; i++) { - seeds.add(random.nextInt(255)); + seeds.add(seedingRandom.nextInt(255)); } - secureRandom.seed(new KeyParameter(Uint8List.fromList(seeds))); + secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); return secureRandom; } diff --git a/lib/src/security/security_manager.dart b/lib/src/security/security_manager.dart new file mode 100644 index 00000000..bb09fbed --- /dev/null +++ b/lib/src/security/security_manager.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:ox_coi/src/platform/preferences.dart'; +import 'package:ox_coi/src/security/security_generator.dart'; +import 'package:pointycastle/pointycastle.dart'; +// ignore: implementation_imports +import 'package:pointycastle/src/utils.dart'; // Required implementation import to allow encoding / decoding of BigInt <-> ByteArray + +Future generateAndPersistPushKeyPairAsync() async { + final keyPair = generateEcKeyPair(); + final publicKey = _extractBase64PublicEcKey(keyPair); + final privateKey = _extractBase64PrivateEcKey(keyPair); + await setPreference(preferenceNotificationKeyPublic, publicKey); + await setPreference(preferenceNotificationKeyPrivate, privateKey); +} + +String _extractBase64PublicEcKey(AsymmetricKeyPair keyPair) { + final ECPublicKey publicKey = keyPair.publicKey; + final encodedKey = publicKey.Q.getEncoded(false); + return base64UrlEncode(encodedKey); +} + +String _extractBase64PrivateEcKey(AsymmetricKeyPair keyPair) { + final ECPrivateKey privateKey = keyPair.privateKey; + final encodedKey = encodeBigInt(privateKey.d); + return base64UrlEncode(encodedKey); +} + +Future generateAndPersistPushAuthAsync() async { + final auth = generateRandomBytes(); + final encodedAuth = base64UrlEncode(auth); + await setPreference(preferenceNotificationsAuth, encodedAuth); +} + +Future getPushPrivateKeyAsync() async { + return await getPreference(preferenceNotificationKeyPrivate); +} + +Future getPushPublicKeyAsync() async { + return await getPreference(preferenceNotificationKeyPublic); +} + +Future getPushAuthAsync() async { + return await getPreference(preferenceNotificationsAuth); +} \ No newline at end of file diff --git a/lib/src/share/shared_data.dart b/lib/src/share/incoming_shared_data.dart similarity index 77% rename from lib/src/share/shared_data.dart rename to lib/src/share/incoming_shared_data.dart index e7bcfafd..402ac6e8 100644 --- a/lib/src/share/shared_data.dart +++ b/lib/src/share/incoming_shared_data.dart @@ -40,21 +40,18 @@ * for more details. */ -class SharedData { - static const String sharedMimeType = "shared_mime_type"; - static const String sharedText = "shared_text"; - static const String sharedPath = "shared_path"; - static const String sharedFileName = "shared_file_name"; +import 'package:ox_coi/src/platform/method_channels.dart'; +class IncomingSharedData { String mimeType; String text; String path; String fileName; - SharedData(Map data) { - mimeType = data.containsKey(sharedMimeType) ? data[sharedMimeType] : ""; - text = data.containsKey(sharedText) ? data[sharedText] : ""; - path = data.containsKey(sharedPath) ? data[sharedPath] : ""; - fileName = data.containsKey(sharedFileName) ? data[sharedFileName] : ""; + IncomingSharedData(Map data) { + mimeType = data.containsKey(SharingChannel.kArgumentMimeType) ? data[SharingChannel.kArgumentMimeType] : ""; + text = data.containsKey(SharingChannel.kArgumentText) ? data[SharingChannel.kArgumentText] : ""; + path = data.containsKey(SharingChannel.kArgumentPath) ? data[SharingChannel.kArgumentPath] : ""; + fileName = data.containsKey(SharingChannel.kArgumentFileName) ? data[SharingChannel.kArgumentFileName] : ""; } } diff --git a/lib/src/share/outgoing_shared_data.dart b/lib/src/share/outgoing_shared_data.dart new file mode 100644 index 00000000..133e40bb --- /dev/null +++ b/lib/src/share/outgoing_shared_data.dart @@ -0,0 +1,56 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +import 'package:flutter/widgets.dart'; + +class OutgoingSharedData { + final String title; + final String path; + final String mimeType; + final String text; + + OutgoingSharedData({this.title = '', this.path = '', @required this.mimeType, @required this.text}); + + Map toMap() { + return {'title': '$text', 'path': '$path', 'mimeType': '$mimeType', 'text': '$text'}; + } +} diff --git a/lib/src/share/share.dart b/lib/src/share/share.dart index 2c9e3ee3..ad29b853 100644 --- a/lib/src/share/share.dart +++ b/lib/src/share/share.dart @@ -53,7 +53,7 @@ import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; import 'package:ox_coi/src/share/share_bloc.dart'; import 'package:ox_coi/src/share/share_event_state.dart'; -import 'package:ox_coi/src/share/shared_data.dart'; +import 'package:ox_coi/src/share/incoming_shared_data.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; import 'package:ox_coi/src/utils/key_generator.dart'; import 'package:ox_coi/src/widgets/dynamic_appbar.dart'; @@ -62,7 +62,7 @@ import 'package:ox_coi/src/widgets/state_info.dart'; class Share extends StatefulWidget { final List msgIds; final MessageActionTag messageActionTag; - final SharedData sharedData; + final IncomingSharedData sharedData; Share({this.msgIds, this.messageActionTag, this.sharedData}); diff --git a/lib/src/share/share_bloc.dart b/lib/src/share/share_bloc.dart index ee666e04..e4c3b883 100644 --- a/lib/src/share/share_bloc.dart +++ b/lib/src/share/share_bloc.dart @@ -44,21 +44,19 @@ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:delta_chat_core/delta_chat_core.dart'; -import 'package:flutter/services.dart'; import 'package:ox_coi/src/chatlist/chat_list_bloc.dart'; import 'package:ox_coi/src/chatlist/chat_list_event_state.dart'; import 'package:ox_coi/src/contact/contact_list_bloc.dart'; import 'package:ox_coi/src/contact/contact_list_event_state.dart'; import 'package:ox_coi/src/data/contact_repository.dart'; +import 'package:ox_coi/src/platform/method_channels.dart'; import 'package:ox_coi/src/platform/preferences.dart'; import 'package:ox_coi/src/share/share_event_state.dart'; -import 'package:ox_coi/src/share/shared_data.dart'; -import 'package:ox_coi/src/utils/constants.dart'; +import 'package:ox_coi/src/share/incoming_shared_data.dart'; class ShareBloc extends Bloc { ChatListBloc _chatListBloc = ChatListBloc(); ContactListBloc _contactListBloc = ContactListBloc(); - static const sharingChannel = const MethodChannel(kMethodChannelSharing); @override ShareState get initialState => ShareStateInitial(); @@ -137,10 +135,10 @@ class ShareBloc extends Bloc { return; } if (data.length > 0) { - var sharedData = SharedData(data); + var sharedData = IncomingSharedData(data); add(SharedDataLoaded(sharedData: sharedData)); } } - Future _getSharedData() async => await sharingChannel.invokeMethod('getSharedData'); + Future _getSharedData() async => await SharingChannel.instance.invokeMethod(SharingChannel.kMethodGetSharedData); } diff --git a/lib/src/share/share_event_state.dart b/lib/src/share/share_event_state.dart index ab3de21a..ac7e6638 100644 --- a/lib/src/share/share_event_state.dart +++ b/lib/src/share/share_event_state.dart @@ -41,7 +41,7 @@ */ import 'package:meta/meta.dart'; -import 'package:ox_coi/src/share/shared_data.dart'; +import 'package:ox_coi/src/share/incoming_shared_data.dart'; abstract class ShareEvent {} @@ -72,7 +72,7 @@ class ForwardMessages extends ShareEvent { class LoadSharedData extends ShareEvent {} class SharedDataLoaded extends ShareEvent { - final SharedData sharedData; + final IncomingSharedData sharedData; SharedDataLoaded({@required this.sharedData}); } @@ -87,7 +87,7 @@ class ShareStateSuccess extends ShareState { final List chatAndContactIds; final int chatIdCount; final int contactIdCount; - final SharedData sharedData; + final IncomingSharedData sharedData; ShareStateSuccess({ this.chatAndContactIds, diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 54b36139..7deb95e5 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -53,10 +53,6 @@ const maxAttachmentSize = 100 * 1024 * 1024; // Means 100 MB // Extension database - the file is placed in the apps folder structure under ~/databases/$extensionDbName const extensionDbName = "extension.db"; -// Method channels -const kMethodChannelSecurity = 'oxcoi.security'; -const kMethodChannelSharing = 'oxcoi.sharing'; - // External services const defaultCoiPushServiceUrl = "https://push.coi.me/push/resource/"; const defaultCoiInviteServiceUrl = "https://invite.coi.me/invite/"; diff --git a/test/push/push.dart b/test/push/push.dart index 1e69de13..125500d0 100644 --- a/test/push/push.dart +++ b/test/push/push.dart @@ -44,11 +44,19 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:ox_coi/src/security/security_generator.dart'; import 'package:pointycastle/export.dart'; import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; void main() { + test("secure randoms", () { + final bytes = generateRandomBytes(); + print("Bytes size (16): ${bytes.length}"); + final bytes32 = generateRandomBytes(32); + print("Bytes size (32): ${bytes32.length}"); + }); + test('p256dh', () { var domainParameters = ECCurve_secp256r1(); var params = ECKeyGeneratorParameters(domainParameters); @@ -65,7 +73,7 @@ void main() { test('UUID', () { var uuid = new Uuid(); - print("Secret ${uuid.v4()}"); + print("UUID ${uuid.v4()}"); }); }