Skip to content
This repository has been archived by the owner on Oct 19, 2022. It is now read-only.

Commit

Permalink
Moved key generation into the dart part #501 OT-779
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Boehrsi committed May 18, 2020
1 parent 3d09ac6 commit 63a7ef6
Show file tree
Hide file tree
Showing 20 changed files with 303 additions and 227 deletions.
103 changes: 39 additions & 64 deletions android/app/src/main/java/com/openxchange/oxcoi/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -65,130 +63,107 @@
public class MainActivity extends FlutterActivity {
private Map<String, String> 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
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();

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) {
startString = data.toString();
}
}

private void shareFile(Object arguments) {
private void shareDataWithPlatform(Object arguments) {
@SuppressWarnings("unchecked")
HashMap<String, String> argsMap = (HashMap<String, String>) 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/src/chat/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import 'package:ox_coi/src/message/message_list_bloc.dart';
import 'package:ox_coi/src/message/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';
Expand All @@ -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});
Expand Down
5 changes: 3 additions & 2 deletions lib/src/extensions/numbers_apis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 63a7ef6

Please sign in to comment.